#!/usr/bin/env python3 # meta-compile v1.65 — 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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') def esc_xml_text(s): # Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw). return s.replace('&', '&').replace('<', '<').replace('>', '>') 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-значение: строка → один ru; dict {lang: content} → item на язык (в порядке ключей). def emit_ml_items(indent, val): if isinstance(val, dict): for k, v in val.items(): X(f'{indent}') X(f'{indent}\t{k}') X(f'{indent}\t{esc_xml_text(str(v))}') X(f'{indent}') else: X(f'{indent}') X(f'{indent}\tru') X(f'{indent}\t{esc_xml_text(str(val))}') X(f'{indent}') 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}') # --------------------------------------------------------------------------- # 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) # Лоуэркейзим хвост, СОХРАНЯЯ аббревиатуры (зеркало эвристики платформы): максимальный прогон # заглавных длиной >=2, если сразу за ним НЕ буква (пробел/цифра/спецсимвол/конец) — остаётся заглавным # (НДС, ЕГАИС, ОС, ЭП). Прилипшие предлоги (СКлиентами)/бренды (ЮКасса) идут перед буквой → лоуэркейз. # Первый символ строки — как есть. if len(result) > 1: chars = list(result) n = len(chars) keep = [False] * n i = 0 while i < n: if chars[i].isupper(): j = i while j < n and chars[j].isupper(): j += 1 after_boundary = (j == n) or (not chars[j].isalpha()) if j - i >= 2 and after_boundary: for k in range(i, j): keep[k] = True i = j else: i += 1 out = [] for idx, c in enumerate(chars): if idx == 0 or keep[idx]: out.append(c) elif c.isupper(): out.append(c.lower()) else: out.append(c) result = ''.join(out) 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', 'ФункциональнаяОпция': 'FunctionalOption', } # 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'], 'FullTextSearchOnInputByString': ['Use', 'DontUse'], 'Category': ['NavigationPanel', 'ActionsPanel', 'FormCommandBar', 'FormNavigationPanel'], } # --- Группы команд объекта (командный интерфейс) --- # Группы командного интерфейса РАЗДЕЛА (панель навигации/действий): команда БЕЗ параметра. Группы формы: параметр доступен. SECTION_COMMAND_GROUPS = [ 'NavigationPanelImportant', 'NavigationPanelOrdinary', 'NavigationPanelSeeAlso', 'ActionsPanelCreate', 'ActionsPanelReports', 'ActionsPanelTools', ] FORM_COMMAND_GROUPS = [ 'FormCommandBarImportant', 'FormCommandBarCreateBasedOn', 'FormNavigationPanelImportant', 'FormNavigationPanelGoTo', 'FormNavigationPanelSeeAlso', ] VALID_COMMAND_GROUPS = SECTION_COMMAND_GROUPS + FORM_COMMAND_GROUPS COMMAND_GROUP_ALIASES = { 'Панель навигации.Важное': 'NavigationPanelImportant', 'Панель навигации.Обычное': 'NavigationPanelOrdinary', 'Панель навигации.См. также': 'NavigationPanelSeeAlso', 'Панель действий.Создать': 'ActionsPanelCreate', 'Панель действий.Отчеты': 'ActionsPanelReports', 'Панель действий.Отчёты': 'ActionsPanelReports', 'Панель действий.Сервис': 'ActionsPanelTools', 'Командная панель формы.Важное': 'FormCommandBarImportant', 'Командная панель формы.Создать на основании': 'FormCommandBarCreateBasedOn', 'Панель навигации формы.Важное': 'FormNavigationPanelImportant', 'Панель навигации формы.Перейти': 'FormNavigationPanelGoTo', 'Панель навигации формы.См. также': 'FormNavigationPanelSeeAlso', } def resolve_command_group(raw, cmd_name): g = (str(raw) if raw is not None else '').strip() if not g: sys.stderr.write(f"Команде '{cmd_name}' не задана группа (group). 1С требует группу. " f"Валидные: {', '.join(VALID_COMMAND_GROUPS)}; либо CommandGroup.<Имя> — кастомная группа.\n") sys.exit(1) if g in COMMAND_GROUP_ALIASES: return COMMAND_GROUP_ALIASES[g] m = re.match(r'^(?:CommandGroup|ГруппаКоманд)\.(.+)$', g) if m: return f'CommandGroup.{m.group(1)}' return g 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): """Ссылка на форму по умолчанию: непустая → значение, иначе .""" if val: X(f'{i}<{tag}>{esc_xml(normalize_form_ref(str(val)))}') else: X(f'{i}<{tag}/>') def emit_verbatim_ref(i, tag, val): """Ссылка verbatim (без normalize_form_ref): формы/схемы/хранилища Report/DataProcessor, где имя формы может быть буквально «Форма» (normalize перевёл бы имя-сегмент Форма→Form) либо ref не-форменного вида.""" if val: X(f'{i}<{tag}>{esc_xml(str(val))}') 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', 'FunctionalOption', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', ] 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', # Платформенные коллекции (прощающий ввод рус. форм). 'таблицазначений': 'ValueTable', 'деревозначений': 'ValueTree', 'списокзначений': 'ValueListType', 'стандартныйпериод': 'StandardPeriod', } # Платформенные типы, требующие префикса v8: (коллекции/периоды, частые в реквизитах обработок/отчётов). v8_platform_types = {"ValueTable", "ValueTree", "ValueList", "ValueListType", "StandardPeriod", "StandardBeginningDate", "PointInTime", "TypeDescription", "FixedArray", "FixedMap", "FixedStructure"} # Типы со ВЫДЕЛЕННЫМ пространством имён (локальный xmlns на ). prefix — канон корпуса. type_namespace_map = { "Chart": {"ns": "http://v8.1c.ru/8.2/data/chart", "prefix": "d5p1"}, "SettingsComposer": {"ns": "http://v8.1c.ru/8.1/data-composition-system/settings", "prefix": "dcsset"}, "SpreadsheetDocument": {"ns": "http://v8.1c.ru/8.2/data/spreadsheet", "prefix": "mxl"}, } # Типы current-config пространства (cfg:, объявлено в корне): голые и объектные. Ссылочные — отдельно (d5p1). cfg_bare_types = {"ConstantsSet", "ReportBuilder", "FilterCriterion"} cfg_object_kinds = {"Catalog", "Document", "Enum", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ChartOfCalculationTypes", "ExchangePlan", "BusinessProcess", "Task", "InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister", "DataProcessor", "Report", "DocumentJournal", "Constant", "ConstantValue", "Sequence", "Recalculation"} 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}xs:boolean') 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}xs:string') X(f'{indent}') X(f'{indent}\t{length}') X(f'{indent}\t{al}') X(f'{indent}') return # Number without params -> Number(10,0) if type_str == 'Number': X(f'{indent}xs:decimal') X(f'{indent}') X(f'{indent}\t10') X(f'{indent}\t0') X(f'{indent}\tAny') X(f'{indent}') 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}xs:decimal') X(f'{indent}') X(f'{indent}\t{digits}') X(f'{indent}\t{fraction}') X(f'{indent}\t{sign}') X(f'{indent}') return # Date / DateTime / Time — общая структура xs:dateTime + DateFractions (различаются лишь составом). if type_str in ('Date', 'DateTime', 'Time'): X(f'{indent}xs:dateTime') X(f'{indent}') X(f'{indent}\t{type_str}') X(f'{indent}') return # TypeSet — тип-множество: ОпределяемыйТип (DefinedType) ИЛИ Характеристика ПВХ (Characteristic). if re.match(r'^(DefinedType|Characteristic)\.(.+)$', type_str): X(f'{indent}cfg:{type_str}') 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}cfg:{type_str}') return # ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary). if type_str == 'ValueStorage': X(f'{indent}v8:ValueStorage') return # UUID (УникальныйИдентификатор) if type_str == 'UUID': X(f'{indent}v8:UUID') return # Платформенные типы-коллекции/периоды (ТаблицаЗначений/ДеревоЗначений/…) — канон с префиксом v8:. if type_str in v8_platform_types: X(f'{indent}v8:{type_str}') return # Типы с выделенным пространством имён (Chart/SettingsComposer/SpreadsheetDocument) — локальный xmlns. if type_str in type_namespace_map: m2 = type_namespace_map[type_str] X(f'{indent}{m2["prefix"]}:{type_str}') return # Типы current-config (cfg:): голые (ConstantsSet/…) и объектные (CatalogObject.X/DataProcessorObject.X/…). if type_str in cfg_bare_types: X(f'{indent}cfg:{type_str}') return m3 = re.match(r'^(\w+)(Object|List|Manager|Selection|RecordSet|RecordKey|RecordManager)\.(.+)$', type_str) if m3 and m3.group(1) in cfg_object_kinds: X(f'{indent}cfg:{type_str}') return # Голый объектный метатип (без имени): Object/RecordSet + ConstantValueManager → «любой объект категории» = TypeSet cfg:; # прочие Manager/List/Selection → сам тип менеджера/списка = Type cfg: (напр. в Source подписки на событие). mbs = re.match(r'^(\w+)(Object|RecordSet)$', type_str) if (mbs and mbs.group(1) in cfg_object_kinds) or type_str == 'ConstantValueManager': X(f'{indent}cfg:{type_str}') return mbt = re.match(r'^(\w+)(Manager|List|Selection|RecordKey|RecordManager)$', type_str) if mbt and mbt.group(1) in cfg_object_kinds: X(f'{indent}cfg:{type_str}') return # Reference types — use local xmlns declaration for 1C compatibility m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str) if m: X(f'{indent}d5p1:{type_str}') return # Fallback X(f'{indent}{type_str}') def emit_value_type(indent, type_str): X(f'{indent}') emit_type_content(f'{indent}\t', type_str) X(f'{indent}') # --- 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', 'ложь', 'нет') # Прощающий ввод MDObjectRef-путей (Location/Content функц. опции): русские корни метаданных + подвиды → # английские. Виды на ЧЁТНЫХ позициях (0,2,4…); имена (нечётные) не трогаем. Английские пути неизменны. md_ref_roots = { 'справочник': 'Catalog', 'документ': 'Document', 'перечисление': 'Enum', 'константа': 'Constant', 'регистрсведений': 'InformationRegister', 'регистрнакопления': 'AccumulationRegister', 'регистрбухгалтерии': 'AccountingRegister', 'регистррасчета': 'CalculationRegister', 'регистррасчёта': 'CalculationRegister', 'плансчетов': 'ChartOfAccounts', 'планвидовхарактеристик': 'ChartOfCharacteristicTypes', 'планвидоврасчета': 'ChartOfCalculationTypes', 'планвидоврасчёта': 'ChartOfCalculationTypes', 'планобмена': 'ExchangePlan', 'бизнеспроцесс': 'BusinessProcess', 'задача': 'Task', 'журналдокументов': 'DocumentJournal', 'отчет': 'Report', 'отчёт': 'Report', 'обработка': 'DataProcessor', 'табличнаячасть': 'TabularSection', 'реквизит': 'Attribute', 'измерение': 'Dimension', 'ресурс': 'Resource', 'стандартныйреквизит': 'StandardAttribute', 'значениеперечисления': 'EnumValue', 'команда': 'Command', 'признакучета': 'AccountingFlag', 'признакучёта': 'AccountingFlag', } def normalize_md_object_ref(ref): if not ref or '.' not in ref: return ref parts = ref.split('.') for k in range(0, len(parts), 2): t = md_ref_roots.get(parts[k].lower()) if t: parts[k] = t return '.'.join(parts) # Значения платформенного перечисления ВидСчета (ent:AccountType) — FillValue реквизита Тип у Плана счетов. ACCOUNT_TYPE_VALUES = ('Active', 'Passive', 'ActivePassive') # «Только обороты» () — предопределённый признак учёта субконто; токен в списке 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, type_empty=False): """spec — значение ключа fillValue (None при явном nil-override), has_spec — присутствует ли ключ. type_empty — реквизит с пустым : форма пустого значения nil, НЕ xs:string.""" cat = 'Other' if type_empty else get_fill_type_category(type_str) if not has_spec: if cat == 'String': X(f'{indent}') elif cat == 'Number': X(f'{indent}0') else: X(f'{indent}') return if spec is None: X(f'{indent}') # явный nil-override return if isinstance(spec, dict) and spec.get('nil') is True: X(f'{indent}') # явный nil на типизированном (маркер декомпилятора) return if isinstance(spec, dict) and spec.get('emptyRef') is True: X(f'{indent}') # пустая ссылка (маркер декомпилятора) return if isinstance(spec, bool): X(f'{indent}{"true" if spec else "false"}') return if isinstance(spec, (int, float)): X(f'{indent}{format_fill_num(spec)}') return xsi_type, text = resolve_fill_value_spec(str(spec), type_str) if text == '' and xsi_type == 'xs:string': X(f'{indent}') return X(f'{indent}{esc_xml_text(text)}') # --------------------------------------------------------------------------- # 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': '', 'typeEmpty': False, '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), # Явный `type: ""` (ключ есть, значение пустое) ≠ отсутствие: пустой (реквизит без типа). 'typeEmpty': ('type' in val and str(val.get('type') or '').strip() == '' and not val.get('valueType')), '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, 'markNegatives': True if val.get('markNegatives') is True else False, 'choiceForm': str(val['choiceForm']) if val.get('choiceForm') else '', 'choiceFoldersAndItems': str(val['choiceFoldersAndItems']) if val.get('choiceFoldersAndItems') else '', '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'), 'master': val.get('master') is True, 'mainFilter': val.get('mainFilter') is True, 'denyIncompleteValues': val.get('denyIncompleteValues') is True, 'useInTotals': (val.get('useInTotals') is True) if val.get('useInTotals') is not None else True, 'baseDimension': val.get('baseDimension') is True, 'scheduleLink': val.get('scheduleLink'), 'balance': val.get('balance') is True, 'accountingFlag': val.get('accountingFlag'), 'extDimensionAccountingFlag': val.get('extDimensionAccountingFlag'), 'addressingDimension': val.get('addressingDimension'), } 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, # строка ИЛИ {ru,en} → emit_mltext; None → авто из имени 'synonym': val['synonym'] if val.get('synonym') is not None 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'}, ], 'Sequence': [ {'prefix': 'SequenceRecord', 'category': 'Record'}, {'prefix': 'SequenceManager', 'category': 'Manager'}, {'prefix': 'SequenceRecordSet', 'category': 'RecordSet'}, ], 'FilterCriterion': [ {'prefix': 'FilterCriterionManager', 'category': 'Manager'}, {'prefix': 'FilterCriterionList', 'category': 'List'}, ], 'SettingsStorage': [ {'prefix': 'SettingsStorageManager', 'category': 'Manager'}, ], 'WSReference': [ {'prefix': 'WSReferenceManager', 'category': 'Manager'}, ], } def emit_internal_info(indent, object_type, object_name): types = generated_types.get(object_type) if not types: return X(f'{indent}') if object_type == 'ExchangePlan': X(f'{indent}\t{new_uuid()}') for gt in types: full_name = f"{gt['prefix']}.{object_name}" X(f'{indent}\t') X(f'{indent}\t\t{new_uuid()}') X(f'{indent}\t\t{new_uuid()}') X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 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', 'ActionPeriodIsBasic', 'Description', 'Code'], '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'}, }, # ChartOfCalculationTypes: Наименование → FillChecking=ShowError (Код здесь DontCheck). 'ChartOfCalculationTypes': { 'Description': {'FillChecking': 'ShowError'}, }, # Document: Дата → FillChecking=ShowError (974/1010 доков acc+erp; дата обязательна). 'Document': { 'Date': {'FillChecking': 'ShowError'}, }, } # 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}') # LinkByType стандартного реквизита (напр. ExtDimensionN→Account у регистра бухгалтерии). DataPath verbatim. lbt = ov.get('LinkByType') if lbt: lbt_dp = str(lbt.get('dataPath')) if isinstance(lbt, dict) else str(lbt) lbt_li = lbt.get('linkItem', 0) if isinstance(lbt, dict) else 0 X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(lbt_dp)}') X(f'{indent}\t\t{lbt_li}') X(f'{indent}\t') else: X(f'{indent}\t') X(f'{indent}\t{fc}') X(f'{indent}\tfalse') X(f'{indent}\t{ffv}') X(f'{indent}\tAuto') X(f'{indent}\t') emit_mltext(f'{indent}\t', 'xr:ToolTip', tt) X(f'{indent}\tfalse') emit_mltext(f'{indent}\t', 'xr:Format', fmt) if cf: X(f'{indent}\t{esc_xml(str(cf))}') else: X(f'{indent}\t') X(f'{indent}\tAuto') X(f'{indent}\t{chi}') emit_mltext(f'{indent}\t', 'xr:EditFormat', efmt) X(f'{indent}\tfalse') X(f'{indent}\t{dh}') X(f'{indent}\tfalse') X(f'{indent}\t') emit_mltext(f'{indent}\t', 'xr:Synonym', syn) if cmt: X(f'{indent}\t{esc_xml_text(str(cmt))}') else: X(f'{indent}\t') X(f'{indent}\t{fts}') 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') elif isinstance(fv_raw, dict) and fv_raw.get('emptyRef') is True: X(f'{indent}\t') elif isinstance(fv_raw, dict) and fv_raw.get('typeDescription') is True: X(f'{indent}\t') # пустое типизированное (ValueType ПВХ) else: fv_xt, fv_tx = normalize_choice_value(fv_raw) if fv_tx == '' or fv_tx is None: X(f'{indent}\t') else: X(f'{indent}\t{esc_xml(fv_tx)}') if msk: X(f'{indent}\t{esc_xml_text(str(msk))}') else: X(f'{indent}\t') emit_choice_parameters(f'{indent}\t', ov.get('ChoiceParameters'), 'xr:ChoiceParameters') X(f'{indent}') # Единый эмиттер блока StandardAttributes — поведение правят ДАННЫЕ, не форк кода (см. коммент в .ps1). # std_attr_conditional_types: типы, где блок только при кастомизации (DSL-ключ standardAttributes). # Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты. std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'} 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 if isinstance(sa, str) and sa == '': return # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок) 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}') 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('format') is not None: ov['Format'] = d['format'] # строка ИЛИ {ru,en} if d.get('editFormat') is not None: ov['EditFormat'] = d['editFormat'] if d.get('choiceForm'): ov['ChoiceForm'] = str(d['choiceForm']) if d.get('linkByType') is not None: ov['LinkByType'] = d['linkByType'] emit_standard_attribute(f'{indent}\t', a, ov) X(f'{indent}') 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']) if line_number.get('fillValue') is not None: ov['FillValue'] = line_number['fillValue'] X(f'{indent}') emit_standard_attribute(f'{indent}\t', 'LineNumber', ov) X(f'{indent}') # --------------------------------------------------------------------------- # 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'^-?\d+$', s): return s # голый (отрицательный) индекс-маркер (напр. -8 в ChoiceParameterLinks) — verbatim 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): """ (связь по типу): DataPath + LinkItem. spec — {dataPath, linkItem?} или строка-путь; нет → .""" if not spec: X(f'{indent}') 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}') return dp = expand_data_path(dp) X(f'{indent}') X(f'{indent}\t{esc_xml(str(dp))}') X(f'{indent}\t{li}') X(f'{indent}') def emit_field_block(indent, tag, fields): """ со списком (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{esc_xml(str(f))}') X(f'{indent}') def emit_based_on(indent, items): """ — «ввод на основании», список MDObjectRef. Нет/пусто → self-close.""" arr = [it for it in (items or []) if it] if not arr: X(f'{indent}') return X(f'{indent}') for it in arr: X(f'{indent}\t{esc_xml(str(it))}') X(f'{indent}') # --- Параметры/связи выбора (порт из 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') if not has_val: X(f'{indent}\t\t') elif val_is_array: X(f'{indent}\t\t') for v in val: xt, tx = normalize_choice_value_t(v, ptype) if tx == '' or tx is None: X(f'{indent}\t\t\t') else: X(f'{indent}\t\t\t{esc_xml(tx)}') X(f'{indent}\t\t') else: xt, tx = normalize_choice_value_t(val, ptype) if tx == '' or tx is None: X(f'{indent}\t\t') else: X(f'{indent}\t\t{esc_xml(tx)}') X(f'{indent}\t') X(f'{indent}') 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') X(f'{indent}\t\t{esc_xml(str(name))}') X(f'{indent}\t\t{esc_xml(str(dp))}') X(f'{indent}\t\t{vc}') X(f'{indent}\t') X(f'{indent}') # --- 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}') return X(f'{indent}') 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') X(f'{indent}\t\t') X(f'{indent}\t\t\t{esc_xml(key)}') X(f'{indent}\t\t\t{esc_xml(tff)}') # 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') else: tfv_xt, tfv_tx = normalize_choice_value(tfv_raw) if tfv_tx == '' or tfv_tx is None: X(f'{indent}\t\t\t') else: X(f'{indent}\t\t\t{esc_xml(tfv_tx)}') X(f'{indent}\t\t\t{dpf}') X(f'{indent}\t\t\t{mvu}') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t\t{esc_xml(obj)}') X(f'{indent}\t\t\t{esc_xml(typ)}') X(f'{indent}\t\t\t{esc_xml(val)}') X(f'{indent}\t\t\t{mvk}') X(f'{indent}\t\t\t{mvo}') X(f'{indent}\t\t') X(f'{indent}\t') X(f'{indent}') def emit_min_max_value(indent, tag, val): """/ — граница диапазона. 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))}') def emit_attribute(indent, parsed, context, elem_tag='Attribute'): # context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register", # "account" (реквизит Плана счетов: как catalog, но без ), "account-flag" (признак учёта ПС: # как account, но без /, тип по умолчанию 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') X(f'{indent}\t\t{esc_xml(parsed["name"])}') emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym']) if parsed.get('comment'): X(f'{indent}\t\t{esc_xml_text(parsed["comment"])}') else: X(f'{indent}\t\t') type_str = parsed['type'] if parsed.get('typeEmpty'): # Явный пустой тип (реквизит без типа / произвольный) → . X(f'{indent}\t\t') elif type_str: emit_value_type(f'{indent}\t\t', type_str) elif context == 'account-flag': X(f'{indent}\t\t') X(f'{indent}\t\t\txs:boolean') X(f'{indent}\t\t') else: X(f'{indent}\t\t') X(f'{indent}\t\t\txs:string') X(f'{indent}\t\t') pw_mode = 'true' if parsed.get('passwordMode') is True else 'false' X(f'{indent}\t\t{pw_mode}') 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{"true" if parsed.get("markNegatives") is True else "false"}') if parsed.get('mask'): X(f'{indent}\t\t{esc_xml_text(parsed["mask"])}') else: X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{multi_line}') ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false' X(f'{indent}\t\t{ext_edit}') 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', 'register-accum', 'register-calc', 'register-account'): ffv = 'true' if (parsed.get('fillFromFillingValue') is True or (elem_tag == 'Dimension' and 'master' in parsed.get('flags', []))) else 'false' X(f'{indent}\t\t{ffv}') if context not in ('tabular', 'processor', 'chart', 'register-other', 'register-accum', 'register-calc', 'register-account'): emit_fill_value(f'{indent}\t\t', type_str, parsed.get('fillValue'), parsed.get('hasFillValue'), bool(parsed.get('typeEmpty'))) 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{fill_checking}') X(f'{indent}\t\t{parsed.get("choiceFoldersAndItems") or "Items"}') 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{parsed.get("quickChoice") or "Auto"}') X(f'{indent}\t\t{parsed.get("createOnInput") or "Auto"}') X(f'{indent}\t\t{esc_xml(str(parsed["choiceForm"]))}' if parsed.get('choiceForm') else f'{indent}\t\t') emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType')) chi = parsed.get('choiceHistoryOnInput') or 'Auto' X(f'{indent}\t\t{chi}') # Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing). if elem_tag == 'Dimension' and context == 'register-info': master = 'true' if (parsed.get('master') is True or 'master' in parsed.get('flags', [])) else 'false' main_filter = 'true' if (parsed.get('mainFilter') is True or 'mainfilter' in parsed.get('flags', [])) else 'false' deny_incomplete = 'true' if (parsed.get('denyIncompleteValues') is True or 'denyincomplete' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{master}') X(f'{indent}\t\t{main_filter}') X(f'{indent}\t\t{deny_incomplete}') # Измерение регистра накопления: DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing). if elem_tag == 'Dimension' and context == 'register-accum': deny_incomplete = 'true' if (parsed.get('denyIncompleteValues') is True or 'denyincomplete' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{deny_incomplete}') # Измерение регистра расчёта: DenyIncompleteValues + BaseDimension. if elem_tag == 'Dimension' and context == 'register-calc': deny_incomplete = 'true' if (parsed.get('denyIncompleteValues') is True or 'denyincomplete' in parsed.get('flags', [])) else 'false' base_dimension = 'true' if (parsed.get('baseDimension') is True or 'base' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{deny_incomplete}') X(f'{indent}\t\t{base_dimension}') # Регистр расчёта: ScheduleLink у измерений и реквизитов (НЕ ресурсов), перед Indexing. Дефолт пустой. if context == 'register-calc' and elem_tag in ('Dimension', 'Attribute'): if parsed.get('scheduleLink'): X(f'{indent}\t\t{esc_xml(str(parsed["scheduleLink"]))}') else: X(f'{indent}\t\t') # Измерение/ресурс регистра бухгалтерии: Balance + AccountingFlag, затем DenyIncompleteValues (изм.) / ExtDimensionAccountingFlag (рес.). if context == 'register-account' and elem_tag in ('Dimension', 'Resource'): balance = 'true' if (parsed.get('balance') is True or 'balance' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{balance}') if parsed.get('accountingFlag'): X(f'{indent}\t\t{esc_xml(str(parsed["accountingFlag"]))}') else: X(f'{indent}\t\t') if elem_tag == 'Dimension': deny_incomplete = 'true' if (parsed.get('denyIncompleteValues') is True or 'denyincomplete' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{deny_incomplete}') else: if parsed.get('extDimensionAccountingFlag'): X(f'{indent}\t\t{esc_xml(str(parsed["extDimensionAccountingFlag"]))}') else: X(f'{indent}\t\t') if context == 'catalog': X(f'{indent}\t\t{parsed.get("use") or "ForItem"}') if context not in ('processor', 'processor-tabular'): # Признаки учёта ПС (account-flag) не имеют /, но имеют . if context != 'account-flag': # Ресурс регистра накопления НЕ имеет (только ); измерение/реквизит — имеют. if not (context in ('register-accum', 'register-calc', 'register-account') and elem_tag == 'Resource'): 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}') # Реквизит адресации задачи: AddressingDimension (между Indexing и FullTextSearch). if context == 'task-addressing' and elem_tag == 'AddressingAttribute': if parsed.get('addressingDimension'): X(f'{indent}\t\t{esc_xml(str(parsed["addressingDimension"]))}') else: X(f'{indent}\t\t') X(f'{indent}\t\t{parsed.get("fullTextSearch") or "Use"}') # Измерение регистра накопления: UseInTotals (после FullTextSearch, дефолт true). if elem_tag == 'Dimension' and context == 'register-accum': use_in_totals = 'false' if (parsed.get('useInTotals') is False or 'nouseintotals' in parsed.get('flags', [])) else 'true' X(f'{indent}\t\t{use_in_totals}') # DataHistory — not for Chart* types and non-InformationRegister register family if context not in ('chart', 'register-other', 'register-accum', 'register-calc', 'register-account'): X(f'{indent}\t\t{parsed.get("dataHistory") or "Use"}') X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 9. TabularSection emitter # --------------------------------------------------------------------------- def emit_command_picture(indent, cmd): """ команды — структурный блок (зеркало form-compile). Дефолт LoadTransparent=true (конвенция кнопки/команды): фиксируем только false. Значение: строка-ref + sibling loadTransparent ЛИБО объект {src, loadTransparent?, transparentPixel?}. src с префиксом "abs:" → , иначе . Нет → .""" pic = cmd.get('picture') if not pic: X(f'{indent}') 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}') return X(f'{indent}') m = re.match(r'^abs:(.*)$', src) if m: X(f'{indent}\t{esc_xml(m.group(1))}') else: X(f'{indent}\t{esc_xml(src)}') X(f'{indent}\t{"true" if lt else "false"}') if tpx: X(f'{indent}\t') X(f'{indent}') def emit_command(indent, cmd_name, cmd): X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(cmd_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{esc_xml_text(str(cmd["comment"]))}') else: X(f'{indent}\t\t') group = resolve_command_group(cmd.get('group'), cmd_name) if cmd.get('commandParameterType') and group in SECTION_COMMAND_GROUPS: sys.stderr.write(f"Команда '{cmd_name}': тип параметра (commandParameterType) недоступен для команд " f"командного интерфейса раздела ('{group}'). Тип параметра — только для групп формы " f"(FormCommandBar*/FormNavigationPanel*) или CommandGroup.<Имя>.\n") sys.exit(1) X(f'{indent}\t\t{esc_xml(group)}') if cmd.get('commandParameterType'): X(f'{indent}\t\t') emit_type_content(f'{indent}\t\t\t', str(cmd['commandParameterType'])) X(f'{indent}\t\t') else: X(f'{indent}\t\t') X(f'{indent}\t\t{cmd.get("parameterUseMode") or "Single"}') X(f'{indent}\t\t{"true" if cmd.get("modifiesData") is True else "false"}') X(f'{indent}\t\t{cmd.get("representation") or "Auto"}') 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{esc_xml(str(cmd["shortcut"]))}') else: X(f'{indent}\t\t') X(f'{indent}\t\t{cmd.get("onMainServerUnavalableBehavior") or "Auto"}') X(f'{indent}\t') X(f'{indent}') 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, ts_fill_checking=None, ts_use=None): uid = new_uuid() X(f'{indent}') type_prefix = f'{object_type}TabularSection' row_prefix = f'{object_type}TabularSectionRow' X(f'{indent}\t') X(f'{indent}\t\t') X(f'{indent}\t\t\t{new_uuid()}') X(f'{indent}\t\t\t{new_uuid()}') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t\t{new_uuid()}') X(f'{indent}\t\t\t{new_uuid()}') X(f'{indent}\t\t') X(f'{indent}\t') ts_synonym = ts_synonym_arg if ts_synonym_arg is not None else split_camel_case(ts_name) X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(ts_name)}') emit_mltext(f'{indent}\t\t', 'Synonym', ts_synonym) if ts_comment: X(f'{indent}\t\t{esc_xml_text(ts_comment)}') else: X(f'{indent}\t\t') emit_mltext(f'{indent}\t\t', 'ToolTip', ts_tooltip) X(f'{indent}\t\t{ts_fill_checking if ts_fill_checking else "DontCheck"}') # TS-блок стандартных реквизитов (LineNumber) эмитим ВСЕГДА, кроме подавления `lineNumber: ""` (дом-конвенция # суппресса): ~6% ТЧ исторически опускают блок (правило не выводимо — Товары all-default его имеет, соседи нет). if not (isinstance(ts_line_number, str) and ts_line_number == ''): emit_tabular_standard_attributes(f'{indent}\t\t', ts_line_number) if object_type in ('Catalog', 'ChartOfCharacteristicTypes'): X(f'{indent}\t\t{ts_use if ts_use else "ForItem"}') X(f'{indent}\t') ts_context = 'processor-tabular' if object_type in ('DataProcessor', 'Report') else 'tabular' X(f'{indent}\t') for col in columns: parsed = parse_attribute_shorthand(col) emit_attribute(f'{indent}\t\t', parsed, ts_context) X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 10. EnumValue emitter # --------------------------------------------------------------------------- def emit_enum_value(indent, parsed): uid = new_uuid() X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(parsed["name"])}') emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym']) if parsed.get('comment'): X(f'{indent}\t\t{esc_xml_text(parsed["comment"])}') else: X(f'{indent}\t\t') X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 11. Dimension emitter # --------------------------------------------------------------------------- def emit_dimension(indent, parsed, register_type): uid = new_uuid() X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(parsed["name"])}') emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym']) X(f'{indent}\t\t') type_str = parsed['type'] if type_str: emit_value_type(f'{indent}\t\t', type_str) else: X(f'{indent}\t\t') X(f'{indent}\t\t\txs:string') X(f'{indent}\t\t') X(f'{indent}\t\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t{"true" if parsed.get("markNegatives") is True else "false"}') X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{multi_line}') ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false' X(f'{indent}\t\t{ext_edit}') 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{fill_from}') X(f'{indent}\t\t') fill_checking = 'DontCheck' if 'req' in flags: fill_checking = 'ShowError' X(f'{indent}\t\t{fill_checking}') X(f'{indent}\t\t{parsed.get("choiceFoldersAndItems") or "Items"}') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') X(f'{indent}\t\tAuto') X(f'{indent}\t\t{esc_xml(str(parsed["choiceForm"]))}' if parsed.get('choiceForm') else f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') 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}') X(f'{indent}\t\t{main_filter}') X(f'{indent}\t\t{deny_incomplete}') if register_type == 'AccumulationRegister': deny_incomplete = 'true' if 'denyincomplete' in flags else 'false' X(f'{indent}\t\t{deny_incomplete}') indexing = 'DontIndex' if 'index' in flags: indexing = 'Index' X(f'{indent}\t\t{indexing}') X(f'{indent}\t\tUse') if register_type == 'AccumulationRegister': use_in_totals = 'false' if 'nouseintotals' in flags else 'true' X(f'{indent}\t\t{use_in_totals}') if register_type == 'InformationRegister': X(f'{indent}\t\tUse') X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 12. Resource emitter # --------------------------------------------------------------------------- def emit_resource(indent, parsed, register_type): uid = new_uuid() X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(parsed["name"])}') emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym']) X(f'{indent}\t\t') type_str = parsed['type'] if type_str: emit_value_type(f'{indent}\t\t', type_str) else: X(f'{indent}\t\t') X(f'{indent}\t\t\txs:decimal') X(f'{indent}\t\t\t') X(f'{indent}\t\t\t\t15') X(f'{indent}\t\t\t\t2') X(f'{indent}\t\t\t\tAny') X(f'{indent}\t\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t{"true" if parsed.get("markNegatives") is True else "false"}') X(f'{indent}\t\t') multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false' X(f'{indent}\t\t{multi_line}') ext_edit = 'true' if parsed.get('extendedEdit') is True else 'false' X(f'{indent}\t\t{ext_edit}') 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\tfalse') X(f'{indent}\t\t') flags = parsed.get('flags', []) fill_checking = 'DontCheck' if 'req' in flags: fill_checking = 'ShowError' X(f'{indent}\t\t{fill_checking}') X(f'{indent}\t\t{parsed.get("choiceFoldersAndItems") or "Items"}') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') X(f'{indent}\t\tAuto') X(f'{indent}\t\t{esc_xml(str(parsed["choiceForm"]))}' if parsed.get('choiceForm') else f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') if register_type == 'InformationRegister': X(f'{indent}\t\tDontIndex') X(f'{indent}\t\tUse') X(f'{indent}\t\tUse') if register_type == 'AccumulationRegister': X(f'{indent}\t\tUse') X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 13. Property emitters per type # --------------------------------------------------------------------------- def emit_catalog_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') hierarchical = 'true' if defn.get('hierarchical') is True else 'false' hierarchy_type = get_enum_prop('HierarchyType', 'hierarchyType', 'HierarchyFoldersAndItems') X(f'{i}{hierarchical}') X(f'{i}{hierarchy_type}') 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}{limit_level_count}') X(f'{i}{level_count}') X(f'{i}{folders_on_top}') use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmds}') owners = defn.get('owners', []) if owners: X(f'{i}') for owner_ref in owners: full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}' X(f'{i}\t{full_ref}') X(f'{i}') else: X(f'{i}') subordination_use = get_enum_prop('SubordinationUse', 'subordinationUse', 'ToItems') X(f'{i}{subordination_use}') 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}{code_length}') X(f'{i}{description_length}') X(f'{i}{code_type}') X(f'{i}{code_allowed_length}') code_series = get_enum_prop('CodeSeries', 'codeSeries', 'WholeCatalog') X(f'{i}{code_series}') X(f'{i}{check_unique}') X(f'{i}{autonumbering}') default_presentation = get_enum_prop('DefaultPresentation', 'defaultPresentation', 'AsDescription') X(f'{i}{default_presentation}') emit_standard_attributes(i, 'Catalog') emit_characteristics(i, defn.get('characteristics')) X(f'{i}{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') quick_choice = 'true' if defn.get('quickChoice') is True else 'false' choice_mode = get_enum_prop('ChoiceMode', 'choiceMode', 'BothWays') X(f'{i}{quick_choice}') X(f'{i}{choice_mode}') # 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}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}Directly') 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}{incl_help}') 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', 'Managed') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') 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}{get_enum_prop("CreateOnInput", "createOnInput", "Use")}') X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') X(f'{i}DontUse') X(f'{i}false') X(f'{i}false') def emit_document_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') use_std_cmd = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmd}') if defn.get('numerator'): X(f'{i}{esc_xml(str(defn["numerator"]))}') else: X(f'{i}') 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('NumberPeriodicity', '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}{number_type}') X(f'{i}{number_length}') X(f'{i}{number_allowed_length}') X(f'{i}{number_periodicity}') X(f'{i}{check_unique}') X(f'{i}{autonumbering}') emit_standard_attributes(i, 'Document') emit_characteristics(i, defn.get('characteristics')) emit_based_on(i, defn.get('basedOn')) # InputByString: override `inputByString` ЛИБО дефолт [Номер]. if 'inputByString' in defn: ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])] else: ib_fields = [f'Document.{obj_name}.StandardAttribute.Number'] emit_field_block(i, 'InputByString', ib_fields) X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "Use")}') X(f'{i}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}Directly') 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')) X(f'{i}{get_enum_prop("Posting", "posting", "Allow")}') X(f'{i}{get_enum_prop("RealTimePosting", "realTimePosting", "Deny")}') X(f'{i}{get_enum_prop("RegisterRecordsDeletion", "registerRecordsDeletion", "AutoDelete")}') X(f'{i}{get_enum_prop("RegisterRecordsWritingOnPost", "registerRecordsWritingOnPost", "WriteSelected")}') X(f'{i}{get_enum_prop("SequenceFilling", "sequenceFilling", "AutoFill")}') # RegisterRecords — движения (список MDObjectRef, синонимы типов резолвятся). 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}') for rr in reg_records: X(f'{i}\t{rr}') X(f'{i}') else: X(f'{i}') 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}{post_in_priv}') X(f'{i}{unpost_in_priv}') incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') dl_fields = [expand_data_path(str(x)) for x in (defn.get('dataLockFields') or [])] if 'dataLockFields' in defn else [] emit_field_block(i, 'DataLockFields', dl_fields) X(f'{i}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') 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}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') X(f'{i}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') upd_dh = 'true' if get_bool_prop('updateDataHistoryImmediatelyAfterWrite', False) else 'false' X(f'{i}{upd_dh}') exec_dh = 'true' if get_bool_prop('executeAfterWriteDataHistoryVersionProcessing', False) else 'false' X(f'{i}{exec_dh}') def emit_enum_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(defn["comment"])}') else: X(f'{i}') use_std_cmds = 'true' if get_bool_prop('useStandardCommands', False) else 'false' X(f'{i}{use_std_cmds}') emit_standard_attributes(i, 'Enum') emit_characteristics(i, defn.get('characteristics')) quick_choice = 'false' if defn.get('quickChoice') is False else 'true' X(f'{i}{quick_choice}') X(f'{i}{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}') emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm')) emit_form_ref(i, 'DefaultChoiceForm', defn.get('defaultChoiceForm')) emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm')) emit_form_ref(i, 'AuxiliaryChoiceForm', defn.get('auxiliaryChoiceForm')) emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') def emit_constant_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') # Type — valueType (явный '' → , реквизит без типа; отсутствие → String дефолт). value_type = build_type_str(defn) type_empty = (defn.get('valueType') is not None and str(defn.get('valueType')).strip() == '') or \ (defn.get('type') is not None and str(defn.get('type')).strip() == '') if type_empty: X(f'{i}') else: emit_value_type(i, value_type or 'String') use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmds}') emit_verbatim_ref(i, 'DefaultForm', defn.get('defaultForm')) emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) X(f'{i}{"true" if get_bool_prop("passwordMode", False) else "false"}') emit_mltext(i, 'Format', defn.get('format')) emit_mltext(i, 'EditFormat', defn.get('editFormat')) emit_mltext(i, 'ToolTip', defn.get('tooltip')) X(f'{i}{"true" if get_bool_prop("markNegatives", False) else "false"}') if defn.get('mask'): X(f'{i}{esc_xml_text(str(defn["mask"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("multiLine", False) else "false"}') X(f'{i}{"true" if get_bool_prop("extendedEdit", False) else "false"}') emit_min_max_value(i, 'MinValue', defn.get('minValue')) emit_min_max_value(i, 'MaxValue', defn.get('maxValue')) X(f'{i}{get_enum_prop("FillChecking", "fillChecking", "DontCheck")}') X(f'{i}{get_enum_prop("ChoiceFoldersAndItems", "choiceFoldersAndItems", "Items")}') emit_choice_parameter_links(i, defn.get('choiceParameterLinks')) emit_choice_parameters(i, defn.get('choiceParameters')) X(f'{i}{get_enum_prop("QuickChoice", "quickChoice", "Auto")}') if defn.get('choiceForm'): X(f'{i}{esc_xml(str(defn["choiceForm"]))}') else: X(f'{i}') emit_link_by_type(i, defn.get('linkByType')) X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') X(f'{i}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') X(f'{i}{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}') X(f'{i}{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}') def emit_information_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') use_std_cmd = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmd}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') emit_form_ref(i, 'DefaultRecordForm', defn.get('defaultRecordForm')) emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm')) emit_form_ref(i, 'AuxiliaryRecordForm', defn.get('auxiliaryRecordForm')) emit_form_ref(i, 'AuxiliaryListForm', defn.get('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 = 'true' if get_bool_prop('mainFilterOnPeriod', False) else 'false' X(f'{i}{periodicity}') X(f'{i}{write_mode}') X(f'{i}{main_filter_on_period}') incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Managed') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') en_tot_first = 'true' if get_bool_prop('enableTotalsSliceFirst', False) else 'false' en_tot_last = 'true' if get_bool_prop('enableTotalsSliceLast', False) else 'false' X(f'{i}{en_tot_first}') X(f'{i}{en_tot_last}') emit_mltext(i, 'RecordPresentation', defn.get('recordPresentation')) emit_mltext(i, 'ExtendedRecordPresentation', defn.get('extendedRecordPresentation')) emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) X(f'{i}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') upd_dh = 'true' if get_bool_prop('updateDataHistoryImmediatelyAfterWrite', False) else 'false' X(f'{i}{upd_dh}') exec_dh = 'true' if get_bool_prop('executeAfterWriteDataHistoryVersionProcessing', False) else 'false' X(f'{i}{exec_dh}') def emit_accumulation_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') use_std_cmd = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmd}') emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm')) emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm')) register_type = get_enum_prop('RegisterType', 'registerType', 'Balance') X(f'{i}{register_type}') incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') emit_standard_attributes(i, 'AccumulationRegister') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Managed') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') enable_totals_splitting = 'false' if defn.get('enableTotalsSplitting') is False else 'true' X(f'{i}{enable_totals_splitting}') emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) # --- 13a. DefinedType, CommonModule, ScheduledJob, EventSubscription --- def emit_defined_type_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') # Type — единый эмиттер emit_value_type/emit_type_content (составной через ' + '); valueType или valueTypes. if defn.get('valueType'): vt = str(defn['valueType']) elif defn.get('valueTypes'): vt = ' + '.join(str(x) for x in defn['valueTypes']) else: vt = '' if vt: emit_value_type(i, vt) else: X(f'{i}') def emit_functional_option_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') # Location — хранилище значения опции (ссылка verbatim; location или value). loc = str(defn['location']) if defn.get('location') else (str(defn['value']) if defn.get('value') else '') if loc: X(f'{i}{esc_xml(normalize_md_object_ref(loc))}') else: X(f'{i}') # PrivilegedGetMode — дефолт true (корпус 2864/2864). X(f'{i}{"true" if get_bool_prop("privilegedGetMode", True) else "false"}') # Content — зависимые объекты (список MDObjectRef-путей). omit-on-empty. content = list(defn['content']) if defn.get('content') else [] if content: X(f'{i}') for obj in content: X(f'{i}\t{esc_xml(normalize_md_object_ref(str(obj)))}') X(f'{i}') else: X(f'{i}') def emit_md_ref_list(indent, tag, items): """Список MDObjectRef (Documents/RegisterRecords/DocumentMap/…) с . omit-on-empty.""" arr = list(items) if items else [] if arr: X(f'{indent}<{tag}>') for it in arr: X(f'{indent}\t{esc_xml(normalize_md_object_ref(str(it)))}') X(f'{indent}') else: X(f'{indent}<{tag}/>') def emit_sequence_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{get_enum_prop("MoveBoundaryOnPosting", "moveBoundaryOnPosting", "DontMove")}') emit_md_ref_list(i, 'Documents', defn.get('documents')) emit_md_ref_list(i, 'RegisterRecords', defn.get('registerRecords')) X(f'{i}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') def emit_filter_criterion_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') if defn.get('valueType'): vt = str(defn['valueType']) elif defn.get('valueTypes'): vt = ' + '.join(str(x) for x in defn['valueTypes']) else: vt = '' if vt: emit_value_type(i, vt) else: X(f'{i}') use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmds}') content = list(defn['content']) if defn.get('content') else [] if content: X(f'{i}') for obj in content: X(f'{i}\t{esc_xml(normalize_md_object_ref(str(obj)))}') X(f'{i}') else: X(f'{i}') emit_verbatim_ref(i, 'DefaultForm', defn.get('defaultForm')) emit_verbatim_ref(i, 'AuxiliaryForm', defn.get('auxiliaryForm')) emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) def emit_document_numerator_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{get_enum_prop("NumberType", "numberType", "String")}') num_len = str(defn['numberLength']) if defn.get('numberLength') is not None else '11' X(f'{i}{num_len}') X(f'{i}{get_enum_prop("NumberAllowedLength", "numberAllowedLength", "Variable")}') X(f'{i}{get_enum_prop("NumberPeriodicity", "numberPeriodicity", "Year")}') X(f'{i}{"true" if get_bool_prop("checkUnique", True) else "false"}') def emit_settings_storage_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') emit_verbatim_ref(i, 'DefaultSaveForm', defn.get('defaultSaveForm')) emit_verbatim_ref(i, 'DefaultLoadForm', defn.get('defaultLoadForm')) emit_verbatim_ref(i, 'AuxiliarySaveForm', defn.get('auxiliarySaveForm')) emit_verbatim_ref(i, 'AuxiliaryLoadForm', defn.get('auxiliaryLoadForm')) def emit_common_form_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{get_enum_prop("FormType", "formType", "Managed")}') incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') purposes = list(defn['usePurposes']) if defn.get('usePurposes') else ['PlatformApplication', 'MobilePlatformApplication'] if purposes: X(f'{i}') for p in purposes: X(f'{i}\t{p}') X(f'{i}') else: X(f'{i}') use_std_cmds = 'true' if get_bool_prop('useStandardCommands', False) else 'false' X(f'{i}{use_std_cmds}') emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) def _emit_comment(i): if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') def emit_session_parameter_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) if defn.get('valueType'): vt = str(defn['valueType']) elif defn.get('valueTypes'): vt = ' + '.join(str(x) for x in defn['valueTypes']) else: vt = '' if vt: emit_value_type(i, vt) else: X(f'{i}') def emit_functional_options_parameter_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) emit_md_ref_list(i, 'Use', defn.get('use')) def emit_ws_reference_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) url = str(defn['locationURL']) if defn.get('locationURL') else (str(defn['locationUrl']) if defn.get('locationUrl') else '') if url: X(f'{i}{esc_xml_text(url)}') else: X(f'{i}') def emit_common_picture_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) X(f'{i}{"true" if get_bool_prop("availabilityForChoice", False) else "false"}') X(f'{i}{"true" if get_bool_prop("availabilityForAppearance", False) else "false"}') def emit_common_template_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) X(f'{i}{get_enum_prop("TemplateType", "templateType", "SpreadsheetDocument")}') def emit_command_group_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) X(f'{i}{get_enum_prop("Representation", "representation", "Auto")}') emit_mltext(i, 'ToolTip', defn.get('tooltip')) emit_command_picture(i, defn) X(f'{i}{get_enum_prop("Category", "category", "NavigationPanel")}') def emit_common_command_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) group = str(defn['group']) if defn.get('group') else '' if group: X(f'{i}{esc_xml(group)}') else: X(f'{i}') X(f'{i}{get_enum_prop("Representation", "representation", "Auto")}') emit_mltext(i, 'ToolTip', defn.get('tooltip')) emit_command_picture(i, defn) if defn.get('shortcut'): X(f'{i}{esc_xml(str(defn["shortcut"]))}') else: X(f'{i}') incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') if defn.get('commandParameterType'): X(f'{i}') emit_type_content(f'{i}\t', str(defn['commandParameterType'])) X(f'{i}') else: X(f'{i}') X(f'{i}{get_enum_prop("ParameterUseMode", "parameterUseMode", "Single")}') X(f'{i}{"true" if get_bool_prop("modifiesData", False) else "false"}') X(f'{i}{get_enum_prop("OnMainServerUnavalableBehavior", "onMainServerUnavalableBehavior", "Auto")}') def emit_common_attribute_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) _emit_comment(i) vt = str(defn['valueType']) if defn.get('valueType') else 'String(0)' emit_value_type(i, vt) X(f'{i}{"true" if get_bool_prop("passwordMode", False) else "false"}') emit_mltext(i, 'Format', defn.get('format')) emit_mltext(i, 'EditFormat', defn.get('editFormat')) emit_mltext(i, 'ToolTip', defn.get('tooltip')) X(f'{i}{"true" if get_bool_prop("markNegatives", False) else "false"}') if defn.get('mask'): X(f'{i}{esc_xml_text(str(defn["mask"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("multiLine", False) else "false"}') X(f'{i}{"true" if get_bool_prop("extendedEdit", False) else "false"}') emit_min_max_value(i, 'MinValue', defn.get('minValue')) emit_min_max_value(i, 'MaxValue', defn.get('maxValue')) X(f'{i}{"true" if get_bool_prop("fillFromFillingValue", False) else "false"}') emit_fill_value(i, vt, defn.get('fillValue'), defn.get('fillValue') is not None) X(f'{i}{get_enum_prop("FillChecking", "fillChecking", "DontCheck")}') X(f'{i}{get_enum_prop("ChoiceFoldersAndItems", "choiceFoldersAndItems", "Items")}') emit_choice_parameter_links(i, defn.get('choiceParameterLinks')) emit_choice_parameters(i, defn.get('choiceParameters')) X(f'{i}{get_enum_prop("QuickChoice", "quickChoice", "Auto")}') X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "Auto")}') if defn.get('choiceForm'): X(f'{i}{esc_xml(str(defn["choiceForm"]))}') else: X(f'{i}') emit_link_by_type(i, defn.get('linkByType')) X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') content = list(defn['content']) if defn.get('content') else [] if content: X(f'{i}') for c in content: md = c if isinstance(c, str) else str(c.get('metadata', '')) use = 'Use' if isinstance(c, str) else (str(c['use']) if c.get('use') else 'Use') X(f'{i}\t') X(f'{i}\t\t{esc_xml(normalize_md_object_ref(md))}') X(f'{i}\t\t{use}') cs = '' if isinstance(c, str) else (str(c['conditionalSeparation']) if c.get('conditionalSeparation') else '') if cs: X(f'{i}\t\t{esc_xml(cs)}') else: X(f'{i}\t\t') X(f'{i}\t') X(f'{i}') else: X(f'{i}') X(f'{i}{get_enum_prop("AutoUse", "autoUse", "DontUse")}') X(f'{i}{get_enum_prop("DataSeparation", "dataSeparation", "DontUse")}') X(f'{i}{get_enum_prop("SeparatedDataUse", "separatedDataUse", "Independently")}') dsv = str(defn['dataSeparationValue']) if defn.get('dataSeparationValue') else '' X(f'{i}{esc_xml(dsv)}' if dsv else f'{i}') dsu = str(defn['dataSeparationUse']) if defn.get('dataSeparationUse') else '' X(f'{i}{esc_xml(dsu)}' if dsu else f'{i}') cs2 = str(defn['conditionalSeparation']) if defn.get('conditionalSeparation') else '' X(f'{i}{esc_xml(cs2)}' if cs2 else f'{i}') X(f'{i}{get_enum_prop("UsersSeparation", "usersSeparation", "DontUse")}') X(f'{i}{get_enum_prop("AuthenticationSeparation", "authenticationSeparation", "DontUse")}') X(f'{i}{get_enum_prop("ConfigurationExtensionsSeparation", "configurationExtensionsSeparation", "DontUse")}') X(f'{i}{get_enum_prop("Indexing", "indexing", "DontIndex")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') X(f'{i}{get_enum_prop("DataHistory", "dataHistory", "Use")}') def emit_sequence_dimension(indent, dim_def): uid = new_uuid() parsed = parse_attribute_shorthand(dim_def) X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(parsed["name"])}') emit_mltext(f'{indent}\t\t', 'Synonym', parsed['synonym']) if parsed.get('comment'): X(f'{indent}\t\t{esc_xml_text(parsed["comment"])}') else: X(f'{indent}\t\t') if parsed.get('typeEmpty'): X(f'{indent}\t\t') elif parsed['type']: emit_value_type(f'{indent}\t\t', parsed['type']) else: X(f'{indent}\t\t') dm = None if isinstance(dim_def, str) else dim_def.get('documentMap') rrm = None if isinstance(dim_def, str) else dim_def.get('registerRecordsMap') emit_md_ref_list(f'{indent}\t\t', 'DocumentMap', dm) emit_md_ref_list(f'{indent}\t\t', 'RegisterRecordsMap', rrm) X(f'{indent}\t') X(f'{indent}') def emit_common_module_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') 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_val}') X(f'{i}{client_managed}') X(f'{i}{server}') X(f'{i}{external_connection}') X(f'{i}{client_ordinary}') X(f'{i}{server_call}') X(f'{i}{privileged}') return_values_reuse = get_enum_prop('ReturnValuesReuse', 'returnValuesReuse', 'DontUse') X(f'{i}{return_values_reuse}') def emit_scheduled_job_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') 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}{esc_xml(method_name)}') # Description — плоская строка (дефолт ПУСТО, не синоним — иначе роундтрип рвётся). description = str(defn['description']) if defn.get('description') else '' if description: X(f'{i}{esc_xml_text(description)}') else: X(f'{i}') key = str(defn['key']) if defn.get('key') else '' X(f'{i}{esc_xml(key)}') use = 'true' if defn.get('use') is True else 'false' X(f'{i}{use}') predefined = 'true' if defn.get('predefined') is True else 'false' X(f'{i}{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}{restart_count}') X(f'{i}{restart_interval}') def emit_event_subscription_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') sources = list(defn.get('source', [])) if sources: X(f'{i}') for src in sources: emit_type_content(f'{i}\t', resolve_type_str(str(src))) X(f'{i}') else: X(f'{i}') event = str(defn['event']) if defn.get('event') else 'BeforeWrite' X(f'{i}{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}{esc_xml(handler)}') # --- 13b. Report, DataProcessor --- def emit_report_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(defn["comment"])}') else: X(f'{i}') # UseStandardCommands: дефолт true (авторски-безопасно — доступность через стандартный командный интерфейс; # при false и без переопределения размещения команд объект доступен лишь по навигационной ссылке). use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmds}') emit_verbatim_ref(i, 'DefaultForm', defn.get('defaultForm')) emit_verbatim_ref(i, 'AuxiliaryForm', defn.get('auxiliaryForm')) emit_verbatim_ref(i, 'MainDataCompositionSchema', defn.get('mainDataCompositionSchema')) emit_verbatim_ref(i, 'DefaultSettingsForm', defn.get('defaultSettingsForm')) emit_verbatim_ref(i, 'AuxiliarySettingsForm', defn.get('auxiliarySettingsForm')) emit_verbatim_ref(i, 'DefaultVariantForm', defn.get('defaultVariantForm')) emit_verbatim_ref(i, 'VariantsStorage', defn.get('variantsStorage')) emit_verbatim_ref(i, 'SettingsStorage', defn.get('settingsStorage')) incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) def emit_data_processor_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(defn["comment"])}') else: X(f'{i}') use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmds}') emit_verbatim_ref(i, 'DefaultForm', defn.get('defaultForm')) emit_verbatim_ref(i, 'AuxiliaryForm', defn.get('auxiliaryForm')) incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') emit_mltext(i, 'ExtendedPresentation', defn.get('extendedPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) # --- 13c. ExchangePlan, ChartOfCharacteristicTypes, DocumentJournal --- def emit_exchange_plan_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') 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}{code_length}') X(f'{i}{code_allowed_length}') X(f'{i}{description_length}') X(f'{i}{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsDescription")}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') X(f'{i}{"true" if defn.get("quickChoice") is True else "false"}') X(f'{i}{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}') # 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}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}Directly') 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}{distributed}') X(f'{i}{include_ext}') X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}') X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') 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}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') 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}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') X(f'{i}{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}') X(f'{i}{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}') def emit_chart_of_characteristic_types_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') if defn.get('characteristicExtValues'): X(f'{i}{esc_xml(str(defn["characteristicExtValues"]))}') else: X(f'{i}') vt = defn.get('valueType') if not vt and defn.get('valueTypes'): vt = ' + '.join(defn['valueTypes']) if vt: X(f'{i}') emit_type_content(f'{i}\t', str(vt)) X(f'{i}') else: X(f'{i}') X(f'{i}\txs:boolean') X(f'{i}\txs:string') X(f'{i}\t') X(f'{i}\t\t100') X(f'{i}\t\tVariable') X(f'{i}\t') X(f'{i}\txs:decimal') X(f'{i}\t') X(f'{i}\t\t15') X(f'{i}\t\t2') X(f'{i}\t\tAny') X(f'{i}\t') X(f'{i}\txs:dateTime') X(f'{i}\t') X(f'{i}\t\tDateTime') X(f'{i}\t') X(f'{i}') X(f'{i}{"true" if defn.get("hierarchical") is True else "false"}') X(f'{i}{"false" if defn.get("foldersOnTop") is False else "true"}') 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}{code_length}') X(f'{i}{get_enum_prop("CodeAllowedLength", "codeAllowedLength", "Variable")}') X(f'{i}{description_length}') X(f'{i}{get_enum_prop("CodeSeries", "codeSeries", "WholeCharacteristicKind")}') X(f'{i}{"false" if defn.get("checkUnique") is False else "true"}') X(f'{i}{"false" if defn.get("autonumbering") is False else "true"}') X(f'{i}{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsDescription")}') emit_standard_attributes(i, 'ChartOfCharacteristicTypes') emit_characteristics(i, defn.get('characteristics')) X(f'{i}{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') X(f'{i}{"true" if defn.get("quickChoice") is True else "false"}') X(f'{i}{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}') 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}{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}') X(f'{i}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}Directly') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') 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}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') 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}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') X(f'{i}{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}') X(f'{i}{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}') def emit_document_journal_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') emit_verbatim_ref(i, 'DefaultForm', defn.get('defaultForm')) emit_verbatim_ref(i, 'AuxiliaryForm', defn.get('auxiliaryForm')) use_std_cmds = 'true' if get_bool_prop('useStandardCommands', True) else 'false' X(f'{i}{use_std_cmds}') reg_docs = list(defn.get('registeredDocuments', [])) if reg_docs: X(f'{i}') for rd in reg_docs: X(f'{i}\t{esc_xml(normalize_md_object_ref(str(rd)))}') X(f'{i}') else: X(f'{i}') incl_help = 'true' if get_bool_prop('includeHelpInContents', False) else 'false' X(f'{i}{incl_help}') emit_standard_attributes(i, 'DocumentJournal') emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('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}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') 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}{esc_xml(ext_dim_types)}') else: X(f'{i}') # Количество субконто: без ПВХ (extDimensionTypes) платформа не даёт > 0 → дефолт 0; с ПВХ — 3. max_ext_dim = str(defn['maxExtDimensionCount']) if defn.get('maxExtDimensionCount') is not None else ('3' if ext_dim_types else '0') X(f'{i}{max_ext_dim}') if defn.get('codeMask'): X(f'{i}{esc_xml_text(str(defn["codeMask"]))}') else: X(f'{i}') 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}{code_length}') X(f'{i}{description_length}') X(f'{i}{get_enum_prop("CodeSeries", "codeSeries", "WholeChartOfAccounts")}') X(f'{i}{"false" if defn.get("checkUnique") is False else "true"}') X(f'{i}{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsCode")}') 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}') X(f'{i}\t') X(f'{i}\t\t') X(f'{i}\t\t\t') X(f'{i}\t\t\t\t') X(f'{i}\t\t\t\tВиды субконто') X(f'{i}\t\t\t') X(f'{i}\t\t') X(f'{i}\t\t') X(f'{i}\t\t') X(f'{i}\t\tDontCheck') X(f'{i}\t\t') 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') X(f'{i}\t') X(f'{i}') X(f'{i}{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') X(f'{i}{"true" if defn.get("quickChoice") is True else "false"}') X(f'{i}{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}') # 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}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}Directly') X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}') X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') 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}{auto_order}') order_length = str(defn['orderLength']) if defn.get('orderLength') is not None else '9' X(f'{i}{order_length}') 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}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') X(f'{i}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') upd_dh = 'true' if get_bool_prop('updateDataHistoryImmediatelyAfterWrite', False) else 'false' X(f'{i}{upd_dh}') exec_dh = 'true' if get_bool_prop('executeAfterWriteDataHistoryVersionProcessing', False) else 'false' X(f'{i}{exec_dh}') 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}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') chart_of_accounts = str(defn['chartOfAccounts']) if defn.get('chartOfAccounts') else '' if chart_of_accounts: X(f'{i}{esc_xml(chart_of_accounts)}') else: X(f'{i}') correspondence = 'true' if defn.get('correspondence') is True else 'false' X(f'{i}{correspondence}') period_adj_len = str(defn['periodAdjustmentLength']) if defn.get('periodAdjustmentLength') is not None else '0' X(f'{i}{period_adj_len}') emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm')) emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm')) emit_standard_attributes(i, 'AccountingRegister') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Managed') X(f'{i}{data_lock_control_mode}') X(f'{i}{"false" if defn.get("enableTotalsSplitting") is False else "true"}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) # Стандартные ТЧ ПВР: Ведущие/Вытесняющие/Базовые виды расчёта (обёртка платформенно-константна, пустой lang; # вложены Predefined/CalculationType(ShowError)/LineNumber). CALC_TYPES_STD_TABULAR = [ ('LeadingCalculationTypes', 'Ведущие виды расчета'), ('DisplacingCalculationTypes', 'Вытесняющие виды расчета'), ('BaseCalculationTypes', 'Базовые виды расчета'), ] def emit_calc_types_std_tabular(i): X(f'{i}') for name, syn in CALC_TYPES_STD_TABULAR: X(f'{i}\t') X(f'{i}\t\t') X(f'{i}\t\t\t') X(f'{i}\t\t\t\t') X(f'{i}\t\t\t\t{esc_xml_text(syn)}') X(f'{i}\t\t\t') X(f'{i}\t\t') X(f'{i}\t\t') X(f'{i}\t\t') X(f'{i}\t\tDontCheck') X(f'{i}\t\t') for st_attr in ('Predefined', 'CalculationType', 'LineNumber'): st_ov = {'FillChecking': 'ShowError'} if st_attr == 'CalculationType' else None emit_standard_attribute(f'{i}\t\t\t', st_attr, st_ov) X(f'{i}\t\t') X(f'{i}\t') X(f'{i}') def emit_chart_of_calculation_types_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '5' description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '100' X(f'{i}{code_length}') X(f'{i}{description_length}') X(f'{i}{get_enum_prop("CodeType", "codeType", "String")}') X(f'{i}{get_enum_prop("CodeAllowedLength", "codeAllowedLength", "Variable")}') X(f'{i}{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsDescription")}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') X(f'{i}{"true" if defn.get("quickChoice") is True else "false"}') X(f'{i}{get_enum_prop("ChoiceMode", "choiceMode", "BothWays")}') 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'ChartOfCalculationTypes.{obj_name}.StandardAttribute.Description') if int(code_length) > 0: ib_fields.append(f'ChartOfCalculationTypes.{obj_name}.StandardAttribute.Code') emit_field_block(i, 'InputByString', ib_fields) X(f'{i}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}Directly') X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}') X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') 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_based_on(i, defn.get('basedOn')) X(f'{i}{get_enum_prop("DependenceOnCalculationTypes", "dependenceOnCalculationTypes", "DontUse")}') base_types = [resolve_type_prefix_syn(str(x)) for x in defn.get('baseCalculationTypes', [])] if base_types: X(f'{i}') for bt in base_types: X(f'{i}\t{esc_xml(bt)}') X(f'{i}') else: X(f'{i}') X(f'{i}{"true" if defn.get("actionPeriodUse") is True else "false"}') emit_standard_attributes(i, 'ChartOfCalculationTypes') emit_characteristics(i, defn.get('characteristics')) emit_calc_types_std_tabular(i) X(f'{i}{get_enum_prop("PredefinedDataUpdate", "predefinedDataUpdate", "Auto")}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') 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}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') 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}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') X(f'{i}{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}') X(f'{i}{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}') def emit_calculation_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') emit_form_ref(i, 'DefaultListForm', defn.get('defaultListForm')) emit_form_ref(i, 'AuxiliaryListForm', defn.get('auxiliaryListForm')) periodicity = get_enum_prop('InformationRegisterPeriodicity', 'periodicity', 'Month') X(f'{i}{periodicity}') action_period = 'true' if defn.get('actionPeriod') is True else 'false' X(f'{i}{action_period}') base_period = 'true' if defn.get('basePeriod') is True else 'false' X(f'{i}{base_period}') schedule = str(defn['schedule']) if defn.get('schedule') else '' if schedule: X(f'{i}{esc_xml(schedule)}') else: X(f'{i}') schedule_value = str(defn['scheduleValue']) if defn.get('scheduleValue') else '' if schedule_value: X(f'{i}{esc_xml(schedule_value)}') else: X(f'{i}') schedule_date = str(defn['scheduleDate']) if defn.get('scheduleDate') else '' if schedule_date: X(f'{i}{esc_xml(schedule_date)}') else: X(f'{i}') chart_of_calc_types = str(defn['chartOfCalculationTypes']) if defn.get('chartOfCalculationTypes') else '' if chart_of_calc_types: X(f'{i}{esc_xml(chart_of_calc_types)}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') emit_standard_attributes(i, 'CalculationRegister') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Managed') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') emit_mltext(i, 'ListPresentation', defn.get('listPresentation')) emit_mltext(i, 'ExtendedListPresentation', defn.get('extendedListPresentation')) emit_mltext(i, 'Explanation', defn.get('explanation')) def emit_business_process_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') if 'inputByString' in defn: ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])] else: ib_fields = [f'BusinessProcess.{obj_name}.StandardAttribute.Number'] emit_field_block(i, 'InputByString', ib_fields) X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}') X(f'{i}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}Directly') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') 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')) X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') 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' X(f'{i}{number_type}') X(f'{i}{number_length}') X(f'{i}{number_allowed_length}') X(f'{i}{check_unique}') emit_standard_attributes(i, 'BusinessProcess') emit_characteristics(i, defn.get('characteristics')) X(f'{i}{"false" if defn.get("autonumbering") is False else "true"}') emit_based_on(i, defn.get('basedOn')) X(f'{i}{get_enum_prop("NumberPeriodicity", "numberPeriodicity", "Nonperiodical")}') task_ref = str(defn['task']) if defn.get('task') else '' if task_ref: X(f'{i}{esc_xml(task_ref)}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("createTaskInPrivilegedMode", True) else "false"}') 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}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') 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}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') X(f'{i}{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}') X(f'{i}{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}') def emit_task_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) if defn.get('comment'): X(f'{i}{esc_xml_text(str(defn["comment"]))}') else: X(f'{i}') X(f'{i}{"true" if get_bool_prop("useStandardCommands", True) else "false"}') 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}{number_type}') X(f'{i}{number_length}') X(f'{i}{number_allowed_length}') X(f'{i}{check_unique}') X(f'{i}{autonumbering}') X(f'{i}{task_number_auto_prefix}') X(f'{i}{description_length}') X(f'{i}{esc_xml(str(defn["addressing"]))}' if defn.get('addressing') else f'{i}') X(f'{i}{esc_xml(str(defn["mainAddressingAttribute"]))}' if defn.get('mainAddressingAttribute') else f'{i}') X(f'{i}{esc_xml(str(defn["currentPerformer"]))}' if defn.get('currentPerformer') else f'{i}') emit_based_on(i, defn.get('basedOn')) emit_standard_attributes(i, 'Task') emit_characteristics(i, defn.get('characteristics')) X(f'{i}{get_enum_prop("DefaultPresentation", "defaultPresentation", "AsDescription")}') X(f'{i}{get_enum_prop("EditType", "editType", "InDialog")}') if 'inputByString' in defn: ib_fields = [expand_data_path(str(x)) for x in (defn.get('inputByString') or [])] else: ib_fields = [f'Task.{obj_name}.StandardAttribute.Number'] emit_field_block(i, 'InputByString', ib_fields) X(f'{i}{get_enum_prop("SearchStringModeOnInputByString", "searchStringModeOnInputByString", "Begin")}') X(f'{i}{get_enum_prop("FullTextSearchOnInputByString", "fullTextSearchOnInputByString", "DontUse")}') X(f'{i}Directly') X(f'{i}{get_enum_prop("CreateOnInput", "createOnInput", "DontUse")}') 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')) X(f'{i}{get_enum_prop("ChoiceHistoryOnInput", "choiceHistoryOnInput", "Auto")}') X(f'{i}{"true" if get_bool_prop("includeHelpInContents", False) else "false"}') 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}{get_enum_prop("DataLockControlMode", "dataLockControlMode", "Managed")}') X(f'{i}{get_enum_prop("FullTextSearch", "fullTextSearch", "Use")}') 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}{get_enum_prop("DataHistory", "dataHistory", "DontUse")}') X(f'{i}{"true" if get_bool_prop("updateDataHistoryImmediatelyAfterWrite", False) else "false"}') X(f'{i}{"true" if get_bool_prop("executeAfterWriteDataHistoryVersionProcessing", False) else "false"}') def emit_http_service_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') root_url = str(defn['rootURL']) if defn.get('rootURL') else obj_name.lower() X(f'{i}{esc_xml(root_url)}') reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse') X(f'{i}{reuse_sessions}') session_max_age = str(defn['sessionMaxAge']) if defn.get('sessionMaxAge') is not None else '20' X(f'{i}{session_max_age}') def emit_web_service_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') namespace = str(defn['namespace']) if defn.get('namespace') else '' X(f'{i}{esc_xml(namespace)}') xdto_packages = str(defn['xdtoPackages']) if defn.get('xdtoPackages') else '' if xdto_packages: X(f'{i}{xdto_packages}') else: X(f'{i}') reuse_sessions = get_enum_prop('ReuseSessions', 'reuseSessions', 'DontUse') X(f'{i}{reuse_sessions}') session_max_age = str(defn['sessionMaxAge']) if defn.get('sessionMaxAge') is not None else '20' X(f'{i}{session_max_age}') # --- 13g. ChildObjects emitters for new types --- def emit_column(indent, col_def): uid = new_uuid() name = '' col_synonym = None comment = '' 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 = col_def['synonym'] if col_def.get('synonym') is not None else split_camel_case(name) # строка ИЛИ {ru,en} if col_def.get('comment'): comment = str(col_def['comment']) if col_def.get('indexing'): indexing = str(col_def['indexing']) if col_def.get('references'): references = list(col_def['references']) X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(name)}') emit_mltext(f'{indent}\t\t', 'Synonym', col_synonym) if comment: X(f'{indent}\t\t{esc_xml_text(comment)}') else: X(f'{indent}\t\t') X(f'{indent}\t\t{indexing}') if references: X(f'{indent}\t\t') for ref in references: X(f'{indent}\t\t\t{esc_xml(normalize_md_object_ref(str(ref)))}') X(f'{indent}\t\t') else: X(f'{indent}\t\t') X(f'{indent}\t') X(f'{indent}') 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}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(tmpl_name)}') emit_mltext(f'{indent}\t\t', 'Synonym', tmpl_synonym) X(f'{indent}\t\t') X(f'{indent}\t') if methods: X(f'{indent}\t') 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') X(f'{indent}\t\t\t') X(f'{indent}\t\t\t\t{esc_xml(method_name)}') emit_mltext(f'{indent}\t\t\t\t', 'Synonym', method_synonym) X(f'{indent}\t\t\t\t{http_method}') X(f'{indent}\t\t\t\t{esc_xml(handler)}') X(f'{indent}\t\t\t') X(f'{indent}\t\t') X(f'{indent}\t') else: X(f'{indent}\t') X(f'{indent}') 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}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(op_name)}') emit_mltext(f'{indent}\t\t', 'Synonym', op_synonym) X(f'{indent}\t\t') X(f'{indent}\t\t{return_type}') X(f'{indent}\t\t{nillable}') X(f'{indent}\t\t{transactioned}') X(f'{indent}\t\t{esc_xml(handler)}') X(f'{indent}\t') if params: X(f'{indent}\t') 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') X(f'{indent}\t\t\t') X(f'{indent}\t\t\t\t{esc_xml(param_name)}') emit_mltext(f'{indent}\t\t\t\t', 'Synonym', param_synonym) X(f'{indent}\t\t\t\t{param_type}') X(f'{indent}\t\t\t\t{param_nillable}') X(f'{indent}\t\t\t\t{param_dir}') X(f'{indent}\t\t\t') X(f'{indent}\t\t') X(f'{indent}\t') else: X(f'{indent}\t') X(f'{indent}') def emit_addressing_attribute(indent, addr_def): # Реквизит адресации = полный object-слой реквизита (контекст task-addressing) + AddressingDimension. parsed = parse_attribute_shorthand(addr_def) emit_attribute(indent, parsed, 'task-addressing', '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']+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('') X(f'') X(f'\t<{obj_type} uuid="{obj_uuid}">') # InternalInfo emit_internal_info('\t\t', obj_type, obj_name) # Properties X('\t\t') 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, 'FunctionalOption': emit_functional_option_properties, 'Sequence': emit_sequence_properties, 'FilterCriterion': emit_filter_criterion_properties, 'DocumentNumerator': emit_document_numerator_properties, 'SettingsStorage': emit_settings_storage_properties, 'CommonForm': emit_common_form_properties, 'SessionParameter': emit_session_parameter_properties, 'CommonCommand': emit_common_command_properties, 'CommandGroup': emit_command_group_properties, 'CommonAttribute': emit_common_attribute_properties, 'FunctionalOptionsParameter': emit_functional_options_parameter_properties, 'WSReference': emit_ws_reference_properties, 'CommonPicture': emit_common_picture_properties, 'CommonTemplate': emit_common_template_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') # 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, 'fillChecking': None, 'use': 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'), 'fillChecking': val.get('fillChecking'), 'use': val.get('use')} 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') 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 in ('ChartOfAccounts', 'ChartOfCalculationTypes'): context = 'account' # как catalog, но БЕЗ (реквизиты ПС/ПВР не иерархичны как справочник) 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'), e.get('fillChecking'), e.get('use')) 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') else: X('\t\t') # --- 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') for v in values: emit_enum_value('\t\t\t', v) X('\t\t') else: X('\t\t') # --- 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)) reg_commands = [] if defn.get('commands'): cd = defn['commands'] if isinstance(cd, list): for c in cd: reg_commands.append({'name': str(c.get('name', '')), 'def': c}) else: for k, v in cd.items(): reg_commands.append({'name': k, 'def': v}) if dims or resources or reg_attrs or reg_commands: has_children = True X('\t\t') # InformationRegister.Attribute несёт FillFromFillingValue/FillValue/DataHistory; CalculationRegister.Attribute # несёт (register-calc); прочие — register-other. reg_ctx = {'InformationRegister': 'register-info', 'CalculationRegister': 'register-calc'}.get(obj_type, 'register-other') # Все семейства регистров: ресурсы/измерения — через богатый emit_attribute (общий слой object-свойств). dim_res_ctx = {'InformationRegister': 'register-info', 'AccumulationRegister': 'register-accum', 'CalculationRegister': 'register-calc', 'AccountingRegister': 'register-account'}.get(obj_type) for r in resources: if dim_res_ctx: emit_attribute('\t\t\t', r, dim_res_ctx, 'Resource') else: emit_resource('\t\t\t', r, obj_type) for d in dims: if dim_res_ctx: emit_attribute('\t\t\t', d, dim_res_ctx, 'Dimension') else: emit_dimension('\t\t\t', d, obj_type) for a in reg_attrs: emit_attribute('\t\t\t', a, reg_ctx) for cmd in reg_commands: emit_command('\t\t\t', cmd['name'], cmd['def']) X('\t\t') else: X('\t\t') # --- DocumentJournal: columns + commands --- if obj_type == 'DocumentJournal': columns = list(defn.get('columns', [])) dj_commands = [] if defn.get('commands'): cd = defn['commands'] if isinstance(cd, list): for c in cd: dj_commands.append({'name': str(c.get('name', '')), 'def': c}) else: for k, v in cd.items(): dj_commands.append({'name': k, 'def': v}) if columns or dj_commands: has_children = True X('\t\t') for col in columns: emit_column('\t\t\t', col) for cmd in dj_commands: emit_command('\t\t\t', cmd['name'], cmd['def']) X('\t\t') else: X('\t\t') # --- Sequence: dimensions --- if obj_type == 'Sequence': seq_dims = list(defn.get('dimensions', [])) if seq_dims: has_children = True X('\t\t') for d in seq_dims: emit_sequence_dimension('\t\t\t', d) X('\t\t') else: X('\t\t') # --- FilterCriterion / SettingsStorage: ChildObjects (формы стрипаются; FilterCriterion может нести ) --- if obj_type in ('FilterCriterion', 'SettingsStorage'): fc_commands = [] if defn.get('commands'): cd = defn['commands'] if isinstance(cd, list): for c in cd: fc_commands.append({'name': str(c.get('name', '')), 'def': c}) else: for k, v in cd.items(): fc_commands.append({'name': k, 'def': v}) if fc_commands: has_children = True X('\t\t') for cmd in fc_commands: emit_command('\t\t\t', cmd['name'], cmd['def']) X('\t\t') else: X('\t\t') # DocumentNumerator: 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') for tmpl_name in sorted(url_tmpl_order): emit_url_template('\t\t\t', tmpl_name, url_templates[tmpl_name]) X('\t\t') else: X('\t\t') # --- 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') for op_name in sorted(op_order): emit_operation('\t\t\t', op_name, operations[op_name]) X('\t\t') else: X('\t\t') # --- CommonModule: no ChildObjects --- X(f'\t') X('') 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', 'FunctionalOption': 'FunctionalOptions', 'Sequence': 'Sequences', 'FilterCriterion': 'FilterCriteria', 'DocumentNumerator': 'DocumentNumerators', 'SettingsStorage': 'SettingsStorages', 'CommonForm': 'CommonForms', 'SessionParameter': 'SessionParameters', 'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'CommonAttribute': 'CommonAttributes', 'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'WSReference': 'WSReferences', 'CommonPicture': 'CommonPictures', 'CommonTemplate': 'CommonTemplates', } 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) # CommonCommand — заготовка модуля команды (CommandModule.bsl). if obj_type == 'CommonCommand': module_path = os.path.join(ext_dir, 'CommandModule.bsl') if not os.path.isfile(module_path): ensure_ext_dir() write_utf8_bom(module_path, '') modules_created.append(module_path) # CommonForm — заготовка структуры формы под компиляцию (form-compile наполняет содержимое). if obj_type == 'CommonForm': ensure_ext_dir() cf_form_xml_path = os.path.join(ext_dir, 'Form.xml') if not os.path.isfile(cf_form_xml_path): cf_ns = ('xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" ' 'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" ' 'xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" ' 'xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" ' 'xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" ' 'xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" ' 'xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"') cf_form_xml = ('\n
\n' '\t\n\t\ttrue\n\t\n' '\t\n\n') write_utf8_bom(cf_form_xml_path, cf_form_xml) modules_created.append(cf_form_xml_path) cf_module_dir = os.path.join(ext_dir, 'Form') os.makedirs(cf_module_dir, exist_ok=True) cf_module_path = os.path.join(cf_module_dir, 'Module.bsl') if not os.path.isfile(cf_module_path): write_utf8_bom(cf_module_path, '') modules_created.append(cf_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() # '' → пустой 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}') out.append(f'{indent}\t{esc_xml_text(r["name"])}') if not r['code']: out.append(f'{indent}\t') elif code_type == 'Number': out.append(f'{indent}\t{esc_xml_text(r["code"])}') else: out.append(f'{indent}\t{esc_xml_text(r["code"])}') if r['desc'] == '': out.append(f'{indent}\t') else: out.append(f'{indent}\t{esc_xml_text(r["desc"])}') # Type — тип значения предопределённой характеристики (ПВХ). None→нет блока; ''→; 'A + B'→наполненный. rt = r.get('type') if rt is not None and str(rt) == '': out.append(f'{indent}\t') elif rt: out.append(f'{indent}\t') global lines saved = lines lines = [] emit_type_content(indent + '\t\t', str(rt)) out.extend(lines) lines = saved out.append(f'{indent}\t') out.append(f'{indent}\t{"true" if r["isFolder"] else "false"}') if r['children']: out.append(f'{indent}\t') for c in r['children']: emit_predef_item(out, c, indent + '\t\t', code_type) out.append(f'{indent}\t') out.append(f'{indent}') def build_predefined_xml(items, xsi_type, code_type): out = [''] out.append(f'') for it in items: emit_predef_item(out, it, '\t', code_type) out.append('') 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{v}') out.append(f'{indent}') 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}') out.append(f'{indent}\t{esc_xml_text(name)}') out.append(f'{indent}\t' if not code else f'{indent}\t{esc_xml_text(code)}') out.append(f'{indent}\t' if desc == '' else f'{indent}\t{esc_xml_text(desc)}') out.append(f'{indent}\t{acct_type}') out.append(f'{indent}\t{off}') out.append(f'{indent}\t{esc_xml_text(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') else: out.append(f'{indent}\t') 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') out.append(f'{indent}\t\t\t{sc_turn}') 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') out.append(f'{indent}\t') child_arr = list(children) if children else [] if child_arr: out.append(f'{indent}\t') 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') out.append(f'{indent}') def build_predefined_account_xml(items, obj_nm, acct_flag_names, ext_dim_flag_names, ext_dim_types_ref=''): out = [''] out.append(f'') 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('') return '\n'.join(out) + '\n' # Предопределённые ВИДЫ РАСЧЁТА (плоские: Name/Code/Description/ActionPeriodIsBase). def emit_predef_calc_type(out, val, indent): r = resolve_predef_item(val) apib = 'false' if not isinstance(val, str): if _predef_acc_get(val, ('actionPeriodIsBase', 'периодДействияБазовый')) is True: apib = 'true' out.append(f'{indent}') out.append(f'{indent}\t{esc_xml_text(r["name"])}') out.append(f'{indent}\t' if not r['code'] else f'{indent}\t{esc_xml_text(r["code"])}') out.append(f'{indent}\t' if r['desc'] == '' else f'{indent}\t{esc_xml_text(r["desc"])}') out.append(f'{indent}\t{apib}') out.append(f'{indent}') def build_predefined_calc_type_xml(items): out = [''] out.append(f'') for it in items: emit_predef_calc_type(out, it, '\t') out.append('') return '\n'.join(out) + '\n' # Special files # --- Состав плана обмена (ExchangePlan, Ext/Content.xml). Ключ `content`/`Состав`: # [ "MDRef" (AutoRecord=Deny, дефолт) | "MDRef: autoRecord" (Allow) | {metadata, autoRecord} ]. --- def parse_exchange_content_item(entry): if isinstance(entry, str): ref, ar = entry, 'Deny' ci = entry.rfind(':') if ci >= 0: ref = entry[:ci].strip() flag = entry[ci + 1:].strip() if re.match(r'^(autoRecord|АвтоРегистрация|Allow|Разрешить)$', flag, re.I): ar = 'Allow' elif re.match(r'^(Deny|Запретить)$', flag, re.I): ar = 'Deny' return {'metadata': ref.strip(), 'autoRecord': ar} ref = '' for k in ('metadata', 'Метаданные', 'объект'): if entry.get(k) is not None: ref = str(entry[k]); break raw_ar = entry.get('autoRecord') if raw_ar is None: raw_ar = entry.get('АвтоРегистрация') ar = 'Deny' if isinstance(raw_ar, bool): if raw_ar: ar = 'Allow' elif raw_ar is not None and re.match(r'^(Allow|Разрешить|true|autoRecord|АвтоРегистрация)$', str(raw_ar), re.I): ar = 'Allow' return {'metadata': ref.strip(), 'autoRecord': ar} if obj_type == 'ExchangePlan': content_path = os.path.join(ext_dir, 'Content.xml') xep_ns = 'xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" 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"' c_src = defn.get('content') if c_src is None: c_src = defn.get('Состав') c_items = [] if c_src: for e in (c_src if isinstance(c_src, list) else [c_src]): it = parse_exchange_content_item(e) if it['metadata']: c_items.append(it) if c_items: ensure_ext_dir() parts = ['\r\n', f'\r\n'] for it in c_items: parts.append('\t\r\n') parts.append(f'\t\t{esc_xml(it["metadata"])}\r\n') parts.append(f'\t\t{it["autoRecord"]}\r\n') parts.append('\t\r\n') parts.append('\r\n') write_utf8_bom(content_path, ''.join(parts)) modules_created.append(content_path) elif not os.path.isfile(content_path): ensure_ext_dir() content_xml = f'\r\n\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'\r\n\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 == 'ChartOfCalculationTypes' and defn.get('predefined'): ensure_ext_dir() predef_xml = build_predefined_calc_type_xml(defn['predefined']) 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): # Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation). with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f: config_content = f.read() ns = 'http://v8.1c.ru/8.3/MDClasses' # ET is used ONLY read-only here: to locate ChildObjects and detect a duplicate. # We deliberately do NOT re-serialize Configuration.xml with ElementTree.write(): # it drops every xmlns declaration used only inside attribute VALUES (e.g. # xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees those # prefixes in element/attribute names. The dropped declaration makes XDTO read the # value as anyType and Designer refuses to load the file (issue #38). Registration is # therefore done by raw-text insertion, preserving BOM, EOL and all namespaces # byte-for-byte (same approach as subsystem-compile). 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 None: reg_result = 'no-childobj' else: existing = child_objects.findall(f'{{{ns}}}{child_tag}') already_exists = any((e.text or '').strip() == obj_name for e in existing) if already_exists: reg_result = 'already' else: eol = '\r\n' if '\r\n' in config_content else '\n' entry = f'<{child_tag}>{esc_xml(obj_name)}' block = re.search(r'.*?', config_content, re.S) if block is None: # Empty self-closing => open it with the first entry. empty = re.search(r'', config_content) if empty is None: reg_result = 'no-childobj' else: replacement = f'{eol}\t\t\t{entry}{eol}\t\t' new_content = config_content[:empty.start()] + replacement + config_content[empty.end():] write_utf8_bom(config_xml_path, new_content) reg_result = 'added' else: close_same = f'' last_same = config_content.rfind(close_same, block.start(), block.end()) if last_same != -1: # After the last element of the same type (keeps them grouped). insert_at = last_same + len(close_same) new_content = (config_content[:insert_at] + f'{eol}\t\t\t{entry}' + config_content[insert_at:]) else: # No element of this type yet: new line before , # reusing the block's existing closing indent for . close_at = config_content.rfind('', block.start(), block.end()) new_content = (config_content[:close_at] + f'\t{entry}{eol}\t\t' + config_content[close_at:]) write_utf8_bom(config_xml_path, new_content) reg_result = 'added' 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} added to ChildObjects') elif reg_result == 'already': print(f' Configuration.xml: <{child_tag}>{obj_name} already registered') elif reg_result == 'no-childobj': print('WARNING: Configuration.xml found but 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"')