Files
cc-1c-skills/.claude/skills/meta-compile/scripts/meta-compile.py
T
Nick ShirokovandClaude Opus 4.8 90a05a58e2 feat(meta-compile,meta-decompile): «Только обороты» как токен-признак субконто (v1.39/v0.28)
«Только обороты» (<Turnover>) — предопределённый (встроенный) признак учёта субконто. Теперь он
выражается токеном `Turnover` (синонимы ТолькоОбороты/«Только обороты») в том же списке признаков,
что и добавленные: "Номенклатура | Turnover, Суммовой". Строковая форма стала полностью
самодостаточной — объектная {type, turnover?, flags?} остаётся принимаемым эквивалентом.

Компилятор вынимает токен turnover из списка flags (регистронезависимо) → <Turnover>true.
Декомпилятор всегда пишет строкой, ставя `Turnover` первым при Turnover=true (объектную форму
больше не порождает). Роундтрип 3/3 match; обе формы + смешанный массив проверены, ps1↔py identical.
Регресс 47/47 ps1+py. spec §7.2c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:30:58 +03:00

3938 lines
194 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# meta-compile v1.39 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import uuid
import xml.etree.ElementTree as ET
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
# ---------------------------------------------------------------------------
# Inline utilities
# ---------------------------------------------------------------------------
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw).
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
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)
# ---------------------------------------------------------------------------
# XML builder (lines list)
# ---------------------------------------------------------------------------
lines = []
def X(text):
lines.append(text)
# ML-значение: строка → один <v8:item> ru; dict {lang: content} → item на язык (в порядке ключей).
def emit_ml_items(indent, val):
if isinstance(val, dict):
for k, v in val.items():
X(f'{indent}<v8:item>')
X(f'{indent}\t<v8:lang>{k}</v8:lang>')
X(f'{indent}\t<v8:content>{esc_xml_text(str(v))}</v8:content>')
X(f'{indent}</v8:item>')
else:
X(f'{indent}<v8:item>')
X(f'{indent}\t<v8:lang>ru</v8:lang>')
X(f'{indent}\t<v8:content>{esc_xml_text(str(val))}</v8:content>')
X(f'{indent}</v8:item>')
def emit_mltext(indent, tag, text):
# Пусто (None / '') → самозакрывающийся тег.
if text is None or (isinstance(text, str) and text == ''):
X(f'{indent}<{tag}/>')
return
X(f'{indent}<{tag}>')
emit_ml_items(f'{indent}\t', text)
X(f'{indent}</{tag}>')
# ---------------------------------------------------------------------------
# CamelCase splitter
# ---------------------------------------------------------------------------
def split_camel_case(name):
if not name:
return name
result = re.sub(r'([а-яё])([А-ЯЁ])', r'\1 \2', name)
result = re.sub(r'([a-z])([A-Z])', r'\1 \2', result)
if len(result) > 1:
result = result[0] + result[1:].lower()
return result
# ---------------------------------------------------------------------------
# 1. Load and validate JSON
# ---------------------------------------------------------------------------
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument('-JsonPath', required=True)
parser.add_argument('-OutputDir', required=True)
args = parser.parse_args()
json_path = args.JsonPath
output_dir = args.OutputDir
if not os.path.isfile(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:
json_text = f.read()
defn = json.loads(json_text)
assert_edit_allowed(output_dir, "editable")
# --- Batch mode: JSON array of objects ---
if isinstance(defn, list):
batch_ok = 0
batch_fail = 0
for idx, item in enumerate(defn, 1):
tmp_fd, tmp_path = tempfile.mkstemp(suffix='.json', prefix=f'meta-compile-batch-{idx}-')
try:
with os.fdopen(tmp_fd, 'w', encoding='utf-8') as f:
json.dump(item, f, ensure_ascii=False, indent=2)
rc = subprocess.call([sys.executable, __file__, '-JsonPath', tmp_path, '-OutputDir', output_dir])
if rc == 0:
batch_ok += 1
else:
batch_fail += 1
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
print()
print(f"=== Batch: {len(defn)} objects, {batch_ok} compiled, {batch_fail} failed ===")
sys.exit(1 if batch_fail > 0 else 0)
# Normalize field synonyms: accept "objectType" as alias for "type"
if not defn.get('type') and defn.get('objectType'):
defn['type'] = defn['objectType']
# Object type synonyms (Russian -> English)
object_type_synonyms = {
'Справочник': 'Catalog',
'Каталог': 'Catalog',
'Документ': 'Document',
'Перечисление': 'Enum',
'Константа': 'Constant',
'РегистрСведений': 'InformationRegister',
'РегистрНакопления': 'AccumulationRegister',
'РегистрБухгалтерии': 'AccountingRegister',
'РегистрРасчёта': 'CalculationRegister',
'РегистрРасчета': 'CalculationRegister',
'ПланСчетов': 'ChartOfAccounts',
'ПланВидовХарактеристик': 'ChartOfCharacteristicTypes',
'ПланВидовРасчёта': 'ChartOfCalculationTypes',
'ПланВидовРасчета': 'ChartOfCalculationTypes',
'БизнесПроцесс': 'BusinessProcess',
'Задача': 'Task',
'ПланОбмена': 'ExchangePlan',
'ЖурналДокументов': 'DocumentJournal',
'Отчёт': 'Report',
'Отчет': 'Report',
'Обработка': 'DataProcessor',
'ОбщийМодуль': 'CommonModule',
'РегламентноеЗадание': 'ScheduledJob',
'ПодпискаНаСобытие': 'EventSubscription',
'HTTPСервис': 'HTTPService',
'ВебСервис': 'WebService',
'ОпределяемыйТип': 'DefinedType',
}
# Enum property value synonyms — model often gets these slightly wrong
enum_value_aliases = {
# RegisterType (AccumulationRegister)
'Balances': 'Balance', 'Остатки': 'Balance', 'Обороты': 'Turnovers',
# WriteMode (InformationRegister)
'RecordSubordinate': 'RecorderSubordinate', 'Subordinate': 'RecorderSubordinate',
'ПодчинениеРегистратору': 'RecorderSubordinate', 'Независимый': 'Independent',
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse', 'NotUsed': 'DontUse',
'Depend': 'OnActionPeriod', 'ПоПериодуДействия': 'OnActionPeriod',
# InformationRegisterPeriodicity
'None': 'Nonperiodical', 'Daily': 'Day', 'Monthly': 'Month',
'Quarterly': 'Quarter', 'Yearly': 'Year',
'Непериодический': 'Nonperiodical', 'Секунда': 'Second', 'День': 'Day',
'Месяц': 'Month', 'Квартал': 'Quarter', 'Год': 'Year',
'ПозицияРегистратора': 'RecorderPosition',
# DataLockControlMode
'Автоматический': 'Automatic', 'Управляемый': 'Managed',
# FullTextSearch
'Использовать': 'Use', 'НеИспользовать': 'DontUse',
# Posting
'Разрешить': 'Allow', 'Запретить': 'Deny',
# EditType
'ВДиалоге': 'InDialog', 'ВСписке': 'InList', 'ОбаСпособа': 'BothWays',
# DefaultPresentation
'ВВидеНаименования': 'AsDescription', 'ВВидеКода': 'AsCode',
# FillChecking
'НеПроверять': 'DontCheck', 'Ошибка': 'ShowError', 'Предупреждение': 'ShowWarning',
# Indexing
'НеИндексировать': 'DontIndex', 'Индексировать': 'Index',
'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder',
}
# Valid enum values per property (from meta-validate)
valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'],
'WriteMode': ['Independent', 'RecorderSubordinate'],
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
'DataLockControlMode': ['Automatic', 'Managed'],
'FullTextSearch': ['Use', 'DontUse'],
'DataHistory': ['Use', 'DontUse'],
'DefaultPresentation': ['AsDescription', 'AsCode'],
'Posting': ['Allow', 'Deny'],
'RealTimePosting': ['Allow', 'Deny'],
'EditType': ['InDialog', 'InList', 'BothWays'],
'HierarchyType': ['HierarchyFoldersAndItems', 'HierarchyOfItems'],
'CodeType': ['String', 'Number'],
'CodeAllowedLength': ['Variable', 'Fixed'],
'NumberType': ['String', 'Number'],
'NumberAllowedLength': ['Variable', 'Fixed'],
'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'],
'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'],
'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'],
'ReuseSessions': ['DontUse', 'AutoUse'],
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
'SubordinationUse': ['ToItems', 'ToFolders', 'ToFoldersAndItems'],
'CodeSeries': ['WholeCatalog', 'WithinSubordination', 'WithinOwnerSubordination', 'WholeCharacteristicKind', 'WholeChartOfAccounts'],
'ChoiceMode': ['BothWays', 'QuickChoice', 'FromForm'],
'CreateOnInput': ['Auto', 'Use', 'DontUse'],
'ChoiceHistoryOnInput': ['Auto', 'DontUse'],
'PredefinedDataUpdate': ['Auto', 'DontAutoUpdate', 'AutoUpdate'],
'SearchStringModeOnInputByString': ['Begin', 'AnyPart'],
}
def normalize_enum_value(prop_name, value):
# 1. Check alias dictionary — silent auto-correct
if value in enum_value_aliases:
return enum_value_aliases[value]
# 2. Case-insensitive match against valid values — silent
valid = valid_enum_values.get(prop_name)
if valid:
for v in valid:
if v.lower() == value.lower():
return v
# 3. Known property, unknown value — error with hint
print(f"Invalid value '{value}' for property '{prop_name}'. Valid values: {', '.join(valid)}", file=sys.stderr)
sys.exit(1)
# 4. Unknown property — pass-through (no validation data)
return value
def get_enum_prop(prop_name, field_name, default):
val = defn.get(field_name)
raw = str(val) if val else default
return normalize_enum_value(prop_name, raw)
def get_bool_prop(field_name, default):
"""Bool object-свойство: presence-aware (иначе false спутать с отсутствием). Прощаем строки."""
val = defn.get(field_name)
if val is None:
return default
if isinstance(val, bool):
return val
return str(val).lower() in ('true', '1', 'да', 'истина')
def normalize_form_ref(s):
"""Прощающая нормализация ссылки на форму: рус корень, Форма→Form, короткая запись → вставка Form."""
if not s:
return s
parts = s.split('.')
if len(parts) < 3:
return s
root = fill_ref_roots.get(parts[0].lower())
if root:
parts[0] = root
for k in range(1, len(parts)):
if parts[k].lower() == 'форма':
parts[k] = 'Form'
if 'Form' not in parts and len(parts) == 3:
parts = [parts[0], parts[1], 'Form', parts[2]]
return '.'.join(parts)
def emit_form_ref(i, tag, val):
"""Ссылка на форму по умолчанию: непустая → <Tag>значение</Tag>, иначе <Tag/>."""
if val:
X(f'{i}<{tag}>{esc_xml(normalize_form_ref(str(val)))}</{tag}>')
else:
X(f'{i}<{tag}/>')
if not defn.get('type'):
print("JSON must have 'type' field", file=sys.stderr)
sys.exit(1)
obj_type = str(defn['type'])
if obj_type in object_type_synonyms:
obj_type = object_type_synonyms[obj_type]
valid_types = [
'Catalog', 'Document', 'Enum', 'Constant', 'InformationRegister',
'AccumulationRegister', 'AccountingRegister', 'CalculationRegister',
'ChartOfAccounts', 'ChartOfCharacteristicTypes', 'ChartOfCalculationTypes',
'BusinessProcess', 'Task', 'ExchangePlan', 'DocumentJournal',
'Report', 'DataProcessor', 'CommonModule', 'ScheduledJob',
'EventSubscription', 'HTTPService', 'WebService', 'DefinedType',
]
if obj_type not in valid_types:
print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr)
sys.exit(1)
if not defn.get('name'):
print("JSON must have 'name' field", file=sys.stderr)
sys.exit(1)
obj_name = str(defn['name'])
# Auto-synonym
# Проброс без стрингификации (строка ИЛИ {ru,en} — мультиязычный синоним объекта).
synonym = defn['synonym'] if defn.get('synonym') is not None else split_camel_case(obj_name)
comment = str(defn['comment']) if defn.get('comment') else ''
# ---------------------------------------------------------------------------
# 4. Type system
# ---------------------------------------------------------------------------
type_synonyms = {
'число': 'Number',
'строка': 'String',
'булево': 'Boolean',
'дата': 'Date',
'датавремя': 'DateTime',
'время': 'Time',
'time': 'Time',
'number': 'Number',
'string': 'String',
'boolean': 'Boolean',
'date': 'Date',
'datetime': 'DateTime',
'bool': 'Boolean',
# ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон).
'valuestorage': 'ValueStorage',
'base64binary': 'ValueStorage',
'хранилищезначений': 'ValueStorage',
'хранилищезначения': 'ValueStorage',
'uuid': 'UUID',
'уникальныйидентификатор': 'UUID',
# Reference synonyms (Russian, lowercase)
'справочникссылка': 'CatalogRef',
'документссылка': 'DocumentRef',
'перечислениессылка': 'EnumRef',
'плансчетовссылка': 'ChartOfAccountsRef',
'планвидовхарактеристикссылка': 'ChartOfCharacteristicTypesRef',
'планвидоврасчётассылка': 'ChartOfCalculationTypesRef',
'планвидоврасчетассылка': 'ChartOfCalculationTypesRef',
'планобменассылка': 'ExchangePlanRef',
'бизнеспроцессссылка': 'BusinessProcessRef',
'задачассылка': 'TaskRef',
'определяемыйтип': 'DefinedType',
'definedtype': 'DefinedType',
# English lowercase ref synonyms
'catalogref': 'CatalogRef',
'documentref': 'DocumentRef',
'enumref': 'EnumRef',
}
def resolve_type_str(type_str):
if not type_str:
return type_str
# Parameterized types: Number(15,2), Строка(100), etc.
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base_name = m.group(1).strip()
params = m.group(2)
resolved = type_synonyms.get(base_name.lower())
if resolved:
return f'{resolved}({params})'
return type_str
# Reference types: СправочникСсылка.Организации -> CatalogRef.Организации
if '.' in type_str:
dot_idx = type_str.index('.')
prefix = type_str[:dot_idx]
suffix = type_str[dot_idx:] # includes the dot
resolved = type_synonyms.get(prefix.lower())
if resolved:
return f'{resolved}{suffix}'
return type_str
# Simple name lookup
resolved = type_synonyms.get(type_str.lower())
if resolved:
return resolved
return type_str
def emit_type_content(indent, type_str):
if not type_str:
return
# Composite type: "Type1 + Type2 + Type3"
if ' + ' in type_str:
parts = [p.strip() for p in type_str.split('+')]
for part in parts:
emit_type_content(indent, part)
return
type_str = resolve_type_str(type_str)
# Boolean
if type_str == 'Boolean':
X(f'{indent}<v8:Type>xs:boolean</v8:Type>')
return
# String or String(N) or String(N,fixed|variable) — AllowedLength: Variable дефолт / Fixed (фикс. длина).
m = re.match(r'^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else '10'
al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable'
X(f'{indent}<v8:Type>xs:string</v8:Type>')
X(f'{indent}<v8:StringQualifiers>')
X(f'{indent}\t<v8:Length>{length}</v8:Length>')
X(f'{indent}\t<v8:AllowedLength>{al}</v8:AllowedLength>')
X(f'{indent}</v8:StringQualifiers>')
return
# Number without params -> Number(10,0)
if type_str == 'Number':
X(f'{indent}<v8:Type>xs:decimal</v8:Type>')
X(f'{indent}<v8:NumberQualifiers>')
X(f'{indent}\t<v8:Digits>10</v8:Digits>')
X(f'{indent}\t<v8:FractionDigits>0</v8:FractionDigits>')
X(f'{indent}\t<v8:AllowedSign>Any</v8:AllowedSign>')
X(f'{indent}</v8:NumberQualifiers>')
return
# Number(D,F) or Number(D,F,nonneg)
m = re.match(r'^Number\((\d+),(\d+)(,nonneg)?\)$', type_str)
if m:
digits = m.group(1)
fraction = m.group(2)
sign = 'Nonnegative' if m.group(3) else 'Any'
X(f'{indent}<v8:Type>xs:decimal</v8:Type>')
X(f'{indent}<v8:NumberQualifiers>')
X(f'{indent}\t<v8:Digits>{digits}</v8:Digits>')
X(f'{indent}\t<v8:FractionDigits>{fraction}</v8:FractionDigits>')
X(f'{indent}\t<v8:AllowedSign>{sign}</v8:AllowedSign>')
X(f'{indent}</v8:NumberQualifiers>')
return
# Date / DateTime / Time — общая структура xs:dateTime + DateFractions (различаются лишь составом).
if type_str in ('Date', 'DateTime', 'Time'):
X(f'{indent}<v8:Type>xs:dateTime</v8:Type>')
X(f'{indent}<v8:DateQualifiers>')
X(f'{indent}\t<v8:DateFractions>{type_str}</v8:DateFractions>')
X(f'{indent}</v8:DateQualifiers>')
return
# TypeSet — тип-множество: ОпределяемыйТип (DefinedType) ИЛИ Характеристика ПВХ (Characteristic).
if re.match(r'^(DefinedType|Characteristic)\.(.+)$', type_str):
X(f'{indent}<v8:TypeSet>cfg:{type_str}</v8:TypeSet>')
return
# Голый метатип-категория (CatalogRef/DocumentRef/…/AnyRef/AnyIBRef без имени объекта) — множество
# «любой объект категории» → TypeSet (а не конкретный Type с именем).
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$', type_str):
X(f'{indent}<v8:TypeSet>cfg:{type_str}</v8:TypeSet>')
return
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary).
if type_str == 'ValueStorage':
X(f'{indent}<v8:Type>v8:ValueStorage</v8:Type>')
return
# UUID (УникальныйИдентификатор)
if type_str == 'UUID':
X(f'{indent}<v8:Type>v8:UUID</v8:Type>')
return
# Reference types — use local xmlns declaration for 1C compatibility
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$', type_str)
if m:
X(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{type_str}</v8:Type>')
return
# Fallback
X(f'{indent}<v8:Type>{type_str}</v8:Type>')
def emit_value_type(indent, type_str):
X(f'{indent}<Type>')
emit_type_content(f'{indent}\t', type_str)
X(f'{indent}</Type>')
# --- FillValue (значение заполнения реквизита) ---
# Пара FillFromFillingValue+FillValue — единый блок «заполнения» (недоступен у реквизитов ТЧ).
# Форма пустого FillValue зависит от типа реквизита (то же значение по умолчанию, что и «пустое»
# значение типа): String→typed-empty, Number→0, всё остальное (Boolean/Date/Ref/составной/TypeSet)→nil.
# Реальное значение задаётся ключом `fillValue` (интерпретация по типу реквизита; см. §4.2 spec).
def get_fill_type_category(type_str):
if not type_str:
return 'String' # реквизит без типа → неквалифиц. строка
if '+' in type_str:
return 'Other' # составной тип → nil-дефолт
t = resolve_type_str(type_str)
if re.match(r'^Boolean$', t):
return 'Boolean'
if re.match(r'^String(\(|$)', t):
return 'String'
if re.match(r'^Number(\(|$)', t):
return 'Number'
if re.match(r'^(Date|DateTime)$', t):
return 'Date'
return 'Other' # ссылки, TypeSet, ValueStorage, … → nil-дефолт
# Прощающий ввод для ссылочных путей DTR: рус/англ корни, ПустаяСсылка/EmptyRef, ЗначениеПеречисления/EnumValue.
fill_ref_roots = {
'перечисление': 'Enum', 'справочник': 'Catalog', 'документ': 'Document',
'плансчетов': 'ChartOfAccounts', 'планвидовхарактеристик': 'ChartOfCharacteristicTypes',
'планвидоврасчета': 'ChartOfCalculationTypes', 'планвидоврасчёта': 'ChartOfCalculationTypes',
'планобмена': 'ExchangePlan', 'бизнеспроцесс': 'BusinessProcess', 'задача': 'Task',
'enum': 'Enum', 'catalog': 'Catalog', 'document': 'Document', 'chartofaccounts': 'ChartOfAccounts',
'chartofcharacteristictypes': 'ChartOfCharacteristicTypes', 'chartofcalculationtypes': 'ChartOfCalculationTypes',
'exchangeplan': 'ExchangePlan', 'businessprocess': 'BusinessProcess', 'task': 'Task',
}
fill_empty_ref_words = ('emptyref', 'пустаяссылка')
fill_enum_val_words = ('enumvalue', 'значениеперечисления')
fill_bool_true = ('true', 'истина', 'да')
fill_bool_false = ('false', 'ложь', 'нет')
# Значения платформенного перечисления ВидСчета (ent:AccountType) — FillValue реквизита Тип у Плана счетов.
ACCOUNT_TYPE_VALUES = ('Active', 'Passive', 'ActivePassive')
# «Только обороты» (<Turnover>) — предопределённый признак учёта субконто; токен в списке flags наравне с добавленными.
SUBCONTO_TURNOVER_TOKENS = ('turnover', 'толькообороты', 'только обороты', 'оборотный')
# XxxRef (тип реквизита) → корень DTR-пути (для разворота короткой записи значения).
fill_ref_kind_root = {
'catalogref': 'Catalog', 'documentref': 'Document', 'enumref': 'Enum',
'chartofaccountsref': 'ChartOfAccounts', 'chartofcharacteristictypesref': 'ChartOfCharacteristicTypes',
'chartofcalculationtypesref': 'ChartOfCalculationTypes', 'exchangeplanref': 'ExchangePlan',
'businessprocessref': 'BusinessProcess', 'taskref': 'Task',
}
def normalize_fill_ref(s):
"""Строка → нормализованный DTR-путь ЛИБО None (не ссылка)."""
if not s:
return None
if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$', s):
return s
parts = s.split('.')
if len(parts) < 2:
return None
root = fill_ref_roots.get(parts[0].lower())
if not root:
return None
type_name = parts[1]
if root == 'Enum':
if len(parts) == 2:
return None
if len(parts) == 3:
if parts[2].lower() in fill_empty_ref_words:
return f'Enum.{type_name}.EmptyRef'
return f'Enum.{type_name}.EnumValue.{parts[2]}'
if parts[2].lower() in fill_enum_val_words:
rest = '.'.join(parts[3:])
else:
rest = '.'.join(parts[2:])
return f'Enum.{type_name}.EnumValue.{rest}'
tail = list(parts[1:])
for i in range(len(tail)):
if tail[i].lower() in fill_empty_ref_words:
tail[i] = 'EmptyRef'
return f'{root}.' + '.'.join(tail)
def expand_fill_short_ref(s, type_str):
"""Короткая запись значения ссылочного реквизита (без точки) → полный DTR-путь по типу, либо None."""
if not type_str:
return None
if '+' in type_str: # составной тип — короткая форма неоднозначна
return None
t = resolve_type_str(type_str)
m = re.match(r'^(\w+Ref)\.(.+)$', t)
if not m:
return None
root = fill_ref_kind_root.get(m.group(1).lower())
if not root:
return None
type_name = m.group(2)
if s.lower() in fill_empty_ref_words:
return f'{root}.{type_name}.EmptyRef'
if root == 'Enum':
return f'Enum.{type_name}.EnumValue.{s}'
return f'{root}.{type_name}.{s}'
def resolve_fill_value_spec(s, type_str):
"""Строковый spec → (xsi_type, text). Интерпретация по типу реквизита."""
cat = get_fill_type_category(type_str)
if s == '':
return ('xs:string', '')
if cat == 'String':
return ('xs:string', s)
if cat == 'Boolean' or s.lower() in fill_bool_true or s.lower() in fill_bool_false:
if s.lower() in fill_bool_true:
return ('xs:boolean', 'true')
if s.lower() in fill_bool_false:
return ('xs:boolean', 'false')
if cat == 'Number':
return ('xs:decimal', s)
if cat == 'Date' or re.match(r'^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$', s):
if re.match(r'^\d{4}-\d{2}-\d{2}$', s):
s = f'{s}T00:00:00'
return ('xs:dateTime', s)
ref = normalize_fill_ref(s)
if ref:
return ('xr:DesignTimeRef', ref)
short = expand_fill_short_ref(s, type_str)
if short:
return ('xr:DesignTimeRef', short)
return ('xs:string', s)
def format_fill_num(n):
if isinstance(n, bool):
return 'true' if n else 'false'
return str(n)
def emit_fill_value(indent, type_str, spec, has_spec):
"""spec — значение ключа fillValue (None при явном nil-override), has_spec — присутствует ли ключ."""
cat = get_fill_type_category(type_str)
if not has_spec:
if cat == 'String':
X(f'{indent}<FillValue xsi:type="xs:string"/>')
elif cat == 'Number':
X(f'{indent}<FillValue xsi:type="xs:decimal">0</FillValue>')
else:
X(f'{indent}<FillValue xsi:nil="true"/>')
return
if spec is None:
X(f'{indent}<FillValue xsi:nil="true"/>') # явный nil-override
return
if isinstance(spec, bool):
X(f'{indent}<FillValue xsi:type="xs:boolean">{"true" if spec else "false"}</FillValue>')
return
if isinstance(spec, (int, float)):
X(f'{indent}<FillValue xsi:type="xs:decimal">{format_fill_num(spec)}</FillValue>')
return
xsi_type, text = resolve_fill_value_spec(str(spec), type_str)
if text == '' and xsi_type == 'xs:string':
X(f'{indent}<FillValue xsi:type="xs:string"/>')
return
X(f'{indent}<FillValue xsi:type="{xsi_type}">{esc_xml_text(text)}</FillValue>')
# ---------------------------------------------------------------------------
# 5. Attribute shorthand parser
# ---------------------------------------------------------------------------
def build_type_str(obj):
t = str(obj.get('valueType') or obj.get('type') or '')
if t and '(' not in t:
if t == 'String' and obj.get('length'):
t = f"String({obj['length']})"
elif t == 'Number' and obj.get('length'):
prec = obj.get('precision', 0)
nn = ',nonneg' if obj.get('nonneg') or obj.get('nonnegative') else ''
t = f"Number({obj['length']},{prec}{nn})"
return t
def parse_attribute_shorthand(val):
if isinstance(val, str):
parsed = {
'name': '',
'type': '',
'synonym': '',
'comment': '',
'flags': [],
'fillChecking': '',
'indexing': '',
'hasFillValue': False,
'fillValue': None,
}
parts = val.split('|', 1)
main_part = parts[0].strip()
if len(parts) > 1:
flag_str = parts[1].strip()
parsed['flags'] = [f.strip().lower() for f in flag_str.split(',') if f.strip()]
colon_parts = main_part.split(':', 1)
parsed['name'] = colon_parts[0].strip()
if len(colon_parts) > 1:
parsed['type'] = colon_parts[1].strip()
parsed['synonym'] = split_camel_case(parsed['name'])
return parsed
# Object form. synonym/tooltip — сквозной проброс (строка ИЛИ dict {ru,en}), НЕ стрингифаим.
name = str(val.get('name', ''))
# fillCheck — синоним fillChecking (bool true→ShowError). quickChoice — прощаем bool (true→Use, false→DontUse).
if val.get('fillChecking'):
fc = str(val['fillChecking'])
elif val.get('fillCheck') is not None:
fc = ('ShowError' if val['fillCheck'] else '') if isinstance(val['fillCheck'], bool) else str(val['fillCheck'])
else:
fc = ''
if val.get('quickChoice') is not None:
qc = ('Use' if val['quickChoice'] else 'DontUse') if isinstance(val['quickChoice'], bool) else str(val['quickChoice'])
else:
qc = ''
return {
'name': name,
'type': build_type_str(val),
'synonym': val['synonym'] if val.get('synonym') is not None else split_camel_case(name),
'tooltip': val.get('tooltip'),
'comment': str(val['comment']) if val.get('comment') else '',
# Лоуэркейз как в строковом пути (стр.809): проверки флагов регистронезависимы (зеркало PS -contains).
'flags': [str(f).strip().lower() for f in val.get('flags', [])],
'fillChecking': fc,
'indexing': str(val['indexing']) if val.get('indexing') else '',
'multiLine': True if val.get('multiLine') is True else False,
'choiceHistoryOnInput': str(val['choiceHistoryOnInput']) if val.get('choiceHistoryOnInput') else '',
'fullTextSearch': str(val['fullTextSearch']) if val.get('fullTextSearch') else '',
'fillFromFillingValue': True if val.get('fillFromFillingValue') is True else False,
'createOnInput': str(val['createOnInput']) if val.get('createOnInput') else '',
'quickChoice': qc,
'dataHistory': str(val['dataHistory']) if val.get('dataHistory') else '',
'use': str(val['use']) if val.get('use') else '',
'passwordMode': True if val.get('passwordMode') is True else False,
'format': val.get('format'),
'editFormat': val.get('editFormat'),
'mask': str(val['mask']) if val.get('mask') else '',
'extendedEdit': True if val.get('extendedEdit') is True else False,
'minValue': val.get('minValue'),
'maxValue': val.get('maxValue'),
'hasFillValue': ('fillValue' in val),
'fillValue': val.get('fillValue'),
'linkByType': val.get('linkByType'),
'choiceParameterLinks': val.get('choiceParameterLinks'),
'choiceParameters': val.get('choiceParameters'),
}
def parse_enum_value_shorthand(val):
if isinstance(val, str):
return {
'name': val,
'synonym': split_camel_case(val),
'comment': '',
}
name = str(val.get('name', ''))
return {
'name': name,
'synonym': str(val['synonym']) if val.get('synonym') else split_camel_case(name),
'comment': str(val['comment']) if val.get('comment') else '',
}
# ---------------------------------------------------------------------------
# 6. GeneratedType categories
# ---------------------------------------------------------------------------
generated_types = {
'Catalog': [
{'prefix': 'CatalogObject', 'category': 'Object'},
{'prefix': 'CatalogRef', 'category': 'Ref'},
{'prefix': 'CatalogSelection', 'category': 'Selection'},
{'prefix': 'CatalogList', 'category': 'List'},
{'prefix': 'CatalogManager', 'category': 'Manager'},
],
'Document': [
{'prefix': 'DocumentObject', 'category': 'Object'},
{'prefix': 'DocumentRef', 'category': 'Ref'},
{'prefix': 'DocumentSelection', 'category': 'Selection'},
{'prefix': 'DocumentList', 'category': 'List'},
{'prefix': 'DocumentManager', 'category': 'Manager'},
],
'Enum': [
{'prefix': 'EnumRef', 'category': 'Ref'},
{'prefix': 'EnumManager', 'category': 'Manager'},
{'prefix': 'EnumList', 'category': 'List'},
],
'Constant': [
{'prefix': 'ConstantManager', 'category': 'Manager'},
{'prefix': 'ConstantValueManager', 'category': 'ValueManager'},
{'prefix': 'ConstantValueKey', 'category': 'ValueKey'},
],
'InformationRegister': [
{'prefix': 'InformationRegisterRecord', 'category': 'Record'},
{'prefix': 'InformationRegisterManager', 'category': 'Manager'},
{'prefix': 'InformationRegisterSelection', 'category': 'Selection'},
{'prefix': 'InformationRegisterList', 'category': 'List'},
{'prefix': 'InformationRegisterRecordSet', 'category': 'RecordSet'},
{'prefix': 'InformationRegisterRecordKey', 'category': 'RecordKey'},
{'prefix': 'InformationRegisterRecordManager', 'category': 'RecordManager'},
],
'AccumulationRegister': [
{'prefix': 'AccumulationRegisterRecord', 'category': 'Record'},
{'prefix': 'AccumulationRegisterManager', 'category': 'Manager'},
{'prefix': 'AccumulationRegisterSelection', 'category': 'Selection'},
{'prefix': 'AccumulationRegisterList', 'category': 'List'},
{'prefix': 'AccumulationRegisterRecordSet', 'category': 'RecordSet'},
{'prefix': 'AccumulationRegisterRecordKey', 'category': 'RecordKey'},
],
'AccountingRegister': [
{'prefix': 'AccountingRegisterRecord', 'category': 'Record'},
{'prefix': 'AccountingRegisterExtDimensions', 'category': 'ExtDimensions'},
{'prefix': 'AccountingRegisterRecordSet', 'category': 'RecordSet'},
{'prefix': 'AccountingRegisterRecordKey', 'category': 'RecordKey'},
{'prefix': 'AccountingRegisterSelection', 'category': 'Selection'},
{'prefix': 'AccountingRegisterList', 'category': 'List'},
{'prefix': 'AccountingRegisterManager', 'category': 'Manager'},
],
'CalculationRegister': [
{'prefix': 'CalculationRegisterRecord', 'category': 'Record'},
{'prefix': 'CalculationRegisterManager', 'category': 'Manager'},
{'prefix': 'CalculationRegisterSelection', 'category': 'Selection'},
{'prefix': 'CalculationRegisterList', 'category': 'List'},
{'prefix': 'CalculationRegisterRecordSet', 'category': 'RecordSet'},
{'prefix': 'CalculationRegisterRecordKey', 'category': 'RecordKey'},
{'prefix': 'RecalculationsManager', 'category': 'Recalcs'},
],
'ChartOfAccounts': [
{'prefix': 'ChartOfAccountsObject', 'category': 'Object'},
{'prefix': 'ChartOfAccountsRef', 'category': 'Ref'},
{'prefix': 'ChartOfAccountsSelection', 'category': 'Selection'},
{'prefix': 'ChartOfAccountsList', 'category': 'List'},
{'prefix': 'ChartOfAccountsManager', 'category': 'Manager'},
{'prefix': 'ChartOfAccountsExtDimensionTypes', 'category': 'ExtDimensionTypes'},
{'prefix': 'ChartOfAccountsExtDimensionTypesRow', 'category': 'ExtDimensionTypesRow'},
],
'ChartOfCharacteristicTypes': [
{'prefix': 'ChartOfCharacteristicTypesObject', 'category': 'Object'},
{'prefix': 'ChartOfCharacteristicTypesRef', 'category': 'Ref'},
{'prefix': 'ChartOfCharacteristicTypesSelection', 'category': 'Selection'},
{'prefix': 'ChartOfCharacteristicTypesList', 'category': 'List'},
{'prefix': 'Characteristic', 'category': 'Characteristic'},
{'prefix': 'ChartOfCharacteristicTypesManager', 'category': 'Manager'},
],
'ChartOfCalculationTypes': [
{'prefix': 'ChartOfCalculationTypesObject', 'category': 'Object'},
{'prefix': 'ChartOfCalculationTypesRef', 'category': 'Ref'},
{'prefix': 'ChartOfCalculationTypesSelection', 'category': 'Selection'},
{'prefix': 'ChartOfCalculationTypesList', 'category': 'List'},
{'prefix': 'ChartOfCalculationTypesManager', 'category': 'Manager'},
{'prefix': 'DisplacingCalculationTypes', 'category': 'DisplacingCalculationTypes'},
{'prefix': 'DisplacingCalculationTypesRow', 'category': 'DisplacingCalculationTypesRow'},
{'prefix': 'BaseCalculationTypes', 'category': 'BaseCalculationTypes'},
{'prefix': 'BaseCalculationTypesRow', 'category': 'BaseCalculationTypesRow'},
{'prefix': 'LeadingCalculationTypes', 'category': 'LeadingCalculationTypes'},
{'prefix': 'LeadingCalculationTypesRow', 'category': 'LeadingCalculationTypesRow'},
],
'BusinessProcess': [
{'prefix': 'BusinessProcessObject', 'category': 'Object'},
{'prefix': 'BusinessProcessRef', 'category': 'Ref'},
{'prefix': 'BusinessProcessSelection', 'category': 'Selection'},
{'prefix': 'BusinessProcessList', 'category': 'List'},
{'prefix': 'BusinessProcessManager', 'category': 'Manager'},
{'prefix': 'BusinessProcessRoutePointRef', 'category': 'RoutePointRef'},
],
'Task': [
{'prefix': 'TaskObject', 'category': 'Object'},
{'prefix': 'TaskRef', 'category': 'Ref'},
{'prefix': 'TaskSelection', 'category': 'Selection'},
{'prefix': 'TaskList', 'category': 'List'},
{'prefix': 'TaskManager', 'category': 'Manager'},
],
'ExchangePlan': [
{'prefix': 'ExchangePlanObject', 'category': 'Object'},
{'prefix': 'ExchangePlanRef', 'category': 'Ref'},
{'prefix': 'ExchangePlanSelection', 'category': 'Selection'},
{'prefix': 'ExchangePlanList', 'category': 'List'},
{'prefix': 'ExchangePlanManager', 'category': 'Manager'},
],
'DefinedType': [
{'prefix': 'DefinedType', 'category': 'DefinedType'},
],
'DocumentJournal': [
{'prefix': 'DocumentJournalSelection', 'category': 'Selection'},
{'prefix': 'DocumentJournalList', 'category': 'List'},
{'prefix': 'DocumentJournalManager', 'category': 'Manager'},
],
'Report': [
{'prefix': 'ReportObject', 'category': 'Object'},
{'prefix': 'ReportManager', 'category': 'Manager'},
],
'DataProcessor': [
{'prefix': 'DataProcessorObject', 'category': 'Object'},
{'prefix': 'DataProcessorManager', 'category': 'Manager'},
],
}
def emit_internal_info(indent, object_type, object_name):
types = generated_types.get(object_type)
if not types:
return
X(f'{indent}<InternalInfo>')
if object_type == 'ExchangePlan':
X(f'{indent}\t<xr:ThisNode>{new_uuid()}</xr:ThisNode>')
for gt in types:
full_name = f"{gt['prefix']}.{object_name}"
X(f'{indent}\t<xr:GeneratedType name="{full_name}" category="{gt["category"]}">')
X(f'{indent}\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
X(f'{indent}\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
X(f'{indent}\t</xr:GeneratedType>')
X(f'{indent}</InternalInfo>')
# ---------------------------------------------------------------------------
# 7. StandardAttributes
# ---------------------------------------------------------------------------
standard_attributes_by_type = {
'Catalog': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'IsFolder', 'Owner', 'Parent', 'Description', 'Code'],
'Document': ['Posted', 'Ref', 'DeletionMark', 'Date', 'Number'],
'Enum': ['Order', 'Ref'],
'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
'AccumulationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
'AccountingRegister': ['Active', 'Period', 'Recorder', 'LineNumber', 'Account'],
'CalculationRegister': ['Active', 'Recorder', 'LineNumber', 'RegistrationPeriod', 'CalculationType', 'ReversingEntry'],
'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
'ChartOfCharacteristicTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'ValueType'],
'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'ActionPeriodIsBasic'],
'BusinessProcess': ['Ref', 'DeletionMark', 'Date', 'Number', 'Started', 'Completed', 'HeadTask'],
'Task': ['Ref', 'DeletionMark', 'Date', 'Number', 'Executed', 'Description', 'RoutePoint', 'BusinessProcess'],
'ExchangePlan': ['Ref', 'DeletionMark', 'Code', 'Description', 'ThisNode', 'SentNo', 'ReceivedNo'],
'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'],
}
# Профиль материализованного блока StandardAttributes (см. коммент в .ps1). Пока только Catalog.
std_attr_profile = {
'Catalog': {
'Owner': {'FillChecking': 'ShowError', 'FillFromFillingValue': 'true'},
'Parent': {'FillFromFillingValue': 'true'},
'Description': {'FillChecking': 'ShowError'},
},
# ExchangePlan: блок при кастомизации; Наименование/Код → FillChecking=ShowError (корпус 40/38 из 41).
'ExchangePlan': {
'Description': {'FillChecking': 'ShowError'},
'Code': {'FillChecking': 'ShowError'},
},
# ChartOfCharacteristicTypes: Наименование → FillChecking=ShowError (21/23), Родитель → FFV=true (23/23).
'ChartOfCharacteristicTypes': {
'Description': {'FillChecking': 'ShowError'},
'Parent': {'FillFromFillingValue': 'true'},
},
# ChartOfAccounts: Наименование/Код → FillChecking=ShowError (3/3), Родитель → FFV=true (3/3).
'ChartOfAccounts': {
'Description': {'FillChecking': 'ShowError'},
'Code': {'FillChecking': 'ShowError'},
'Parent': {'FillFromFillingValue': 'true'},
},
}
# ov — dict переопределений (профиль + DSL): FillChecking, FillFromFillingValue, Synonym,
# FullTextSearch, DataHistory. Прочие поля — фиксированный schema-дефолт.
def emit_standard_attribute(indent, attr_name, ov=None):
ov = ov or {}
fc = ov.get('FillChecking', 'DontCheck')
ffv = ov.get('FillFromFillingValue', 'false')
dh = ov.get('DataHistory', 'Use')
fts = ov.get('FullTextSearch', 'Use')
syn = ov.get('Synonym', '')
tt = ov.get('ToolTip', '')
cf = ov.get('ChoiceForm', '')
cmt = ov.get('Comment', '')
msk = ov.get('Mask', '')
fmt = ov.get('Format')
efmt = ov.get('EditFormat')
chi = ov.get('ChoiceHistoryOnInput', 'Auto')
X(f'{indent}<xr:StandardAttribute name="{attr_name}">')
X(f'{indent}\t<xr:LinkByType/>')
X(f'{indent}\t<xr:FillChecking>{fc}</xr:FillChecking>')
X(f'{indent}\t<xr:MultiLine>false</xr:MultiLine>')
X(f'{indent}\t<xr:FillFromFillingValue>{ffv}</xr:FillFromFillingValue>')
X(f'{indent}\t<xr:CreateOnInput>Auto</xr:CreateOnInput>')
X(f'{indent}\t<xr:MaxValue xsi:nil="true"/>')
emit_mltext(f'{indent}\t', 'xr:ToolTip', tt)
X(f'{indent}\t<xr:ExtendedEdit>false</xr:ExtendedEdit>')
emit_mltext(f'{indent}\t', 'xr:Format', fmt)
if cf:
X(f'{indent}\t<xr:ChoiceForm>{esc_xml(str(cf))}</xr:ChoiceForm>')
else:
X(f'{indent}\t<xr:ChoiceForm/>')
X(f'{indent}\t<xr:QuickChoice>Auto</xr:QuickChoice>')
X(f'{indent}\t<xr:ChoiceHistoryOnInput>{chi}</xr:ChoiceHistoryOnInput>')
emit_mltext(f'{indent}\t', 'xr:EditFormat', efmt)
X(f'{indent}\t<xr:PasswordMode>false</xr:PasswordMode>')
X(f'{indent}\t<xr:DataHistory>{dh}</xr:DataHistory>')
X(f'{indent}\t<xr:MarkNegatives>false</xr:MarkNegatives>')
X(f'{indent}\t<xr:MinValue xsi:nil="true"/>')
emit_mltext(f'{indent}\t', 'xr:Synonym', syn)
if cmt:
X(f'{indent}\t<xr:Comment>{esc_xml_text(str(cmt))}</xr:Comment>')
else:
X(f'{indent}\t<xr:Comment/>')
X(f'{indent}\t<xr:FullTextSearch>{fts}</xr:FullTextSearch>')
emit_choice_parameter_links(f'{indent}\t', ov.get('ChoiceParameterLinks'), 'xr:ChoiceParameterLinks')
fv_raw = ov.get('FillValue', None)
if fv_raw is None:
X(f'{indent}\t<xr:FillValue xsi:nil="true"/>')
else:
fv_xt, fv_tx = normalize_choice_value(fv_raw)
if fv_tx == '' or fv_tx is None:
X(f'{indent}\t<xr:FillValue xsi:type="{fv_xt}"/>')
else:
X(f'{indent}\t<xr:FillValue xsi:type="{fv_xt}">{esc_xml(fv_tx)}</xr:FillValue>')
if msk:
X(f'{indent}\t<xr:Mask>{esc_xml_text(str(msk))}</xr:Mask>')
else:
X(f'{indent}\t<xr:Mask/>')
emit_choice_parameters(f'{indent}\t', ov.get('ChoiceParameters'), 'xr:ChoiceParameters')
X(f'{indent}</xr:StandardAttribute>')
# Единый эмиттер блока StandardAttributes — поведение правят ДАННЫЕ, не форк кода (см. коммент в .ps1).
# std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes).
# Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты.
std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts'}
def emit_standard_attributes(indent, object_type):
attrs = standard_attributes_by_type.get(object_type)
if not attrs:
return
conditional = object_type in std_attr_conditional_types
sa = defn.get('standardAttributes')
if conditional and sa is None:
return
profile = std_attr_profile.get(object_type, {})
# Доп. (опциональные) стандартные реквизиты вне фикс-списка — напр. ExchangeDate у части ПланОбмена
# (легаси, присутствие не выводится). Эмитим по факту ключа в DSL, ПЕРЕД фикс-списком (их позиция).
extra = [k for k in sa if k not in attrs] if isinstance(sa, dict) else []
X(f'{indent}<StandardAttributes>')
for a in extra + list(attrs):
ov = dict(profile.get(a, {}))
if isinstance(sa, dict):
d = sa.get(a)
if d:
if d.get('synonym') is not None:
ov['Synonym'] = d['synonym'] # строка ИЛИ {ru,en}
if d.get('tooltip') is not None:
ov['ToolTip'] = d['tooltip'] # строка ИЛИ {ru,en}
if d.get('fillChecking'):
ov['FillChecking'] = str(d['fillChecking'])
if d.get('fillFromFillingValue') is not None:
ov['FillFromFillingValue'] = 'true' if d['fillFromFillingValue'] else 'false'
if d.get('fullTextSearch'):
ov['FullTextSearch'] = str(d['fullTextSearch'])
if d.get('dataHistory'):
ov['DataHistory'] = str(d['dataHistory'])
if d.get('fillValue') is not None:
ov['FillValue'] = d['fillValue']
if d.get('choiceParameterLinks') is not None:
ov['ChoiceParameterLinks'] = d['choiceParameterLinks']
if d.get('choiceParameters') is not None:
ov['ChoiceParameters'] = d['choiceParameters']
if d.get('comment'):
ov['Comment'] = str(d['comment'])
if d.get('mask'):
ov['Mask'] = str(d['mask'])
if d.get('choiceForm'):
ov['ChoiceForm'] = str(d['choiceForm'])
emit_standard_attribute(f'{indent}\t', a, ov)
X(f'{indent}</StandardAttributes>')
def emit_tabular_standard_attributes(indent, line_number=None):
"""LineNumber/НомерСтроки ТЧ. Блок эмитится всегда. DSL `lineNumber` переопределяет
synonym/comment/fullTextSearch/tooltip/format/editFormat/choiceHistoryOnInput."""
ov = None
if line_number:
ov = {}
if line_number.get('synonym') is not None:
ov['Synonym'] = line_number['synonym']
if line_number.get('comment'):
ov['Comment'] = str(line_number['comment'])
if line_number.get('fullTextSearch'):
ov['FullTextSearch'] = str(line_number['fullTextSearch'])
if line_number.get('tooltip') is not None:
ov['ToolTip'] = line_number['tooltip']
if line_number.get('format') is not None:
ov['Format'] = line_number['format']
if line_number.get('editFormat') is not None:
ov['EditFormat'] = line_number['editFormat']
if line_number.get('choiceHistoryOnInput'):
ov['ChoiceHistoryOnInput'] = str(line_number['choiceHistoryOnInput'])
X(f'{indent}<StandardAttributes>')
emit_standard_attribute(f'{indent}\t', 'LineNumber', ov)
X(f'{indent}</StandardAttributes>')
# ---------------------------------------------------------------------------
# 8. Attribute emitter
# ---------------------------------------------------------------------------
RESERVED_ATTR_NAMES = {
'Ref', 'DeletionMark', 'Code', 'Description', 'Date', 'Number', 'Posted',
'Parent', 'Owner', 'IsFolder', 'Predefined', 'PredefinedDataName',
'Recorder', 'Period', 'LineNumber', 'Active', 'Order', 'Type', 'OffBalance',
'Started', 'Completed', 'HeadTask', 'Executed', 'RoutePoint', 'BusinessProcess',
'ThisNode', 'SentNo', 'ReceivedNo', 'CalculationType', 'RegistrationPeriod',
'ReversingEntry', 'Account', 'ValueType', 'ActionPeriodIsBasic',
}
RESERVED_ATTR_NAMES_RU = {
'\u0421\u0441\u044b\u043b\u043a\u0430', '\u041f\u043e\u043c\u0435\u0442\u043a\u0430\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f',
'\u041a\u043e\u0434', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435',
'\u0414\u0430\u0442\u0430', '\u041d\u043e\u043c\u0435\u0440', '\u041f\u0440\u043e\u0432\u0435\u0434\u0435\u043d',
'\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446',
'\u042d\u0442\u043e\u0413\u0440\u0443\u043f\u043f\u0430', '\u041f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439',
'\u0418\u043c\u044f\u041f\u0440\u0435\u0434\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445\u0414\u0430\u043d\u043d\u044b\u0445',
'\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440', '\u041f\u0435\u0440\u0438\u043e\u0434',
'\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438', '\u0410\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c',
'\u041f\u043e\u0440\u044f\u0434\u043e\u043a', '\u0422\u0438\u043f', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439',
'\u0421\u0442\u0430\u0440\u0442\u043e\u0432\u0430\u043d', '\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d',
'\u0412\u0435\u0434\u0443\u0449\u0430\u044f\u0417\u0430\u0434\u0430\u0447\u0430',
'\u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430', '\u0422\u043e\u0447\u043a\u0430\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0430',
'\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441',
'\u042d\u0442\u043e\u0442\u0423\u0437\u0435\u043b', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e',
'\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e',
'\u0412\u0438\u0434\u0420\u0430\u0441\u0447\u0435\u0442\u0430', '\u041f\u0435\u0440\u0438\u043e\u0434\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438',
'\u0421\u0442\u043e\u0440\u043d\u043e\u0417\u0430\u043f\u0438\u0441\u044c',
'\u0421\u0447\u0435\u0442', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f',
'\u041f\u0435\u0440\u0438\u043e\u0434\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f\u0411\u0430\u0437\u043e\u0432\u044b\u0439',
}
# Стандартные реквизиты по типу объекта (EN + RU). Совпадение имени реквизита с ними платформа
# не позволит — жёсткий отказ. Контексты вне карты → мягкое предупреждение по плоскому списку.
RESERVED_BY_CONTEXT = {
'catalog': {
'ref', 'ссылка', # Ссылка
'deletionmark', 'пометкаудаления', # ПометкаУдаления
'predefined', 'предопределенный', # Предопределенный
'predefineddataname', 'имяпредопределенныхданных', # ИмяПредопределенныхДанных
'code', 'код', # Код
'description', 'наименование', # Наименование
'owner', 'владелец', # Владелец
'parent', 'родитель', # Родитель
'isfolder', 'этогруппа', # ЭтоГруппа
},
'document': {
'ref', 'ссылка', # Ссылка
'deletionmark', 'пометкаудаления', # ПометкаУдаления
'date', 'дата', # Дата
'number', 'номер', # Номер
'posted', 'проведен', # Проведен
},
}
# Стандартные реквизиты по типу для разворота dataPath: [(EN, RU), ...].
STD_ATTR_BY_TYPE = {
'Catalog': [('Ref', 'Ссылка'), ('DeletionMark', 'ПометкаУдаления'), ('Predefined', 'Предопределенный'),
('PredefinedDataName', 'ИмяПредопределенныхДанных'), ('Code', 'Код'), ('Description', 'Наименование'),
('Owner', 'Владелец'), ('Parent', 'Родитель'), ('IsFolder', 'ЭтоГруппа')],
'Document': [('Ref', 'Ссылка'), ('DeletionMark', 'ПометкаУдаления'), ('Date', 'Дата'), ('Number', 'Номер'),
('Posted', 'Проведен')],
}
def resolve_std_attr_en(name):
pairs = STD_ATTR_BY_TYPE.get(obj_type)
if not pairs:
return None
low = name.lower()
for en, ru in pairs:
if low == en.lower() or low == ru.lower():
return en
return None
def expand_data_path(dp):
"""Путь к реквизиту самого объекта: "Ссылка"→<Тип>.<Имя>.StandardAttribute.Ref; обычное имя→.Attribute.имя;
частичное StandardAttribute.X/Attribute.X→префикс; полный путь→verbatim."""
if not dp:
return dp
s = str(dp)
if re.search(r'[:/]', s):
return s # спец-путь (напр. 0:GUID/0:GUID в зависимостях ПВХ) — не разворачиваем
if re.match(r'^(StandardAttribute|Attribute)\.', s):
return f'{obj_type}.{obj_name}.{s}'
if '.' not in s:
en = resolve_std_attr_en(s)
if en:
return f'{obj_type}.{obj_name}.StandardAttribute.{en}'
return f'{obj_type}.{obj_name}.Attribute.{s}'
return s
def emit_link_by_type(indent, spec):
"""<LinkByType> (связь по типу): DataPath + LinkItem. spec — {dataPath, linkItem?} или строка-путь; нет → <LinkByType/>."""
if not spec:
X(f'{indent}<LinkByType/>')
return
if isinstance(spec, str):
dp, li = spec, 0
else:
dp = str(spec.get('dataPath') or spec.get('path') or spec.get('путь') or '')
li = spec.get('linkItem')
if li is None:
li = spec.get('элементСвязи')
if li is None:
li = 0
if not dp:
X(f'{indent}<LinkByType/>')
return
dp = expand_data_path(dp)
X(f'{indent}<LinkByType>')
X(f'{indent}\t<xr:DataPath>{esc_xml(str(dp))}</xr:DataPath>')
X(f'{indent}\t<xr:LinkItem>{li}</xr:LinkItem>')
X(f'{indent}</LinkByType>')
def emit_field_block(indent, tag, fields):
"""<Tag> со списком <xr:Field> (InputByString/DataLockFields). Пусто → self-close."""
arr = [f for f in (fields or []) if str(f) != '']
if not arr:
X(f'{indent}<{tag}/>')
return
X(f'{indent}<{tag}>')
for f in arr:
X(f'{indent}\t<xr:Field>{esc_xml(str(f))}</xr:Field>')
X(f'{indent}</{tag}>')
def emit_based_on(indent, items):
"""<BasedOn> — «ввод на основании», список MDObjectRef. Нет/пусто → self-close."""
arr = [it for it in (items or []) if it]
if not arr:
X(f'{indent}<BasedOn/>')
return
X(f'{indent}<BasedOn>')
for it in arr:
X(f'{indent}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(str(it))}</xr:Item>')
X(f'{indent}</BasedOn>')
# --- Параметры/связи выбора (порт из form-compile) ---
def ch_el_prop(obj, names):
if obj is None:
return None
if isinstance(obj, dict):
for n in names:
if n in obj:
return obj[n]
return None
def convert_to_ch_scalar(s):
t = str(s).strip()
if re.match(r'^(?i:true|истина)$', t):
return True
if re.match(r'^(?i:false|ложь)$', t):
return False
if re.match(r'^-?\d+$', t):
return int(t)
if re.match(r'^-?\d+\.\d+$', t):
return float(t)
return t
def expand_choice_ref_value(value, type_str):
"""Голое значение (без точки) + тип параметра → полный DTR-путь, либо None. Принимает EnumRef.X / Enum.X / рус."""
if not type_str:
return None
t = resolve_type_str(type_str)
root = tn = None
m = re.match(r'^(\w+Ref)\.(.+)$', t)
if m:
root = fill_ref_kind_root.get(m.group(1).lower())
tn = m.group(2)
else:
m = re.match(r'^([^.]+)\.(.+)$', t)
if m:
root = fill_ref_roots.get(m.group(1).lower())
tn = m.group(2)
if not root:
return None
if str(value).lower() in fill_empty_ref_words:
return f'{root}.{tn}.EmptyRef'
if root == 'Enum':
return f'Enum.{tn}.EnumValue.{value}'
return f'{root}.{tn}.{value}'
def normalize_choice_value_t(value, type_str):
"""Значение параметра выбора → (xsi_type, text). type_str разворачивает голые ref-имена."""
if type_str and isinstance(value, str) and '.' not in value:
ex = expand_choice_ref_value(value, type_str)
if ex:
return ('xr:DesignTimeRef', ex)
return normalize_choice_value(value)
def normalize_choice_value(value):
"""Значение параметра выбора → (xsi_type, text). Авто-детект по значению."""
if isinstance(value, bool):
return ('xs:boolean', 'true' if value else 'false')
if isinstance(value, (int, float)):
return ('xs:decimal', format_fill_num(value))
s = str(value)
if s == '':
return ('xs:string', '')
if re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$', s):
return ('xs:dateTime', s)
ref = normalize_fill_ref(s)
if ref:
return ('xr:DesignTimeRef', ref)
if s in ACCOUNT_TYPE_VALUES:
return ('ent:AccountType', s)
return ('xs:string', s)
def convert_from_ch_param_shorthand(s):
eq = s.find('=')
if eq < 0:
return {'name': s.strip()}
name = s[:eq].strip()
rest = s[eq + 1:]
if ',' in rest:
return {'name': name, 'value': [convert_to_ch_scalar(p) for p in rest.split(',')]}
return {'name': name, 'value': convert_to_ch_scalar(rest)}
def convert_from_ch_link_shorthand(s):
eq = s.find('=')
if eq < 0:
return {'name': s.strip()}
o = {'name': s[:eq].strip()}
rest = s[eq + 1:].strip()
m = re.match(r'^(.*):(?i:(Clear|DontChange|очистить|неизменять))$', rest)
if m:
o['dataPath'] = m.group(1).strip()
o['valueChange'] = m.group(2)
else:
o['dataPath'] = rest
return o
def emit_choice_parameters(indent, cp, tag='ChoiceParameters'):
if not cp:
X(f'{indent}<{tag}/>')
return
if isinstance(cp, (str, dict)):
cp = [cp]
X(f'{indent}<{tag}>')
for item in cp:
if isinstance(item, str):
item = convert_from_ch_param_shorthand(item)
name = ch_el_prop(item, ['name', 'имя'])
ptype = ch_el_prop(item, ['type', 'тип'])
has_val = isinstance(item, dict) and ('value' in item or 'значение' in item)
val = item.get('value', item.get('значение')) if has_val else None
val_is_array = isinstance(val, (list, tuple))
X(f'{indent}\t<app:item name="{esc_xml(str(name))}">')
if not has_val:
X(f'{indent}\t\t<app:value xsi:nil="true"/>')
elif val_is_array:
X(f'{indent}\t\t<app:value xsi:type="v8:FixedArray">')
for v in val:
xt, tx = normalize_choice_value_t(v, ptype)
if tx == '' or tx is None:
X(f'{indent}\t\t\t<v8:Value xsi:type="{xt}"/>')
else:
X(f'{indent}\t\t\t<v8:Value xsi:type="{xt}">{esc_xml(tx)}</v8:Value>')
X(f'{indent}\t\t</app:value>')
else:
xt, tx = normalize_choice_value_t(val, ptype)
if tx == '' or tx is None:
X(f'{indent}\t\t<app:value xsi:type="{xt}"/>')
else:
X(f'{indent}\t\t<app:value xsi:type="{xt}">{esc_xml(tx)}</app:value>')
X(f'{indent}\t</app:item>')
X(f'{indent}</{tag}>')
def emit_choice_parameter_links(indent, cpl, tag='ChoiceParameterLinks'):
if not cpl:
X(f'{indent}<{tag}/>')
return
if isinstance(cpl, (str, dict)):
cpl = [cpl]
X(f'{indent}<{tag}>')
for lk in cpl:
if isinstance(lk, str):
lk = convert_from_ch_link_shorthand(lk)
name = ch_el_prop(lk, ['name', 'имя'])
dp = expand_data_path(ch_el_prop(lk, ['dataPath', 'path', 'путь']))
vc_raw = ch_el_prop(lk, ['valueChange', 'режимИзменения'])
vc = 'Clear'
if vc_raw:
low = str(vc_raw).lower()
if re.match(r'^(clear|очистить|очистка)$', low):
vc = 'Clear'
elif re.match(r'^(dontchange|неизменять|неменять|нет)$', low):
vc = 'DontChange'
else:
vc = str(vc_raw)
X(f'{indent}\t<xr:Link>')
X(f'{indent}\t\t<xr:Name>{esc_xml(str(name))}</xr:Name>')
X(f'{indent}\t\t<xr:DataPath xsi:type="xs:string">{esc_xml(str(dp))}</xr:DataPath>')
X(f'{indent}\t\t<xr:ValueChange>{vc}</xr:ValueChange>')
X(f'{indent}\t</xr:Link>')
X(f'{indent}</{tag}>')
# --- Characteristics (привязка ПВХ «Дополнительные реквизиты и сведения») ---
CHAR_FROM_TS_TYPES = {'Catalog', 'Document', 'ChartOfCharacteristicTypes', 'ChartOfCalculationTypes',
'ChartOfAccounts', 'ExchangePlan', 'BusinessProcess', 'Task'}
CHAR_MEMBER_RU = {'ТабличнаяЧасть': 'TabularSection', 'Измерение': 'Dimension', 'Ресурс': 'Resource', 'Реквизит': 'Attribute'}
def normalize_char_from(from_):
if not from_:
return from_
parts = str(from_).split('.')
if parts[0] in object_type_synonyms:
parts[0] = object_type_synonyms[parts[0]]
for i in range(1, len(parts)):
if parts[i] in CHAR_MEMBER_RU:
parts[i] = CHAR_MEMBER_RU[parts[i]]
if len(parts) == 3 and parts[0] in CHAR_FROM_TS_TYPES:
parts = [parts[0], parts[1], 'TabularSection', parts[2]]
return '.'.join(parts)
def resolve_char_std_en(name):
n = str(name).lower()
if n in ('ref', 'ссылка'):
return 'Ref'
if n in ('parent', 'родитель'):
return 'Parent'
if n in ('owner', 'владелец'):
return 'Owner'
return None
def char_int_field(obj, names):
v = ch_el_prop(obj, names)
if v is None or str(v) == '':
return -1
return int(v)
def expand_char_field(field, from_):
s = str(field or '')
if not s:
return s
if s == '-1':
return '-1' # поле не задано (empty-характеристика)
if re.match(r'^(StandardAttribute|Attribute|Dimension|Resource)\.', s):
return f'{from_}.{s}'
if '.' not in s:
en = resolve_char_std_en(s)
if en:
return f'{from_}.StandardAttribute.{en}'
return f'{from_}.Attribute.{s}'
return s
def emit_characteristics(indent, chars):
if not chars:
X(f'{indent}<Characteristics/>')
return
X(f'{indent}<Characteristics>')
for ch in chars:
types = ch_el_prop(ch, ['types', 'characteristicTypes', 'типы'])
values = ch_el_prop(ch, ['values', 'characteristicValues', 'значения'])
t_from = normalize_char_from(ch_el_prop(types, ['from', 'source', 'источник']) or '')
v_from = normalize_char_from(ch_el_prop(values, ['from', 'source', 'источник']) or '')
key = expand_char_field(ch_el_prop(types, ['key', 'keyField']), t_from)
tff = expand_char_field(ch_el_prop(types, ['filterField', 'typesFilterField']), t_from)
obj = expand_char_field(ch_el_prop(values, ['object', 'objectField']), v_from)
typ = expand_char_field(ch_el_prop(values, ['type', 'typeField']), v_from)
val = expand_char_field(ch_el_prop(values, ['value', 'valueField']), v_from)
dpf = char_int_field(types, ['dataPathField'])
mvu = char_int_field(types, ['multipleValuesUseField'])
mvk = char_int_field(values, ['multipleValuesKeyField'])
mvo = char_int_field(values, ['multipleValuesOrderField'])
X(f'{indent}\t<xr:Characteristic>')
X(f'{indent}\t\t<xr:CharacteristicTypes from="{esc_xml(t_from)}">')
X(f'{indent}\t\t\t<xr:KeyField>{esc_xml(key)}</xr:KeyField>')
X(f'{indent}\t\t\t<xr:TypesFilterField>{esc_xml(tff)}</xr:TypesFilterField>')
# filterValue: None→nil; голое→xs:string, полный путь→DTR, bool→xs:boolean.
tfv_raw = ch_el_prop(types, ['filterValue', 'typesFilterValue'])
if tfv_raw is None:
X(f'{indent}\t\t\t<xr:TypesFilterValue xsi:nil="true"/>')
else:
tfv_xt, tfv_tx = normalize_choice_value(tfv_raw)
if tfv_tx == '' or tfv_tx is None:
X(f'{indent}\t\t\t<xr:TypesFilterValue xsi:type="{tfv_xt}"/>')
else:
X(f'{indent}\t\t\t<xr:TypesFilterValue xsi:type="{tfv_xt}">{esc_xml(tfv_tx)}</xr:TypesFilterValue>')
X(f'{indent}\t\t\t<xr:DataPathField>{dpf}</xr:DataPathField>')
X(f'{indent}\t\t\t<xr:MultipleValuesUseField>{mvu}</xr:MultipleValuesUseField>')
X(f'{indent}\t\t</xr:CharacteristicTypes>')
X(f'{indent}\t\t<xr:CharacteristicValues from="{esc_xml(v_from)}">')
X(f'{indent}\t\t\t<xr:ObjectField>{esc_xml(obj)}</xr:ObjectField>')
X(f'{indent}\t\t\t<xr:TypeField>{esc_xml(typ)}</xr:TypeField>')
X(f'{indent}\t\t\t<xr:ValueField>{esc_xml(val)}</xr:ValueField>')
X(f'{indent}\t\t\t<xr:MultipleValuesKeyField>{mvk}</xr:MultipleValuesKeyField>')
X(f'{indent}\t\t\t<xr:MultipleValuesOrderField>{mvo}</xr:MultipleValuesOrderField>')
X(f'{indent}\t\t</xr:CharacteristicValues>')
X(f'{indent}\t</xr:Characteristic>')
X(f'{indent}</Characteristics>')
def emit_min_max_value(indent, tag, val):
"""<MinValue>/<MaxValue> — граница диапазона. None → nil. Число → xs:decimal, строка → xs:string."""
if val is None:
X(f'{indent}<{tag} xsi:nil="true"/>')
return
t = 'xs:string' if isinstance(val, str) else 'xs:decimal'
X(f'{indent}<{tag} xsi:type="{t}">{esc_xml(str(val))}</{tag}>')
def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
# context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register",
# "account" (реквизит Плана счетов: как catalog, но без <Use>), "account-flag" (признак учёта ПС:
# как account, но без <Indexing>/<FullTextSearch>, тип по умолчанию Boolean; elem_tag = AccountingFlag/ExtDimensionAccountingFlag)
attr_name = parsed['name']
ctx_reserved = RESERVED_BY_CONTEXT.get(context)
if ctx_reserved is not None:
if attr_name.lower() in ctx_reserved:
print(f"meta-compile: имя реквизита '{attr_name}' зарезервировано стандартным реквизитом объекта '{context}'. Выберите другое имя.", file=sys.stderr)
sys.exit(1)
elif context not in ('tabular', 'processor-tabular') and (attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU):
print(f"WARNING: Attribute '{attr_name}' conflicts with a standard attribute name. This may cause errors when loading into 1C.", file=sys.stderr)
uid = new_uuid()
X(f'{indent}<{elem_tag} uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(parsed["name"])}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym'])
if parsed.get('comment'):
X(f'{indent}\t\t<Comment>{esc_xml_text(parsed["comment"])}</Comment>')
else:
X(f'{indent}\t\t<Comment/>')
type_str = parsed['type']
if type_str:
emit_value_type(f'{indent}\t\t', type_str)
elif context == 'account-flag':
X(f'{indent}\t\t<Type>')
X(f'{indent}\t\t\t<v8:Type>xs:boolean</v8:Type>')
X(f'{indent}\t\t</Type>')
else:
X(f'{indent}\t\t<Type>')
X(f'{indent}\t\t\t<v8:Type>xs:string</v8:Type>')
X(f'{indent}\t\t</Type>')
pw_mode = 'true' if parsed.get('passwordMode') is True else 'false'
X(f'{indent}\t\t<PasswordMode>{pw_mode}</PasswordMode>')
emit_mltext(f'{indent}\t\t', 'Format', parsed.get('format'))
emit_mltext(f'{indent}\t\t', 'EditFormat', parsed.get('editFormat'))
emit_mltext(f'{indent}\t\t', 'ToolTip', parsed.get('tooltip'))
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
if parsed.get('mask'):
X(f'{indent}\t\t<Mask>{esc_xml_text(parsed["mask"])}</Mask>')
else:
X(f'{indent}\t\t<Mask/>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false'
X(f'{indent}\t\t<ExtendedEdit>{ext_edit}</ExtendedEdit>')
emit_min_max_value(f'{indent}\t\t', 'MinValue', parsed.get('minValue'))
emit_min_max_value(f'{indent}\t\t', 'MaxValue', parsed.get('maxValue'))
# FillFromFillingValue / FillValue — not for tabular/processor/chart/register-other
# (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these)
if context not in ('tabular', 'processor', 'chart', 'register-other'):
ffv = 'true' if parsed.get('fillFromFillingValue') is True else 'false'
X(f'{indent}\t\t<FillFromFillingValue>{ffv}</FillFromFillingValue>')
if context not in ('tabular', 'processor', 'chart', 'register-other'):
emit_fill_value(f'{indent}\t\t', type_str, parsed.get('fillValue'), parsed.get('hasFillValue'))
fill_checking = 'DontCheck'
if 'req' in parsed.get('flags', []):
fill_checking = 'ShowError'
if parsed.get('fillChecking'):
fill_checking = parsed['fillChecking']
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
X(f'{indent}\t\t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>')
emit_choice_parameter_links(f'{indent}\t\t', parsed.get('choiceParameterLinks'))
emit_choice_parameters(f'{indent}\t\t', parsed.get('choiceParameters'))
X(f'{indent}\t\t<QuickChoice>{parsed.get("quickChoice") or "Auto"}</QuickChoice>')
X(f'{indent}\t\t<CreateOnInput>{parsed.get("createOnInput") or "Auto"}</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm/>')
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
if context == 'catalog':
X(f'{indent}\t\t<Use>{parsed.get("use") or "ForItem"}</Use>')
if context not in ('processor', 'processor-tabular'):
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
if context != 'account-flag':
indexing = 'DontIndex'
if 'index' in parsed.get('flags', []):
indexing = 'Index'
if 'indexadditional' in parsed.get('flags', []):
indexing = 'IndexWithAdditionalOrder'
if parsed.get('indexing'):
indexing = parsed['indexing']
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
X(f'{indent}\t\t<FullTextSearch>{parsed.get("fullTextSearch") or "Use"}</FullTextSearch>')
# DataHistory — not for Chart* types and non-InformationRegister register family
if context not in ('chart', 'register-other'):
X(f'{indent}\t\t<DataHistory>{parsed.get("dataHistory") or "Use"}</DataHistory>')
X(f'{indent}\t</Properties>')
X(f'{indent}</{elem_tag}>')
# ---------------------------------------------------------------------------
# 9. TabularSection emitter
# ---------------------------------------------------------------------------
def emit_command_picture(indent, cmd):
"""<Picture> команды — структурный блок (зеркало form-compile). Дефолт LoadTransparent=true (конвенция
кнопки/команды): фиксируем только false. Значение: строка-ref + sibling loadTransparent ЛИБО объект
{src, loadTransparent?, transparentPixel?}. src с префиксом "abs:" → <xr:Abs>, иначе <xr:Ref>. Нет → <Picture/>."""
pic = cmd.get('picture')
if not pic:
X(f'{indent}<Picture/>')
return
lt = True
tpx = None
if isinstance(pic, str):
src = pic
if cmd.get('loadTransparent') is False:
lt = False
else:
src = str(pic.get('src') or pic.get('ref') or '')
if pic.get('loadTransparent') is False:
lt = False
tpx = pic.get('transparentPixel')
if not src:
X(f'{indent}<Picture/>')
return
X(f'{indent}<Picture>')
m = re.match(r'^abs:(.*)$', src)
if m:
X(f'{indent}\t<xr:Abs>{esc_xml(m.group(1))}</xr:Abs>')
else:
X(f'{indent}\t<xr:Ref>{esc_xml(src)}</xr:Ref>')
X(f'{indent}\t<xr:LoadTransparent>{"true" if lt else "false"}</xr:LoadTransparent>')
if tpx:
X(f'{indent}\t<xr:TransparentPixel x="{tpx.get("x")}" y="{tpx.get("y")}"/>')
X(f'{indent}</Picture>')
def emit_command(indent, cmd_name, cmd):
X(f'{indent}<Command uuid="{new_uuid()}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(cmd_name)}</Name>')
syn = cmd['synonym'] if cmd.get('synonym') is not None else split_camel_case(cmd_name)
emit_mltext(f'{indent}\t\t', 'Synonym', syn)
if cmd.get('comment'):
X(f'{indent}\t\t<Comment>{esc_xml_text(str(cmd["comment"]))}</Comment>')
else:
X(f'{indent}\t\t<Comment/>')
X(f'{indent}\t\t<Group>{cmd.get("group") or ""}</Group>')
if cmd.get('commandParameterType'):
X(f'{indent}\t\t<CommandParameterType>')
emit_type_content(f'{indent}\t\t\t', str(cmd['commandParameterType']))
X(f'{indent}\t\t</CommandParameterType>')
else:
X(f'{indent}\t\t<CommandParameterType/>')
X(f'{indent}\t\t<ParameterUseMode>{cmd.get("parameterUseMode") or "Single"}</ParameterUseMode>')
X(f'{indent}\t\t<ModifiesData>{"true" if cmd.get("modifiesData") is True else "false"}</ModifiesData>')
X(f'{indent}\t\t<Representation>{cmd.get("representation") or "Auto"}</Representation>')
emit_mltext(f'{indent}\t\t', 'ToolTip', cmd.get('tooltip'))
emit_command_picture(f'{indent}\t\t', cmd)
if cmd.get('shortcut'):
X(f'{indent}\t\t<Shortcut>{esc_xml(str(cmd["shortcut"]))}</Shortcut>')
else:
X(f'{indent}\t\t<Shortcut/>')
X(f'{indent}\t\t<OnMainServerUnavalableBehavior>{cmd.get("onMainServerUnavalableBehavior") or "Auto"}</OnMainServerUnavalableBehavior>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Command>')
def emit_tabular_section(indent, ts_name, columns, object_type, object_name, ts_synonym_arg=None, ts_tooltip=None, ts_comment=None, ts_line_number=None):
uid = new_uuid()
X(f'{indent}<TabularSection uuid="{uid}">')
type_prefix = f'{object_type}TabularSection'
row_prefix = f'{object_type}TabularSectionRow'
X(f'{indent}\t<InternalInfo>')
X(f'{indent}\t\t<xr:GeneratedType name="{type_prefix}.{object_name}.{ts_name}" category="TabularSection">')
X(f'{indent}\t\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
X(f'{indent}\t\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
X(f'{indent}\t\t</xr:GeneratedType>')
X(f'{indent}\t\t<xr:GeneratedType name="{row_prefix}.{object_name}.{ts_name}" category="TabularSectionRow">')
X(f'{indent}\t\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
X(f'{indent}\t\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
X(f'{indent}\t\t</xr:GeneratedType>')
X(f'{indent}\t</InternalInfo>')
ts_synonym = ts_synonym_arg if ts_synonym_arg is not None else split_camel_case(ts_name)
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(ts_name)}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', ts_synonym)
if ts_comment:
X(f'{indent}\t\t<Comment>{esc_xml_text(ts_comment)}</Comment>')
else:
X(f'{indent}\t\t<Comment/>')
emit_mltext(f'{indent}\t\t', 'ToolTip', ts_tooltip)
X(f'{indent}\t\t<FillChecking>DontCheck</FillChecking>')
emit_tabular_standard_attributes(f'{indent}\t\t', ts_line_number)
if object_type in ('Catalog', 'ChartOfCharacteristicTypes'):
X(f'{indent}\t\t<Use>ForItem</Use>')
X(f'{indent}\t</Properties>')
ts_context = 'processor-tabular' if object_type in ('DataProcessor', 'Report') else 'tabular'
X(f'{indent}\t<ChildObjects>')
for col in columns:
parsed = parse_attribute_shorthand(col)
emit_attribute(f'{indent}\t\t', parsed, ts_context)
X(f'{indent}\t</ChildObjects>')
X(f'{indent}</TabularSection>')
# ---------------------------------------------------------------------------
# 10. EnumValue emitter
# ---------------------------------------------------------------------------
def emit_enum_value(indent, parsed):
uid = new_uuid()
X(f'{indent}<EnumValue uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(parsed["name"])}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym'])
X(f'{indent}\t\t<Comment/>')
X(f'{indent}\t</Properties>')
X(f'{indent}</EnumValue>')
# ---------------------------------------------------------------------------
# 11. Dimension emitter
# ---------------------------------------------------------------------------
def emit_dimension(indent, parsed, register_type):
uid = new_uuid()
X(f'{indent}<Dimension uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(parsed["name"])}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym'])
X(f'{indent}\t\t<Comment/>')
type_str = parsed['type']
if type_str:
emit_value_type(f'{indent}\t\t', type_str)
else:
X(f'{indent}\t\t<Type>')
X(f'{indent}\t\t\t<v8:Type>xs:string</v8:Type>')
X(f'{indent}\t\t</Type>')
X(f'{indent}\t\t<PasswordMode>false</PasswordMode>')
X(f'{indent}\t\t<Format/>')
X(f'{indent}\t\t<EditFormat/>')
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false'
X(f'{indent}\t\t<ExtendedEdit>{ext_edit}</ExtendedEdit>')
emit_min_max_value(f'{indent}\t\t', 'MinValue', parsed.get('minValue'))
emit_min_max_value(f'{indent}\t\t', 'MaxValue', parsed.get('maxValue'))
flags = parsed.get('flags', [])
if register_type == 'InformationRegister':
fill_from = 'true' if 'master' in flags else 'false'
X(f'{indent}\t\t<FillFromFillingValue>{fill_from}</FillFromFillingValue>')
X(f'{indent}\t\t<FillValue xsi:nil="true"/>')
fill_checking = 'DontCheck'
if 'req' in flags:
fill_checking = 'ShowError'
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
X(f'{indent}\t\t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>')
X(f'{indent}\t\t<ChoiceParameterLinks/>')
X(f'{indent}\t\t<ChoiceParameters/>')
X(f'{indent}\t\t<QuickChoice>Auto</QuickChoice>')
X(f'{indent}\t\t<CreateOnInput>Auto</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm/>')
X(f'{indent}\t\t<LinkByType/>')
X(f'{indent}\t\t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
if register_type == 'InformationRegister':
master = 'true' if 'master' in flags else 'false'
main_filter = 'true' if 'mainfilter' in flags else 'false'
deny_incomplete = 'true' if 'denyincomplete' in flags else 'false'
X(f'{indent}\t\t<Master>{master}</Master>')
X(f'{indent}\t\t<MainFilter>{main_filter}</MainFilter>')
X(f'{indent}\t\t<DenyIncompleteValues>{deny_incomplete}</DenyIncompleteValues>')
if register_type == 'AccumulationRegister':
deny_incomplete = 'true' if 'denyincomplete' in flags else 'false'
X(f'{indent}\t\t<DenyIncompleteValues>{deny_incomplete}</DenyIncompleteValues>')
indexing = 'DontIndex'
if 'index' in flags:
indexing = 'Index'
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
X(f'{indent}\t\t<FullTextSearch>Use</FullTextSearch>')
if register_type == 'AccumulationRegister':
use_in_totals = 'false' if 'nouseintotals' in flags else 'true'
X(f'{indent}\t\t<UseInTotals>{use_in_totals}</UseInTotals>')
if register_type == 'InformationRegister':
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Dimension>')
# ---------------------------------------------------------------------------
# 12. Resource emitter
# ---------------------------------------------------------------------------
def emit_resource(indent, parsed, register_type):
uid = new_uuid()
X(f'{indent}<Resource uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(parsed["name"])}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym'])
X(f'{indent}\t\t<Comment/>')
type_str = parsed['type']
if type_str:
emit_value_type(f'{indent}\t\t', type_str)
else:
X(f'{indent}\t\t<Type>')
X(f'{indent}\t\t\t<v8:Type>xs:decimal</v8:Type>')
X(f'{indent}\t\t\t<v8:NumberQualifiers>')
X(f'{indent}\t\t\t\t<v8:Digits>15</v8:Digits>')
X(f'{indent}\t\t\t\t<v8:FractionDigits>2</v8:FractionDigits>')
X(f'{indent}\t\t\t\t<v8:AllowedSign>Any</v8:AllowedSign>')
X(f'{indent}\t\t\t</v8:NumberQualifiers>')
X(f'{indent}\t\t</Type>')
X(f'{indent}\t\t<PasswordMode>false</PasswordMode>')
X(f'{indent}\t\t<Format/>')
X(f'{indent}\t\t<EditFormat/>')
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false'
X(f'{indent}\t\t<ExtendedEdit>{ext_edit}</ExtendedEdit>')
emit_min_max_value(f'{indent}\t\t', 'MinValue', parsed.get('minValue'))
emit_min_max_value(f'{indent}\t\t', 'MaxValue', parsed.get('maxValue'))
if register_type == 'InformationRegister':
X(f'{indent}\t\t<FillFromFillingValue>false</FillFromFillingValue>')
X(f'{indent}\t\t<FillValue xsi:nil="true"/>')
flags = parsed.get('flags', [])
fill_checking = 'DontCheck'
if 'req' in flags:
fill_checking = 'ShowError'
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
X(f'{indent}\t\t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>')
X(f'{indent}\t\t<ChoiceParameterLinks/>')
X(f'{indent}\t\t<ChoiceParameters/>')
X(f'{indent}\t\t<QuickChoice>Auto</QuickChoice>')
X(f'{indent}\t\t<CreateOnInput>Auto</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm/>')
X(f'{indent}\t\t<LinkByType/>')
X(f'{indent}\t\t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
if register_type == 'InformationRegister':
X(f'{indent}\t\t<Indexing>DontIndex</Indexing>')
X(f'{indent}\t\t<FullTextSearch>Use</FullTextSearch>')
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
if register_type == 'AccumulationRegister':
X(f'{indent}\t\t<FullTextSearch>Use</FullTextSearch>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Resource>')
# ---------------------------------------------------------------------------
# 13. Property emitters per type
# ---------------------------------------------------------------------------
def emit_catalog_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
if defn.get('comment'):
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
else:
X(f'{i}<Comment/>')
hierarchical = 'true' if defn.get('hierarchical') is True else 'false'
hierarchy_type = get_enum_prop('HierarchyType', 'hierarchyType', 'HierarchyFoldersAndItems')
X(f'{i}<Hierarchical>{hierarchical}</Hierarchical>')
X(f'{i}<HierarchyType>{hierarchy_type}</HierarchyType>')
limit_level_count = 'true' if defn.get('limitLevelCount') is True else 'false'
level_count = str(defn['levelCount']) if defn.get('levelCount') is not None else '2'
folders_on_top = 'false' if defn.get('foldersOnTop') is False else 'true'
X(f'{i}<LimitLevelCount>{limit_level_count}</LimitLevelCount>')
X(f'{i}<LevelCount>{level_count}</LevelCount>')
X(f'{i}<FoldersOnTop>{folders_on_top}</FoldersOnTop>')
use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false'
X(f'{i}<UseStandardCommands>{use_std_cmds}</UseStandardCommands>')
owners = defn.get('owners', [])
if owners:
X(f'{i}<Owners>')
for owner_ref in owners:
full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}'
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{full_ref}</xr:Item>')
X(f'{i}</Owners>')
else:
X(f'{i}<Owners/>')
subordination_use = get_enum_prop('SubordinationUse', 'subordinationUse', 'ToItems')
X(f'{i}<SubordinationUse>{subordination_use}</SubordinationUse>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = get_enum_prop('CodeType', 'codeType', 'String')
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
check_unique = 'true' if defn.get('checkUnique') is True else 'false'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
code_series = get_enum_prop('CodeSeries', 'codeSeries', 'WholeCatalog')
X(f'{i}<CodeSeries>{code_series}</CodeSeries>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
default_presentation = get_enum_prop('DefaultPresentation', 'defaultPresentation', 'AsDescription')
X(f'{i}<DefaultPresentation>{default_presentation}</DefaultPresentation>')
emit_standard_attributes(i, 'Catalog')
emit_characteristics(i, defn.get('characteristics'))
X(f'{i}<PredefinedDataUpdate>{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}</PredefinedDataUpdate>')
X(f'{i}<EditType>{get_enum_prop("EditType", "editType", "InDialog")}</EditType>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
choice_mode = get_enum_prop('ChoiceMode', 'choiceMode', 'BothWays')
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>{choice_mode}</ChoiceMode>')
# InputByString: override `inputByString` (массив имён, авто-резолв; [] = пусто) ЛИБО дефолт [Descr при D>0]+[Code при C>0].
if 'inputByString' in defn:
ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])]
else:
ib_fields = []
if int(description_length) > 0:
ib_fields.append(f'Catalog.{obj_name}.StandardAttribute.Description')
if int(code_length) > 0:
ib_fields.append(f'Catalog.{obj_name}.StandardAttribute.Code')
emit_field_block(i, 'InputByString', ib_fields)
X(f'{i}<SearchStringModeOnInputByString>{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
emit_form_ref(i, 'DefaultObjectForm', defn.get('defaultObjectForm'))
emit_form_ref(i, 'DefaultFolderForm', defn.get('defaultFolderForm'))
emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm'))
emit_form_ref(i, 'DefaultChoiceForm', defn.get('defaultChoiceForm'))
emit_form_ref(i, 'DefaultFolderChoiceForm', defn.get('defaultFolderChoiceForm'))
emit_form_ref(i, 'AuxiliaryObjectForm', defn.get('auxiliaryObjectForm'))
emit_form_ref(i, 'AuxiliaryFolderForm', defn.get('auxiliaryFolderForm'))
emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm'))
emit_form_ref(i, 'AuxiliaryChoiceForm', defn.get('auxiliaryChoiceForm'))
emit_form_ref(i, 'AuxiliaryFolderChoiceForm', defn.get('auxiliaryFolderChoiceForm'))
incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false'
X(f'{i}<IncludeHelpInContents>{incl_help}</IncludeHelpInContents>')
emit_based_on(i, defn.get('basedOn'))
dl_fields = [expand_data_path(str(x)) for x in defn.get('dataLockFields', [])] if 'dataLockFields' in defn else []
emit_field_block(i, 'DataLockFields', dl_fields)
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<CreateOnInput>{get_enum_prop("CreateOnInput", "createOnInput", "Use")}</CreateOnInput>')
X(f'{i}<ChoiceHistoryOnInput>{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}</ChoiceHistoryOnInput>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_document_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<Numerator/>')
number_type = get_enum_prop('NumberType', 'numberType', 'String')
number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '11'
number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable')
number_periodicity = get_enum_prop('InformationRegisterPeriodicity', 'numberPeriodicity', 'Year')
check_unique = 'false' if defn.get('checkUnique') is False else 'true'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
X(f'{i}<NumberType>{number_type}</NumberType>')
X(f'{i}<NumberLength>{number_length}</NumberLength>')
X(f'{i}<NumberAllowedLength>{number_allowed_length}</NumberAllowedLength>')
X(f'{i}<NumberPeriodicity>{number_periodicity}</NumberPeriodicity>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
emit_standard_attributes(i, 'Document')
X(f'{i}<Characteristics/>')
X(f'{i}<BasedOn/>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>Document.{obj_name}.StandardAttribute.Number</xr:Field>')
X(f'{i}</InputByString>')
X(f'{i}<CreateOnInput>DontUse</CreateOnInput>')
X(f'{i}<SearchStringModeOnInputByString>Begin</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
X(f'{i}<DefaultObjectForm/>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
X(f'{i}<AuxiliaryObjectForm/>')
X(f'{i}<AuxiliaryListForm/>')
X(f'{i}<AuxiliaryChoiceForm/>')
posting = get_enum_prop('Posting', 'posting', 'Allow')
real_time_posting = get_enum_prop('RealTimePosting', 'realTimePosting', 'Deny')
reg_records_deletion = get_enum_prop('RegisterRecordsDeletion', 'registerRecordsDeletion', 'AutoDelete')
reg_records_writing = get_enum_prop('RegisterRecordsWritingOnPost', 'registerRecordsWritingOnPost', 'WriteModified')
sequence_filling = str(defn['sequenceFilling']) if defn.get('sequenceFilling') else 'AutoFill'
post_in_priv = 'false' if defn.get('postInPrivilegedMode') is False else 'true'
unpost_in_priv = 'false' if defn.get('unpostInPrivilegedMode') is False else 'true'
X(f'{i}<Posting>{posting}</Posting>')
X(f'{i}<RealTimePosting>{real_time_posting}</RealTimePosting>')
X(f'{i}<RegisterRecordsDeletion>{reg_records_deletion}</RegisterRecordsDeletion>')
X(f'{i}<RegisterRecordsWritingOnPost>{reg_records_writing}</RegisterRecordsWritingOnPost>')
X(f'{i}<SequenceFilling>{sequence_filling}</SequenceFilling>')
# RegisterRecords
reg_records = []
if defn.get('registerRecords'):
for rr in defn['registerRecords']:
rr_str = str(rr)
if '.' in rr_str:
dot_idx = rr_str.index('.')
rr_prefix = rr_str[:dot_idx]
rr_suffix = rr_str[dot_idx + 1:]
if rr_prefix in object_type_synonyms:
rr_prefix = object_type_synonyms[rr_prefix]
reg_records.append(f'{rr_prefix}.{rr_suffix}')
else:
reg_records.append(rr_str)
if reg_records:
X(f'{i}<RegisterRecords>')
for rr in reg_records:
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{rr}</xr:Item>')
X(f'{i}</RegisterRecords>')
else:
X(f'{i}<RegisterRecords/>')
X(f'{i}<PostInPrivilegedMode>{post_in_priv}</PostInPrivilegedMode>')
X(f'{i}<UnpostInPrivilegedMode>{unpost_in_priv}</UnpostInPrivilegedMode>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_enum_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>false</UseStandardCommands>')
emit_standard_attributes(i, 'Enum')
X(f'{i}<Characteristics/>')
quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
X(f'{i}<AuxiliaryListForm/>')
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
X(f'{i}<Explanation/>')
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
def emit_constant_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
# Type
value_type = build_type_str(defn) or 'String'
emit_value_type(i, value_type)
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<DefaultForm/>')
X(f'{i}<ExtendedPresentation/>')
X(f'{i}<Explanation/>')
X(f'{i}<PasswordMode>false</PasswordMode>')
X(f'{i}<Format/>')
X(f'{i}<EditFormat/>')
X(f'{i}<ToolTip/>')
X(f'{i}<MarkNegatives>false</MarkNegatives>')
X(f'{i}<Mask/>')
X(f'{i}<MultiLine>false</MultiLine>')
X(f'{i}<ExtendedEdit>false</ExtendedEdit>')
X(f'{i}<MinValue xsi:nil="true"/>')
X(f'{i}<MaxValue xsi:nil="true"/>')
X(f'{i}<FillChecking>DontCheck</FillChecking>')
X(f'{i}<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>')
X(f'{i}<ChoiceParameterLinks/>')
X(f'{i}<ChoiceParameters/>')
X(f'{i}<QuickChoice>Auto</QuickChoice>')
X(f'{i}<ChoiceForm/>')
X(f'{i}<LinkByType/>')
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_information_register_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<DefaultRecordForm/>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<AuxiliaryRecordForm/>')
X(f'{i}<AuxiliaryListForm/>')
emit_standard_attributes(i, 'InformationRegister')
periodicity = get_enum_prop('InformationRegisterPeriodicity', 'periodicity', 'Nonperiodical')
write_mode = get_enum_prop('WriteMode', 'writeMode', 'Independent')
main_filter_on_period = 'false'
if defn.get('mainFilterOnPeriod') is not None:
main_filter_on_period = 'true' if defn['mainFilterOnPeriod'] is True else 'false'
elif periodicity != 'Nonperiodical':
main_filter_on_period = 'true'
X(f'{i}<InformationRegisterPeriodicity>{periodicity}</InformationRegisterPeriodicity>')
X(f'{i}<WriteMode>{write_mode}</WriteMode>')
X(f'{i}<MainFilterOnPeriod>{main_filter_on_period}</MainFilterOnPeriod>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<EnableTotalsSliceFirst>false</EnableTotalsSliceFirst>')
X(f'{i}<EnableTotalsSliceLast>false</EnableTotalsSliceLast>')
X(f'{i}<RecordPresentation/>')
X(f'{i}<ExtendedRecordPresentation/>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
X(f'{i}<Explanation/>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_accumulation_register_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<AuxiliaryListForm/>')
register_type = get_enum_prop('RegisterType', 'registerType', 'Balance')
X(f'{i}<RegisterType>{register_type}</RegisterType>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
emit_standard_attributes(i, 'AccumulationRegister')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
enable_totals_splitting = 'false' if defn.get('enableTotalsSplitting') is False else 'true'
X(f'{i}<EnableTotalsSplitting>{enable_totals_splitting}</EnableTotalsSplitting>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
X(f'{i}<Explanation/>')
# --- 13a. DefinedType, CommonModule, ScheduledJob, EventSubscription ---
def emit_defined_type_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
# Accept both valueType and valueTypes
value_types = list(defn.get('valueTypes', []))
if not value_types and defn.get('valueType'):
vt_raw = defn['valueType']
value_types = list(vt_raw) if isinstance(vt_raw, list) else [vt_raw]
if value_types:
X(f'{i}<Type>')
for vt in value_types:
resolved = resolve_type_str(str(vt))
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.', resolved):
X(f'{i}\t<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{resolved}</v8:Type>')
elif resolved == 'Boolean':
X(f'{i}\t<v8:Type>xs:boolean</v8:Type>')
elif re.match(r'^String', resolved):
X(f'{i}\t<v8:Type>xs:string</v8:Type>')
X(f'{i}\t<v8:StringQualifiers>')
X(f'{i}\t\t<v8:Length>0</v8:Length>')
X(f'{i}\t\t<v8:AllowedLength>Variable</v8:AllowedLength>')
X(f'{i}\t</v8:StringQualifiers>')
else:
X(f'{i}\t<v8:Type>cfg:{resolved}</v8:Type>')
X(f'{i}</Type>')
else:
X(f'{i}<Type/>')
def emit_common_module_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
context = str(defn['context']) if defn.get('context') else ''
global_val = 'true' if defn.get('global') is True else 'false'
server = 'false'
server_call = 'false'
client_managed = 'false'
client_ordinary = 'false'
external_connection = 'false'
privileged = 'false'
if context == 'server' or context == 'serverCall':
server = 'true'
server_call = 'true'
elif context == 'client':
client_managed = 'true'
elif context == 'serverClient':
server = 'true'
client_managed = 'true'
else:
if defn.get('server') is True:
server = 'true'
if defn.get('serverCall') is True:
server_call = 'true'
if defn.get('clientManagedApplication') is True:
client_managed = 'true'
if defn.get('clientOrdinaryApplication') is True:
client_ordinary = 'true'
if defn.get('externalConnection') is True:
external_connection = 'true'
if defn.get('privileged') is True:
privileged = 'true'
X(f'{i}<Global>{global_val}</Global>')
X(f'{i}<ClientManagedApplication>{client_managed}</ClientManagedApplication>')
X(f'{i}<Server>{server}</Server>')
X(f'{i}<ExternalConnection>{external_connection}</ExternalConnection>')
X(f'{i}<ClientOrdinaryApplication>{client_ordinary}</ClientOrdinaryApplication>')
X(f'{i}<ServerCall>{server_call}</ServerCall>')
X(f'{i}<Privileged>{privileged}</Privileged>')
return_values_reuse = get_enum_prop('ReturnValuesReuse', 'returnValuesReuse', 'DontUse')
X(f'{i}<ReturnValuesReuse>{return_values_reuse}</ReturnValuesReuse>')
def emit_scheduled_job_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
method_name = str(defn['methodName']) if defn.get('methodName') else ''
# Ensure CommonModule. prefix
if method_name and not method_name.startswith('CommonModule.'):
method_name = f'CommonModule.{method_name}'
X(f'{i}<MethodName>{esc_xml(method_name)}</MethodName>')
# synonym может быть {ru,en}; Description — плоская строка, берём ru-текст.
description = str(defn['description']) if defn.get('description') else (synonym if isinstance(synonym, str) else '')
X(f'{i}<Description>{esc_xml(description)}</Description>')
key = str(defn['key']) if defn.get('key') else ''
X(f'{i}<Key>{esc_xml(key)}</Key>')
use = 'true' if defn.get('use') is True else 'false'
X(f'{i}<Use>{use}</Use>')
predefined = 'true' if defn.get('predefined') is True else 'false'
X(f'{i}<Predefined>{predefined}</Predefined>')
restart_count = str(defn['restartCountOnFailure']) if defn.get('restartCountOnFailure') is not None else '3'
restart_interval = str(defn['restartIntervalOnFailure']) if defn.get('restartIntervalOnFailure') is not None else '10'
X(f'{i}<RestartCountOnFailure>{restart_count}</RestartCountOnFailure>')
X(f'{i}<RestartIntervalOnFailure>{restart_interval}</RestartIntervalOnFailure>')
def emit_event_subscription_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
sources = list(defn.get('source', []))
if sources:
X(f'{i}<Source>')
for src in sources:
resolved = resolve_type_str(str(src))
X(f'{i}\t<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{resolved}</v8:Type>')
X(f'{i}</Source>')
else:
X(f'{i}<Source/>')
event = str(defn['event']) if defn.get('event') else 'BeforeWrite'
X(f'{i}<Event>{event}</Event>')
handler = str(defn['handler']) if defn.get('handler') else ''
# Ensure CommonModule. prefix
if handler and not handler.startswith('CommonModule.'):
handler = f'CommonModule.{handler}'
X(f'{i}<Handler>{esc_xml(handler)}</Handler>')
# --- 13b. Report, DataProcessor ---
def emit_report_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
default_form = str(defn['defaultForm']) if defn.get('defaultForm') else ''
if default_form:
X(f'{i}<DefaultForm>{default_form}</DefaultForm>')
else:
X(f'{i}<DefaultForm/>')
aux_form = str(defn['auxiliaryForm']) if defn.get('auxiliaryForm') else ''
if aux_form:
X(f'{i}<AuxiliaryForm>{aux_form}</AuxiliaryForm>')
else:
X(f'{i}<AuxiliaryForm/>')
main_dcs = str(defn['mainDataCompositionSchema']) if defn.get('mainDataCompositionSchema') else ''
if main_dcs:
X(f'{i}<MainDataCompositionSchema>{main_dcs}</MainDataCompositionSchema>')
else:
X(f'{i}<MainDataCompositionSchema/>')
def_settings = str(defn['defaultSettingsForm']) if defn.get('defaultSettingsForm') else ''
if def_settings:
X(f'{i}<DefaultSettingsForm>{def_settings}</DefaultSettingsForm>')
else:
X(f'{i}<DefaultSettingsForm/>')
aux_settings = str(defn['auxiliarySettingsForm']) if defn.get('auxiliarySettingsForm') else ''
if aux_settings:
X(f'{i}<AuxiliarySettingsForm>{aux_settings}</AuxiliarySettingsForm>')
else:
X(f'{i}<AuxiliarySettingsForm/>')
def_variant = str(defn['defaultVariantForm']) if defn.get('defaultVariantForm') else ''
if def_variant:
X(f'{i}<DefaultVariantForm>{def_variant}</DefaultVariantForm>')
else:
X(f'{i}<DefaultVariantForm/>')
X(f'{i}<VariantsStorage/>')
X(f'{i}<SettingsStorage/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<ExtendedPresentation/>')
X(f'{i}<Explanation/>')
def emit_data_processor_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>false</UseStandardCommands>')
default_form = str(defn['defaultForm']) if defn.get('defaultForm') else ''
if default_form:
X(f'{i}<DefaultForm>{default_form}</DefaultForm>')
else:
X(f'{i}<DefaultForm/>')
aux_form = str(defn['auxiliaryForm']) if defn.get('auxiliaryForm') else ''
if aux_form:
X(f'{i}<AuxiliaryForm>{aux_form}</AuxiliaryForm>')
else:
X(f'{i}<AuxiliaryForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<ExtendedPresentation/>')
X(f'{i}<Explanation/>')
# --- 13c. ExchangePlan, ChartOfCharacteristicTypes, DocumentJournal ---
def emit_exchange_plan_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
if defn.get('comment'):
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
else:
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>{"true" if get_bool_prop("useStandardCommands", True) else "false"}</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '150'
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<DefaultPresentation>{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsDescription")}</DefaultPresentation>')
X(f'{i}<EditType>{get_enum_prop("EditType", "editType", "InDialog")}</EditType>')
X(f'{i}<QuickChoice>{"true" if defn.get("quickChoice") is True else "false"}</QuickChoice>')
X(f'{i}<ChoiceMode>{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}</ChoiceMode>')
# InputByString: override `inputByString` ЛИБО дефолт [Descr при D>0]+[Code при C>0] (prefix ExchangePlan).
if 'inputByString' in defn:
ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])]
else:
ib_fields = []
if int(description_length) > 0:
ib_fields.append(f'ExchangePlan.{obj_name}.StandardAttribute.Description')
if int(code_length) > 0:
ib_fields.append(f'ExchangePlan.{obj_name}.StandardAttribute.Code')
emit_field_block(i, 'InputByString', ib_fields)
X(f'{i}<SearchStringModeOnInputByString>{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
emit_form_ref(i, 'DefaultObjectForm', defn.get('defaultObjectForm'))
emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm'))
emit_form_ref(i, 'DefaultChoiceForm', defn.get('defaultChoiceForm'))
emit_form_ref(i, 'AuxiliaryObjectForm', defn.get('auxiliaryObjectForm'))
emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm'))
emit_form_ref(i, 'AuxiliaryChoiceForm', defn.get('auxiliaryChoiceForm'))
emit_standard_attributes(i, 'ExchangePlan')
emit_characteristics(i, defn.get('characteristics'))
emit_based_on(i, defn.get('basedOn'))
distributed = 'true' if defn.get('distributedInfoBase') is True else 'false'
include_ext = 'true' if defn.get('includeConfigurationExtensions') is True else 'false'
X(f'{i}<DistributedInfoBase>{distributed}</DistributedInfoBase>')
X(f'{i}<IncludeConfigurationExtensions>{include_ext}</IncludeConfigurationExtensions>')
X(f'{i}<CreateOnInput>{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}</CreateOnInput>')
X(f'{i}<ChoiceHistoryOnInput>{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}</ChoiceHistoryOnInput>')
X(f'{i}<IncludeHelpInContents>{"true" if get_bool_prop("includeHelpInContents", False) else "false"}</IncludeHelpInContents>')
dl_fields = [expand_data_path(str(x)) for x in defn.get('dataLockFields', [])] if 'dataLockFields' in defn else []
emit_field_block(i, 'DataLockFields', dl_fields)
X(f'{i}<DataLockControlMode>{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}</DataLockControlMode>')
X(f'{i}<FullTextSearch>{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<DataHistory>{get_enum_prop("DataHistory", "dataHistory", "DontUse")}</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_chart_of_characteristic_types_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
if defn.get('comment'):
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
else:
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>{"true" if get_bool_prop("useStandardCommands", True) else "false"}</UseStandardCommands>')
X(f'{i}<IncludeHelpInContents>{"true" if get_bool_prop("includeHelpInContents", False) else "false"}</IncludeHelpInContents>')
if defn.get('characteristicExtValues'):
X(f'{i}<CharacteristicExtValues>{esc_xml(str(defn["characteristicExtValues"]))}</CharacteristicExtValues>')
else:
X(f'{i}<CharacteristicExtValues/>')
vt = defn.get('valueType')
if not vt and defn.get('valueTypes'):
vt = ' + '.join(defn['valueTypes'])
if vt:
X(f'{i}<Type>')
emit_type_content(f'{i}\t', str(vt))
X(f'{i}</Type>')
else:
X(f'{i}<Type>')
X(f'{i}\t<v8:Type>xs:boolean</v8:Type>')
X(f'{i}\t<v8:Type>xs:string</v8:Type>')
X(f'{i}\t<v8:StringQualifiers>')
X(f'{i}\t\t<v8:Length>100</v8:Length>')
X(f'{i}\t\t<v8:AllowedLength>Variable</v8:AllowedLength>')
X(f'{i}\t</v8:StringQualifiers>')
X(f'{i}\t<v8:Type>xs:decimal</v8:Type>')
X(f'{i}\t<v8:NumberQualifiers>')
X(f'{i}\t\t<v8:Digits>15</v8:Digits>')
X(f'{i}\t\t<v8:FractionDigits>2</v8:FractionDigits>')
X(f'{i}\t\t<v8:AllowedSign>Any</v8:AllowedSign>')
X(f'{i}\t</v8:NumberQualifiers>')
X(f'{i}\t<v8:Type>xs:dateTime</v8:Type>')
X(f'{i}\t<v8:DateQualifiers>')
X(f'{i}\t\t<v8:DateFractions>DateTime</v8:DateFractions>')
X(f'{i}\t</v8:DateQualifiers>')
X(f'{i}</Type>')
X(f'{i}<Hierarchical>{"true" if defn.get("hierarchical") is True else "false"}</Hierarchical>')
X(f'{i}<FoldersOnTop>{"false" if defn.get("foldersOnTop") is False else "true"}</FoldersOnTop>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '100'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeAllowedLength>{get_enum_prop("CodeAllowedLength", "codeAllowedLength", "Variable")}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CodeSeries>{get_enum_prop("CodeSeries", "codeSeries", "WholeCharacteristicKind")}</CodeSeries>')
X(f'{i}<CheckUnique>{"false" if defn.get("checkUnique") is False else "true"}</CheckUnique>')
X(f'{i}<Autonumbering>{"false" if defn.get("autonumbering") is False else "true"}</Autonumbering>')
X(f'{i}<DefaultPresentation>{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsDescription")}</DefaultPresentation>')
emit_standard_attributes(i, 'ChartOfCharacteristicTypes')
emit_characteristics(i, defn.get('characteristics'))
X(f'{i}<PredefinedDataUpdate>{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}</PredefinedDataUpdate>')
X(f'{i}<EditType>{get_enum_prop("EditType", "editType", "InDialog")}</EditType>')
X(f'{i}<QuickChoice>{"true" if defn.get("quickChoice") is True else "false"}</QuickChoice>')
X(f'{i}<ChoiceMode>{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}</ChoiceMode>')
if 'inputByString' in defn:
ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])]
else:
ib_fields = []
if int(description_length) > 0:
ib_fields.append(f'ChartOfCharacteristicTypes.{obj_name}.StandardAttribute.Description')
if int(code_length) > 0:
ib_fields.append(f'ChartOfCharacteristicTypes.{obj_name}.StandardAttribute.Code')
emit_field_block(i, 'InputByString', ib_fields)
X(f'{i}<CreateOnInput>{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}</CreateOnInput>')
X(f'{i}<SearchStringModeOnInputByString>{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}</SearchStringModeOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceHistoryOnInput>{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}</ChoiceHistoryOnInput>')
emit_form_ref(i, 'DefaultObjectForm', defn.get('defaultObjectForm'))
emit_form_ref(i, 'DefaultFolderForm', defn.get('defaultFolderForm'))
emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm'))
emit_form_ref(i, 'DefaultChoiceForm', defn.get('defaultChoiceForm'))
emit_form_ref(i, 'DefaultFolderChoiceForm', defn.get('defaultFolderChoiceForm'))
emit_form_ref(i, 'AuxiliaryObjectForm', defn.get('auxiliaryObjectForm'))
emit_form_ref(i, 'AuxiliaryFolderForm', defn.get('auxiliaryFolderForm'))
emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm'))
emit_form_ref(i, 'AuxiliaryChoiceForm', defn.get('auxiliaryChoiceForm'))
emit_form_ref(i, 'AuxiliaryFolderChoiceForm', defn.get('auxiliaryFolderChoiceForm'))
emit_based_on(i, defn.get('basedOn'))
dl_fields = [expand_data_path(str(x)) for x in defn.get('dataLockFields', [])] if 'dataLockFields' in defn else []
emit_field_block(i, 'DataLockFields', dl_fields)
X(f'{i}<DataLockControlMode>{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}</DataLockControlMode>')
X(f'{i}<FullTextSearch>{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<DataHistory>{get_enum_prop("DataHistory", "dataHistory", "DontUse")}</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_document_journal_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
default_form = str(defn['defaultForm']) if defn.get('defaultForm') else ''
if default_form:
X(f'{i}<DefaultForm>{default_form}</DefaultForm>')
else:
X(f'{i}<DefaultForm/>')
aux_form = str(defn['auxiliaryForm']) if defn.get('auxiliaryForm') else ''
if aux_form:
X(f'{i}<AuxiliaryForm>{aux_form}</AuxiliaryForm>')
else:
X(f'{i}<AuxiliaryForm/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
reg_docs = list(defn.get('registeredDocuments', []))
if reg_docs:
X(f'{i}<RegisteredDocuments>')
for rd in reg_docs:
rd_str = str(rd)
if '.' in rd_str:
dot_idx = rd_str.index('.')
rd_prefix = rd_str[:dot_idx]
rd_suffix = rd_str[dot_idx + 1:]
if rd_prefix in object_type_synonyms:
rd_prefix = object_type_synonyms[rd_prefix]
rd_str = f'{rd_prefix}.{rd_suffix}'
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{rd_str}</xr:Item>')
X(f'{i}</RegisteredDocuments>')
else:
X(f'{i}<RegisteredDocuments/>')
emit_standard_attributes(i, 'DocumentJournal')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
X(f'{i}<Explanation/>')
def resolve_type_prefix_syn(ref):
"""Ссылка на объект: русский префикс типа → английский (ПланВидовХарактеристик.X → ChartOfCharacteristicTypes.X)."""
if ref and '.' in ref:
p, s = ref.split('.', 1)
if p in object_type_synonyms:
p = object_type_synonyms[p]
return f'{p}.{s}'
return ref
def emit_chart_of_accounts_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
if defn.get('comment'):
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
else:
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>{"true" if get_bool_prop("useStandardCommands", True) else "false"}</UseStandardCommands>')
X(f'{i}<IncludeHelpInContents>{"true" if get_bool_prop("includeHelpInContents", False) else "false"}</IncludeHelpInContents>')
emit_based_on(i, defn.get('basedOn'))
# ExtDimensionTypes — ссылка на ПВХ видов субконто (прощающий ввод: ПланВидовХарактеристик.X → ChartOfCharacteristicTypes.X).
ext_dim_types = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else ''
if ext_dim_types:
X(f'{i}<ExtDimensionTypes>{esc_xml(ext_dim_types)}</ExtDimensionTypes>')
else:
X(f'{i}<ExtDimensionTypes/>')
max_ext_dim = str(defn['maxExtDimensionCount']) if defn.get('maxExtDimensionCount') is not None else '3'
X(f'{i}<MaxExtDimensionCount>{max_ext_dim}</MaxExtDimensionCount>')
if defn.get('codeMask'):
X(f'{i}<CodeMask>{esc_xml_text(str(defn["codeMask"]))}</CodeMask>')
else:
X(f'{i}<CodeMask/>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CodeSeries>{get_enum_prop("CodeSeries", "codeSeries", "WholeChartOfAccounts")}</CodeSeries>')
X(f'{i}<CheckUnique>{"false" if defn.get("checkUnique") is False else "true"}</CheckUnique>')
X(f'{i}<DefaultPresentation>{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsCode")}</DefaultPresentation>')
emit_standard_attributes(i, 'ChartOfAccounts')
emit_characteristics(i, defn.get('characteristics'))
# StandardTabularSections — ExtDimensionTypes (обёртка платформенно-константна: Synonym с пустым lang «Виды субконто»,
# Comment/ToolTip/FillChecking; 4 вложенных стандартных реквизита, ExtDimensionType → FillChecking=ShowError).
X(f'{i}<StandardTabularSections>')
X(f'{i}\t<xr:StandardTabularSection name="ExtDimensionTypes">')
X(f'{i}\t\t<xr:Synonym>')
X(f'{i}\t\t\t<v8:item>')
X(f'{i}\t\t\t\t<v8:lang/>')
X(f'{i}\t\t\t\t<v8:content>Виды субконто</v8:content>')
X(f'{i}\t\t\t</v8:item>')
X(f'{i}\t\t</xr:Synonym>')
X(f'{i}\t\t<xr:Comment/>')
X(f'{i}\t\t<xr:ToolTip/>')
X(f'{i}\t\t<xr:FillChecking>DontCheck</xr:FillChecking>')
X(f'{i}\t\t<xr:StandardAttributes>')
for st_attr in ['TurnoversOnly', 'Predefined', 'ExtDimensionType', 'LineNumber']:
st_ov = {'FillChecking': 'ShowError'} if st_attr == 'ExtDimensionType' else None
emit_standard_attribute(f'{i}\t\t\t', st_attr, st_ov)
X(f'{i}\t\t</xr:StandardAttributes>')
X(f'{i}\t</xr:StandardTabularSection>')
X(f'{i}</StandardTabularSections>')
X(f'{i}<PredefinedDataUpdate>{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}</PredefinedDataUpdate>')
X(f'{i}<EditType>{get_enum_prop("EditType", "editType", "InDialog")}</EditType>')
X(f'{i}<QuickChoice>{"true" if defn.get("quickChoice") is True else "false"}</QuickChoice>')
X(f'{i}<ChoiceMode>{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}</ChoiceMode>')
# InputByString: override ЛИБО дефолт [Descr при D>0]+[Code при C>0] (prefix ChartOfAccounts).
if 'inputByString' in defn:
ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])]
else:
ib_fields = []
if int(description_length) > 0:
ib_fields.append(f'ChartOfAccounts.{obj_name}.StandardAttribute.Description')
if int(code_length) > 0:
ib_fields.append(f'ChartOfAccounts.{obj_name}.StandardAttribute.Code')
emit_field_block(i, 'InputByString', ib_fields)
X(f'{i}<SearchStringModeOnInputByString>{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
X(f'{i}<CreateOnInput>{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}</CreateOnInput>')
X(f'{i}<ChoiceHistoryOnInput>{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}</ChoiceHistoryOnInput>')
emit_form_ref(i, 'DefaultObjectForm', defn.get('defaultObjectForm'))
emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm'))
emit_form_ref(i, 'DefaultChoiceForm', defn.get('defaultChoiceForm'))
emit_form_ref(i, 'AuxiliaryObjectForm', defn.get('auxiliaryObjectForm'))
emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm'))
emit_form_ref(i, 'AuxiliaryChoiceForm', defn.get('auxiliaryChoiceForm'))
auto_order = 'false' if defn.get('autoOrderByCode') is False else 'true'
X(f'{i}<AutoOrderByCode>{auto_order}</AutoOrderByCode>')
order_length = str(defn['orderLength']) if defn.get('orderLength') is not None else '9'
X(f'{i}<OrderLength>{order_length}</OrderLength>')
dl_fields = [expand_data_path(str(x)) for x in defn.get('dataLockFields', [])] if 'dataLockFields' in defn else []
emit_field_block(i, 'DataLockFields', dl_fields)
X(f'{i}<DataLockControlMode>{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Automatic")}</DataLockControlMode>')
X(f'{i}<FullTextSearch>{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}</FullTextSearch>')
X(f'{i}<DataHistory>{get_enum_prop("DataHistory", "dataHistory", "DontUse")}</DataHistory>')
upd_dh = 'true' if get_bool_prop('updateDataHistoryImmediatelyAfterWrite', False) else 'false'
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>{upd_dh}</UpdateDataHistoryImmediatelyAfterWrite>')
exec_dh = 'true' if get_bool_prop('executeAfterWriteDataHistoryVersionProcessing', False) else 'false'
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>{exec_dh}</ExecuteAfterWriteDataHistoryVersionProcessing>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
def emit_accounting_register_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<AuxiliaryListForm/>')
chart_of_accounts = str(defn['chartOfAccounts']) if defn.get('chartOfAccounts') else ''
if chart_of_accounts:
X(f'{i}<ChartOfAccounts>{chart_of_accounts}</ChartOfAccounts>')
else:
X(f'{i}<ChartOfAccounts/>')
correspondence = 'true' if defn.get('correspondence') is True else 'false'
X(f'{i}<Correspondence>{correspondence}</Correspondence>')
period_adj_len = str(defn['periodAdjustmentLength']) if defn.get('periodAdjustmentLength') is not None else '0'
X(f'{i}<PeriodAdjustmentLength>{period_adj_len}</PeriodAdjustmentLength>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
emit_standard_attributes(i, 'AccountingRegister')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
X(f'{i}<Explanation/>')
def emit_chart_of_calculation_types_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = get_enum_prop('CodeType', 'codeType', 'String')
code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable')
X(f'{i}<CodeLength>{code_length}</CodeLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<DefaultPresentation>AsDescription</DefaultPresentation>')
dependence = get_enum_prop('DependenceOnCalculationTypes', 'dependenceOnCalculationTypes', 'DontUse')
X(f'{i}<DependenceOnCalculationTypes>{dependence}</DependenceOnCalculationTypes>')
base_types = list(defn.get('baseCalculationTypes', []))
if base_types:
X(f'{i}<BaseCalculationTypes>')
for bt in base_types:
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{bt}</xr:Item>')
X(f'{i}</BaseCalculationTypes>')
else:
X(f'{i}<BaseCalculationTypes/>')
action_period_use = 'true' if defn.get('actionPeriodUse') is True else 'false'
X(f'{i}<ActionPeriodUse>{action_period_use}</ActionPeriodUse>')
emit_standard_attributes(i, 'ChartOfCalculationTypes')
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfCalculationTypes.{obj_name}.StandardAttribute.Description</xr:Field>')
X(f'{i}\t<xr:Field>ChartOfCalculationTypes.{obj_name}.StandardAttribute.Code</xr:Field>')
X(f'{i}</InputByString>')
X(f'{i}<SearchStringModeOnInputByString>Begin</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
X(f'{i}<DefaultObjectForm/>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
X(f'{i}<AuxiliaryObjectForm/>')
X(f'{i}<AuxiliaryListForm/>')
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<BasedOn/>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<CreateOnInput>DontUse</CreateOnInput>')
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
def emit_calculation_register_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<AuxiliaryListForm/>')
chart_of_calc_types = str(defn['chartOfCalculationTypes']) if defn.get('chartOfCalculationTypes') else ''
if chart_of_calc_types:
X(f'{i}<ChartOfCalculationTypes>{chart_of_calc_types}</ChartOfCalculationTypes>')
else:
X(f'{i}<ChartOfCalculationTypes/>')
periodicity = get_enum_prop('InformationRegisterPeriodicity', 'periodicity', 'Month')
X(f'{i}<Periodicity>{periodicity}</Periodicity>')
action_period = 'true' if defn.get('actionPeriod') is True else 'false'
X(f'{i}<ActionPeriod>{action_period}</ActionPeriod>')
base_period = 'true' if defn.get('basePeriod') is True else 'false'
X(f'{i}<BasePeriod>{base_period}</BasePeriod>')
schedule = str(defn['schedule']) if defn.get('schedule') else ''
if schedule:
X(f'{i}<Schedule>{schedule}</Schedule>')
else:
X(f'{i}<Schedule/>')
schedule_value = str(defn['scheduleValue']) if defn.get('scheduleValue') else ''
if schedule_value:
X(f'{i}<ScheduleValue>{schedule_value}</ScheduleValue>')
else:
X(f'{i}<ScheduleValue/>')
schedule_date = str(defn['scheduleDate']) if defn.get('scheduleDate') else ''
if schedule_date:
X(f'{i}<ScheduleDate>{schedule_date}</ScheduleDate>')
else:
X(f'{i}<ScheduleDate/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
emit_standard_attributes(i, 'CalculationRegister')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
X(f'{i}<ListPresentation/>')
X(f'{i}<ExtendedListPresentation/>')
X(f'{i}<Explanation/>')
def emit_business_process_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
edit_type = get_enum_prop('EditType', 'editType', 'InDialog')
X(f'{i}<EditType>{edit_type}</EditType>')
number_type = get_enum_prop('NumberType', 'numberType', 'String')
number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '11'
number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable')
check_unique = 'false' if defn.get('checkUnique') is False else 'true'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
X(f'{i}<NumberType>{number_type}</NumberType>')
X(f'{i}<NumberLength>{number_length}</NumberLength>')
X(f'{i}<NumberAllowedLength>{number_allowed_length}</NumberAllowedLength>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
emit_standard_attributes(i, 'BusinessProcess')
X(f'{i}<Characteristics/>')
task_ref = str(defn['task']) if defn.get('task') else ''
if task_ref:
X(f'{i}<Task>{task_ref}</Task>')
else:
X(f'{i}<Task/>')
X(f'{i}<BasedOn/>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>BusinessProcess.{obj_name}.StandardAttribute.Number</xr:Field>')
X(f'{i}</InputByString>')
X(f'{i}<CreateOnInput>DontUse</CreateOnInput>')
X(f'{i}<SearchStringModeOnInputByString>Begin</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
X(f'{i}<DefaultObjectForm/>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
X(f'{i}<AuxiliaryObjectForm/>')
X(f'{i}<AuxiliaryListForm/>')
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_task_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
number_type = get_enum_prop('NumberType', 'numberType', 'String')
number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '14'
number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable')
check_unique = 'false' if defn.get('checkUnique') is False else 'true'
autonumbering = 'false' if defn.get('autonumbering') is False else 'true'
task_number_auto_prefix = str(defn['taskNumberAutoPrefix']) if defn.get('taskNumberAutoPrefix') else 'BusinessProcessNumber'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '150'
X(f'{i}<NumberType>{number_type}</NumberType>')
X(f'{i}<NumberLength>{number_length}</NumberLength>')
X(f'{i}<NumberAllowedLength>{number_allowed_length}</NumberAllowedLength>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
X(f'{i}<TaskNumberAutoPrefix>{task_number_auto_prefix}</TaskNumberAutoPrefix>')
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
addressing = str(defn['addressing']) if defn.get('addressing') else ''
if addressing:
X(f'{i}<Addressing>{addressing}</Addressing>')
else:
X(f'{i}<Addressing/>')
main_addressing = str(defn['mainAddressingAttribute']) if defn.get('mainAddressingAttribute') else ''
if main_addressing:
X(f'{i}<MainAddressingAttribute>{main_addressing}</MainAddressingAttribute>')
else:
X(f'{i}<MainAddressingAttribute/>')
current_performer = str(defn['currentPerformer']) if defn.get('currentPerformer') else ''
if current_performer:
X(f'{i}<CurrentPerformer>{current_performer}</CurrentPerformer>')
else:
X(f'{i}<CurrentPerformer/>')
emit_standard_attributes(i, 'Task')
X(f'{i}<Characteristics/>')
X(f'{i}<BasedOn/>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>Task.{obj_name}.StandardAttribute.Number</xr:Field>')
X(f'{i}</InputByString>')
X(f'{i}<CreateOnInput>DontUse</CreateOnInput>')
X(f'{i}<SearchStringModeOnInputByString>Begin</SearchStringModeOnInputByString>')
X(f'{i}<FullTextSearchOnInputByString>DontUse</FullTextSearchOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>Directly</ChoiceDataGetModeOnInputByString>')
X(f'{i}<DefaultObjectForm/>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
X(f'{i}<AuxiliaryObjectForm/>')
X(f'{i}<AuxiliaryListForm/>')
X(f'{i}<AuxiliaryChoiceForm/>')
X(f'{i}<IncludeHelpInContents>false</IncludeHelpInContents>')
X(f'{i}<DataLockFields/>')
data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{data_lock_control_mode}</DataLockControlMode>')
full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use')
X(f'{i}<FullTextSearch>{full_text_search}</FullTextSearch>')
emit_mltext(i, 'ObjectPresentation', defn.get('objectPresentation'))
emit_mltext(i, 'ExtendedObjectPresentation', defn.get('extendedObjectPresentation'))
emit_mltext(i, 'ListPresentation', defn.get('listPresentation'))
emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation'))
emit_mltext(i, 'Explanation', defn.get('explanation'))
X(f'{i}<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
X(f'{i}<DataHistory>DontUse</DataHistory>')
X(f'{i}<UpdateDataHistoryImmediatelyAfterWrite>false</UpdateDataHistoryImmediatelyAfterWrite>')
X(f'{i}<ExecuteAfterWriteDataHistoryVersionProcessing>false</ExecuteAfterWriteDataHistoryVersionProcessing>')
def emit_http_service_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
root_url = str(defn['rootURL']) if defn.get('rootURL') else obj_name.lower()
X(f'{i}<RootURL>{esc_xml(root_url)}</RootURL>')
reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse')
X(f'{i}<ReuseSessions>{reuse_sessions}</ReuseSessions>')
session_max_age = str(defn['sessionMaxAge']) if defn.get('sessionMaxAge') is not None else '20'
X(f'{i}<SessionMaxAge>{session_max_age}</SessionMaxAge>')
def emit_web_service_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
X(f'{i}<Comment/>')
namespace = str(defn['namespace']) if defn.get('namespace') else ''
X(f'{i}<Namespace>{esc_xml(namespace)}</Namespace>')
xdto_packages = str(defn['xdtoPackages']) if defn.get('xdtoPackages') else ''
if xdto_packages:
X(f'{i}<XDTOPackages>{xdto_packages}</XDTOPackages>')
else:
X(f'{i}<XDTOPackages/>')
reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse')
X(f'{i}<ReuseSessions>{reuse_sessions}</ReuseSessions>')
session_max_age = str(defn['sessionMaxAge']) if defn.get('sessionMaxAge') is not None else '20'
X(f'{i}<SessionMaxAge>{session_max_age}</SessionMaxAge>')
# --- 13g. ChildObjects emitters for new types ---
def emit_column(indent, col_def):
uid = new_uuid()
name = ''
col_synonym = ''
indexing = 'DontIndex'
references = []
if isinstance(col_def, str):
name = col_def
col_synonym = split_camel_case(name)
else:
name = str(col_def.get('name', ''))
col_synonym = str(col_def['synonym']) if col_def.get('synonym') else split_camel_case(name)
if col_def.get('indexing'):
indexing = str(col_def['indexing'])
if col_def.get('references'):
references = list(col_def['references'])
X(f'{indent}<Column uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(name)}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', col_synonym)
X(f'{indent}\t\t<Comment/>')
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
if references:
X(f'{indent}\t\t<References>')
for ref in references:
X(f'{indent}\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{ref}</xr:Item>')
X(f'{indent}\t\t</References>')
else:
X(f'{indent}\t\t<References/>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Column>')
def emit_url_template(indent, tmpl_name, tmpl_def):
uid = new_uuid()
tmpl_synonym = split_camel_case(tmpl_name)
template = ''
methods = {}
if isinstance(tmpl_def, str):
template = tmpl_def
else:
template = str(tmpl_def['template']) if tmpl_def.get('template') else f'/{tmpl_name.lower()}'
if tmpl_def.get('methods'):
for k, v in tmpl_def['methods'].items():
methods[k] = str(v)
X(f'{indent}<URLTemplate uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(tmpl_name)}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', tmpl_synonym)
X(f'{indent}\t\t<Template>{esc_xml(template)}</Template>')
X(f'{indent}\t</Properties>')
if methods:
X(f'{indent}\t<ChildObjects>')
for method_name, http_method in sorted(methods.items()):
method_uuid = new_uuid()
method_synonym = split_camel_case(method_name)
handler = f'{tmpl_name}{method_name}'
X(f'{indent}\t\t<Method uuid="{method_uuid}">')
X(f'{indent}\t\t\t<Properties>')
X(f'{indent}\t\t\t\t<Name>{esc_xml(method_name)}</Name>')
emit_mltext(f'{indent}\t\t\t\t', 'Synonym', method_synonym)
X(f'{indent}\t\t\t\t<HTTPMethod>{http_method}</HTTPMethod>')
X(f'{indent}\t\t\t\t<Handler>{esc_xml(handler)}</Handler>')
X(f'{indent}\t\t\t</Properties>')
X(f'{indent}\t\t</Method>')
X(f'{indent}\t</ChildObjects>')
else:
X(f'{indent}\t<ChildObjects/>')
X(f'{indent}</URLTemplate>')
def emit_operation(indent, op_name, op_def):
uid = new_uuid()
op_synonym = split_camel_case(op_name)
return_type = 'xs:string'
nillable = 'false'
transactioned = 'false'
handler = op_name
params = {}
if isinstance(op_def, str):
return_type = op_def
else:
if op_def.get('returnType'):
return_type = str(op_def['returnType'])
if op_def.get('nillable') is True:
nillable = 'true'
if op_def.get('transactioned') is True:
transactioned = 'true'
if op_def.get('handler'):
handler = str(op_def['handler'])
if op_def.get('parameters'):
for k, v in op_def['parameters'].items():
params[k] = v
X(f'{indent}<Operation uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(op_name)}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', op_synonym)
X(f'{indent}\t\t<Comment/>')
X(f'{indent}\t\t<XDTOReturningValueType>{return_type}</XDTOReturningValueType>')
X(f'{indent}\t\t<Nillable>{nillable}</Nillable>')
X(f'{indent}\t\t<Transactioned>{transactioned}</Transactioned>')
X(f'{indent}\t\t<ProcedureName>{esc_xml(handler)}</ProcedureName>')
X(f'{indent}\t</Properties>')
if params:
X(f'{indent}\t<ChildObjects>')
for param_name, param_def in sorted(params.items()):
param_uuid = new_uuid()
param_synonym = split_camel_case(param_name)
param_type = 'xs:string'
param_nillable = 'true'
param_dir = 'In'
if isinstance(param_def, str):
param_type = param_def
else:
if param_def.get('type'):
param_type = str(param_def['type'])
if param_def.get('nillable') is False:
param_nillable = 'false'
if param_def.get('direction'):
param_dir = str(param_def['direction'])
X(f'{indent}\t\t<Parameter uuid="{param_uuid}">')
X(f'{indent}\t\t\t<Properties>')
X(f'{indent}\t\t\t\t<Name>{esc_xml(param_name)}</Name>')
emit_mltext(f'{indent}\t\t\t\t', 'Synonym', param_synonym)
X(f'{indent}\t\t\t\t<XDTOValueType>{param_type}</XDTOValueType>')
X(f'{indent}\t\t\t\t<Nillable>{param_nillable}</Nillable>')
X(f'{indent}\t\t\t\t<TransferDirection>{param_dir}</TransferDirection>')
X(f'{indent}\t\t\t</Properties>')
X(f'{indent}\t\t</Parameter>')
X(f'{indent}\t</ChildObjects>')
else:
X(f'{indent}\t<ChildObjects/>')
X(f'{indent}</Operation>')
def emit_addressing_attribute(indent, addr_def):
uid = new_uuid()
name = ''
attr_synonym = ''
type_str = ''
addressing_dimension = ''
indexing = 'Index'
parsed = parse_attribute_shorthand(addr_def)
name = parsed['name']
attr_synonym = parsed['synonym']
type_str = parsed['type']
if not isinstance(addr_def, str):
if addr_def.get('addressingDimension'):
addressing_dimension = str(addr_def['addressingDimension'])
if addr_def.get('indexing'):
indexing = str(addr_def['indexing'])
X(f'{indent}<AddressingAttribute uuid="{uid}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml(name)}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', attr_synonym)
X(f'{indent}\t\t<Comment/>')
if type_str:
emit_value_type(f'{indent}\t\t', type_str)
else:
X(f'{indent}\t\t<Type>')
X(f'{indent}\t\t\t<v8:Type>xs:string</v8:Type>')
X(f'{indent}\t\t</Type>')
if addressing_dimension:
X(f'{indent}\t\t<AddressingDimension>{addressing_dimension}</AddressingDimension>')
else:
X(f'{indent}\t\t<AddressingDimension/>')
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
X(f'{indent}\t\t<FullTextSearch>Use</FullTextSearch>')
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
X(f'{indent}\t</Properties>')
X(f'{indent}</AddressingAttribute>')
# ---------------------------------------------------------------------------
# 14. Namespaces
# ---------------------------------------------------------------------------
xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" 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:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" 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"'
# ---------------------------------------------------------------------------
# 14a. Detect format version from existing Configuration.xml
# ---------------------------------------------------------------------------
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"
format_version = detect_format_version(output_dir)
# ---------------------------------------------------------------------------
# 15. Main assembler
# ---------------------------------------------------------------------------
obj_uuid = new_uuid()
X('<?xml version="1.0" encoding="UTF-8"?>')
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
X(f'\t<{obj_type} uuid="{obj_uuid}">')
# InternalInfo
emit_internal_info('\t\t', obj_type, obj_name)
# Properties
X('\t\t<Properties>')
property_emitters = {
'Catalog': emit_catalog_properties,
'Document': emit_document_properties,
'Enum': emit_enum_properties,
'Constant': emit_constant_properties,
'InformationRegister': emit_information_register_properties,
'AccumulationRegister': emit_accumulation_register_properties,
'DefinedType': emit_defined_type_properties,
'CommonModule': emit_common_module_properties,
'ScheduledJob': emit_scheduled_job_properties,
'EventSubscription': emit_event_subscription_properties,
'Report': emit_report_properties,
'DataProcessor': emit_data_processor_properties,
'ExchangePlan': emit_exchange_plan_properties,
'ChartOfCharacteristicTypes': emit_chart_of_characteristic_types_properties,
'DocumentJournal': emit_document_journal_properties,
'ChartOfAccounts': emit_chart_of_accounts_properties,
'AccountingRegister': emit_accounting_register_properties,
'ChartOfCalculationTypes': emit_chart_of_calculation_types_properties,
'CalculationRegister': emit_calculation_register_properties,
'BusinessProcess': emit_business_process_properties,
'Task': emit_task_properties,
'HTTPService': emit_http_service_properties,
'WebService': emit_web_service_properties,
}
property_emitters[obj_type]('\t\t\t')
X('\t\t</Properties>')
# ChildObjects
has_children = False
# --- Types with Attributes + TabularSections ---
types_with_attr_ts = [
'Catalog', 'Document', 'Report', 'DataProcessor', 'ExchangePlan',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes',
'BusinessProcess', 'Task',
]
commands = [] # заполняется внутри блока types_with_attr_ts; на уровне модуля для записи модулей команд
if obj_type in types_with_attr_ts:
def _as_list(val):
"""Normalize attributes: dict {"K":"V"} → ["K:V"], list/other → list."""
if val is None:
return []
if isinstance(val, dict):
return [f"{k}:{v}" for k, v in val.items()]
return list(val)
attrs = []
if defn.get('attributes'):
for a in _as_list(defn['attributes']):
attrs.append(parse_attribute_shorthand(a))
ts_sections = {}
ts_order = []
if defn.get('tabularSections'):
ts_data = defn['tabularSections']
# Значение ТЧ: массив колонок (синоним авто) ЛИБО объект {attributes/columns, synonym, tooltip, comment}.
def new_ts_entry(val):
if isinstance(val, list):
return {'columns': val, 'synonym': None, 'tooltip': None, 'comment': None, 'lineNumber': None}
cols = _as_list(val.get('attributes') or val.get('columns') or [])
return {'columns': cols, 'synonym': val.get('synonym'), 'tooltip': val.get('tooltip'),
'comment': str(val['comment']) if val.get('comment') else None, 'lineNumber': val.get('lineNumber')}
if isinstance(ts_data, list):
for ts in ts_data:
ts_sections[ts['name']] = new_ts_entry(ts)
ts_order.append(ts['name'])
else:
for k, v in ts_data.items():
ts_sections[k] = new_ts_entry(v)
ts_order.append(k)
# ChartOfAccounts: AccountingFlags + ExtDimensionAccountingFlags (признаки учёта — структурно как реквизит,
# но без Indexing/FullTextSearch/Use; тип по умолчанию Boolean). Парсим как реквизиты.
acct_flags = []
ext_dim_flags = []
if obj_type == 'ChartOfAccounts':
if defn.get('accountingFlags'):
acct_flags = [parse_attribute_shorthand(af) for af in _as_list(defn['accountingFlags'])]
if defn.get('extDimensionAccountingFlags'):
ext_dim_flags = [parse_attribute_shorthand(edf) for edf in _as_list(defn['extDimensionAccountingFlags'])]
# Task: AddressingAttributes
addr_attrs = []
if obj_type == 'Task' and defn.get('addressingAttributes'):
addr_attrs = _as_list(defn['addressingAttributes'])
# Commands (map имя→объект ИЛИ array [{name,...}])
commands = []
if defn.get('commands'):
cd = defn['commands']
if isinstance(cd, list):
for c in cd:
commands.append({'name': str(c.get('name', '')), 'def': c})
else:
for k, v in cd.items():
commands.append({'name': k, 'def': v})
child_count = len(attrs) + len(ts_sections) + len(acct_flags) + len(ext_dim_flags) + len(addr_attrs) + len(commands)
if child_count > 0:
has_children = True
X('\t\t<ChildObjects>')
if obj_type == 'Catalog':
context = 'catalog'
elif obj_type == 'Document':
context = 'document'
elif obj_type in ('DataProcessor', 'Report'):
context = 'processor'
elif obj_type == 'ChartOfCharacteristicTypes':
context = 'catalog' # реквизиты ПВХ структурно как у справочника (Use/FillFromFillingValue/DataHistory)
elif obj_type == 'ChartOfAccounts':
context = 'account' # как catalog, но БЕЗ <Use> (реквизиты ПС не иерархичны как справочник)
elif obj_type == 'ChartOfCalculationTypes':
context = 'chart'
else:
context = 'object'
for a in attrs:
emit_attribute('\t\t\t', a, context)
for ts_name in ts_order:
e = ts_sections[ts_name]
emit_tabular_section('\t\t\t', ts_name, e['columns'], obj_type, obj_name, e['synonym'], e['tooltip'], e['comment'], e.get('lineNumber'))
for af in acct_flags:
emit_attribute('\t\t\t', af, 'account-flag', 'AccountingFlag')
for edf in ext_dim_flags:
emit_attribute('\t\t\t', edf, 'account-flag', 'ExtDimensionAccountingFlag')
for aa in addr_attrs:
emit_addressing_attribute('\t\t\t', aa)
for cmd in commands:
emit_command('\t\t\t', cmd['name'], cmd['def'])
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- Enum: enum values ---
if obj_type == 'Enum':
values = []
if defn.get('values'):
for v in defn['values']:
values.append(parse_enum_value_shorthand(v))
if values:
has_children = True
X('\t\t<ChildObjects>')
for v in values:
emit_enum_value('\t\t\t', v)
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- Constant, DefinedType, ScheduledJob, EventSubscription: no ChildObjects ---
# --- Registers: dimensions + resources + attributes ---
if obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister'):
dims = []
resources = []
reg_attrs = []
if defn.get('dimensions'):
for d in defn['dimensions']:
dims.append(parse_attribute_shorthand(d))
if defn.get('resources'):
for r in defn['resources']:
resources.append(parse_attribute_shorthand(r))
if defn.get('attributes'):
for a in defn['attributes']:
reg_attrs.append(parse_attribute_shorthand(a))
if dims or resources or reg_attrs:
has_children = True
X('\t\t<ChildObjects>')
for r in resources:
emit_resource('\t\t\t', r, obj_type)
for d in dims:
emit_dimension('\t\t\t', d, obj_type)
# InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory;
# AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT.
reg_ctx = 'register-info' if obj_type == 'InformationRegister' else 'register-other'
for a in reg_attrs:
emit_attribute('\t\t\t', a, reg_ctx)
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- DocumentJournal: columns ---
if obj_type == 'DocumentJournal':
columns = list(defn.get('columns', []))
if columns:
has_children = True
X('\t\t<ChildObjects>')
for col in columns:
emit_column('\t\t\t', col)
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- HTTPService: URLTemplates ---
if obj_type == 'HTTPService':
url_templates = {}
url_tmpl_order = []
if defn.get('urlTemplates'):
for k, v in defn['urlTemplates'].items():
url_templates[k] = v
url_tmpl_order.append(k)
if url_templates:
has_children = True
X('\t\t<ChildObjects>')
for tmpl_name in sorted(url_tmpl_order):
emit_url_template('\t\t\t', tmpl_name, url_templates[tmpl_name])
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- WebService: Operations ---
if obj_type == 'WebService':
operations = {}
op_order = []
if defn.get('operations'):
for k, v in defn['operations'].items():
operations[k] = v
op_order.append(k)
if operations:
has_children = True
X('\t\t<ChildObjects>')
for op_name in sorted(op_order):
emit_operation('\t\t\t', op_name, operations[op_name])
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- CommonModule: no ChildObjects ---
X(f'\t</{obj_type}>')
X('</MetaDataObject>')
metadata_xml = '\n'.join(lines) + '\n'
# ---------------------------------------------------------------------------
# 16. Write files
# ---------------------------------------------------------------------------
type_plural_map = {
'Catalog': 'Catalogs',
'Document': 'Documents',
'Enum': 'Enums',
'Constant': 'Constants',
'InformationRegister': 'InformationRegisters',
'AccumulationRegister': 'AccumulationRegisters',
'AccountingRegister': 'AccountingRegisters',
'CalculationRegister': 'CalculationRegisters',
'ChartOfAccounts': 'ChartsOfAccounts',
'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes',
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
'BusinessProcess': 'BusinessProcesses',
'Task': 'Tasks',
'ExchangePlan': 'ExchangePlans',
'DocumentJournal': 'DocumentJournals',
'Report': 'Reports',
'DataProcessor': 'DataProcessors',
'CommonModule': 'CommonModules',
'ScheduledJob': 'ScheduledJobs',
'EventSubscription': 'EventSubscriptions',
'HTTPService': 'HTTPServices',
'WebService': 'WebServices',
'DefinedType': 'DefinedTypes',
}
type_plural = type_plural_map[obj_type]
type_dir = os.path.join(output_dir, type_plural)
# Main XML file
main_xml_path = os.path.join(type_dir, f'{obj_name}.xml')
# Types that don't have subdirectory structure
types_no_sub_dir = ['DefinedType', 'ScheduledJob', 'EventSubscription']
obj_sub_dir = os.path.join(type_dir, obj_name)
ext_dir = os.path.join(obj_sub_dir, 'Ext')
os.makedirs(type_dir, exist_ok=True)
if obj_type not in types_no_sub_dir:
os.makedirs(obj_sub_dir, exist_ok=True)
write_utf8_bom(main_xml_path, metadata_xml)
# Module files
modules_created = []
types_with_object_module = [
'Catalog', 'Document', 'Report', 'DataProcessor', 'ExchangePlan',
'ChartOfAccounts', 'ChartOfCharacteristicTypes', 'ChartOfCalculationTypes',
'BusinessProcess', 'Task',
]
types_with_record_set_module = [
'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister',
]
types_with_manager_module = ['Report', 'DataProcessor', 'Constant', 'Enum']
types_with_value_manager_module = ['Constant']
types_with_module = ['CommonModule', 'HTTPService', 'WebService']
def ensure_ext_dir():
os.makedirs(ext_dir, exist_ok=True)
if obj_type in types_with_object_module:
module_path = os.path.join(ext_dir, 'ObjectModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_manager_module:
module_path = os.path.join(ext_dir, 'ManagerModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_value_manager_module:
module_path = os.path.join(ext_dir, 'ValueManagerModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_record_set_module:
module_path = os.path.join(ext_dir, 'RecordSetModule.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
if obj_type in types_with_module:
module_path = os.path.join(ext_dir, 'Module.bsl')
if not os.path.isfile(module_path):
ensure_ext_dir()
write_utf8_bom(module_path, '')
modules_created.append(module_path)
# --- Predefined data (Ext/Predefined.xml). Элемент: "(Код) Имя [Наименование]" ИЛИ объект (+рус. синонимы).
# Наименование: нет [..]/ключа → авто(Split-CamelCase); [] / "" → пусто; [текст]/текст → как есть.
def resolve_predef_item(val):
if isinstance(val, str):
# Грамматика "(Код) Имя [Наименование]: Тип": сначала вынуть [Наим] (может содержать ':'), затем тип по ':'.
s = val
ptype = None
desc_raw = None
has_desc = False
md = re.search(r'\[(.*)\]', s)
if md:
desc_raw = md.group(1)
has_desc = True
s = re.sub(r'\s*\[.*\]', '', s)
if ':' in s:
left, right = s.split(':', 1)
s = left
ptype = right.strip() # '' → пустой <Type/>
m = re.match(r'^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$', s.strip())
name = m.group(2)
code = m.group(1) if m.group(1) is not None else ''
desc = desc_raw if has_desc else split_camel_case(name)
return {'name': name, 'code': code, 'desc': desc, 'isFolder': False, 'children': [], 'type': ptype}
def gv(keys):
for k in keys:
if k in val:
return val[k]
return None
name = str(gv(['name', 'имя']) or '')
code_v = gv(['code', 'код'])
code = str(code_v) if code_v is not None else ''
has_desc = ('description' in val) or ('наименование' in val)
desc_v = gv(['description', 'наименование'])
desc = ('' if desc_v is None else str(desc_v)) if has_desc else split_camel_case(name)
is_folder = gv(['isFolder', 'группа']) is True
subs = gv(['childItems', 'подчиненные']) or []
type_v = gv(['type', 'тип']) # тип значения характеристики (ПВХ): строка "A + B" ИЛИ массив
if isinstance(type_v, list):
type_v = ' + '.join(type_v)
return {'name': name, 'code': code, 'desc': desc, 'isFolder': is_folder, 'children': list(subs), 'type': type_v}
def emit_predef_item(out, val, indent, code_type):
r = resolve_predef_item(val)
out.append(f'{indent}<Item id="{new_uuid()}">')
out.append(f'{indent}\t<Name>{esc_xml_text(r["name"])}</Name>')
if not r['code']:
out.append(f'{indent}\t<Code/>')
elif code_type == 'Number':
out.append(f'{indent}\t<Code xsi:type="xs:decimal">{esc_xml_text(r["code"])}</Code>')
else:
out.append(f'{indent}\t<Code>{esc_xml_text(r["code"])}</Code>')
if r['desc'] == '':
out.append(f'{indent}\t<Description/>')
else:
out.append(f'{indent}\t<Description>{esc_xml_text(r["desc"])}</Description>')
# Type — тип значения предопределённой характеристики (ПВХ). None→нет блока; ''→<Type/>; 'A + B'→наполненный.
rt = r.get('type')
if rt is not None and str(rt) == '':
out.append(f'{indent}\t<Type/>')
elif rt:
out.append(f'{indent}\t<Type>')
global lines
saved = lines
lines = []
emit_type_content(indent + '\t\t', str(rt))
out.extend(lines)
lines = saved
out.append(f'{indent}\t</Type>')
out.append(f'{indent}\t<IsFolder>{"true" if r["isFolder"] else "false"}</IsFolder>')
if r['children']:
out.append(f'{indent}\t<ChildItems>')
for c in r['children']:
emit_predef_item(out, c, indent + '\t\t', code_type)
out.append(f'{indent}\t</ChildItems>')
out.append(f'{indent}</Item>')
def build_predefined_xml(items, xsi_type, code_type):
out = ['<?xml version="1.0" encoding="UTF-8"?>']
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" 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" xsi:type="{xsi_type}" version="{format_version}">')
for it in items:
emit_predef_item(out, it, '\t', code_type)
out.append('</PredefinedData>')
return '\n'.join(out) + '\n'
# --- Предопределённые СЧЕТА Плана счетов (отдельная грамматика: AccountType/OffBalance/Order/AccountingFlags/
# ExtDimensionTypes/ChildItems). Флаги перечисляем по def-порядку признаков плана; в DSL — только TRUE. ---
def _predef_acc_get(o, keys):
for k in keys:
if isinstance(o, dict):
if k in o:
return o[k]
return None
def emit_predef_account_flags(out, indent, tag, ref_kind, obj_nm, flag_names, true_set):
if not flag_names:
out.append(f'{indent}<{tag}/>')
return
tset = set(str(t) for t in (true_set or []))
out.append(f'{indent}<{tag}>')
for fn in flag_names:
v = 'true' if fn in tset else 'false'
out.append(f'{indent}\t<Flag ref="ChartOfAccounts.{obj_nm}.{ref_kind}.{fn}">{v}</Flag>')
out.append(f'{indent}</{tag}>')
def emit_predef_account(out, val, indent, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''):
name = str(_predef_acc_get(val, ('name', 'имя')) or '')
code_v = _predef_acc_get(val, ('code', 'код'))
code = str(code_v) if code_v is not None else ''
has_desc = isinstance(val, dict) and ('description' in val or 'наименование' in val)
desc_v = _predef_acc_get(val, ('description', 'наименование'))
desc = str(desc_v) if has_desc else split_camel_case(name)
acct_type = str(_predef_acc_get(val, ('accountType', 'видСчета', 'вид')) or '') or 'ActivePassive'
off = 'true' if _predef_acc_get(val, ('offBalance', 'забалансовый')) is True else 'false'
order = str(_predef_acc_get(val, ('order', 'порядок')) or '')
flags = _predef_acc_get(val, ('flags', 'признаки'))
subconto = _predef_acc_get(val, ('subconto', 'extDimensionTypes', 'видыСубконто'))
children = _predef_acc_get(val, ('childItems', 'подчиненные'))
out.append(f'{indent}<Item id="{new_uuid()}">')
out.append(f'{indent}\t<Name>{esc_xml_text(name)}</Name>')
out.append(f'{indent}\t<Code/>' if not code else f'{indent}\t<Code>{esc_xml_text(code)}</Code>')
out.append(f'{indent}\t<Description/>' if desc == '' else f'{indent}\t<Description>{esc_xml_text(desc)}</Description>')
out.append(f'{indent}\t<AccountType>{acct_type}</AccountType>')
out.append(f'{indent}\t<OffBalance>{off}</OffBalance>')
out.append(f'{indent}\t<Order>{esc_xml_text(order)}</Order>')
emit_predef_account_flags(out, f'{indent}\t', 'AccountingFlags', 'AccountingFlag', obj_nm, acct_flag_names, flags)
sub_arr = list(subconto) if subconto else []
if not sub_arr:
out.append(f'{indent}\t<ExtDimensionTypes/>')
else:
out.append(f'{indent}\t<ExtDimensionTypes>')
for sc in sub_arr:
# Строковая форма "Тип | Признак1, Признак2" (флаги после |, turnover=false). Объектная — {type, turnover?, flags?}.
if isinstance(sc, str):
sc_turn_v = None
if '|' in sc:
tpart, fpart = sc.split('|', 1)
sc_type = tpart.strip()
sc_flags = [x.strip() for x in fpart.split(',') if x.strip()]
else:
sc_type = sc.strip()
sc_flags = None
else:
sc_type = str(_predef_acc_get(sc, ('type', 'тип')) or '')
sc_turn_v = _predef_acc_get(sc, ('turnover', 'толькоОбороты', 'оборотный'))
sc_flags = _predef_acc_get(sc, ('flags', 'признаки'))
# Короткая запись: голое имя значения → префикс ПВХ видов субконто плана (extDimensionTypes); иначе резолв синонима.
if '.' not in sc_type:
if ext_dim_types_ref:
sc_type = f'{ext_dim_types_ref}.{sc_type}'
else:
sc_type = resolve_type_prefix_syn(sc_type)
# «Только обороты» — токен в списке flags (или отдельный ключ turnover); вынимаем из настоящих признаков.
sc_turn = 'true' if sc_turn_v is True else 'false'
real_flags = []
for f in (sc_flags or []):
if str(f).strip().lower() in SUBCONTO_TURNOVER_TOKENS:
sc_turn = 'true'
else:
real_flags.append(f)
sc_flags = real_flags
out.append(f'{indent}\t\t<ExtDimensionType name="{esc_xml(sc_type)}">')
out.append(f'{indent}\t\t\t<Turnover>{sc_turn}</Turnover>')
emit_predef_account_flags(out, f'{indent}\t\t\t', 'AccountingFlags', 'ExtDimensionAccountingFlag', obj_nm, ext_dim_flag_names, sc_flags)
out.append(f'{indent}\t\t</ExtDimensionType>')
out.append(f'{indent}\t</ExtDimensionTypes>')
child_arr = list(children) if children else []
if child_arr:
out.append(f'{indent}\t<ChildItems>')
for c in child_arr:
emit_predef_account(out, c, f'{indent}\t\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref)
out.append(f'{indent}\t</ChildItems>')
out.append(f'{indent}</Item>')
def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''):
out = ['<?xml version="1.0" encoding="UTF-8"?>']
out.append(f'<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" xmlns:v8="http://v8.1c.ru/8.1/data/core" 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" xsi:type="ChartOfAccountsPredefinedItems" version="{format_version}">')
for it in items:
emit_predef_account(out, it, '\t', obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref)
out.append('</PredefinedData>')
return '\n'.join(out) + '\n'
# Special files
if obj_type == 'ExchangePlan':
content_path = os.path.join(ext_dir, 'Content.xml')
if not os.path.isfile(content_path):
ensure_ext_dir()
content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" version="{format_version}"/>\r\n'
write_utf8_bom(content_path, content_xml)
modules_created.append(content_path)
if obj_type == 'BusinessProcess':
flowchart_path = os.path.join(ext_dir, 'Flowchart.xml')
if not os.path.isfile(flowchart_path):
ensure_ext_dir()
flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n'
write_utf8_bom(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path)
# Предопределённые элементы (Ext/Predefined.xml). Root-элемент по типу.
predef_root_by_type = {'Catalog': 'CatalogPredefinedItems', 'ChartOfCharacteristicTypes': 'PlanOfCharacteristicKindPredefinedItems'}
if obj_type == 'ChartOfAccounts' and defn.get('predefined'):
# Предопределённые СЧЕТА — отдельная грамматика (флаги разворачиваются по def-порядку признаков плана).
ensure_ext_dir()
af_names = [parse_attribute_shorthand(af)['name'] for af in _as_list(defn['accountingFlags'])] if defn.get('accountingFlags') else []
edf_names = [parse_attribute_shorthand(edf)['name'] for edf in _as_list(defn['extDimensionAccountingFlags'])] if defn.get('extDimensionAccountingFlags') else []
edt_ref = resolve_type_prefix_syn(str(defn['extDimensionTypes'])) if defn.get('extDimensionTypes') else ''
predef_xml = build_predefined_account_xml(defn['predefined'], obj_name, af_names, edf_names, edt_ref)
predef_path = os.path.join(ext_dir, 'Predefined.xml')
write_utf8_bom(predef_path, predef_xml)
modules_created.append(predef_path)
elif obj_type in predef_root_by_type and defn.get('predefined'):
ensure_ext_dir()
cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String'
predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type)
predef_path = os.path.join(ext_dir, 'Predefined.xml')
write_utf8_bom(predef_path, predef_xml)
modules_created.append(predef_path)
# Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика.
if commands:
cmd_module_stub = '&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n'
for cmd in commands:
cmd_dir = os.path.join(obj_sub_dir, 'Commands', cmd['name'], 'Ext')
os.makedirs(cmd_dir, exist_ok=True)
cmd_mod_path = os.path.join(cmd_dir, 'CommandModule.bsl')
write_utf8_bom(cmd_mod_path, cmd_module_stub)
modules_created.append(cmd_mod_path)
# ---------------------------------------------------------------------------
# 17. Register in Configuration.xml
# ---------------------------------------------------------------------------
config_xml_path = os.path.join(output_dir, 'Configuration.xml')
reg_result = None
child_tag = obj_type
if os.path.isfile(config_xml_path):
# Parse preserving whitespace via raw string manipulation
with open(config_xml_path, 'r', encoding='utf-8-sig') as f:
config_content = f.read()
ns = 'http://v8.1c.ru/8.3/MDClasses'
ET.register_namespace('', ns)
# Parse all namespaces used in the file
# Use iterparse to collect namespace prefixes
namespaces_in_file = {}
for evt, elem in ET.iterparse(config_xml_path, events=['start-ns']):
prefix, uri = elem
if prefix:
namespaces_in_file[prefix] = uri
ET.register_namespace(prefix, uri)
tree = ET.parse(config_xml_path)
root = tree.getroot()
child_objects = root.find(f'{{{ns}}}Configuration/{{{ns}}}ChildObjects')
if child_objects is None:
# Try direct path
config_elem = root.find(f'{{{ns}}}Configuration')
if config_elem is not None:
child_objects = config_elem.find(f'{{{ns}}}ChildObjects')
if child_objects is not None:
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
already_exists = False
for e in existing:
if (e.text or '').strip() == obj_name:
already_exists = True
break
if already_exists:
reg_result = 'already'
else:
new_elem = ET.SubElement(child_objects, f'{{{ns}}}{child_tag}')
new_elem.text = obj_name
if existing:
# Insert after last existing element of same type
last_elem = existing[-1]
all_children = list(child_objects)
idx = all_children.index(last_elem)
child_objects.remove(new_elem)
child_objects.insert(idx + 1, new_elem)
# Write back preserving BOM
tree.write(config_xml_path, encoding='utf-8', xml_declaration=True)
# Re-read to add BOM, fix declaration quotes, ensure trailing newline
with open(config_xml_path, 'r', encoding='utf-8') as f:
raw = f.read()
if raw.startswith("<?xml version='1.0' encoding='utf-8'?>"):
raw = raw.replace("<?xml version='1.0' encoding='utf-8'?>", '<?xml version="1.0" encoding="UTF-8"?>', 1)
if not raw.endswith('\n'):
raw += '\n'
write_utf8_bom(config_xml_path, raw)
reg_result = 'added'
else:
reg_result = 'no-childobj'
else:
reg_result = 'no-config'
# ---------------------------------------------------------------------------
# 18. Summary
# ---------------------------------------------------------------------------
attr_count = len(defn.get('attributes', []))
ts_count = 0
if defn.get('tabularSections'):
ts_data = defn['tabularSections']
if isinstance(ts_data, list):
ts_count = len(ts_data)
else:
ts_count = len(ts_data)
dim_count = len(defn.get('dimensions', []))
res_count = len(defn.get('resources', []))
val_count = len(defn.get('values', []))
col_count = len(defn.get('columns', []))
print(f"[OK] {obj_type} '{obj_name}' compiled")
print(f' UUID: {obj_uuid}')
print(f' File: {main_xml_path}')
details = []
if attr_count > 0:
details.append(f'Attributes: {attr_count}')
if ts_count > 0:
details.append(f'TabularSections: {ts_count}')
if dim_count > 0:
details.append(f'Dimensions: {dim_count}')
if res_count > 0:
details.append(f'Resources: {res_count}')
if val_count > 0:
details.append(f'Values: {val_count}')
if col_count > 0:
details.append(f'Columns: {col_count}')
if details:
print(f" {', '.join(details)}")
for mc in modules_created:
print(f' Module: {mc}')
if reg_result == 'added':
print(f' Configuration.xml: <{child_tag}>{obj_name}</{child_tag}> added to ChildObjects')
elif reg_result == 'already':
print(f' Configuration.xml: <{child_tag}>{obj_name}</{child_tag}> already registered')
elif reg_result == 'no-childobj':
print('WARNING: Configuration.xml found but <ChildObjects> not found', file=sys.stderr)
elif reg_result == 'no-config':
print(f' Configuration.xml: not found at {config_xml_path} (register manually)')
# Cross-reference hints
if obj_type == 'AccountingRegister' and not defn.get('chartOfAccounts'):
print('[HINT] AccountingRegister requires ChartOfAccounts reference:')
print(' /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
if obj_type == 'CalculationRegister' and not defn.get('chartOfCalculationTypes'):
print('[HINT] CalculationRegister requires ChartOfCalculationTypes reference:')
print(' /meta-edit -Operation modify-property -Value "ChartOfCalculationTypes=ChartOfCalculationTypes.XXX"')
if obj_type == 'BusinessProcess' and not defn.get('task'):
print('[HINT] BusinessProcess requires Task reference:')
print(' /meta-edit -Operation modify-property -Value "Task=Task.XXX"')
if obj_type == 'ChartOfAccounts':
max_ext_dim = int(defn['maxExtDimensionCount']) if defn.get('maxExtDimensionCount') is not None else 0
if max_ext_dim > 0 and not defn.get('extDimensionTypes'):
print('[HINT] ChartOfAccounts with MaxExtDimensionCount>0 requires ExtDimensionTypes:')
print(' /meta-edit -Operation modify-property -Value "ExtDimensionTypes=ChartOfCharacteristicTypes.XXX"')