#!/usr/bin/env python3 # meta-compile v1.35 — 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) if len(result) > 1: result = result[0] + result[1:].lower() return result # --------------------------------------------------------------------------- # 1. Load and validate JSON # --------------------------------------------------------------------------- parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument('-JsonPath', required=True) parser.add_argument('-OutputDir', required=True) args = parser.parse_args() json_path = args.JsonPath output_dir = args.OutputDir if not os.path.isfile(json_path): print(f'File not found: {json_path}', file=sys.stderr) sys.exit(1) with open(json_path, 'r', encoding='utf-8-sig') as f: json_text = f.read() defn = json.loads(json_text) assert_edit_allowed(output_dir, "editable") # --- Batch mode: JSON array of objects --- if isinstance(defn, list): batch_ok = 0 batch_fail = 0 for idx, item in enumerate(defn, 1): tmp_fd, tmp_path = tempfile.mkstemp(suffix='.json', prefix=f'meta-compile-batch-{idx}-') try: with os.fdopen(tmp_fd, 'w', encoding='utf-8') as f: json.dump(item, f, ensure_ascii=False, indent=2) rc = subprocess.call([sys.executable, __file__, '-JsonPath', tmp_path, '-OutputDir', output_dir]) if rc == 0: batch_ok += 1 else: batch_fail += 1 finally: if os.path.exists(tmp_path): os.unlink(tmp_path) print() print(f"=== Batch: {len(defn)} objects, {batch_ok} compiled, {batch_fail} failed ===") sys.exit(1 if batch_fail > 0 else 0) # Normalize field synonyms: accept "objectType" as alias for "type" if not defn.get('type') and defn.get('objectType'): defn['type'] = defn['objectType'] # Object type synonyms (Russian -> English) object_type_synonyms = { 'Справочник': 'Catalog', 'Каталог': 'Catalog', 'Документ': 'Document', 'Перечисление': 'Enum', 'Константа': 'Constant', 'РегистрСведений': 'InformationRegister', 'РегистрНакопления': 'AccumulationRegister', 'РегистрБухгалтерии': 'AccountingRegister', 'РегистрРасчёта': 'CalculationRegister', 'РегистрРасчета': 'CalculationRegister', 'ПланСчетов': 'ChartOfAccounts', 'ПланВидовХарактеристик': 'ChartOfCharacteristicTypes', 'ПланВидовРасчёта': 'ChartOfCalculationTypes', 'ПланВидовРасчета': 'ChartOfCalculationTypes', 'БизнесПроцесс': 'BusinessProcess', 'Задача': 'Task', 'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal', 'Отчёт': 'Report', 'Отчет': 'Report', 'Обработка': 'DataProcessor', 'ОбщийМодуль': 'CommonModule', 'РегламентноеЗадание': 'ScheduledJob', 'ПодпискаНаСобытие': 'EventSubscription', 'HTTPСервис': 'HTTPService', 'ВебСервис': 'WebService', 'ОпределяемыйТип': 'DefinedType', } # Enum property value synonyms — model often gets these slightly wrong enum_value_aliases = { # RegisterType (AccumulationRegister) 'Balances': 'Balance', 'Остатки': 'Balance', 'Обороты': 'Turnovers', # WriteMode (InformationRegister) 'RecordSubordinate': 'RecorderSubordinate', 'Subordinate': 'RecorderSubordinate', 'ПодчинениеРегистратору': 'RecorderSubordinate', 'Независимый': 'Independent', # DependenceOnCalculationTypes (ChartOfCalculationTypes) 'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse', 'NotUsed': 'DontUse', 'Depend': 'OnActionPeriod', 'ПоПериодуДействия': 'OnActionPeriod', # InformationRegisterPeriodicity 'None': 'Nonperiodical', 'Daily': 'Day', 'Monthly': 'Month', 'Quarterly': 'Quarter', 'Yearly': 'Year', 'Непериодический': 'Nonperiodical', 'Секунда': 'Second', 'День': 'Day', 'Месяц': 'Month', 'Квартал': 'Quarter', 'Год': 'Year', 'ПозицияРегистратора': 'RecorderPosition', # DataLockControlMode 'Автоматический': 'Automatic', 'Управляемый': 'Managed', # FullTextSearch 'Использовать': 'Use', 'НеИспользовать': 'DontUse', # Posting 'Разрешить': 'Allow', 'Запретить': 'Deny', # EditType 'ВДиалоге': 'InDialog', 'ВСписке': 'InList', 'ОбаСпособа': 'BothWays', # DefaultPresentation 'ВВидеНаименования': 'AsDescription', 'ВВидеКода': 'AsCode', # FillChecking 'НеПроверять': 'DontCheck', 'Ошибка': 'ShowError', 'Предупреждение': 'ShowWarning', # Indexing 'НеИндексировать': 'DontIndex', 'Индексировать': 'Index', 'ИндексироватьСДопУпорядочиванием': 'IndexWithAdditionalOrder', } # Valid enum values per property (from meta-validate) valid_enum_values = { 'RegisterType': ['Balance', 'Turnovers'], 'WriteMode': ['Independent', 'RecorderSubordinate'], 'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'], 'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'], 'DataLockControlMode': ['Automatic', 'Managed'], 'FullTextSearch': ['Use', 'DontUse'], 'DataHistory': ['Use', 'DontUse'], 'DefaultPresentation': ['AsDescription', 'AsCode'], 'Posting': ['Allow', 'Deny'], 'RealTimePosting': ['Allow', 'Deny'], 'EditType': ['InDialog', 'InList', 'BothWays'], 'HierarchyType': ['HierarchyFoldersAndItems', 'HierarchyOfItems'], 'CodeType': ['String', 'Number'], 'CodeAllowedLength': ['Variable', 'Fixed'], 'NumberType': ['String', 'Number'], 'NumberAllowedLength': ['Variable', 'Fixed'], 'RegisterRecordsDeletion': ['AutoDelete', 'AutoDeleteOnUnpost', 'AutoDeleteOff'], 'RegisterRecordsWritingOnPost': ['WriteModified', 'WriteSelected', 'WriteAll'], 'ReturnValuesReuse': ['DontUse', 'DuringRequest', 'DuringSession'], 'ReuseSessions': ['DontUse', 'AutoUse'], 'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'], 'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'], 'SubordinationUse': ['ToItems', 'ToFolders', 'ToFoldersAndItems'], 'CodeSeries': ['WholeCatalog', 'WithinSubordination', 'WithinOwnerSubordination', 'WholeCharacteristicKind'], 'ChoiceMode': ['BothWays', 'QuickChoice', 'FromForm'], 'CreateOnInput': ['Auto', 'Use', 'DontUse'], 'ChoiceHistoryOnInput': ['Auto', 'DontUse'], 'PredefinedDataUpdate': ['Auto', 'DontAutoUpdate', 'AutoUpdate'], 'SearchStringModeOnInputByString': ['Begin', 'AnyPart'], } def normalize_enum_value(prop_name, value): # 1. Check alias dictionary — silent auto-correct if value in enum_value_aliases: return enum_value_aliases[value] # 2. Case-insensitive match against valid values — silent valid = valid_enum_values.get(prop_name) if valid: for v in valid: if v.lower() == value.lower(): return v # 3. Known property, unknown value — error with hint print(f"Invalid value '{value}' for property '{prop_name}'. Valid values: {', '.join(valid)}", file=sys.stderr) sys.exit(1) # 4. Unknown property — pass-through (no validation data) return value def get_enum_prop(prop_name, field_name, default): val = defn.get(field_name) raw = str(val) if val else default return normalize_enum_value(prop_name, raw) def get_bool_prop(field_name, default): """Bool object-свойство: presence-aware (иначе false спутать с отсутствием). Прощаем строки.""" val = defn.get(field_name) if val is None: return default if isinstance(val, bool): return val return str(val).lower() in ('true', '1', 'да', 'истина') def normalize_form_ref(s): """Прощающая нормализация ссылки на форму: рус корень, Форма→Form, короткая запись → вставка Form.""" if not s: return s parts = s.split('.') if len(parts) < 3: return s root = fill_ref_roots.get(parts[0].lower()) if root: parts[0] = root for k in range(1, len(parts)): if parts[k].lower() == 'форма': parts[k] = 'Form' if 'Form' not in parts and len(parts) == 3: parts = [parts[0], parts[1], 'Form', parts[2]] return '.'.join(parts) def emit_form_ref(i, tag, val): """Ссылка на форму по умолчанию: непустая → значение, иначе .""" if val: X(f'{i}<{tag}>{esc_xml(normalize_form_ref(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', ] if obj_type not in valid_types: print(f"Unsupported type: {obj_type}. Valid: {', '.join(valid_types)}", file=sys.stderr) sys.exit(1) if not defn.get('name'): print("JSON must have 'name' field", file=sys.stderr) sys.exit(1) obj_name = str(defn['name']) # Auto-synonym # Проброс без стрингификации (строка ИЛИ {ru,en} — мультиязычный синоним объекта). synonym = defn['synonym'] if defn.get('synonym') is not None else split_camel_case(obj_name) comment = str(defn['comment']) if defn.get('comment') else '' # --------------------------------------------------------------------------- # 4. Type system # --------------------------------------------------------------------------- type_synonyms = { 'число': 'Number', 'строка': 'String', 'булево': 'Boolean', 'дата': 'Date', 'датавремя': 'DateTime', 'время': 'Time', 'time': 'Time', 'number': 'Number', 'string': 'String', 'boolean': 'Boolean', 'date': 'Date', 'datetime': 'DateTime', 'bool': 'Boolean', # ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон). 'valuestorage': 'ValueStorage', 'base64binary': 'ValueStorage', 'хранилищезначений': 'ValueStorage', 'хранилищезначения': 'ValueStorage', 'uuid': 'UUID', 'уникальныйидентификатор': 'UUID', # Reference synonyms (Russian, lowercase) 'справочникссылка': 'CatalogRef', 'документссылка': 'DocumentRef', 'перечислениессылка': 'EnumRef', 'плансчетовссылка': 'ChartOfAccountsRef', 'планвидовхарактеристикссылка': 'ChartOfCharacteristicTypesRef', 'планвидоврасчётассылка': 'ChartOfCalculationTypesRef', 'планвидоврасчетассылка': 'ChartOfCalculationTypesRef', 'планобменассылка': 'ExchangePlanRef', 'бизнеспроцессссылка': 'BusinessProcessRef', 'задачассылка': 'TaskRef', 'определяемыйтип': 'DefinedType', 'definedtype': 'DefinedType', # English lowercase ref synonyms 'catalogref': 'CatalogRef', 'documentref': 'DocumentRef', 'enumref': 'EnumRef', } def resolve_type_str(type_str): if not type_str: return type_str # Parameterized types: Number(15,2), Строка(100), etc. m = re.match(r'^([^(]+)\((.+)\)$', type_str) if m: base_name = m.group(1).strip() params = m.group(2) resolved = type_synonyms.get(base_name.lower()) if resolved: return f'{resolved}({params})' return type_str # Reference types: СправочникСсылка.Организации -> CatalogRef.Организации if '.' in type_str: dot_idx = type_str.index('.') prefix = type_str[:dot_idx] suffix = type_str[dot_idx:] # includes the dot resolved = type_synonyms.get(prefix.lower()) if resolved: return f'{resolved}{suffix}' return type_str # Simple name lookup resolved = type_synonyms.get(type_str.lower()) if resolved: return resolved return type_str def emit_type_content(indent, type_str): if not type_str: return # Composite type: "Type1 + Type2 + Type3" if ' + ' in type_str: parts = [p.strip() for p in type_str.split('+')] for part in parts: emit_type_content(indent, part) return type_str = resolve_type_str(type_str) # Boolean if type_str == 'Boolean': X(f'{indent}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 # Reference types — use local xmlns declaration for 1C compatibility m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$', type_str) if m: X(f'{indent}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', 'ложь', 'нет') # XxxRef (тип реквизита) → корень DTR-пути (для разворота короткой записи значения). fill_ref_kind_root = { 'catalogref': 'Catalog', 'documentref': 'Document', 'enumref': 'Enum', 'chartofaccountsref': 'ChartOfAccounts', 'chartofcharacteristictypesref': 'ChartOfCharacteristicTypes', 'chartofcalculationtypesref': 'ChartOfCalculationTypes', 'exchangeplanref': 'ExchangePlan', 'businessprocessref': 'BusinessProcess', 'taskref': 'Task', } def normalize_fill_ref(s): """Строка → нормализованный DTR-путь ЛИБО None (не ссылка).""" if not s: return None if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$', s): return s parts = s.split('.') if len(parts) < 2: return None root = fill_ref_roots.get(parts[0].lower()) if not root: return None type_name = parts[1] if root == 'Enum': if len(parts) == 2: return None if len(parts) == 3: if parts[2].lower() in fill_empty_ref_words: return f'Enum.{type_name}.EmptyRef' return f'Enum.{type_name}.EnumValue.{parts[2]}' if parts[2].lower() in fill_enum_val_words: rest = '.'.join(parts[3:]) else: rest = '.'.join(parts[2:]) return f'Enum.{type_name}.EnumValue.{rest}' tail = list(parts[1:]) for i in range(len(tail)): if tail[i].lower() in fill_empty_ref_words: tail[i] = 'EmptyRef' return f'{root}.' + '.'.join(tail) def expand_fill_short_ref(s, type_str): """Короткая запись значения ссылочного реквизита (без точки) → полный DTR-путь по типу, либо None.""" if not type_str: return None if '+' in type_str: # составной тип — короткая форма неоднозначна return None t = resolve_type_str(type_str) m = re.match(r'^(\w+Ref)\.(.+)$', t) if not m: return None root = fill_ref_kind_root.get(m.group(1).lower()) if not root: return None type_name = m.group(2) if s.lower() in fill_empty_ref_words: return f'{root}.{type_name}.EmptyRef' if root == 'Enum': return f'Enum.{type_name}.EnumValue.{s}' return f'{root}.{type_name}.{s}' def resolve_fill_value_spec(s, type_str): """Строковый spec → (xsi_type, text). Интерпретация по типу реквизита.""" cat = get_fill_type_category(type_str) if s == '': return ('xs:string', '') if cat == 'String': return ('xs:string', s) if cat == 'Boolean' or s.lower() in fill_bool_true or s.lower() in fill_bool_false: if s.lower() in fill_bool_true: return ('xs:boolean', 'true') if s.lower() in fill_bool_false: return ('xs:boolean', 'false') if cat == 'Number': return ('xs:decimal', s) if cat == 'Date' or re.match(r'^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$', s): if re.match(r'^\d{4}-\d{2}-\d{2}$', s): s = f'{s}T00:00:00' return ('xs:dateTime', s) ref = normalize_fill_ref(s) if ref: return ('xr:DesignTimeRef', ref) short = expand_fill_short_ref(s, type_str) if short: return ('xr:DesignTimeRef', short) return ('xs:string', s) def format_fill_num(n): if isinstance(n, bool): return 'true' if n else 'false' return str(n) def emit_fill_value(indent, type_str, spec, has_spec): """spec — значение ключа fillValue (None при явном nil-override), has_spec — присутствует ли ключ.""" cat = get_fill_type_category(type_str) if not has_spec: if cat == 'String': X(f'{indent}') 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, 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': '', 'synonym': '', 'comment': '', 'flags': [], 'fillChecking': '', 'indexing': '', 'hasFillValue': False, 'fillValue': None, } parts = val.split('|', 1) main_part = parts[0].strip() if len(parts) > 1: flag_str = parts[1].strip() parsed['flags'] = [f.strip().lower() for f in flag_str.split(',') if f.strip()] colon_parts = main_part.split(':', 1) parsed['name'] = colon_parts[0].strip() if len(colon_parts) > 1: parsed['type'] = colon_parts[1].strip() parsed['synonym'] = split_camel_case(parsed['name']) return parsed # Object form. synonym/tooltip — сквозной проброс (строка ИЛИ dict {ru,en}), НЕ стрингифаим. name = str(val.get('name', '')) # fillCheck — синоним fillChecking (bool true→ShowError). quickChoice — прощаем bool (true→Use, false→DontUse). if val.get('fillChecking'): fc = str(val['fillChecking']) elif val.get('fillCheck') is not None: fc = ('ShowError' if val['fillCheck'] else '') if isinstance(val['fillCheck'], bool) else str(val['fillCheck']) else: fc = '' if val.get('quickChoice') is not None: qc = ('Use' if val['quickChoice'] else 'DontUse') if isinstance(val['quickChoice'], bool) else str(val['quickChoice']) else: qc = '' return { 'name': name, 'type': build_type_str(val), 'synonym': val['synonym'] if val.get('synonym') is not None else split_camel_case(name), 'tooltip': val.get('tooltip'), 'comment': str(val['comment']) if val.get('comment') else '', # Лоуэркейз как в строковом пути (стр.809): проверки флагов регистронезависимы (зеркало PS -contains). 'flags': [str(f).strip().lower() for f in val.get('flags', [])], 'fillChecking': fc, 'indexing': str(val['indexing']) if val.get('indexing') else '', 'multiLine': True if val.get('multiLine') is True else False, 'choiceHistoryOnInput': str(val['choiceHistoryOnInput']) if val.get('choiceHistoryOnInput') else '', 'fullTextSearch': str(val['fullTextSearch']) if val.get('fullTextSearch') else '', 'fillFromFillingValue': True if val.get('fillFromFillingValue') is True else False, 'createOnInput': str(val['createOnInput']) if val.get('createOnInput') else '', 'quickChoice': qc, 'dataHistory': str(val['dataHistory']) if val.get('dataHistory') else '', 'use': str(val['use']) if val.get('use') else '', 'passwordMode': True if val.get('passwordMode') is True else False, 'format': val.get('format'), 'editFormat': val.get('editFormat'), 'mask': str(val['mask']) if val.get('mask') else '', 'extendedEdit': True if val.get('extendedEdit') is True else False, 'minValue': val.get('minValue'), 'maxValue': val.get('maxValue'), 'hasFillValue': ('fillValue' in val), 'fillValue': val.get('fillValue'), 'linkByType': val.get('linkByType'), 'choiceParameterLinks': val.get('choiceParameterLinks'), 'choiceParameters': val.get('choiceParameters'), } def parse_enum_value_shorthand(val): if isinstance(val, str): return { 'name': val, 'synonym': split_camel_case(val), 'comment': '', } name = str(val.get('name', '')) return { 'name': name, 'synonym': str(val['synonym']) if val.get('synonym') else split_camel_case(name), 'comment': str(val['comment']) if val.get('comment') else '', } # --------------------------------------------------------------------------- # 6. GeneratedType categories # --------------------------------------------------------------------------- generated_types = { 'Catalog': [ {'prefix': 'CatalogObject', 'category': 'Object'}, {'prefix': 'CatalogRef', 'category': 'Ref'}, {'prefix': 'CatalogSelection', 'category': 'Selection'}, {'prefix': 'CatalogList', 'category': 'List'}, {'prefix': 'CatalogManager', 'category': 'Manager'}, ], 'Document': [ {'prefix': 'DocumentObject', 'category': 'Object'}, {'prefix': 'DocumentRef', 'category': 'Ref'}, {'prefix': 'DocumentSelection', 'category': 'Selection'}, {'prefix': 'DocumentList', 'category': 'List'}, {'prefix': 'DocumentManager', 'category': 'Manager'}, ], 'Enum': [ {'prefix': 'EnumRef', 'category': 'Ref'}, {'prefix': 'EnumManager', 'category': 'Manager'}, {'prefix': 'EnumList', 'category': 'List'}, ], 'Constant': [ {'prefix': 'ConstantManager', 'category': 'Manager'}, {'prefix': 'ConstantValueManager', 'category': 'ValueManager'}, {'prefix': 'ConstantValueKey', 'category': 'ValueKey'}, ], 'InformationRegister': [ {'prefix': 'InformationRegisterRecord', 'category': 'Record'}, {'prefix': 'InformationRegisterManager', 'category': 'Manager'}, {'prefix': 'InformationRegisterSelection', 'category': 'Selection'}, {'prefix': 'InformationRegisterList', 'category': 'List'}, {'prefix': 'InformationRegisterRecordSet', 'category': 'RecordSet'}, {'prefix': 'InformationRegisterRecordKey', 'category': 'RecordKey'}, {'prefix': 'InformationRegisterRecordManager', 'category': 'RecordManager'}, ], 'AccumulationRegister': [ {'prefix': 'AccumulationRegisterRecord', 'category': 'Record'}, {'prefix': 'AccumulationRegisterManager', 'category': 'Manager'}, {'prefix': 'AccumulationRegisterSelection', 'category': 'Selection'}, {'prefix': 'AccumulationRegisterList', 'category': 'List'}, {'prefix': 'AccumulationRegisterRecordSet', 'category': 'RecordSet'}, {'prefix': 'AccumulationRegisterRecordKey', 'category': 'RecordKey'}, ], 'AccountingRegister': [ {'prefix': 'AccountingRegisterRecord', 'category': 'Record'}, {'prefix': 'AccountingRegisterExtDimensions', 'category': 'ExtDimensions'}, {'prefix': 'AccountingRegisterRecordSet', 'category': 'RecordSet'}, {'prefix': 'AccountingRegisterRecordKey', 'category': 'RecordKey'}, {'prefix': 'AccountingRegisterSelection', 'category': 'Selection'}, {'prefix': 'AccountingRegisterList', 'category': 'List'}, {'prefix': 'AccountingRegisterManager', 'category': 'Manager'}, ], 'CalculationRegister': [ {'prefix': 'CalculationRegisterRecord', 'category': 'Record'}, {'prefix': 'CalculationRegisterManager', 'category': 'Manager'}, {'prefix': 'CalculationRegisterSelection', 'category': 'Selection'}, {'prefix': 'CalculationRegisterList', 'category': 'List'}, {'prefix': 'CalculationRegisterRecordSet', 'category': 'RecordSet'}, {'prefix': 'CalculationRegisterRecordKey', 'category': 'RecordKey'}, {'prefix': 'RecalculationsManager', 'category': 'Recalcs'}, ], 'ChartOfAccounts': [ {'prefix': 'ChartOfAccountsObject', 'category': 'Object'}, {'prefix': 'ChartOfAccountsRef', 'category': 'Ref'}, {'prefix': 'ChartOfAccountsSelection', 'category': 'Selection'}, {'prefix': 'ChartOfAccountsList', 'category': 'List'}, {'prefix': 'ChartOfAccountsManager', 'category': 'Manager'}, {'prefix': 'ChartOfAccountsExtDimensionTypes', 'category': 'ExtDimensionTypes'}, {'prefix': 'ChartOfAccountsExtDimensionTypesRow', 'category': 'ExtDimensionTypesRow'}, ], 'ChartOfCharacteristicTypes': [ {'prefix': 'ChartOfCharacteristicTypesObject', 'category': 'Object'}, {'prefix': 'ChartOfCharacteristicTypesRef', 'category': 'Ref'}, {'prefix': 'ChartOfCharacteristicTypesSelection', 'category': 'Selection'}, {'prefix': 'ChartOfCharacteristicTypesList', 'category': 'List'}, {'prefix': 'Characteristic', 'category': 'Characteristic'}, {'prefix': 'ChartOfCharacteristicTypesManager', 'category': 'Manager'}, ], 'ChartOfCalculationTypes': [ {'prefix': 'ChartOfCalculationTypesObject', 'category': 'Object'}, {'prefix': 'ChartOfCalculationTypesRef', 'category': 'Ref'}, {'prefix': 'ChartOfCalculationTypesSelection', 'category': 'Selection'}, {'prefix': 'ChartOfCalculationTypesList', 'category': 'List'}, {'prefix': 'ChartOfCalculationTypesManager', 'category': 'Manager'}, {'prefix': 'DisplacingCalculationTypes', 'category': 'DisplacingCalculationTypes'}, {'prefix': 'DisplacingCalculationTypesRow', 'category': 'DisplacingCalculationTypesRow'}, {'prefix': 'BaseCalculationTypes', 'category': 'BaseCalculationTypes'}, {'prefix': 'BaseCalculationTypesRow', 'category': 'BaseCalculationTypesRow'}, {'prefix': 'LeadingCalculationTypes', 'category': 'LeadingCalculationTypes'}, {'prefix': 'LeadingCalculationTypesRow', 'category': 'LeadingCalculationTypesRow'}, ], 'BusinessProcess': [ {'prefix': 'BusinessProcessObject', 'category': 'Object'}, {'prefix': 'BusinessProcessRef', 'category': 'Ref'}, {'prefix': 'BusinessProcessSelection', 'category': 'Selection'}, {'prefix': 'BusinessProcessList', 'category': 'List'}, {'prefix': 'BusinessProcessManager', 'category': 'Manager'}, {'prefix': 'BusinessProcessRoutePointRef', 'category': 'RoutePointRef'}, ], 'Task': [ {'prefix': 'TaskObject', 'category': 'Object'}, {'prefix': 'TaskRef', 'category': 'Ref'}, {'prefix': 'TaskSelection', 'category': 'Selection'}, {'prefix': 'TaskList', 'category': 'List'}, {'prefix': 'TaskManager', 'category': 'Manager'}, ], 'ExchangePlan': [ {'prefix': 'ExchangePlanObject', 'category': 'Object'}, {'prefix': 'ExchangePlanRef', 'category': 'Ref'}, {'prefix': 'ExchangePlanSelection', 'category': 'Selection'}, {'prefix': 'ExchangePlanList', 'category': 'List'}, {'prefix': 'ExchangePlanManager', 'category': 'Manager'}, ], 'DefinedType': [ {'prefix': 'DefinedType', 'category': 'DefinedType'}, ], 'DocumentJournal': [ {'prefix': 'DocumentJournalSelection', 'category': 'Selection'}, {'prefix': 'DocumentJournalList', 'category': 'List'}, {'prefix': 'DocumentJournalManager', 'category': 'Manager'}, ], 'Report': [ {'prefix': 'ReportObject', 'category': 'Object'}, {'prefix': 'ReportManager', 'category': 'Manager'}, ], 'DataProcessor': [ {'prefix': 'DataProcessorObject', 'category': 'Object'}, {'prefix': 'DataProcessorManager', 'category': 'Manager'}, ], } def emit_internal_info(indent, object_type, object_name): types = generated_types.get(object_type) if not types: return X(f'{indent}') 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', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'Order', 'Type', 'OffBalance'], 'ChartOfCharacteristicTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'ValueType'], 'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'ActionPeriodIsBasic'], 'BusinessProcess': ['Ref', 'DeletionMark', 'Date', 'Number', 'Started', 'Completed', 'HeadTask'], 'Task': ['Ref', 'DeletionMark', 'Date', 'Number', 'Executed', 'Description', 'RoutePoint', 'BusinessProcess'], 'ExchangePlan': ['Ref', 'DeletionMark', 'Code', 'Description', 'ThisNode', 'SentNo', 'ReceivedNo'], 'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'], } # Профиль материализованного блока StandardAttributes (см. коммент в .ps1). Пока только Catalog. std_attr_profile = { 'Catalog': { 'Owner': {'FillChecking': 'ShowError', 'FillFromFillingValue': 'true'}, 'Parent': {'FillFromFillingValue': 'true'}, 'Description': {'FillChecking': 'ShowError'}, }, # ExchangePlan: блок при кастомизации; Наименование/Код → FillChecking=ShowError (корпус 40/38 из 41). 'ExchangePlan': { 'Description': {'FillChecking': 'ShowError'}, 'Code': {'FillChecking': 'ShowError'}, }, # ChartOfCharacteristicTypes: Наименование → FillChecking=ShowError (21/23), Родитель → FFV=true (23/23). 'ChartOfCharacteristicTypes': { 'Description': {'FillChecking': 'ShowError'}, 'Parent': {'FillFromFillingValue': 'true'}, }, } # 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}') 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') 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'} def emit_standard_attributes(indent, object_type): attrs = standard_attributes_by_type.get(object_type) if not attrs: return conditional = object_type in std_attr_conditional_types sa = defn.get('standardAttributes') if conditional and sa is None: return profile = std_attr_profile.get(object_type, {}) # Доп. (опциональные) стандартные реквизиты вне фикс-списка — напр. ExchangeDate у части ПланОбмена # (легаси, присутствие не выводится). Эмитим по факту ключа в DSL, ПЕРЕД фикс-списком (их позиция). extra = [k for k in sa if k not in attrs] if isinstance(sa, dict) else [] X(f'{indent}') for a in extra + list(attrs): ov = dict(profile.get(a, {})) if isinstance(sa, dict): d = sa.get(a) if d: if d.get('synonym') is not None: ov['Synonym'] = d['synonym'] # строка ИЛИ {ru,en} if d.get('tooltip') is not None: ov['ToolTip'] = d['tooltip'] # строка ИЛИ {ru,en} if d.get('fillChecking'): ov['FillChecking'] = str(d['fillChecking']) if d.get('fillFromFillingValue') is not None: ov['FillFromFillingValue'] = 'true' if d['fillFromFillingValue'] else 'false' if d.get('fullTextSearch'): ov['FullTextSearch'] = str(d['fullTextSearch']) if d.get('dataHistory'): ov['DataHistory'] = str(d['dataHistory']) if d.get('fillValue') is not None: ov['FillValue'] = d['fillValue'] if d.get('choiceParameterLinks') is not None: ov['ChoiceParameterLinks'] = d['choiceParameterLinks'] if d.get('choiceParameters') is not None: ov['ChoiceParameters'] = d['choiceParameters'] if d.get('comment'): ov['Comment'] = str(d['comment']) if d.get('mask'): ov['Mask'] = str(d['mask']) if d.get('choiceForm'): ov['ChoiceForm'] = str(d['choiceForm']) emit_standard_attribute(f'{indent}\t', a, ov) X(f'{indent}') def emit_tabular_standard_attributes(indent, line_number=None): """LineNumber/НомерСтроки ТЧ. Блок эмитится всегда. DSL `lineNumber` переопределяет synonym/comment/fullTextSearch/tooltip/format/editFormat/choiceHistoryOnInput.""" ov = None if line_number: ov = {} if line_number.get('synonym') is not None: ov['Synonym'] = line_number['synonym'] if line_number.get('comment'): ov['Comment'] = str(line_number['comment']) if line_number.get('fullTextSearch'): ov['FullTextSearch'] = str(line_number['fullTextSearch']) if line_number.get('tooltip') is not None: ov['ToolTip'] = line_number['tooltip'] if line_number.get('format') is not None: ov['Format'] = line_number['format'] if line_number.get('editFormat') is not None: ov['EditFormat'] = line_number['editFormat'] if line_number.get('choiceHistoryOnInput'): ov['ChoiceHistoryOnInput'] = str(line_number['choiceHistoryOnInput']) X(f'{indent}') 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'^(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) 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): 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}') 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 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') 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\tfalse') 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'): ffv = 'true' if parsed.get('fillFromFillingValue') is True else 'false' X(f'{indent}\t\t{ffv}') if context not in ('tabular', 'processor', 'chart', 'register-other'): emit_fill_value(f'{indent}\t\t', type_str, parsed.get('fillValue'), parsed.get('hasFillValue')) fill_checking = 'DontCheck' if 'req' in parsed.get('flags', []): fill_checking = 'ShowError' if parsed.get('fillChecking'): fill_checking = parsed['fillChecking'] X(f'{indent}\t\t{fill_checking}') X(f'{indent}\t\tItems') 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') emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType')) chi = parsed.get('choiceHistoryOnInput') or 'Auto' X(f'{indent}\t\t{chi}') if context == 'catalog': X(f'{indent}\t\t{parsed.get("use") or "ForItem"}') if context not in ('processor', 'processor-tabular'): 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}') X(f'{indent}\t\t{parsed.get("fullTextSearch") or "Use"}') # DataHistory — not for Chart* types and non-InformationRegister register family if context not in ('chart', 'register-other'): 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') X(f'{indent}\t\t{cmd.get("group") or ""}') 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): 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\tDontCheck') emit_tabular_standard_attributes(f'{indent}\t\t', ts_line_number) if object_type in ('Catalog', 'ChartOfCharacteristicTypes'): X(f'{indent}\t\tForItem') 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']) 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\tfalse') 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\tItems') 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') 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\tfalse') 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\tItems') 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') 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}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', 'Automatic') 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) X(f'{i}') X(f'{i}true') 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('InformationRegisterPeriodicity', 'numberPeriodicity', 'Year') check_unique = 'false' if defn.get('checkUnique') is False else 'true' autonumbering = 'false' if defn.get('autonumbering') is False else 'true' X(f'{i}{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') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}\tDocument.{obj_name}.StandardAttribute.Number') X(f'{i}') X(f'{i}DontUse') X(f'{i}Begin') X(f'{i}DontUse') X(f'{i}Directly') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') posting = get_enum_prop('Posting', 'posting', 'Allow') real_time_posting = get_enum_prop('RealTimePosting', 'realTimePosting', 'Deny') reg_records_deletion = get_enum_prop('RegisterRecordsDeletion', 'registerRecordsDeletion', 'AutoDelete') reg_records_writing = get_enum_prop('RegisterRecordsWritingOnPost', 'registerRecordsWritingOnPost', 'WriteModified') sequence_filling = str(defn['sequenceFilling']) if defn.get('sequenceFilling') else 'AutoFill' post_in_priv = 'false' if defn.get('postInPrivilegedMode') is False else 'true' unpost_in_priv = 'false' if defn.get('unpostInPrivilegedMode') is False else 'true' X(f'{i}{posting}') X(f'{i}{real_time_posting}') X(f'{i}{reg_records_deletion}') X(f'{i}{reg_records_writing}') X(f'{i}{sequence_filling}') # RegisterRecords reg_records = [] if defn.get('registerRecords'): for rr in defn['registerRecords']: rr_str = str(rr) if '.' in rr_str: dot_idx = rr_str.index('.') rr_prefix = rr_str[:dot_idx] rr_suffix = rr_str[dot_idx + 1:] if rr_prefix in object_type_synonyms: rr_prefix = object_type_synonyms[rr_prefix] reg_records.append(f'{rr_prefix}.{rr_suffix}') else: reg_records.append(rr_str) if reg_records: X(f'{i}') for rr in reg_records: X(f'{i}\t{rr}') X(f'{i}') else: X(f'{i}') X(f'{i}{post_in_priv}') X(f'{i}{unpost_in_priv}') X(f'{i}false') X(f'{i}') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') 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}Auto') X(f'{i}DontUse') X(f'{i}false') X(f'{i}false') def emit_enum_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}false') emit_standard_attributes(i, 'Enum') X(f'{i}') quick_choice = 'false' if defn.get('quickChoice') is False else 'true' X(f'{i}{quick_choice}') X(f'{i}BothWays') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}Auto') def emit_constant_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') # Type value_type = build_type_str(defn) or 'String' emit_value_type(i, value_type) X(f'{i}true') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') X(f'{i}false') X(f'{i}false') X(f'{i}') X(f'{i}') X(f'{i}DontCheck') X(f'{i}Items') X(f'{i}') X(f'{i}') X(f'{i}Auto') X(f'{i}') X(f'{i}') X(f'{i}Auto') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') X(f'{i}{data_lock_control_mode}') X(f'{i}DontUse') X(f'{i}false') X(f'{i}false') def emit_information_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') X(f'{i}InDialog') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') emit_standard_attributes(i, 'InformationRegister') periodicity = get_enum_prop('InformationRegisterPeriodicity', 'periodicity', 'Nonperiodical') write_mode = get_enum_prop('WriteMode', 'writeMode', 'Independent') main_filter_on_period = 'false' if defn.get('mainFilterOnPeriod') is not None: main_filter_on_period = 'true' if defn['mainFilterOnPeriod'] is True else 'false' elif periodicity != 'Nonperiodical': main_filter_on_period = 'true' X(f'{i}{periodicity}') X(f'{i}{write_mode}') X(f'{i}{main_filter_on_period}') X(f'{i}false') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') X(f'{i}false') X(f'{i}false') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}DontUse') X(f'{i}false') X(f'{i}false') def emit_accumulation_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') X(f'{i}') X(f'{i}') register_type = get_enum_prop('RegisterType', 'registerType', 'Balance') X(f'{i}{register_type}') X(f'{i}false') emit_standard_attributes(i, 'AccumulationRegister') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') 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}') X(f'{i}') X(f'{i}') X(f'{i}') # --- 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) X(f'{i}') # Accept both valueType and valueTypes value_types = list(defn.get('valueTypes', [])) if not value_types and defn.get('valueType'): vt_raw = defn['valueType'] value_types = list(vt_raw) if isinstance(vt_raw, list) else [vt_raw] if value_types: X(f'{i}') for vt in value_types: resolved = resolve_type_str(str(vt)) if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.', resolved): X(f'{i}\td5p1:{resolved}') elif resolved == 'Boolean': X(f'{i}\txs:boolean') elif re.match(r'^String', resolved): X(f'{i}\txs:string') X(f'{i}\t') X(f'{i}\t\t0') X(f'{i}\t\tVariable') X(f'{i}\t') else: X(f'{i}\tcfg:{resolved}') X(f'{i}') else: X(f'{i}') def emit_common_module_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) 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) 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)}') # synonym может быть {ru,en}; Description — плоская строка, берём ru-текст. description = str(defn['description']) if defn.get('description') else (synonym if isinstance(synonym, str) else '') X(f'{i}{esc_xml(description)}') 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) X(f'{i}') sources = list(defn.get('source', [])) if sources: X(f'{i}') for src in sources: resolved = resolve_type_str(str(src)) X(f'{i}\td5p1:{resolved}') 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) X(f'{i}') X(f'{i}true') default_form = str(defn['defaultForm']) if defn.get('defaultForm') else '' if default_form: X(f'{i}{default_form}') else: X(f'{i}') aux_form = str(defn['auxiliaryForm']) if defn.get('auxiliaryForm') else '' if aux_form: X(f'{i}{aux_form}') else: X(f'{i}') main_dcs = str(defn['mainDataCompositionSchema']) if defn.get('mainDataCompositionSchema') else '' if main_dcs: X(f'{i}{main_dcs}') else: X(f'{i}') def_settings = str(defn['defaultSettingsForm']) if defn.get('defaultSettingsForm') else '' if def_settings: X(f'{i}{def_settings}') else: X(f'{i}') aux_settings = str(defn['auxiliarySettingsForm']) if defn.get('auxiliarySettingsForm') else '' if aux_settings: X(f'{i}{aux_settings}') else: X(f'{i}') def_variant = str(defn['defaultVariantForm']) if defn.get('defaultVariantForm') else '' if def_variant: X(f'{i}{def_variant}') else: X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') X(f'{i}') def emit_data_processor_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}false') default_form = str(defn['defaultForm']) if defn.get('defaultForm') else '' if default_form: X(f'{i}{default_form}') else: X(f'{i}') aux_form = str(defn['auxiliaryForm']) if defn.get('auxiliaryForm') else '' if aux_form: X(f'{i}{aux_form}') else: X(f'{i}') X(f'{i}false') X(f'{i}') X(f'{i}') # --- 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}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}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) X(f'{i}') default_form = str(defn['defaultForm']) if defn.get('defaultForm') else '' if default_form: X(f'{i}{default_form}') else: X(f'{i}') aux_form = str(defn['auxiliaryForm']) if defn.get('auxiliaryForm') else '' if aux_form: X(f'{i}{aux_form}') else: X(f'{i}') X(f'{i}true') reg_docs = list(defn.get('registeredDocuments', [])) if reg_docs: X(f'{i}') for rd in reg_docs: rd_str = str(rd) if '.' in rd_str: dot_idx = rd_str.index('.') rd_prefix = rd_str[:dot_idx] rd_suffix = rd_str[dot_idx + 1:] if rd_prefix in object_type_synonyms: rd_prefix = object_type_synonyms[rd_prefix] rd_str = f'{rd_prefix}.{rd_suffix}' X(f'{i}\t{rd_str}') X(f'{i}') else: X(f'{i}') emit_standard_attributes(i, 'DocumentJournal') X(f'{i}') X(f'{i}') X(f'{i}') def emit_chart_of_accounts_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') ext_dim_types = str(defn['extDimensionTypes']) if defn.get('extDimensionTypes') else '' if ext_dim_types: X(f'{i}{ext_dim_types}') else: X(f'{i}') max_ext_dim = str(defn['maxExtDimensionCount']) if defn.get('maxExtDimensionCount') is not None else '3' X(f'{i}{max_ext_dim}') code_mask = str(defn['codeMask']) if defn.get('codeMask') else '' if code_mask: X(f'{i}{code_mask}') else: X(f'{i}') code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '8' description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '120' code_series = str(defn['codeSeries']) if defn.get('codeSeries') else 'WholeChartOfAccounts' auto_order = 'false' if defn.get('autoOrderByCode') is False else 'true' order_length = str(defn['orderLength']) if defn.get('orderLength') is not None else '5' X(f'{i}{code_length}') X(f'{i}{description_length}') X(f'{i}{code_series}') X(f'{i}false') X(f'{i}AsDescription') X(f'{i}{auto_order}') X(f'{i}{order_length}') X(f'{i}InDialog') emit_standard_attributes(i, 'ChartOfAccounts') X(f'{i}') X(f'{i}\t') X(f'{i}\t\t') for st_attr in ['TurnoversOnly', 'Predefined', 'ExtDimensionType', 'LineNumber']: emit_standard_attribute(f'{i}\t\t\t', st_attr) X(f'{i}\t\t') X(f'{i}\t') X(f'{i}') X(f'{i}') X(f'{i}Auto') quick_choice = 'true' if defn.get('quickChoice') is True else 'false' X(f'{i}{quick_choice}') X(f'{i}BothWays') X(f'{i}') X(f'{i}\tChartOfAccounts.{obj_name}.StandardAttribute.Description') X(f'{i}\tChartOfAccounts.{obj_name}.StandardAttribute.Code') X(f'{i}') X(f'{i}Begin') X(f'{i}DontUse') X(f'{i}Directly') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') X(f'{i}') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') 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}DontUse') X(f'{i}Auto') X(f'{i}DontUse') X(f'{i}false') X(f'{i}false') def emit_accounting_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') X(f'{i}') X(f'{i}') chart_of_accounts = str(defn['chartOfAccounts']) if defn.get('chartOfAccounts') else '' if chart_of_accounts: X(f'{i}{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}') X(f'{i}false') emit_standard_attributes(i, 'AccountingRegister') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') X(f'{i}') X(f'{i}') 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) X(f'{i}') X(f'{i}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 '25' code_type = get_enum_prop('CodeType', 'codeType', 'String') code_allowed_length = get_enum_prop('CodeAllowedLength', 'codeAllowedLength', 'Variable') X(f'{i}{code_length}') X(f'{i}{code_type}') X(f'{i}{code_allowed_length}') X(f'{i}{description_length}') X(f'{i}AsDescription') dependence = get_enum_prop('DependenceOnCalculationTypes', 'dependenceOnCalculationTypes', 'DontUse') X(f'{i}{dependence}') base_types = list(defn.get('baseCalculationTypes', [])) if base_types: X(f'{i}') for bt in base_types: X(f'{i}\t{bt}') X(f'{i}') else: X(f'{i}') action_period_use = 'true' if defn.get('actionPeriodUse') is True else 'false' X(f'{i}{action_period_use}') emit_standard_attributes(i, 'ChartOfCalculationTypes') X(f'{i}') X(f'{i}Auto') X(f'{i}InDialog') quick_choice = 'true' if defn.get('quickChoice') is True else 'false' X(f'{i}{quick_choice}') X(f'{i}BothWays') X(f'{i}') X(f'{i}\tChartOfCalculationTypes.{obj_name}.StandardAttribute.Description') X(f'{i}\tChartOfCalculationTypes.{obj_name}.StandardAttribute.Code') X(f'{i}') X(f'{i}Begin') X(f'{i}DontUse') X(f'{i}Directly') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') X(f'{i}') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') 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}DontUse') X(f'{i}Auto') def emit_calculation_register_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') X(f'{i}') X(f'{i}') chart_of_calc_types = str(defn['chartOfCalculationTypes']) if defn.get('chartOfCalculationTypes') else '' if chart_of_calc_types: X(f'{i}{chart_of_calc_types}') else: X(f'{i}') 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}{schedule}') else: X(f'{i}') schedule_value = str(defn['scheduleValue']) if defn.get('scheduleValue') else '' if schedule_value: X(f'{i}{schedule_value}') else: X(f'{i}') schedule_date = str(defn['scheduleDate']) if defn.get('scheduleDate') else '' if schedule_date: X(f'{i}{schedule_date}') else: X(f'{i}') X(f'{i}false') emit_standard_attributes(i, 'CalculationRegister') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') X(f'{i}{data_lock_control_mode}') full_text_search = get_enum_prop('FullTextSearch', 'fullTextSearch', 'Use') X(f'{i}{full_text_search}') X(f'{i}') X(f'{i}') X(f'{i}') def emit_business_process_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') edit_type = get_enum_prop('EditType', 'editType', 'InDialog') X(f'{i}{edit_type}') number_type = get_enum_prop('NumberType', 'numberType', 'String') number_length = str(defn['numberLength']) if defn.get('numberLength') is not None else '11' number_allowed_length = get_enum_prop('NumberAllowedLength', 'numberAllowedLength', 'Variable') check_unique = 'false' if defn.get('checkUnique') is False else 'true' autonumbering = 'false' if defn.get('autonumbering') is False else 'true' X(f'{i}{number_type}') X(f'{i}{number_length}') X(f'{i}{number_allowed_length}') X(f'{i}{check_unique}') X(f'{i}{autonumbering}') emit_standard_attributes(i, 'BusinessProcess') X(f'{i}') task_ref = str(defn['task']) if defn.get('task') else '' if task_ref: X(f'{i}{task_ref}') else: X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}\tBusinessProcess.{obj_name}.StandardAttribute.Number') X(f'{i}') X(f'{i}DontUse') X(f'{i}Begin') X(f'{i}DontUse') X(f'{i}Directly') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') 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}Auto') X(f'{i}DontUse') X(f'{i}false') X(f'{i}false') def emit_task_properties(indent): i = indent X(f'{i}{esc_xml(obj_name)}') emit_mltext(i, 'Synonym', synonym) X(f'{i}') X(f'{i}true') 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}') addressing = str(defn['addressing']) if defn.get('addressing') else '' if addressing: X(f'{i}{addressing}') else: X(f'{i}') main_addressing = str(defn['mainAddressingAttribute']) if defn.get('mainAddressingAttribute') else '' if main_addressing: X(f'{i}{main_addressing}') else: X(f'{i}') current_performer = str(defn['currentPerformer']) if defn.get('currentPerformer') else '' if current_performer: X(f'{i}{current_performer}') else: X(f'{i}') emit_standard_attributes(i, 'Task') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}\tTask.{obj_name}.StandardAttribute.Number') X(f'{i}') X(f'{i}DontUse') X(f'{i}Begin') X(f'{i}DontUse') X(f'{i}Directly') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}') X(f'{i}false') X(f'{i}') data_lock_control_mode = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') 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}Auto') X(f'{i}DontUse') X(f'{i}false') X(f'{i}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 = '' indexing = 'DontIndex' references = [] if isinstance(col_def, str): name = col_def col_synonym = split_camel_case(name) else: name = str(col_def.get('name', '')) col_synonym = str(col_def['synonym']) if col_def.get('synonym') else split_camel_case(name) if col_def.get('indexing'): indexing = str(col_def['indexing']) if col_def.get('references'): references = list(col_def['references']) X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(name)}') emit_mltext(f'{indent}\t\t', 'Synonym', col_synonym) 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{ref}') X(f'{indent}\t\t') else: X(f'{indent}\t\t') X(f'{indent}\t') X(f'{indent}') def emit_accounting_flag(indent, flag_name): uid = new_uuid() flag_synonym = split_camel_case(flag_name) X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(flag_name)}') emit_mltext(f'{indent}\t\t', 'Synonym', flag_synonym) X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t\txs:boolean') 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\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\tfalse') X(f'{indent}\t\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tDontCheck') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') X(f'{indent}\t') X(f'{indent}') def emit_ext_dimension_accounting_flag(indent, flag_name): uid = new_uuid() flag_synonym = split_camel_case(flag_name) X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(flag_name)}') emit_mltext(f'{indent}\t\t', 'Synonym', flag_synonym) X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\t\txs:boolean') 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\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\tfalse') X(f'{indent}\t\tfalse') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tDontCheck') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') X(f'{indent}\t\t') X(f'{indent}\t\t') X(f'{indent}\t\tAuto') 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): uid = new_uuid() name = '' attr_synonym = '' type_str = '' addressing_dimension = '' indexing = 'Index' parsed = parse_attribute_shorthand(addr_def) name = parsed['name'] attr_synonym = parsed['synonym'] type_str = parsed['type'] if not isinstance(addr_def, str): if addr_def.get('addressingDimension'): addressing_dimension = str(addr_def['addressingDimension']) if addr_def.get('indexing'): indexing = str(addr_def['indexing']) X(f'{indent}') X(f'{indent}\t') X(f'{indent}\t\t{esc_xml(name)}') emit_mltext(f'{indent}\t\t', 'Synonym', attr_synonym) X(f'{indent}\t\t') 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') if addressing_dimension: X(f'{indent}\t\t{addressing_dimension}') else: X(f'{indent}\t\t') X(f'{indent}\t\t{indexing}') X(f'{indent}\t\tUse') X(f'{indent}\t\tUse') X(f'{indent}\t') X(f'{indent}') # --------------------------------------------------------------------------- # 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, '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} cols = _as_list(val.get('attributes') or val.get('columns') or []) return {'columns': cols, 'synonym': val.get('synonym'), 'tooltip': val.get('tooltip'), 'comment': str(val['comment']) if val.get('comment') else None, 'lineNumber': val.get('lineNumber')} if isinstance(ts_data, list): for ts in ts_data: ts_sections[ts['name']] = new_ts_entry(ts) ts_order.append(ts['name']) else: for k, v in ts_data.items(): ts_sections[k] = new_ts_entry(v) ts_order.append(k) # ChartOfAccounts: AccountingFlags + ExtDimensionAccountingFlags acct_flags = [] ext_dim_flags = [] if obj_type == 'ChartOfAccounts': if defn.get('accountingFlags'): acct_flags = _as_list(defn['accountingFlags']) if defn.get('extDimensionAccountingFlags'): ext_dim_flags = _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 = 'chart' else: context = 'object' for a in attrs: emit_attribute('\t\t\t', a, context) for ts_name in ts_order: e = ts_sections[ts_name] emit_tabular_section('\t\t\t', ts_name, e['columns'], obj_type, obj_name, e['synonym'], e['tooltip'], e['comment'], e.get('lineNumber')) for af in acct_flags: af_name = af['name'] if isinstance(af, dict) else str(af) emit_accounting_flag('\t\t\t', af_name) for edf in ext_dim_flags: edf_name = edf['name'] if isinstance(edf, dict) else str(edf) emit_ext_dimension_accounting_flag('\t\t\t', edf_name) 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)) if dims or resources or reg_attrs: has_children = True X('\t\t') for r in resources: emit_resource('\t\t\t', r, obj_type) for d in dims: emit_dimension('\t\t\t', d, obj_type) # InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory; # AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT. reg_ctx = 'register-info' if obj_type == 'InformationRegister' else 'register-other' for a in reg_attrs: emit_attribute('\t\t\t', a, reg_ctx) X('\t\t') else: X('\t\t') # --- DocumentJournal: columns --- if obj_type == 'DocumentJournal': columns = list(defn.get('columns', [])) if columns: has_children = True X('\t\t') for col in columns: emit_column('\t\t\t', col) X('\t\t') else: X('\t\t') # --- 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', } type_plural = type_plural_map[obj_type] type_dir = os.path.join(output_dir, type_plural) # Main XML file main_xml_path = os.path.join(type_dir, f'{obj_name}.xml') # Types that don't have subdirectory structure types_no_sub_dir = ['DefinedType', 'ScheduledJob', 'EventSubscription'] obj_sub_dir = os.path.join(type_dir, obj_name) ext_dir = os.path.join(obj_sub_dir, 'Ext') os.makedirs(type_dir, exist_ok=True) if obj_type not in types_no_sub_dir: os.makedirs(obj_sub_dir, exist_ok=True) write_utf8_bom(main_xml_path, metadata_xml) # Module files modules_created = [] types_with_object_module = [ 'Catalog', 'Document', 'Report', 'DataProcessor', 'ExchangePlan', 'ChartOfAccounts', 'ChartOfCharacteristicTypes', 'ChartOfCalculationTypes', 'BusinessProcess', 'Task', ] types_with_record_set_module = [ 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', ] types_with_manager_module = ['Report', 'DataProcessor', 'Constant', 'Enum'] types_with_value_manager_module = ['Constant'] types_with_module = ['CommonModule', 'HTTPService', 'WebService'] def ensure_ext_dir(): os.makedirs(ext_dir, exist_ok=True) if obj_type in types_with_object_module: module_path = os.path.join(ext_dir, 'ObjectModule.bsl') if not os.path.isfile(module_path): ensure_ext_dir() write_utf8_bom(module_path, '') modules_created.append(module_path) if obj_type in types_with_manager_module: module_path = os.path.join(ext_dir, 'ManagerModule.bsl') if not os.path.isfile(module_path): ensure_ext_dir() write_utf8_bom(module_path, '') modules_created.append(module_path) if obj_type in types_with_value_manager_module: module_path = os.path.join(ext_dir, 'ValueManagerModule.bsl') if not os.path.isfile(module_path): ensure_ext_dir() write_utf8_bom(module_path, '') modules_created.append(module_path) if obj_type in types_with_record_set_module: module_path = os.path.join(ext_dir, 'RecordSetModule.bsl') if not os.path.isfile(module_path): ensure_ext_dir() write_utf8_bom(module_path, '') modules_created.append(module_path) if obj_type in types_with_module: module_path = os.path.join(ext_dir, 'Module.bsl') if not os.path.isfile(module_path): ensure_ext_dir() write_utf8_bom(module_path, '') modules_created.append(module_path) # --- Predefined data (Ext/Predefined.xml). Элемент: "(Код) Имя [Наименование]" ИЛИ объект (+рус. синонимы). # Наименование: нет [..]/ключа → авто(Split-CamelCase); [] / "" → пусто; [текст]/текст → как есть. def resolve_predef_item(val): if isinstance(val, str): m = re.match(r'^\s*(?:\(([^)]*)\)\s*)?(\S+)(?:\s*\[(.*)\])?\s*$', val) name = m.group(2) code = m.group(1) if m.group(1) is not None else '' desc = m.group(3) if m.group(3) is not None else split_camel_case(name) return {'name': name, 'code': code, 'desc': desc, 'isFolder': False, 'children': [], 'type': None} 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' # Special files if obj_type == 'ExchangePlan': content_path = os.path.join(ext_dir, 'Content.xml') if not os.path.isfile(content_path): ensure_ext_dir() content_xml = f'\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 in predef_root_by_type and defn.get('predefined'): ensure_ext_dir() cat_code_type = str(defn['codeType']) if defn.get('codeType') else 'String' predef_xml = build_predefined_xml(defn['predefined'], predef_root_by_type[obj_type], cat_code_type) predef_path = os.path.join(ext_dir, 'Predefined.xml') write_utf8_bom(predef_path, predef_xml) modules_created.append(predef_path) # Модули команд (Commands/<Имя>/Ext/CommandModule.bsl) — заготовка обработчика. if commands: cmd_module_stub = '&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n' for cmd in commands: cmd_dir = os.path.join(obj_sub_dir, 'Commands', cmd['name'], 'Ext') os.makedirs(cmd_dir, exist_ok=True) cmd_mod_path = os.path.join(cmd_dir, 'CommandModule.bsl') write_utf8_bom(cmd_mod_path, cmd_module_stub) modules_created.append(cmd_mod_path) # --------------------------------------------------------------------------- # 17. Register in Configuration.xml # --------------------------------------------------------------------------- config_xml_path = os.path.join(output_dir, 'Configuration.xml') reg_result = None child_tag = obj_type if os.path.isfile(config_xml_path): # Parse preserving whitespace via raw string manipulation with open(config_xml_path, 'r', encoding='utf-8-sig') as f: config_content = f.read() ns = 'http://v8.1c.ru/8.3/MDClasses' ET.register_namespace('', ns) # Parse all namespaces used in the file # Use iterparse to collect namespace prefixes namespaces_in_file = {} for evt, elem in ET.iterparse(config_xml_path, events=['start-ns']): prefix, uri = elem if prefix: namespaces_in_file[prefix] = uri ET.register_namespace(prefix, uri) tree = ET.parse(config_xml_path) root = tree.getroot() child_objects = root.find(f'{{{ns}}}Configuration/{{{ns}}}ChildObjects') if child_objects is None: # Try direct path config_elem = root.find(f'{{{ns}}}Configuration') if config_elem is not None: child_objects = config_elem.find(f'{{{ns}}}ChildObjects') if child_objects is not None: existing = child_objects.findall(f'{{{ns}}}{child_tag}') already_exists = False for e in existing: if (e.text or '').strip() == obj_name: already_exists = True break if already_exists: reg_result = 'already' else: new_elem = ET.SubElement(child_objects, f'{{{ns}}}{child_tag}') new_elem.text = obj_name if existing: # Insert after last existing element of same type last_elem = existing[-1] all_children = list(child_objects) idx = all_children.index(last_elem) child_objects.remove(new_elem) child_objects.insert(idx + 1, new_elem) # Write back preserving BOM tree.write(config_xml_path, encoding='utf-8', xml_declaration=True) # Re-read to add BOM, fix declaration quotes, ensure trailing newline with open(config_xml_path, 'r', encoding='utf-8') as f: raw = f.read() if raw.startswith(""): raw = raw.replace("", '', 1) if not raw.endswith('\n'): raw += '\n' write_utf8_bom(config_xml_path, raw) reg_result = 'added' else: reg_result = 'no-childobj' else: reg_result = 'no-config' # --------------------------------------------------------------------------- # 18. Summary # --------------------------------------------------------------------------- attr_count = len(defn.get('attributes', [])) ts_count = 0 if defn.get('tabularSections'): ts_data = defn['tabularSections'] if isinstance(ts_data, list): ts_count = len(ts_data) else: ts_count = len(ts_data) dim_count = len(defn.get('dimensions', [])) res_count = len(defn.get('resources', [])) val_count = len(defn.get('values', [])) col_count = len(defn.get('columns', [])) print(f"[OK] {obj_type} '{obj_name}' compiled") print(f' UUID: {obj_uuid}') print(f' File: {main_xml_path}') details = [] if attr_count > 0: details.append(f'Attributes: {attr_count}') if ts_count > 0: details.append(f'TabularSections: {ts_count}') if dim_count > 0: details.append(f'Dimensions: {dim_count}') if res_count > 0: details.append(f'Resources: {res_count}') if val_count > 0: details.append(f'Values: {val_count}') if col_count > 0: details.append(f'Columns: {col_count}') if details: print(f" {', '.join(details)}") for mc in modules_created: print(f' Module: {mc}') if reg_result == 'added': print(f' Configuration.xml: <{child_tag}>{obj_name} 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"')