Files
cc-1c-skills/.claude/skills/form-compile/scripts/form-compile.py
T
Nick Shirokov 36cd63d8bb fix(form-compile): AutoTitle=false по умолчанию при заданном Title формы
Когда у формы задан Title (через defn.title или properties.title), эмитим
AutoTitle=false если пользователь явно его не указал. Иначе платформа
добавляет суффикс синонима и получается двойной заголовок (Номенклатура:
Номенклатура). В выгрузках ERP так делает ~95% форм с form-level Title.

Снапшоты form-compile/form-compile-from-object обновлены под новое поведение.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:22:48 +03:00

2816 lines
126 KiB
Python

#!/usr/bin/env python3
# form-compile v1.12 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
import json
import os
import re
import sys
import uuid
import xml.etree.ElementTree as ET
from collections import OrderedDict
# ═══════════════════════════════════════════════════════════════════════════
# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation
# ═══════════════════════════════════════════════════════════════════════════
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'v8': 'http://v8.1c.ru/8.1/data/core',
}
def _et_find(node, path):
"""Find with namespace map."""
return node.find(path, NS)
def _et_findall(node, path):
"""Findall with namespace map."""
return node.findall(path, NS)
def _et_text(node, path, default=''):
"""Get text of a sub-element, or default."""
el = node.find(path, NS)
return el.text if el is not None and el.text else default
def parse_object_meta(object_path):
"""Parse 1C metadata XML and return dict with Type, Name, Synonym, Attributes, TabularSections, etc."""
tree = ET.parse(object_path)
root = tree.getroot()
# Detect object type from root child
meta_root = _et_find(root, '.')
# Root is MetaDataObject; first child is the type node
type_node = None
for child in root:
type_node = child
break
if type_node is None:
print("Not a 1C metadata XML: " + object_path, file=sys.stderr)
sys.exit(1)
# Extract local name (strip namespace)
obj_type = type_node.tag.split('}')[-1] if '}' in type_node.tag else type_node.tag
props_node = _et_find(type_node, 'md:Properties')
child_objs = _et_find(type_node, 'md:ChildObjects')
# Name
obj_name = _et_text(props_node, 'md:Name')
# Synonym (Russian)
synonym = obj_name
syn_node = _et_find(props_node, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
if syn_node is not None and syn_node.text:
synonym = syn_node.text
def extract_type(type_parent):
"""Extract type string from md:Type element."""
if type_parent is None:
return 'string'
types = []
for t in _et_findall(type_parent, 'v8:Type'):
if t.text:
types.append(t.text)
if not types:
return 'string'
return ' | '.join(types)
def is_ref_type(t):
return bool(re.search(r'Ref\.', t) or re.search(r'\u0441\u0441\u044b\u043b\u043a\u0430\.', t))
def extract_fields(parent_node, tag_name='Attribute'):
"""Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag)."""
result = []
if parent_node is None:
return result
for field_node in _et_findall(parent_node, f'md:{tag_name}'):
fp = _et_find(field_node, 'md:Properties')
f_name = _et_text(fp, 'md:Name')
f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name
f_type_node = _et_find(fp, 'md:Type')
f_type = extract_type(f_type_node)
result.append({
'Name': f_name,
'Synonym': f_syn,
'Type': f_type,
'IsRef': is_ref_type(f_type),
})
return result
# Attributes
attributes = extract_fields(child_objs, 'Attribute')
# Tabular sections
tabular_sections = []
if child_objs is not None:
for ts_node in _et_findall(child_objs, 'md:TabularSection'):
tsp = _et_find(ts_node, 'md:Properties')
ts_name = _et_text(tsp, 'md:Name')
ts_syn_node = _et_find(tsp, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
ts_syn = ts_syn_node.text if ts_syn_node is not None and ts_syn_node.text else ts_name
ts_co = _et_find(ts_node, 'md:ChildObjects')
ts_cols = extract_fields(ts_co, 'Attribute')
tabular_sections.append({
'Name': ts_name,
'Synonym': ts_syn,
'Columns': ts_cols,
})
meta = {
'Type': obj_type,
'Name': obj_name,
'Synonym': synonym,
'Attributes': attributes,
'TabularSections': tabular_sections,
}
# Type-specific properties
if obj_type == 'Document':
nt_node = _et_find(props_node, 'md:NumberType')
meta['NumberType'] = nt_node.text if nt_node is not None and nt_node.text else 'String'
elif obj_type == 'Catalog':
cl_node = _et_find(props_node, 'md:CodeLength')
meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
dl_node = _et_find(props_node, 'md:DescriptionLength')
meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
hi_node = _et_find(props_node, 'md:Hierarchical')
meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true')
ht_node = _et_find(props_node, 'md:HierarchyType')
meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
owners = []
for ow in _et_findall(props_node, 'md:Owners/xr:Item'):
if ow.text:
owners.append(ow.text)
meta['Owners'] = owners
elif obj_type == 'InformationRegister':
meta['Dimensions'] = extract_fields(child_objs, 'Dimension')
meta['Resources'] = extract_fields(child_objs, 'Resource')
prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity')
meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical'
wm_node = _et_find(props_node, 'md:WriteMode')
meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent'
elif obj_type == 'AccumulationRegister':
meta['Dimensions'] = extract_fields(child_objs, 'Dimension')
meta['Resources'] = extract_fields(child_objs, 'Resource')
rt_node = _et_find(props_node, 'md:RegisterType')
meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances'
elif obj_type == 'ChartOfCharacteristicTypes':
cl_node = _et_find(props_node, 'md:CodeLength')
meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
dl_node = _et_find(props_node, 'md:DescriptionLength')
meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
hi_node = _et_find(props_node, 'md:Hierarchical')
meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true')
ht_node = _et_find(props_node, 'md:HierarchyType')
meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
owners = []
for ow in _et_findall(props_node, 'md:Owners/xr:Item'):
if ow.text:
owners.append(ow.text)
meta['Owners'] = owners
meta['HasValueType'] = True
elif obj_type == 'ExchangePlan':
cl_node = _et_find(props_node, 'md:CodeLength')
meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
dl_node = _et_find(props_node, 'md:DescriptionLength')
meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
meta['Hierarchical'] = False
meta['HierarchyType'] = None
meta['Owners'] = []
elif obj_type == 'ChartOfAccounts':
cl_node = _et_find(props_node, 'md:CodeLength')
meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
dl_node = _et_find(props_node, 'md:DescriptionLength')
meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
meta['Hierarchical'] = True
ht_node = _et_find(props_node, 'md:HierarchyType')
meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
meta['Owners'] = []
max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount')
meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0
meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag')
meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag')
return meta
def _deep_merge(base, overlay):
"""Deep merge two dicts. overlay wins on conflicts."""
if not overlay:
return base
if not base:
return overlay
result = {}
for k in base:
result[k] = base[k]
for k in overlay:
if k in result and isinstance(result[k], dict) and isinstance(overlay[k], dict):
result[k] = _deep_merge(result[k], overlay[k])
else:
result[k] = overlay[k]
return result
def load_preset(preset_name, script_dir, out_path_resolved):
"""Load preset: hardcoded defaults -> built-in JSON -> project-level JSON, with deep merge."""
defaults = {
'document.item': {
'header': {'position': 'insidePage', 'layout': '2col', 'distribute': 'even', 'dateTitle': '\u043e\u0442'},
'footer': {'fields': ['\u041a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439'], 'position': 'insidePage'},
'tabularSections': {'container': 'pages', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'], 'lineNumber': True},
'additional': {'position': 'page', 'layout': '2col', 'bspGroup': True},
'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
'commandBar': 'auto',
'properties': {'autoTitle': False},
},
'document.list': {
'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True,
'tableCommandBar': 'none', 'commandBar': 'auto',
'properties': {},
},
'document.choice': {
'basedOn': 'document.list',
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
'catalog.item': {
'header': {'layout': '1col', 'distribute': 'left'},
'codeDescription': {'layout': 'horizontal', 'order': 'descriptionFirst'},
'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443', 'position': 'afterCodeDescription'},
'owner': {'readOnly': True, 'position': 'first'},
'tabularSections': {'container': 'inline', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'], 'lineNumber': True},
'footer': {'fields': [], 'position': 'none'},
'additional': {'position': 'none', 'bspGroup': True},
'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
'commandBar': 'auto',
'properties': {},
},
'catalog.folder': {
'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443'},
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
'catalog.list': {
'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True,
'tableCommandBar': 'none', 'commandBar': 'auto',
'properties': {},
},
'catalog.choice': {
'basedOn': 'catalog.list', 'choiceMode': True,
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
# --- Register defaults ---
'informationRegister.record': {
'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
'informationRegister.list': {
'columns': 'all', 'columnType': 'labelField',
'tableCommandBar': 'none', 'commandBar': 'auto',
'properties': {},
},
'accumulationRegister.list': {
'columns': 'all', 'columnType': 'labelField',
'tableCommandBar': 'none', 'commandBar': 'auto',
'properties': {},
},
# --- Catalog-like type defaults ---
'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'},
'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'},
'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'},
'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'},
'exchangePlan.item': {'basedOn': 'catalog.item'},
'exchangePlan.list': {'basedOn': 'catalog.list'},
'exchangePlan.choice': {'basedOn': 'catalog.choice'},
# --- ChartOfAccounts defaults ---
'chartOfAccounts.item': {
'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'},
'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
'properties': {},
},
'chartOfAccounts.folder': {
'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'},
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
'chartOfAccounts.list': {'basedOn': 'catalog.list'},
'chartOfAccounts.choice': {'basedOn': 'catalog.choice'},
}
# Try built-in preset
preset_dir = os.path.join(os.path.dirname(script_dir), 'presets')
built_in_path = os.path.join(preset_dir, f'{preset_name}.json')
if os.path.isfile(built_in_path):
with open(built_in_path, 'r', encoding='utf-8-sig') as f:
preset_data = json.load(f)
for k in list(preset_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
# Try project-level preset (scan up from output path)
scan_dir = os.path.dirname(out_path_resolved)
while scan_dir:
proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json')
if os.path.isfile(proj_preset):
with open(proj_preset, 'r', encoding='utf-8-sig') as f:
proj_data = json.load(f)
for k in list(proj_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), proj_data[k])
break
parent_dir = os.path.dirname(scan_dir)
if parent_dir == scan_dir:
break
scan_dir = parent_dir
# Resolve basedOn references
for k in list(defaults.keys()):
sect = defaults[k]
if isinstance(sect, dict) and 'basedOn' in sect:
base_name = sect['basedOn']
if base_name in defaults:
merged = _deep_merge(defaults[base_name], sect)
merged.pop('basedOn', None)
defaults[k] = merged
return defaults
# Non-displayable types — cannot be bound to form elements
NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения')
def is_displayable_type(type_str):
return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES)
def new_field_element(attr_name, data_path, attr_type, field_defaults, extra_props=None):
"""Build a field element DSL entry."""
is_ref = bool(re.search(r'Ref\.', attr_type))
is_bool = bool(re.match(r'^\s*xs:boolean\s*$', attr_type) or attr_type == 'boolean' or re.search(r'Boolean', attr_type))
el_type = 'input'
if is_bool and field_defaults and field_defaults.get('boolean') and field_defaults['boolean'].get('element') == 'check':
el_type = 'check'
el = OrderedDict()
el[el_type] = attr_name
el['path'] = data_path
# Apply ref defaults
if is_ref and field_defaults and field_defaults.get('ref'):
if field_defaults['ref'].get('choiceButton') is True:
el['choiceButton'] = True
# Extra props
if extra_props:
for k in extra_props:
el[k] = extra_props[k]
return el
# --- Catalog DSL generators ---
def generate_catalog_dsl(meta, preset_data, purpose):
purpose_key = f"catalog.{purpose.lower()}"
p = preset_data.get(purpose_key, {})
fd = p.get('fieldDefaults', {})
dispatch = {
'Folder': lambda: generate_catalog_folder_dsl(meta, p),
'List': lambda: generate_catalog_list_dsl(meta, p),
'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data),
'Item': lambda: generate_catalog_item_dsl(meta, p, fd),
}
return dispatch[purpose]()
def generate_catalog_folder_dsl(meta, p):
elements = []
# Code (if CodeLength > 0)
if meta.get('CodeLength', 0) > 0:
elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
# Description
elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
# Parent
parent_title = p.get('parent', {}).get('title')
parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')])
if parent_title:
parent_el['title'] = parent_title
elements.append(parent_el)
props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')])
if p.get('properties'):
for k in p['properties']:
props[k] = p['properties'][k]
form_props = OrderedDict([('useForFoldersAndItems', 'Folders')])
for k in props:
form_props[k] = props[k]
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', elements),
('attributes', [
OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)])
]),
])
def generate_catalog_list_dsl(meta, p):
columns = []
# Description always first
columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')]))
# Code if present
if meta.get('CodeLength', 0) > 0:
columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')]))
# Custom attributes
for attr in meta['Attributes']:
if not is_displayable_type(attr['Type']):
continue
columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
# Hidden ref
if p.get('hiddenRef', True) is not False:
columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)]))
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns),
])
# Hierarchical properties
if meta.get('Hierarchical'):
table_el['initialTreeView'] = 'ExpandTopLevel'
table_el['enableStartDrag'] = True
table_el['enableDrag'] = True
form_props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
form_props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', [table_el]),
('attributes', [
OrderedDict([
('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True),
('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])),
])
]),
])
def generate_catalog_choice_dsl(meta, p, preset_data):
# Start from list
list_key = 'catalog.list'
lp = preset_data.get(list_key, {})
dsl = generate_catalog_list_dsl(meta, lp)
# Add choice-specific properties
dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow'
if p.get('properties'):
for k in p['properties']:
dsl['properties'][k] = p['properties'][k]
# Set ChoiceMode on table
dsl['elements'][0]['choiceMode'] = True
return dsl
def generate_catalog_item_dsl(meta, p, fd):
header_children = []
# Owner (if subordinate)
if meta.get('Owners') and len(meta['Owners']) > 0:
owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)])
header_children.append(owner_el)
# Code + Description
cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal')
cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst')
has_code = meta.get('CodeLength', 0) > 0
if cd_layout == 'horizontal' and has_code:
cd_children = []
desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])
code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])
if cd_order == 'descriptionFirst':
cd_children = [desc_el, code_el]
else:
cd_children = [code_el, desc_el]
header_children.append(OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False),
('representation', 'none'), ('children', cd_children),
]))
else:
# Vertical or no code
header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
if has_code:
header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
# Parent (for hierarchical catalogs)
parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription')
parent_title = (p.get('parent') or {}).get('title')
if meta.get('Hierarchical'):
parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')])
if parent_title:
parent_el['title'] = parent_title
if parent_pos == 'beforeCodeDescription':
insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0
header_children.insert(insert_idx, parent_el)
else:
# afterCodeDescription (default)
header_children.append(parent_el)
# Custom attributes -> header
footer_field_names = (p.get('footer') or {}).get('fields', [])
for attr in meta['Attributes']:
if attr['Name'] in footer_field_names:
continue
if not is_displayable_type(attr['Type']):
continue
header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
# Build root elements
root_elements = []
# ГруппаШапка
root_elements.append(OrderedDict([
('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False),
('representation', 'none'), ('children', header_children),
]))
# Tabular sections
ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f']
if (p.get('tabularSections') or {}).get('exclude'):
ts_exclude = p['tabularSections']['exclude']
ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True)
visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude]
for ts in visible_ts:
ts_cols = []
if ts_line_number:
ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")]))
for col in ts['Columns']:
ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]))
# Footer fields
for fn in footer_field_names:
f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None)
if f_attr:
root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd))
# BSP group
bsp_group = (p.get('additional') or {}).get('bspGroup', True)
if bsp_group:
root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
# Properties
form_props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
form_props[k] = p['properties'][k]
# UseForFoldersAndItems
if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems':
form_props['useForFoldersAndItems'] = 'Items'
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', root_elements),
('attributes', [
OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)])
]),
])
# --- Document DSL generators ---
def generate_document_dsl(meta, preset_data, purpose):
purpose_key = f"document.{purpose.lower()}"
p = preset_data.get(purpose_key, {})
fd = p.get('fieldDefaults', {})
dispatch = {
'List': lambda: generate_document_list_dsl(meta, p),
'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data),
'Item': lambda: generate_document_item_dsl(meta, p, fd),
}
return dispatch[purpose]()
def generate_document_list_dsl(meta, p):
columns = []
# Standard columns: Number + Date
columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')]))
columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')]))
# All custom attributes as labelField
for attr in meta['Attributes']:
if not is_displayable_type(attr['Type']):
continue
columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
# Hidden ref
if p.get('hiddenRef', True):
columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)]))
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('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'),
('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'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns_list),
])
props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('properties', props),
('elements', [table_el]),
('attributes', [
{'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}}
]),
])
# --- ChartOfCharacteristicTypes (delegates to Catalog) ---
def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose):
# Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc.
dsl = generate_catalog_dsl(meta, preset_data, purpose)
# Post-patch: replace Catalog types with ChartOfCharacteristicTypes types
cat_obj_type = f"CatalogObject.{meta['Name']}"
ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}"
cat_list_type = f"Catalog.{meta['Name']}"
ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}"
for a in dsl['attributes']:
if a.get('type') == cat_obj_type:
a['type'] = ccoct_obj_type
if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type:
a['settings']['mainTable'] = ccoct_list_type
# For Item forms: inject ValueType field after Description/ГруппаКодНаименование
if purpose == 'Item' and dsl.get('elements'):
vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')])
els = dsl['elements']
if isinstance(els, list):
inserted = False
new_els = []
for el in els:
new_els.append(el)
if not inserted and isinstance(el, dict):
name = el.get('input') or el.get('group') or ''
if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'):
new_els.append(vt_el)
inserted = True
if not inserted:
new_els.append(vt_el)
dsl['elements'] = new_els
return dsl
# --- ExchangePlan (delegates to Catalog) ---
def generate_exchange_plan_dsl(meta, preset_data, purpose):
# ExchangePlans are not hierarchical and have no Folder form
dsl = generate_catalog_dsl(meta, preset_data, purpose)
# Post-patch: replace Catalog types with ExchangePlan types
cat_obj_type = f"CatalogObject.{meta['Name']}"
ep_obj_type = f"ExchangePlanObject.{meta['Name']}"
cat_list_type = f"Catalog.{meta['Name']}"
ep_list_type = f"ExchangePlan.{meta['Name']}"
for a in dsl['attributes']:
if a.get('type') == cat_obj_type:
a['type'] = ep_obj_type
if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type:
a['settings']['mainTable'] = ep_list_type
# For Item forms: inject SentNo, ReceivedNo after Code/Description
if purpose == 'Item' and dsl.get('elements'):
sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)])
recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)])
els = dsl['elements']
if isinstance(els, list):
inserted = False
new_els = []
for el in els:
new_els.append(el)
if not inserted and isinstance(el, dict):
name = el.get('input') or el.get('group') or ''
if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'):
new_els.append(sent_el)
new_els.append(recv_el)
inserted = True
if not inserted:
new_els.append(sent_el)
new_els.append(recv_el)
dsl['elements'] = new_els
return dsl
# --- ChartOfAccounts DSL generators ---
def generate_chart_of_accounts_dsl(meta, preset_data, purpose):
p_key = f"chartOfAccounts.{purpose.lower()}"
p = preset_data.get(p_key, {})
fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}
dispatch = {
'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data),
'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p),
'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data),
'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data),
}
return dispatch[purpose]()
def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data):
elements = []
# Header: Code + Parent
header_left_children = []
if meta.get('CodeLength', 0) > 0:
header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
header_right_children = []
if meta.get('Hierarchical'):
parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443')
header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)]))
if len(header_right_children) > 0:
elements.append(OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'),
('children', [
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]),
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]),
]),
]))
elif len(header_left_children) > 0:
elements.extend(header_left_children)
# Description
if meta.get('DescriptionLength', 0) > 0:
elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
# OffBalance
elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')]))
# AccountingFlags as checkboxes
if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0:
flag_children = []
for flag in meta['AccountingFlags']:
flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")]))
elements.append(OrderedDict([
('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'),
('children', flag_children),
]))
# ExtDimensionTypes table
if meta.get('MaxExtDimensionCount', 0) > 0:
ed_cols = []
ed_cols.append(OrderedDict([('input', '\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')]))
ed_cols.append(OrderedDict([('check', '\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')]))
if meta.get('ExtDimensionAccountingFlags'):
for ed_flag in meta['ExtDimensionAccountingFlags']:
ed_cols.append(OrderedDict([('check', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")]))
elements.append(OrderedDict([
('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'),
('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'),
('columns', ed_cols),
]))
# Custom attributes
for attr in meta['Attributes']:
if not is_displayable_type(attr['Type']):
continue
elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
# Tabular sections
ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f']
for ts in meta['TabularSections']:
if ts['Name'] in ts_exclude:
continue
ts_cols = []
for col in ts['Columns']:
if not is_displayable_type(col['Type']):
continue
ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]))
props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('properties', props),
('elements', elements),
('attributes', [
{'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True}
]),
])
def generate_chart_of_accounts_folder_dsl(meta, p):
elements = []
if meta.get('CodeLength', 0) > 0:
elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
if meta.get('DescriptionLength', 0) > 0:
elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
if meta.get('Hierarchical'):
parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443')
elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)]))
props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')])
if p.get('properties'):
for k in p['properties']:
props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('useForFoldersAndItems', 'Folders'),
('properties', props),
('elements', elements),
('attributes', [
{'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True}
]),
])
def generate_chart_of_accounts_list_dsl(meta, preset_data):
# Delegate to Catalog List and patch types
dsl = generate_catalog_dsl(meta, preset_data, 'List')
for a in dsl['attributes']:
if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}":
a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}"
return dsl
def generate_chart_of_accounts_choice_dsl(meta, preset_data):
dsl = generate_catalog_dsl(meta, preset_data, 'Choice')
for a in dsl['attributes']:
if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}":
a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}"
return dsl
# ═══════════════════════════════════════════════════════════════════════════
# END OF FROM-OBJECT MODE FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def emit_mltext(lines, indent, tag, text):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
# --- ID allocator ---
_next_id = 0
def new_id():
global _next_id
_next_id += 1
return _next_id
# --- Event handler name generator ---
EVENT_SUFFIX_MAP = {
"OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438",
"StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430",
"ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430",
"AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440",
"Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430",
"Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435",
"Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435",
"OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438",
"BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
"BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c",
"BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f",
"OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
"OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
"Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438",
"OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b",
"TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430",
"URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438",
"DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f",
"Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435",
"DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f",
"Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
"AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f",
}
KNOWN_EVENTS = {
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
"check": ["OnChange"],
"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", "input", "check", "label", "labelField", "table", "pages", "page",
"button", "picture", "picField", "calendar", "cmdBar", "popup",
"name", "path", "title",
"visible", "hidden", "enabled", "disabled", "readOnly", "userVisible",
"on", "handlers",
"titleLocation", "representation", "width", "height",
"horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight",
"multiLine", "passwordMode", "choiceButton", "clearButton",
"spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint",
"hyperlink",
"showTitle", "united",
"children", "columns",
"changeRowSet", "changeRowOrder", "header", "footer",
"commandBarLocation", "searchStringLocation",
"pagesRepresentation",
"type", "command", "stdCommand", "defaultButton", "locationInCommandBar",
"src",
"autofill",
"choiceMode", "initialTreeView", "enableDrag", "enableStartDrag",
"rowPictureDataPath", "tableAutofill",
}
TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page",
"button", "picture", "picField", "calendar", "cmdBar", "popup"]
def get_handler_name(element_name, event_name):
suffix = EVENT_SUFFIX_MAP.get(event_name)
if suffix:
return f"{element_name}{suffix}"
return f"{element_name}{event_name}"
def get_element_name(el, type_key):
if el.get('name'):
return str(el['name'])
return str(el.get(type_key, ''))
def emit_events(lines, el, element_name, indent, type_key):
if not el.get('on'):
return
# Validate event names
if type_key and type_key in KNOWN_EVENTS:
allowed = KNOWN_EVENTS[type_key]
for evt in el['on']:
if allowed and str(evt) not in allowed:
print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}")
lines.append(f"{indent}<Events>")
for evt in el['on']:
evt_name = str(evt)
handlers = el.get('handlers')
if handlers and handlers.get(evt_name):
handler = str(handlers[evt_name])
else:
handler = get_handler_name(element_name, evt_name)
lines.append(f'{indent}\t<Event name="{evt_name}">{handler}</Event>')
lines.append(f"{indent}</Events>")
def emit_companion(lines, tag, name, indent):
cid = new_id()
lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>')
def emit_common_flags(lines, el, indent):
if el.get('visible') is False or el.get('hidden') is True:
lines.append(f"{indent}<Visible>false</Visible>")
if el.get('userVisible') is False:
lines.append(f"{indent}<UserVisible>")
lines.append(f"{indent}\t<xr:Common>false</xr:Common>")
lines.append(f"{indent}</UserVisible>")
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>")
def emit_title(lines, el, name, indent):
if el.get('title'):
emit_mltext(lines, indent, 'Title', str(el['title']))
# --- Type emitter ---
V8_TYPES = {
"ValueTable": "v8:ValueTable",
"ValueTree": "v8:ValueTree",
"ValueList": "v8:ValueListType",
"TypeDescription": "v8:TypeDescription",
"Universal": "v8:Universal",
"FixedArray": "v8:FixedArray",
"FixedStructure": "v8:FixedStructure",
}
UI_TYPES = {
"FormattedString": "v8ui:FormattedString",
"Picture": "v8ui:Picture",
"Color": "v8ui:Color",
"Font": "v8ui:Font",
}
DCS_MAP = {
"DataCompositionSettings": "dcsset:DataCompositionSettings",
"DataCompositionSchema": "dcssch:DataCompositionSchema",
"DataCompositionComparisonType": "dcscor:DataCompositionComparisonType",
}
CFG_REF_PATTERN = re.compile(
r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|'
r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|'
r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|'
r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|'
r'InformationRegisterRecordSet|InformationRegisterRecordManager|'
r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|'
r'ConstantsSet|DataProcessorObject|ReportObject)\.'
)
KNOWN_INVALID_TYPES = {
'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)',
'FormDataCollection': 'Runtime type. Use ValueTable',
'FormDataTree': 'Runtime type. Use ValueTree',
'FormDataTreeItem': 'Runtime type, not valid in XML',
'FormDataCollectionItem': 'Runtime type, not valid in XML',
'FormGroup': 'UI element type, not a data type',
'FormField': 'UI element type, not a data type',
'FormButton': 'UI element type, not a data type',
'FormDecoration': 'UI element type, not a data type',
'FormTable': 'UI element type, not a data type',
}
_FORM_TYPE_SYNONYMS = {
"строка": "string", "число": "decimal", "булево": "boolean",
"дата": "date", "датавремя": "dateTime",
"number": "decimal", "bool": "boolean",
"справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject",
"документссылка": "DocumentRef", "документобъект": "DocumentObject",
"перечислениессылка": "EnumRef",
"плансчетовссылка": "ChartOfAccountsRef",
"планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef",
"планвидоврасчётассылка": "ChartOfCalculationTypesRef",
"планвидоврасчетассылка": "ChartOfCalculationTypesRef",
"планобменассылка": "ExchangePlanRef",
"бизнеспроцессссылка": "BusinessProcessRef",
"задачассылка": "TaskRef",
"определяемыйтип": "DefinedType",
}
def resolve_type_str(type_str):
if not type_str:
return type_str
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if type_str.startswith('cfg:'):
type_str = type_str[4:]
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base, params = m.group(1).strip(), m.group(2)
r = _FORM_TYPE_SYNONYMS.get(base.lower())
return f"{r}({params})" if r else type_str
if '.' in type_str:
i = type_str.index('.')
prefix, suffix = type_str[:i], type_str[i:]
r = _FORM_TYPE_SYNONYMS.get(prefix.lower())
return f"{r}{suffix}" if r else type_str
r = _FORM_TYPE_SYNONYMS.get(type_str.lower())
return r if r else type_str
def emit_single_type(lines, type_str, indent):
type_str = resolve_type_str(type_str)
# boolean
if type_str == 'boolean':
lines.append(f'{indent}<v8:Type>xs:boolean</v8:Type>')
return
# string or string(N)
m = re.match(r'^string(\((\d+)\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else '0'
lines.append(f'{indent}<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>Variable</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
# DynamicList
if type_str == 'DynamicList':
lines.append(f'{indent}<v8:Type>cfg:DynamicList</v8:Type>')
return
# cfg: references
if CFG_REF_PATTERN.match(type_str):
lines.append(f'{indent}<v8:Type>cfg:{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]}")
if '.' 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):
if not type_str:
lines.append(f'{indent}<Type/>')
return
type_string = str(type_str)
parts = [p.strip() for p in re.split(r'[|+]', type_string)]
lines.append(f'{indent}<Type>')
for part in parts:
emit_single_type(lines, part, f'{indent}\t')
lines.append(f'{indent}</Type>')
# --- Element emitters ---
def emit_element(lines, el, indent):
# Silent synonyms (safety net; top-level autoCmdBar is normally extracted in pre-pass)
_synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'}
for src, dst in _synonyms.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
type_key = None
for key in TYPE_KEYS:
if el.get(key) is not None:
type_key = key
break
if not type_key:
print("WARNING: Unknown element type, skipping", file=sys.stderr)
return
# Validate known keys
for p_name in el.keys():
if p_name not in KNOWN_KEYS:
print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr)
name = get_element_name(el, type_key)
eid = new_id()
emitters = {
'group': emit_group,
'input': emit_input,
'check': emit_check,
'label': emit_label,
'labelField': emit_label_field,
'table': emit_table,
'pages': emit_pages,
'page': emit_page,
'button': emit_button,
'picture': emit_picture_decoration,
'picField': emit_picture_field,
'calendar': emit_calendar,
'cmdBar': emit_command_bar,
'popup': emit_popup,
}
emitter = emitters.get(type_key)
if emitter:
emitter(lines, el, name, eid, indent)
def emit_group(lines, el, name, eid, indent):
lines.append(f'{indent}<UsualGroup name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
# Group orientation
group_val = str(el.get('group', ''))
orientation_map = {
'horizontal': 'Horizontal',
'vertical': 'Vertical',
'alwaysHorizontal': 'AlwaysHorizontal',
'alwaysVertical': 'AlwaysVertical',
}
orientation = orientation_map.get(group_val)
if orientation:
lines.append(f'{inner}<Group>{orientation}</Group>')
# Behavior
if group_val == 'collapsible':
lines.append(f'{inner}<Group>Vertical</Group>')
lines.append(f'{inner}<Behavior>Collapsible</Behavior>')
# 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>')
# ShowTitle
if el.get('showTitle') is False:
lines.append(f'{inner}<ShowTitle>false</ShowTitle>')
# United
if el.get('united') is False:
lines.append(f'{inner}<United>false</United>')
emit_common_flags(lines, el, inner)
# Companion: ExtendedTooltip
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
# Children
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<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_input(lines, el, name, eid, indent):
lines.append(f'{indent}<InputField name="{name}" id="{eid}">')
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('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 True:
lines.append(f'{inner}<MultiLine>true</MultiLine>')
if el.get('passwordMode') is True:
lines.append(f'{inner}<PasswordMode>true</PasswordMode>')
if el.get('choiceButton') is False:
lines.append(f'{inner}<ChoiceButton>false</ChoiceButton>')
if el.get('clearButton') is True:
lines.append(f'{inner}<ClearButton>true</ClearButton>')
if el.get('spinButton') is True:
lines.append(f'{inner}<SpinButton>true</SpinButton>')
if el.get('dropListButton') is True:
lines.append(f'{inner}<DropListButton>true</DropListButton>')
if el.get('markIncomplete') is True:
lines.append(f'{inner}<AutoMarkIncomplete>true</AutoMarkIncomplete>')
if el.get('skipOnInput') is True:
lines.append(f'{inner}<SkipOnInput>true</SkipOnInput>')
if el.get('autoMaxWidth') is False:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
if el.get('autoMaxHeight') is False:
lines.append(f'{inner}<AutoMaxHeight>false</AutoMaxHeight>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
lines.append(f'{inner}<Height>{el["height"]}</Height>')
if el.get('horizontalStretch') is True:
lines.append(f'{inner}<HorizontalStretch>true</HorizontalStretch>')
if el.get('verticalStretch') is True:
lines.append(f'{inner}<VerticalStretch>true</VerticalStretch>')
if el.get('inputHint'):
emit_mltext(lines, inner, 'InputHint', str(el['inputHint']))
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'input')
lines.append(f'{indent}</InputField>')
def emit_check(lines, el, name, eid, indent):
lines.append(f'{indent}<CheckBoxField name="{name}" id="{eid}">')
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)
tl = el.get('titleLocation') or 'Right'
lines.append(f'{inner}<TitleLocation>{tl}</TitleLocation>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'check')
lines.append(f'{indent}</CheckBoxField>')
def emit_label(lines, el, name, eid, indent):
lines.append(f'{indent}<LabelDecoration name="{name}" id="{eid}">')
inner = f'{indent}\t'
if el.get('title'):
formatted = 'true' if el.get('hyperlink') is True else 'false'
lines.append(f'{inner}<Title formatted="{formatted}">')
lines.append(f'{inner}\t<v8:item>')
lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(el["title"]))}</v8:content>')
lines.append(f'{inner}\t</v8:item>')
lines.append(f'{inner}</Title>')
emit_common_flags(lines, el, inner)
if el.get('hyperlink') is True:
lines.append(f'{inner}<Hyperlink>true</Hyperlink>')
if el.get('autoMaxWidth') is False:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
if el.get('autoMaxHeight') is False:
lines.append(f'{inner}<AutoMaxHeight>false</AutoMaxHeight>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
lines.append(f'{inner}<Height>{el["height"]}</Height>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'label')
lines.append(f'{indent}</LabelDecoration>')
def emit_label_field(lines, el, name, eid, indent):
lines.append(f'{indent}<LabelField name="{name}" id="{eid}">')
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('hyperlink') is True:
lines.append(f'{inner}<Hyperlink>true</Hyperlink>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'labelField')
lines.append(f'{indent}</LabelField>')
def emit_table(lines, el, name, eid, indent):
lines.append(f'{indent}<Table name="{name}" id="{eid}">')
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('representation'):
lines.append(f'{inner}<Representation>{el["representation"]}</Representation>')
if el.get('changeRowSet') is True:
lines.append(f'{inner}<ChangeRowSet>true</ChangeRowSet>')
if el.get('changeRowOrder') is True:
lines.append(f'{inner}<ChangeRowOrder>true</ChangeRowOrder>')
if el.get('height'):
lines.append(f'{inner}<HeightInTableRows>{el["height"]}</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>')
if el.get('initialTreeView'):
lines.append(f'{inner}<InitialTreeView>{el["initialTreeView"]}</InitialTreeView>')
if el.get('enableStartDrag') is True:
lines.append(f'{inner}<EnableStartDrag>true</EnableStartDrag>')
if el.get('enableDrag') is True:
lines.append(f'{inner}<EnableDrag>true</EnableDrag>')
if el.get('rowPictureDataPath'):
lines.append(f'{inner}<RowPictureDataPath>{el["rowPictureDataPath"]}</RowPictureDataPath>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
# AutoCommandBar — with optional Autofill control
if el.get('tableAutofill') is not None:
acb_id = new_id()
acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
af_val = 'true' if el['tableAutofill'] else 'false'
lines.append(f'{inner}<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, 'SearchStringAddition', f'{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430', inner)
emit_companion(lines, 'ViewStatusAddition', f'{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430', inner)
emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner)
# Columns
if el.get('columns') and len(el['columns']) > 0:
lines.append(f'{inner}<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}">')
inner = f'{indent}\t'
if el.get('pagesRepresentation'):
lines.append(f'{inner}<PagesRepresentation>{el["pagesRepresentation"]}</PagesRepresentation>')
emit_common_flags(lines, el, inner)
# Companion
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'pages')
# Children (pages)
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<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}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_common_flags(lines, el, inner)
if el.get('group'):
orientation_map = {
'horizontal': 'Horizontal',
'vertical': 'Vertical',
'alwaysHorizontal': 'AlwaysHorizontal',
'alwaysVertical': 'AlwaysVertical',
}
orientation = orientation_map.get(str(el['group']))
if orientation:
lines.append(f'{inner}<Group>{orientation}</Group>')
# Companion
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
# Children
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<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):
lines.append(f'{indent}<Button name="{name}" id="{eid}">')
inner = f'{indent}\t'
# Type
if el.get('type'):
btn_type_map = {'usual': 'UsualButton', 'hyperlink': 'Hyperlink', 'commandBar': 'CommandBarButton'}
btn_type = btn_type_map.get(str(el['type']), str(el['type']))
lines.append(f'{inner}<Type>{btn_type}</Type>')
# CommandName
if el.get('command'):
lines.append(f'{inner}<CommandName>Form.Command.{el["command"]}</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>')
emit_title(lines, el, name, inner)
emit_common_flags(lines, el, inner)
if el.get('defaultButton') is True:
lines.append(f'{inner}<DefaultButton>true</DefaultButton>')
# Picture
if el.get('picture'):
lines.append(f'{inner}<Picture>')
lines.append(f'{inner}\t<xr:Ref>{el["picture"]}</xr:Ref>')
lines.append(f'{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>')
lines.append(f'{inner}</Picture>')
if el.get('representation'):
lines.append(f'{inner}<Representation>{el["representation"]}</Representation>')
if el.get('locationInCommandBar'):
lines.append(f'{inner}<LocationInCommandBar>{el["locationInCommandBar"]}</LocationInCommandBar>')
# Companion
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'button')
lines.append(f'{indent}</Button>')
def emit_picture_decoration(lines, el, name, eid, indent):
lines.append(f'{indent}<PictureDecoration name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_common_flags(lines, el, inner)
if el.get('picture') or el.get('src'):
ref = str(el.get('src') or el.get('picture'))
lines.append(f'{inner}<Picture>')
lines.append(f'{inner}\t<xr:Ref>{ref}</xr:Ref>')
lines.append(f'{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>')
lines.append(f'{inner}</Picture>')
if el.get('hyperlink') is True:
lines.append(f'{inner}<Hyperlink>true</Hyperlink>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
lines.append(f'{inner}<Height>{el["height"]}</Height>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'picture')
lines.append(f'{indent}</PictureDecoration>')
def emit_picture_field(lines, el, name, eid, indent):
lines.append(f'{indent}<PictureField name="{name}" id="{eid}">')
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('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
lines.append(f'{inner}<Height>{el["height"]}</Height>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'picField')
lines.append(f'{indent}</PictureField>')
def emit_calendar(lines, el, name, eid, indent):
lines.append(f'{indent}<CalendarField name="{name}" id="{eid}">')
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)
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
emit_events(lines, el, name, inner, 'calendar')
lines.append(f'{indent}</CalendarField>')
def emit_command_bar(lines, el, name, eid, indent):
lines.append(f'{indent}<CommandBar name="{name}" id="{eid}">')
inner = f'{indent}\t'
if el.get('autofill') is True:
lines.append(f'{inner}<Autofill>true</Autofill>')
emit_common_flags(lines, el, inner)
# 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}</CommandBar>')
def emit_popup(lines, el, name, eid, indent):
lines.append(f'{indent}<Popup name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_common_flags(lines, el, inner)
if el.get('picture'):
lines.append(f'{inner}<Picture>')
lines.append(f'{inner}\t<xr:Ref>{el["picture"]}</xr:Ref>')
lines.append(f'{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>')
lines.append(f'{inner}</Picture>')
if el.get('representation'):
lines.append(f'{inner}<Representation>{el["representation"]}</Representation>')
# 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}</Popup>')
# --- Attribute emitter ---
def emit_attributes(lines, attrs, indent):
if not attrs or len(attrs) == 0:
return
lines.append(f'{indent}<Attributes>')
for attr in attrs:
attr_id = new_id()
attr_name = str(attr['name'])
lines.append(f'{indent}\t<Attribute name="{attr_name}" id="{attr_id}">')
inner = f'{indent}\t\t'
if attr.get('title'):
emit_mltext(lines, inner, 'Title', str(attr['title']))
# Type
if attr.get('type'):
emit_type(lines, str(attr['type']), inner)
else:
lines.append(f'{inner}<Type/>')
if attr.get('main') is True:
lines.append(f'{inner}<MainAttribute>true</MainAttribute>')
main_saved = False
if attr.get('main') is True and attr.get('type'):
t = str(attr['type'])
main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t)
if attr.get('savedData') is True or main_saved:
lines.append(f'{inner}<SavedData>true</SavedData>')
if attr.get('fillChecking'):
lines.append(f'{inner}<FillChecking>{attr["fillChecking"]}</FillChecking>')
# Columns (for ValueTable/ValueTree)
if attr.get('columns') and len(attr['columns']) > 0:
lines.append(f'{inner}<Columns>')
for col in attr['columns']:
col_id = new_id()
lines.append(f'{inner}\t<Column name="{col["name"]}" id="{col_id}">')
if col.get('title'):
emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title']))
emit_type(lines, str(col.get('type', '')), f'{inner}\t\t')
lines.append(f'{inner}\t</Column>')
lines.append(f'{inner}</Columns>')
# Settings (for DynamicList)
if attr.get('settings'):
s = attr['settings']
lines.append(f'{inner}<Settings xsi:type="DynamicList">')
si = f'{inner}\t'
if s.get('mainTable'):
lines.append(f'{si}<MainTable>{s["mainTable"]}</MainTable>')
mq = 'true' if s.get('manualQuery') else 'false'
lines.append(f'{si}<ManualQuery>{mq}</ManualQuery>')
ddr = 'true' if s.get('dynamicDataRead') else 'false'
lines.append(f'{si}<DynamicDataRead>{ddr}</DynamicDataRead>')
lines.append(f'{inner}</Settings>')
lines.append(f'{indent}\t</Attribute>')
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>')
for param in params:
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>')
for cmd in cmds:
cmd_id = new_id()
lines.append(f'{indent}\t<Command name="{cmd["name"]}" id="{cmd_id}">')
inner = f'{indent}\t\t'
if cmd.get('title'):
emit_mltext(lines, inner, 'Title', str(cmd['title']))
if cmd.get('action'):
lines.append(f'{inner}<Action>{cmd["action"]}</Action>')
if cmd.get('shortcut'):
lines.append(f'{inner}<Shortcut>{cmd["shortcut"]}</Shortcut>')
if cmd.get('picture'):
lines.append(f'{inner}<Picture>')
lines.append(f'{inner}\t<xr:Ref>{cmd["picture"]}</xr:Ref>')
lines.append(f'{inner}\t<xr:LoadTransparent>true</xr:LoadTransparent>')
lines.append(f'{inner}</Picture>')
if cmd.get('representation'):
lines.append(f'{inner}<Representation>{cmd["representation"]}</Representation>')
lines.append(f'{indent}\t</Command>')
lines.append(f'{indent}</Commands>')
# --- Properties emitter ---
PROP_MAP = {
"autoTitle": "AutoTitle",
"windowOpeningMode": "WindowOpeningMode",
"commandBarLocation": "CommandBarLocation",
"saveDataInSettings": "SaveDataInSettings",
"autoSaveDataInSettings": "AutoSaveDataInSettings",
"autoTime": "AutoTime",
"usePostingMode": "UsePostingMode",
"repostOnWrite": "RepostOnWrite",
"autoURL": "AutoURL",
"autoFillCheck": "AutoFillCheck",
"customizable": "Customizable",
"enterKeyBehavior": "EnterKeyBehavior",
"verticalScroll": "VerticalScroll",
"scalingMode": "ScalingMode",
"useForFoldersAndItems": "UseForFoldersAndItems",
"reportResult": "ReportResult",
"detailsData": "DetailsData",
"reportFormType": "ReportFormType",
"autoShowState": "AutoShowState",
"width": "Width",
"height": "Height",
"group": "Group",
}
def emit_properties(lines, props, indent):
if not props:
return
for p_name, p_value in props.items():
xml_name = PROP_MAP.get(p_name)
if not xml_name:
# Auto PascalCase
xml_name = p_name[0].upper() + p_name[1:]
# Convert boolean to lowercase
if isinstance(p_value, bool):
val = 'true' if p_value else 'false'
else:
val = str(p_value)
lines.append(f'{indent}<{xml_name}>{val}</{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)
# --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
def _normalize_synonyms(el):
if not isinstance(el, dict):
return
synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'}
for src, dst in synonyms.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
if isinstance(el.get('children'), list):
for child in el['children']:
_normalize_synonyms(child)
if isinstance(el.get('columns'), list):
for child in el['columns']:
_normalize_synonyms(child)
def _has_cmd_bar_recursive(el):
if not isinstance(el, dict):
return False
if el.get('cmdBar') is not None:
return True
if isinstance(el.get('children'), list):
for child in el['children']:
if _has_cmd_bar_recursive(child):
return True
if isinstance(el.get('columns'), list):
for child in el['columns']:
if _has_cmd_bar_recursive(child):
return True
return False
def _apply_dlist_table_heuristic(el, list_name):
if not isinstance(el, dict):
return
if el.get('table') is not None and str(el.get('path', '')) == list_name:
if 'tableAutofill' not in el:
el['tableAutofill'] = False
if 'commandBarLocation' not in el:
el['commandBarLocation'] = 'None'
if isinstance(el.get('children'), list):
for child in el['children']:
_apply_dlist_table_heuristic(child, list_name)
def _is_object_like_type(t):
if not t:
return False
if t == 'DynamicList' or t == 'ConstantsSet':
return True
object_suffixes = (
'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject',
'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject',
'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject',
'ChartOfCalculationTypesObject', 'ExchangePlanObject',
)
record_set_prefixes = (
'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet',
'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet',
'InformationRegisterRecordManager',
)
for s in object_suffixes:
if t.startswith(s + '.'):
return True
for s in record_set_prefixes:
if t.startswith(s + '.'):
return True
return False
# 1b.1: Normalize synonyms recursively
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
_normalize_synonyms(el)
# 1b.2: Extract autoCmdBar element from defn['elements']
main_acb_def = None
if isinstance(defn.get('elements'), list):
auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None]
if len(auto_bars) > 1:
print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr)
sys.exit(1)
if len(auto_bars) == 1:
main_acb_def = auto_bars[0]
defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def]
# 1b.3: Infer main attribute
if isinstance(defn.get('attributes'), list):
has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict))
if not has_explicit_main:
candidates = []
for a in defn['attributes']:
if not isinstance(a, dict):
continue
if 'main' in a and a.get('main') is False:
continue
if _is_object_like_type(str(a.get('type', ''))):
candidates.append(a)
if len(candidates) == 1:
candidates[0]['main'] = True
print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})")
elif len(candidates) > 1:
names = ', '.join(c.get('name', '') for c in candidates)
print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly")
# 1b.4: DynamicList → table heuristic
if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list):
main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None)
if main_attr and str(main_attr.get('type', '')) == 'DynamicList':
for el in defn['elements']:
_apply_dlist_table_heuristic(el, main_attr.get('name', ''))
# 1b.5: Compute main AutoCommandBar Autofill (B3)
def _compute_main_acb_autofill():
if main_acb_def is not None:
if 'autofill' in main_acb_def:
return bool(main_acb_def.get('autofill'))
return True
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
if _has_cmd_bar_recursive(el):
return False
return True
# --- 2. Main compilation ---
_next_id = 0
lines = []
lines.append('<?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', str(form_title))
# Properties (skip 'title' — handled above)
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
props_src = defn.get('properties') or {}
props_clone = OrderedDict()
if form_title and 'autoTitle' not in props_src:
props_clone['autoTitle'] = False
for k, v in props_src.items():
if k != 'title':
props_clone[k] = v
emit_properties(lines, props_clone, '\t')
# CommandSet (excluded commands)
if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0:
lines.append('\t<CommandSet>')
for cmd in defn['excludedCommands']:
lines.append(f'\t\t<ExcludedCommand>{cmd}</ExcludedCommand>')
lines.append('\t</CommandSet>')
# AutoCommandBar (always present, id=-1)
acb_autofill = _compute_main_acb_autofill()
acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
acb_halign = None
if main_acb_def is not None:
v = main_acb_def.get('autoCmdBar')
if v:
acb_name = str(v)
if main_acb_def.get('name'):
acb_name = str(main_acb_def['name'])
if main_acb_def.get('horizontalAlign'):
acb_halign = str(main_acb_def['horizontalAlign'])
has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0)
has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children
if has_inner:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">')
if acb_halign:
lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>')
if not acb_autofill:
lines.append('\t\t<Autofill>false</Autofill>')
if has_acb_children:
lines.append('\t\t<ChildItems>')
for child in main_acb_def['children']:
emit_element(lines, child, '\t\t\t')
lines.append('\t\t</ChildItems>')
lines.append('\t</AutoCommandBar>')
else:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>')
# Events
if defn.get('events'):
for evt_name in defn['events']:
if evt_name not in KNOWN_FORM_EVENTS:
print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}")
lines.append('\t<Events>')
for evt_name, evt_handler in defn['events'].items():
lines.append(f'\t\t<Event name="{evt_name}">{evt_handler}</Event>')
lines.append('\t</Events>')
# ChildItems (elements)
if defn.get('elements') and len(defn['elements']) > 0:
lines.append('\t<ChildItems>')
for el in defn['elements']:
emit_element(lines, el, '\t\t')
lines.append('\t</ChildItems>')
# Attributes
emit_attributes(lines, defn.get('attributes'), '\t')
# Parameters
emit_parameters(lines, defn.get('parameters'), '\t')
# Commands
emit_commands(lines, defn.get('commands'), '\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()