#!/usr/bin/env python3 # meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json import os import re import subprocess import sys import uuid from lxml import etree # ============================================================ # Namespaces # ============================================================ MD_NS = "http://v8.1c.ru/8.3/MDClasses" XR_NS = "http://v8.1c.ru/8.3/xcf/readable" V8_NS = "http://v8.1c.ru/8.1/data/core" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XS_NS = "http://www.w3.org/2001/XMLSchema" CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config" NSMAP_WRAPPER = { None: MD_NS, "xsi": XSI_NS, "v8": V8_NS, "xr": XR_NS, "cfg": CFG_NS, "xs": XS_NS, } # ============================================================ # Global state # ============================================================ xml_tree = None # etree._ElementTree xml_root = None # root obj_element = None # the object type element (e.g. ) obj_type = "" md_ns = "" properties_el = None child_objects_el = None obj_name = "" add_count = 0 remove_count = 0 modify_count = 0 warn_count = 0 # ============================================================ # Utilities # ============================================================ def info(msg): print(f"[INFO] {msg}") def warn(msg): global warn_count print(f"[WARN] {msg}") warn_count += 1 def die(msg): print(msg, file=sys.stderr) sys.exit(1) def localname(el): return etree.QName(el.tag).localname def esc_xml(s): return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) # ============================================================ # Enum value normalization (same as meta-compile) # ============================================================ 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 = { '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', 'HierarchyItemsOnly'], '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'], } 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 new_uuid(): return str(uuid.uuid4()) def split_camel_case(name): if not name: return name # Insert space between lowercase Cyrillic and uppercase Cyrillic result = re.sub(r"([а-яё])([А-ЯЁ])", r"\1 \2", name) # Insert space between lowercase Latin and uppercase Latin result = re.sub(r"([a-z])([A-Z])", r"\1 \2", result) if len(result) > 1: result = result[0] + result[1:].lower() return result # ============================================================ # Synonym tables # ============================================================ operation_synonyms = { "add": "add", "добавить": "add", "remove": "remove", "удалить": "remove", "modify": "modify", "изменить": "modify", } child_type_synonyms = { "attributes": "attributes", "реквизиты": "attributes", "attrs": "attributes", "tabularsections": "tabularSections", "табличныечасти": "tabularSections", "тч": "tabularSections", "ts": "tabularSections", "dimensions": "dimensions", "измерения": "dimensions", "dims": "dimensions", "resources": "resources", "ресурсы": "resources", "res": "resources", "enumvalues": "enumValues", "значения": "enumValues", "values": "enumValues", "columns": "columns", "графы": "columns", "колонки": "columns", "forms": "forms", "формы": "forms", "templates": "templates", "макеты": "templates", "commands": "commands", "команды": "commands", "properties": "properties", "свойства": "properties", } type_synonyms = { "число": "Number", "строка": "String", "булево": "Boolean", "дата": "Date", "датавремя": "DateTime", "хранилищезначения": "ValueStorage", "number": "Number", "string": "String", "boolean": "Boolean", "date": "Date", "datetime": "DateTime", "valuestorage": "ValueStorage", "bool": "Boolean", # Reference synonyms "справочникссылка": "CatalogRef", "документссылка": "DocumentRef", "перечислениессылка": "EnumRef", "плансчетовссылка": "ChartOfAccountsRef", "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", "планвидоврасчётассылка": "ChartOfCalculationTypesRef", "планвидоврасчетассылка": "ChartOfCalculationTypesRef", "планобменассылка": "ExchangePlanRef", "бизнеспроцессссылка": "BusinessProcessRef", "задачассылка": "TaskRef", "определяемыйтип": "DefinedType", "definedtype": "DefinedType", "catalogref": "CatalogRef", "documentref": "DocumentRef", "enumref": "EnumRef", } # ============================================================ # Type system # ============================================================ def resolve_type_str(type_str): if not type_str: return type_str # Parameterized: Number(15,2), Строка(100) 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: СправочникСсылка.Организации if "." in type_str: dot_idx = type_str.index(".") prefix = type_str[:dot_idx] suffix = type_str[dot_idx:] resolved = type_synonyms.get(prefix.lower()) if resolved: return f"{resolved}{suffix}" return type_str # Simple resolved = type_synonyms.get(type_str.lower()) if resolved: return resolved return type_str def build_type_content_xml(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("+")] results = [] for part in parts: inner = build_type_content_xml(indent, part) if inner: results.append(inner) return "\r\n".join(results) type_str = resolve_type_str(type_str) lines = [] # Boolean if type_str == "Boolean": lines.append(f"{indent}xs:boolean") return "\r\n".join(lines) # ValueStorage if type_str == "ValueStorage": lines.append(f"{indent}xs:base64Binary") return "\r\n".join(lines) # String or String(N) m = re.match(r"^String(\((\d+)\))?$", type_str) if m: length = m.group(2) if m.group(2) else "10" lines.append(f"{indent}xs:string") lines.append(f"{indent}") lines.append(f"{indent}\t{length}") lines.append(f"{indent}\tVariable") lines.append(f"{indent}") return "\r\n".join(lines) # 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" lines.append(f"{indent}xs:decimal") lines.append(f"{indent}") lines.append(f"{indent}\t{digits}") lines.append(f"{indent}\t{fraction}") lines.append(f"{indent}\t{sign}") lines.append(f"{indent}") return "\r\n".join(lines) # Number without params -> Number(10,0) if type_str == "Number": lines.append(f"{indent}xs:decimal") lines.append(f"{indent}") lines.append(f"{indent}\t10") lines.append(f"{indent}\t0") lines.append(f"{indent}\tAny") lines.append(f"{indent}") return "\r\n".join(lines) # Date / DateTime if type_str == "Date": lines.append(f"{indent}xs:dateTime") lines.append(f"{indent}") lines.append(f"{indent}\tDate") lines.append(f"{indent}") return "\r\n".join(lines) if type_str == "DateTime": lines.append(f"{indent}xs:dateTime") lines.append(f"{indent}") lines.append(f"{indent}\tDateTime") lines.append(f"{indent}") return "\r\n".join(lines) # DefinedType m = re.match(r"^DefinedType\.(.+)$", type_str) if m: dt_name = m.group(1) lines.append(f"{indent}cfg:DefinedType.{dt_name}") return "\r\n".join(lines) # Reference types — use local xmlns declaration for 1C compatibility m = re.match( r"^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|" r"ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$", type_str, ) if m: lines.append(f'{indent}d5p1:{type_str}') return "\r\n".join(lines) # Fallback lines.append(f"{indent}{type_str}") return "\r\n".join(lines) def build_value_type_xml(indent, type_str): inner = build_type_content_xml(f"{indent}\t", type_str) return f"{indent}\r\n{inner}\r\n{indent}" def build_fill_value_xml(indent, type_str): if not type_str: return f'{indent}' type_str = resolve_type_str(type_str) if type_str == "Boolean": return f'{indent}false' if type_str.startswith("String"): return f'{indent}' if type_str.startswith("Number"): return f'{indent}0' return f'{indent}' def build_mltext_xml(indent, tag, text): if not text: return f"{indent}<{tag}/>" lines = [ f"{indent}<{tag}>", f"{indent}\t", f"{indent}\t\tru", f"{indent}\t\t{esc_xml(text)}", f"{indent}\t", f"{indent}", ] return "\r\n".join(lines) # ============================================================ # DOM helpers # ============================================================ def import_fragment(xml_string): """Parse an XML fragment in the context of our namespace declarations, return list of elements.""" wrapper = ( f'<_W xmlns="{MD_NS}"' f' xmlns:xsi="{XSI_NS}"' f' xmlns:v8="{V8_NS}"' f' xmlns:xr="{XR_NS}"' f' xmlns:cfg="{CFG_NS}"' f' xmlns:xs="{XS_NS}">' f"{xml_string}" ) parser = etree.XMLParser(remove_blank_text=False) frag = etree.fromstring(wrapper.encode("utf-8"), parser) nodes = [] for child in frag: nodes.append(child) return nodes def get_child_indent(container): """Detect indentation of children inside a container element.""" # Check container.text (text before first child) if container.text and "\n" in container.text: after_nl = container.text.rsplit("\n", 1)[-1] if after_nl and not after_nl.strip(): return after_nl # Check tail of child elements for child in container: if child.tail and "\n" in child.tail: after_nl = child.tail.rsplit("\n", 1)[-1] if after_nl and not after_nl.strip(): return after_nl # Fallback: count depth depth = 0 current = container while current is not None: parent = current.getparent() if parent is None: break if parent is xml_root: break depth += 1 current = parent return "\t" * (depth + 1) def insert_before_element(container, new_node, ref_node, child_indent): """Insert new_node into container before ref_node. If ref_node is None, append.""" if ref_node is not None: # Insert before ref_node idx = list(container).index(ref_node) new_node.tail = "\r\n" + child_indent container.insert(idx, new_node) else: # Append: insert before closing tag children = list(container) if len(children) > 0: last = children[-1] # The last element's tail is the whitespace before # We set new_node.tail to what last.tail was (newline + parent indent) new_node.tail = last.tail last.tail = "\r\n" + child_indent container.append(new_node) else: # Container is empty (possibly self-closing) parent_indent = child_indent[:-1] if len(child_indent) > 0 else "" container.text = "\r\n" + child_indent new_node.tail = "\r\n" + parent_indent container.append(new_node) def remove_node_with_whitespace(node): """Remove an element from its parent, cleaning up whitespace.""" parent = node.getparent() prev = node.getprevious() if prev is not None: # Transfer tail to previous sibling if node.tail: prev.tail = node.tail else: # First child: adjust parent.text if node.tail: parent.text = node.tail parent.remove(node) def find_element_by_name(container, elem_local_name, name_value): """Find a child element of given localname whose Properties/Name (or just Name) == name_value.""" for child in container: if localname(child) != elem_local_name: continue # Look for Properties/Name or just Name child props_el = None for gc in child: if localname(gc) == "Properties": props_el = gc break search_in = props_el if props_el is not None else child for gc in search_in: if localname(gc) == "Name": text = (gc.text or "").strip() if text == name_value: return child return None def find_last_element_of_type(container, local_name): last = None for child in container: if localname(child) == local_name: last = child return last def find_first_element_of_type(container, local_name): for child in container: if localname(child) == local_name: return child return None def ensure_child_objects_open(): """Ensure ChildObjects element exists and is open (not self-closing empty).""" global child_objects_el if child_objects_el is not None: # Check if it's empty (no child elements) has_elements = any(True for _ in child_objects_el) if not has_elements: # It's empty - add whitespace for proper formatting indent = get_child_indent(obj_element) child_objects_el.text = "\r\n" + indent return # No ChildObjects at all - create one after Properties indent = get_child_indent(obj_element) co_el = etree.Element(f"{{{md_ns}}}ChildObjects") co_el.text = "\r\n" + indent # Find where to insert: after Properties ref_node = None found_props = False for child in obj_element: if localname(child) == "Properties": found_props = True continue if found_props: ref_node = child break if ref_node is not None: # Insert before ref_node idx = list(obj_element).index(ref_node) co_el.tail = "\r\n" + indent obj_element.insert(idx, co_el) else: # Append children = list(obj_element) if len(children) > 0: last = children[-1] co_el.tail = last.tail last.tail = "\r\n" + indent obj_element.append(co_el) else: parent_indent = indent[:-1] if len(indent) > 0 else "" obj_element.text = "\r\n" + indent co_el.tail = "\r\n" + parent_indent obj_element.append(co_el) child_objects_el = co_el def collapse_child_objects_if_empty(): """Collapse ChildObjects to self-closing if empty.""" global child_objects_el if child_objects_el is None: return has_elements = any(True for _ in child_objects_el) if not has_elements: child_objects_el.text = None # ============================================================ # Fragment builders # ============================================================ def parse_attribute_shorthand(val): """Parse attribute definition from string shorthand or dict object.""" if isinstance(val, str): s = val parsed = { "name": "", "type": "", "synonym": "", "comment": "", "flags": [], "fillChecking": "", "indexing": "", "after": "", "before": "", } # Extract positional markers: >> after Name, << before Name m = re.search(r"\s*>>\s*after\s+(\S+)\s*$", s) if m: parsed["after"] = m.group(1) s = re.sub(r"\s*>>\s*after\s+\S+\s*$", "", s).strip() else: m = re.search(r"\s*<<\s*before\s+(\S+)\s*$", s) if m: parsed["before"] = m.group(1) s = re.sub(r"\s*<<\s*before\s+\S+\s*$", "", s).strip() # Split by | for flags parts = s.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()] # Split by : for name and type 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/dict form name = str(val.get("name", "")) result = { "name": name, "type": " + ".join(str(t) for t in val["type"]) if isinstance(val.get("type"), list) else str(val.get("type", "")), "synonym": str(val.get("synonym", "")) if val.get("synonym") else split_camel_case(name), "comment": str(val.get("comment", "")), "flags": list(val.get("flags", [])), "fillChecking": normalize_enum_value("FillChecking", str(val.get("fillChecking", ""))) if val.get("fillChecking") else "", "indexing": normalize_enum_value("Indexing", str(val.get("indexing", ""))) if val.get("indexing") else "", "after": str(val.get("after", "")), "before": str(val.get("before", "")), } # Map flags to properties if "req" in result["flags"] and not result["fillChecking"]: result["fillChecking"] = "ShowError" if "index" in result["flags"] and not result["indexing"]: result["indexing"] = "Index" if "indexadditional" in result["flags"] and not result["indexing"]: result["indexing"] = "IndexWithAdditionalOrder" return result def parse_enum_value_shorthand(val): """Parse enum value definition from string or dict.""" if isinstance(val, str): name = val return { "name": name, "synonym": split_camel_case(name), "comment": "", "after": "", "before": "", } name = str(val.get("name", "")) return { "name": name, "synonym": str(val.get("synonym", "")) if val.get("synonym") else split_camel_case(name), "comment": str(val.get("comment", "")), "after": str(val.get("after", "")), "before": str(val.get("before", "")), } def get_attribute_context(): """Determine attribute context from object type.""" if obj_type == "Catalog": return "catalog" if obj_type == "Document": return "document" if obj_type in ("InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister"): return "register" if obj_type in ("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"): return "processor" return "object" 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 = { 'Ссылка', 'ПометкаУдаления', 'Код', 'Наименование', 'Дата', 'Номер', 'Проведен', 'Родитель', 'Владелец', 'ЭтоГруппа', 'Предопределенный', 'ИмяПредопределенныхДанных', 'Регистратор', 'Период', 'НомерСтроки', 'Активность', 'Порядок', 'Тип', 'Забалансовый', 'Стартован', 'Завершен', 'ВедущаяЗадача', 'Выполнена', 'ТочкаМаршрута', 'БизнесПроцесс', 'ЭтотУзел', 'НомерОтправленного', 'НомерПринятого', 'ВидРасчета', 'ПериодРегистрации', 'СторноЗапись', 'Счет', 'ТипЗначения', 'ПериодДействияБазовый', } def build_attribute_fragment(parsed, context, indent): """Build XML fragment string for an Attribute element.""" if not context: context = get_attribute_context() # Check reserved attribute names attr_name = parsed['name'] if 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() lines = [] lines.append(f'{indent}') lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(parsed['name'])}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"])) lines.append(f"{indent}\t\t") # Type type_str = parsed["type"] if type_str: lines.append(build_value_type_xml(f"{indent}\t\t", type_str)) else: lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t\txs:string") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\tfalse") lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t') # FillFromFillingValue/FillValue -- not for register, tabular, or processor if context not in ("register", "tabular", "processor"): lines.append(f"{indent}\t\tfalse") lines.append(build_fill_value_xml(f"{indent}\t\t", type_str)) # FillChecking fill_checking = "DontCheck" if "req" in parsed["flags"]: fill_checking = "ShowError" if parsed["fillChecking"]: fill_checking = parsed["fillChecking"] lines.append(f"{indent}\t\t{fill_checking}") lines.append(f"{indent}\t\tItems") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tAuto") # Use -- catalog only if context == "catalog": lines.append(f"{indent}\t\tForItem") # Indexing/FullTextSearch/DataHistory -- not for non-stored objects if context not in ("processor", "processor-tabular"): indexing = "DontIndex" if "index" in parsed["flags"]: indexing = "Index" if "indexadditional" in parsed["flags"]: indexing = "IndexWithAdditionalOrder" if parsed["indexing"]: indexing = parsed["indexing"] lines.append(f"{indent}\t\t{indexing}") lines.append(f"{indent}\t\tUse") lines.append(f"{indent}\t\tUse") lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) def build_tabular_section_fragment(ts_def, indent): """Build XML fragment string for a TabularSection element.""" if isinstance(ts_def, str): ts_def = {"name": ts_def} ts_name = str(ts_def.get("name", "")) ts_synonym = str(ts_def.get("synonym", "")) if ts_def.get("synonym") else split_camel_case(ts_name) uid = new_uuid() type_prefix = f"{obj_type}TabularSection" row_prefix = f"{obj_type}TabularSectionRow" lines = [] lines.append(f'{indent}') # InternalInfo lines.append(f"{indent}\t") lines.append(f'{indent}\t\t') lines.append(f"{indent}\t\t\t{new_uuid()}") lines.append(f"{indent}\t\t\t{new_uuid()}") lines.append(f"{indent}\t\t") lines.append(f'{indent}\t\t') lines.append(f"{indent}\t\t\t{new_uuid()}") lines.append(f"{indent}\t\t\t{new_uuid()}") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t") # Properties lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(ts_name)}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", ts_synonym)) lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tDontCheck") # StandardAttributes (LineNumber) lines.append(f"{indent}\t\t") lines.append(f'{indent}\t\t\t') lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\tDontCheck") lines.append(f"{indent}\t\t\t\tfalse") lines.append(f"{indent}\t\t\t\tfalse") lines.append(f"{indent}\t\t\t\tAuto") lines.append(f'{indent}\t\t\t\t') lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\tfalse") lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\tAuto") lines.append(f"{indent}\t\t\t\tAuto") lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\tfalse") lines.append(f"{indent}\t\t\t\tUse") lines.append(f"{indent}\t\t\t\tfalse") lines.append(f'{indent}\t\t\t\t') lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\tUse") lines.append(f"{indent}\t\t\t\t") lines.append(f'{indent}\t\t\t\t') lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t\t") lines.append(f"{indent}\t\t\t") lines.append(f"{indent}\t\t") # Use -- catalog only if obj_type == "Catalog": lines.append(f"{indent}\t\tForItem") lines.append(f"{indent}\t") # ChildObjects with attrs columns = [] if ts_def.get("attrs"): columns = list(ts_def["attrs"]) elif ts_def.get("attributes"): columns = list(ts_def["attributes"]) elif ts_def.get("реквизиты"): columns = list(ts_def["реквизиты"]) ts_attr_context = "processor-tabular" if obj_type in ("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport") else "tabular" if columns: lines.append(f"{indent}\t") for col in columns: col_parsed = parse_attribute_shorthand(col) lines.append(build_attribute_fragment(col_parsed, ts_attr_context, f"{indent}\t\t")) lines.append(f"{indent}\t") else: lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) def build_dimension_fragment(parsed, register_type, indent): """Build XML fragment string for a Dimension element.""" if not register_type: register_type = obj_type uid = new_uuid() lines = [] lines.append(f'{indent}') lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(parsed['name'])}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"])) lines.append(f"{indent}\t\t") type_str = parsed["type"] if type_str: lines.append(build_value_type_xml(f"{indent}\t\t", type_str)) else: lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t\txs:string") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\tfalse") lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t') # InformationRegister: FillFromFillingValue, FillValue if register_type == "InformationRegister": fill_from = "true" if "master" in parsed["flags"] else "false" lines.append(f"{indent}\t\t{fill_from}") lines.append(f'{indent}\t\t') fill_checking = "DontCheck" if "req" in parsed["flags"]: fill_checking = "ShowError" lines.append(f"{indent}\t\t{fill_checking}") lines.append(f"{indent}\t\tItems") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tAuto") # InformationRegister: Master, MainFilter, DenyIncompleteValues if register_type == "InformationRegister": master = "true" if "master" in parsed["flags"] else "false" main_filter = "true" if "mainfilter" in parsed["flags"] else "false" deny_incomplete = "true" if "denyincomplete" in parsed["flags"] else "false" lines.append(f"{indent}\t\t{master}") lines.append(f"{indent}\t\t{main_filter}") lines.append(f"{indent}\t\t{deny_incomplete}") # AccumulationRegister: DenyIncompleteValues if register_type == "AccumulationRegister": deny_incomplete = "true" if "denyincomplete" in parsed["flags"] else "false" lines.append(f"{indent}\t\t{deny_incomplete}") indexing = "DontIndex" if "index" in parsed["flags"]: indexing = "Index" lines.append(f"{indent}\t\t{indexing}") lines.append(f"{indent}\t\tUse") # AccumulationRegister: UseInTotals if register_type == "AccumulationRegister": use_in_totals = "false" if "nouseintotals" in parsed["flags"] else "true" lines.append(f"{indent}\t\t{use_in_totals}") # InformationRegister: DataHistory if register_type == "InformationRegister": lines.append(f"{indent}\t\tUse") lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) def build_resource_fragment(parsed, register_type, indent): """Build XML fragment string for a Resource element.""" if not register_type: register_type = obj_type uid = new_uuid() lines = [] lines.append(f'{indent}') lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(parsed['name'])}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"])) lines.append(f"{indent}\t\t") type_str = parsed["type"] if type_str: lines.append(build_value_type_xml(f"{indent}\t\t", type_str)) else: # Default: Number(15,2) lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t\txs:decimal") lines.append(f"{indent}\t\t\t") lines.append(f"{indent}\t\t\t\t15") lines.append(f"{indent}\t\t\t\t2") lines.append(f"{indent}\t\t\t\tAny") lines.append(f"{indent}\t\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\tfalse") lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t') # InformationRegister: FillFromFillingValue, FillValue if register_type == "InformationRegister": lines.append(f"{indent}\t\tfalse") lines.append(f'{indent}\t\t') fill_checking = "DontCheck" if "req" in parsed["flags"]: fill_checking = "ShowError" lines.append(f"{indent}\t\t{fill_checking}") lines.append(f"{indent}\t\tItems") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\tAuto") # InformationRegister: Indexing, FullTextSearch, DataHistory if register_type == "InformationRegister": lines.append(f"{indent}\t\tDontIndex") lines.append(f"{indent}\t\tUse") lines.append(f"{indent}\t\tUse") # AccumulationRegister: FullTextSearch if register_type == "AccumulationRegister": lines.append(f"{indent}\t\tUse") lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) def build_enum_value_fragment(parsed, indent): """Build XML fragment string for an EnumValue element.""" uid = new_uuid() lines = [] lines.append(f'{indent}') lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(parsed['name'])}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"])) lines.append(f"{indent}\t\t") lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) def build_column_fragment(col_def, indent): """Build XML fragment string for a Column element.""" uid = new_uuid() name = "" synonym = "" indexing = "DontIndex" references = [] if isinstance(col_def, str): name = col_def synonym = split_camel_case(name) else: name = str(col_def.get("name", "")) synonym = str(col_def.get("synonym", "")) if col_def.get("synonym") else split_camel_case(name) if col_def.get("indexing"): indexing = normalize_enum_value("Indexing", str(col_def["indexing"])) if col_def.get("references"): references = list(col_def["references"]) lines = [] lines.append(f'{indent}') lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(name)}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", synonym)) lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t{indexing}") if references: lines.append(f"{indent}\t\t") for ref in references: lines.append(f'{indent}\t\t\t{ref}') lines.append(f"{indent}\t\t") else: lines.append(f"{indent}\t\t") lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) def build_simple_child_fragment(tag_name, name, indent): """Build XML fragment for Form, Template, Command -- just a name wrapper.""" uid = new_uuid() synonym = split_camel_case(name) lines = [] lines.append(f'{indent}<{tag_name} uuid="{uid}">') lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{esc_xml(name)}") lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", synonym)) lines.append(f"{indent}\t\t") # Forms get additional properties if tag_name == "Form": lines.append(f"{indent}\t\tOrdinary") lines.append(f"{indent}\t\tfalse") lines.append(f"{indent}\t\t") if tag_name == "Template": lines.append(f"{indent}\t\tSpreadsheetDocument") if tag_name == "Command": lines.append(f"{indent}\t\tFormNavigationPanelGoTo") lines.append(f"{indent}\t\tAuto") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t\t") lines.append(f"{indent}\t") lines.append(f"{indent}") return "\r\n".join(lines) # ============================================================ # Name uniqueness check # ============================================================ def get_all_child_names(): """Get dict of all child element names -> element localname.""" names = {} if child_objects_el is None: return names for child in child_objects_el: props_el = None for gc in child: if localname(gc) == "Properties": props_el = gc break if props_el is None: continue for gc in props_el: if localname(gc) == "Name": n = (gc.text or "").strip() if n: names[n] = localname(child) break return names # ============================================================ # Context and allowed child types # ============================================================ valid_child_types = { "Catalog": ["attributes", "tabularSections", "forms", "templates", "commands"], "Document": ["attributes", "tabularSections", "forms", "templates", "commands"], "ExchangePlan": ["attributes", "tabularSections", "forms", "templates", "commands"], "ChartOfAccounts": ["attributes", "tabularSections", "forms", "templates", "commands"], "ChartOfCharacteristicTypes": ["attributes", "tabularSections", "forms", "templates", "commands"], "ChartOfCalculationTypes": ["attributes", "tabularSections", "forms", "templates", "commands"], "BusinessProcess": ["attributes", "tabularSections", "forms", "templates", "commands"], "Task": ["attributes", "tabularSections", "forms", "templates", "commands"], "Report": ["attributes", "tabularSections", "forms", "templates", "commands"], "DataProcessor": ["attributes", "tabularSections", "forms", "templates", "commands"], "Enum": ["enumValues", "forms", "templates", "commands"], "InformationRegister": ["dimensions", "resources", "attributes", "forms", "templates", "commands"], "AccumulationRegister": ["dimensions", "resources", "attributes", "forms", "templates", "commands"], "AccountingRegister": ["dimensions", "resources", "attributes", "forms", "templates", "commands"], "CalculationRegister": ["dimensions", "resources", "attributes", "forms", "templates", "commands"], "DocumentJournal": ["columns", "forms", "templates", "commands"], "Constant": ["forms"], } # Canonical child order in ChildObjects child_order = [ "Resource", "Dimension", "Attribute", "TabularSection", "AccountingFlag", "ExtDimensionAccountingFlag", "EnumValue", "Column", "AddressingAttribute", "Recalculation", "Form", "Template", "Command", ] # Map from DSL child type to XML element name child_type_to_xml_tag = { "attributes": "Attribute", "tabularSections": "TabularSection", "dimensions": "Dimension", "resources": "Resource", "enumValues": "EnumValue", "columns": "Column", "forms": "Form", "templates": "Template", "commands": "Command", } # ============================================================ # DSL key normalization # ============================================================ def resolve_operation_key(key): k = key.lower().strip() return operation_synonyms.get(k) def resolve_child_type_key(key): k = key.lower().strip() return child_type_synonyms.get(k) # ============================================================ # Inline mode converter # ============================================================ def split_by_comma_outside_parens(s): result = [] depth = 0 current = "" for ch in s: if ch == "(": depth += 1 elif ch == ")": depth -= 1 if ch == "," and depth == 0: result.append(current) current = "" else: current += ch if current: result.append(current) return result def convert_inline_to_definition(operation, value): """Convert inline -Operation + -Value to a definition dict.""" op_parts = operation.split("-", 1) op = op_parts[0] # add, remove, modify, set target = op_parts[1] # attribute, ts, owner, owners, property, etc. # Complex property targets complex_target_map = { "owner": "Owners", "owners": "Owners", "registerRecord": "RegisterRecords", "registerRecords": "RegisterRecords", "basedOn": "BasedOn", "inputByString": "InputByString", } if target in complex_target_map: prop_name = complex_target_map[target] values = [v.strip() for v in value.split(";;") if v.strip()] # For InputByString, auto-prefix with MetaType.Name. if prop_name == "InputByString": prefix = f"{obj_type}.{obj_name}." meta_types = ( "Catalog", "Document", "InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister", "ChartOfCharacteristicTypes", "ChartOfCalculationTypes", "ChartOfAccounts", "ExchangePlan", "BusinessProcess", "Task", "Enum", "Report", "DataProcessor", ) new_values = [] for v in values: if "." not in v: new_values.append(f"{prefix}{v}") elif not re.match(r"^(" + "|".join(meta_types) + r")\.", v): new_values.append(f"{prefix}{v}") else: new_values.append(v) values = new_values complex_action = "set" if op == "set" else op return {"_complex": [{"action": complex_action, "property": prop_name, "values": values}]} # TS attribute operations: dot notation "TSName.AttrDef" if target == "ts-attribute": items = [v.strip() for v in value.split(";;") if v.strip()] # Group by TS name ts_groups = {} ts_order = [] for item in items: dot_idx = item.find(".") if dot_idx <= 0: warn(f"Invalid ts-attribute format (expected TSName.AttrDef): {item}") continue ts_name = item[:dot_idx].strip() rest = item[dot_idx + 1:].strip() if ts_name not in ts_groups: ts_groups[ts_name] = [] ts_order.append(ts_name) ts_groups[ts_name].append(rest) # Build: { modify: { tabularSections: { TSName: { add/remove/modify: ... } } } } ts_mod_obj = {} for ts_name in ts_order: ts_changes = {} if op == "add": ts_changes["add"] = ts_groups[ts_name] elif op == "remove": ts_changes["remove"] = ts_groups[ts_name] elif op == "modify": attr_mod_obj = {} for elem_def in ts_groups[ts_name]: colon_idx = elem_def.find(":") if colon_idx <= 0: warn(f"Invalid modify format (expected Name: key=val): {elem_def}") continue elem_name = elem_def[:colon_idx].strip() changes_part = elem_def[colon_idx + 1:].strip() changes_obj = {} change_pairs = split_by_comma_outside_parens(changes_part) for cp in change_pairs: cp = cp.strip() eq_idx = cp.find("=") if eq_idx > 0: ck = cp[:eq_idx].strip() cv = cp[eq_idx + 1:].strip() changes_obj[ck] = cv attr_mod_obj[elem_name] = changes_obj ts_changes["modify"] = attr_mod_obj ts_mod_obj[ts_name] = ts_changes return {"modify": {"tabularSections": ts_mod_obj}} # Target -> JSON DSL child type target_map = { "attribute": "attributes", "ts": "tabularSections", "dimension": "dimensions", "resource": "resources", "enumValue": "enumValues", "column": "columns", "form": "forms", "template": "templates", "command": "commands", "property": "properties", } child_type = target_map.get(target) if not child_type: die(f"Unknown inline target: {target}") definition = {} if op == "add": items = [] if child_type == "tabularSections": # TS format: "TSName: attr1_shorthand, attr2_shorthand, ..." ts_values = [v.strip() for v in value.split(";;") if v.strip()] for ts_val in ts_values: colon_idx = ts_val.find(":") if colon_idx > 0: ts_name = ts_val[:colon_idx].strip() attrs_part = ts_val[colon_idx + 1:].strip() # Split attrs by comma (paren-aware), reassemble if part doesn't start with "Name:" raw_parts = split_by_comma_outside_parens(attrs_part) attr_strs = [] current = "" for rp in raw_parts: rp = rp.strip() if current and re.match(r"^[А-Яа-яЁёA-Za-z_]\w*\s*:", rp): attr_strs.append(current) current = rp elif current: current += f", {rp}" else: current = rp if current: attr_strs.append(current) items.append({"name": ts_name, "attrs": attr_strs}) else: # Just a name, no attrs items.append(ts_val) else: # Batch split by ;; items = [v.strip() for v in value.split(";;") if v.strip()] definition["add"] = {child_type: items} elif op == "remove": items = [v.strip() for v in value.split(";;") if v.strip()] definition["remove"] = {child_type: items} elif op == "modify": if child_type == "properties": # "CodeLength=11 ;; DescriptionLength=150" kv_pairs = [v.strip() for v in value.split(";;") if v.strip()] props_obj = {} for kv in kv_pairs: eq_idx = kv.find("=") if eq_idx > 0: k = kv[:eq_idx].strip() v = kv[eq_idx + 1:].strip() props_obj[k] = v else: warn(f"Invalid property format (expected Key=Value): {kv}") definition["modify"] = {"properties": props_obj} else: # "ElementName: key=val, key=val ;; Element2: key=val" elem_defs = [v.strip() for v in value.split(";;") if v.strip()] child_mod_obj = {} for elem_def in elem_defs: colon_idx = elem_def.find(":") if colon_idx <= 0: warn(f"Invalid modify format (expected Name: key=val): {elem_def}") continue elem_name = elem_def[:colon_idx].strip() changes_part = elem_def[colon_idx + 1:].strip() changes_obj = {} change_pairs = split_by_comma_outside_parens(changes_part) for cp in change_pairs: cp = cp.strip() eq_idx = cp.find("=") if eq_idx > 0: ck = cp[:eq_idx].strip() cv = cp[eq_idx + 1:].strip() changes_obj[ck] = cv child_mod_obj[elem_name] = changes_obj definition["modify"] = {child_type: child_mod_obj} return definition # ============================================================ # ADD operations # ============================================================ def find_insertion_point(xml_tag, parsed): """Find reference node for insertion. Returns element or None (meaning append).""" if child_objects_el is None: return None # Positional: after/before after_name = parsed.get("after", "") before_name = parsed.get("before", "") if after_name: after_el = find_element_by_name(child_objects_el, xml_tag, after_name) if after_el is not None: # Insert after = insert before the next element sibling nxt = after_el.getnext() while nxt is not None and not isinstance(nxt.tag, str): nxt = nxt.getnext() if nxt is not None and localname(nxt) == xml_tag: return nxt return None # append else: warn(f"after='{after_name}': element '{after_name}' not found in {xml_tag}, appending") if before_name: before_el = find_element_by_name(child_objects_el, xml_tag, before_name) if before_el is not None: return before_el warn(f"before='{before_name}': element '{before_name}' not found in {xml_tag}, appending") # Default: after last element of this type, or in canonical position last_of_type = find_last_element_of_type(child_objects_el, xml_tag) if last_of_type is not None: nxt = last_of_type.getnext() while nxt is not None and not isinstance(nxt.tag, str): nxt = nxt.getnext() return nxt # None means append (correct: after last of type) # No elements of this type yet -- find canonical position if xml_tag in child_order: tag_idx = child_order.index(xml_tag) else: return None # Find first element of any type that comes AFTER in the canonical order for i in range(tag_idx + 1, len(child_order)): next_tag = child_order[i] first_of_next = find_first_element_of_type(child_objects_el, next_tag) if first_of_next is not None: return first_of_next return None # append at end def process_add(add_def): global add_count for raw_key, items in add_def.items(): child_type = resolve_child_type_key(raw_key) if not child_type: warn(f"Unknown add child type: {raw_key}") continue # Validate allowed allowed = valid_child_types.get(obj_type) if allowed and child_type not in allowed: warn(f"{child_type} not allowed for {obj_type}, skipping") continue xml_tag = child_type_to_xml_tag.get(child_type) if not xml_tag: warn(f"No XML tag mapping for {child_type}") continue ensure_child_objects_open() indent = get_child_indent(child_objects_el) existing_names = get_all_child_names() if child_type == "attributes": for item in items: parsed = parse_attribute_shorthand(item) if parsed["name"] in existing_names: warn(f"Attribute '{parsed['name']}' already exists, skipping") continue context = get_attribute_context() fragment_xml = build_attribute_fragment(parsed, context, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point("Attribute", parsed) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added attribute: {parsed['name']}") add_count += 1 existing_names[parsed["name"]] = "Attribute" elif child_type == "tabularSections": for item in items: if isinstance(item, str): ts_name = item ts_def = {"name": item} else: ts_name = str(item.get("name", "")) ts_def = item if ts_name in existing_names: warn(f"TabularSection '{ts_name}' already exists, skipping") continue fragment_xml = build_tabular_section_fragment(ts_def, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point("TabularSection", {"after": "", "before": ""}) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added tabular section: {ts_name}") add_count += 1 existing_names[ts_name] = "TabularSection" elif child_type == "dimensions": for item in items: parsed = parse_attribute_shorthand(item) if parsed["name"] in existing_names: warn(f"Dimension '{parsed['name']}' already exists, skipping") continue fragment_xml = build_dimension_fragment(parsed, obj_type, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point("Dimension", parsed) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added dimension: {parsed['name']}") add_count += 1 existing_names[parsed["name"]] = "Dimension" elif child_type == "resources": for item in items: parsed = parse_attribute_shorthand(item) if parsed["name"] in existing_names: warn(f"Resource '{parsed['name']}' already exists, skipping") continue fragment_xml = build_resource_fragment(parsed, obj_type, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point("Resource", parsed) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added resource: {parsed['name']}") add_count += 1 existing_names[parsed["name"]] = "Resource" elif child_type == "enumValues": for item in items: parsed = parse_enum_value_shorthand(item) if parsed["name"] in existing_names: warn(f"EnumValue '{parsed['name']}' already exists, skipping") continue fragment_xml = build_enum_value_fragment(parsed, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point("EnumValue", parsed) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added enum value: {parsed['name']}") add_count += 1 existing_names[parsed["name"]] = "EnumValue" elif child_type == "columns": for item in items: if isinstance(item, str): col_name = item else: col_name = str(item.get("name", "")) if col_name in existing_names: warn(f"Column '{col_name}' already exists, skipping") continue fragment_xml = build_column_fragment(item, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point("Column", {"after": "", "before": ""}) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added column: {col_name}") add_count += 1 existing_names[col_name] = "Column" elif child_type in ("forms", "templates", "commands"): tag_map = {"forms": "Form", "templates": "Template", "commands": "Command"} tag = tag_map[child_type] for item in items: if isinstance(item, str): item_name = item else: item_name = str(item.get("name", "")) if item_name in existing_names: warn(f"{tag} '{item_name}' already exists, skipping") continue fragment_xml = build_simple_child_fragment(tag, item_name, indent) nodes = import_fragment(fragment_xml) ref_node = find_insertion_point(tag, {"after": "", "before": ""}) for node in nodes: insert_before_element(child_objects_el, node, ref_node, indent) info(f"Added {tag.lower()}: {item_name}") add_count += 1 existing_names[item_name] = tag # ============================================================ # REMOVE operations # ============================================================ def process_remove(remove_def): global remove_count for raw_key, names in remove_def.items(): child_type = resolve_child_type_key(raw_key) if not child_type: warn(f"Unknown remove child type: {raw_key}") continue if child_type == "properties": warn("Cannot remove properties -- use modify instead") continue xml_tag = child_type_to_xml_tag.get(child_type) if not xml_tag or child_objects_el is None: warn(f"No ChildObjects or unknown tag for {child_type}") continue for name in names: name_str = str(name) el = find_element_by_name(child_objects_el, xml_tag, name_str) if el is None: warn(f"{xml_tag} '{name_str}' not found, skipping remove") continue remove_node_with_whitespace(el) info(f"Removed {xml_tag.lower()}: {name_str}") remove_count += 1 # Collapse if empty collapse_child_objects_if_empty() # ============================================================ # MODIFY operations # ============================================================ def modify_properties(props_def): global modify_count for prop_name, prop_value in props_def.items(): # Find the property element in Properties prop_el = None for child in properties_el: if localname(child) == prop_name: prop_el = child break if prop_el is None: warn(f"Property '{prop_name}' not found in Properties") continue # Complex property: Owners, RegisterRecords, BasedOn, InputByString if prop_name in complex_property_map: values_list = [] if isinstance(prop_value, list): values_list = [str(v) for v in prop_value] else: values_list = [v.strip() for v in str(prop_value).split(";;") if v.strip()] set_complex_property(prop_name, values_list) continue # Handle boolean values value_str = str(prop_value) if isinstance(prop_value, bool): value_str = "true" if prop_value else "false" # Set inner text — clear children first, set text for ch in list(prop_el): prop_el.remove(ch) prop_el.text = value_str info(f"Modified property: {prop_name} = {value_str}") modify_count += 1 def modify_child_elements(modify_def, child_type): global add_count, remove_count, modify_count, child_objects_el xml_tag = child_type_to_xml_tag.get(child_type) if not xml_tag or child_objects_el is None: warn(f"No ChildObjects or unknown tag for {child_type}") return for elem_name, changes in modify_def.items(): el = find_element_by_name(child_objects_el, xml_tag, elem_name) if el is None: warn(f"{xml_tag} '{elem_name}' not found for modify") continue # Find Properties inside the element props_el = None for gc in el: if localname(gc) == "Properties": props_el = gc break if props_el is None: warn(f"{xml_tag} '{elem_name}': no Properties element found") continue for change_prop, change_value in changes.items(): # TS child attribute operations (add/remove/modify attrs inside a TabularSection) if xml_tag == "TabularSection" and change_prop in ("add", "remove", "modify"): # Find ChildObjects inside this TS element ts_child_obj_el = None for gc in el: if localname(gc) == "ChildObjects": ts_child_obj_el = gc break if change_prop == "add": if ts_child_obj_el is None: warn(f"TS '{elem_name}' has no ChildObjects element, cannot add attributes") continue # Ensure ChildObjects is open (not self-closing empty) has_ts_child_elements = any(True for _ in ts_child_obj_el) if not has_ts_child_elements: ts_co_indent = get_child_indent(el) ts_child_obj_el.text = "\r\n" + ts_co_indent attr_defs = change_value if isinstance(change_value, list) else [change_value] for attr_def in attr_defs: parsed = parse_attribute_shorthand(attr_def) existing = find_element_by_name(ts_child_obj_el, "Attribute", parsed["name"]) if existing is not None: warn(f"Attribute '{parsed['name']}' already exists in TS '{elem_name}', skipping") continue ts_attr_indent = get_child_indent(ts_child_obj_el) ts_attr_context = "processor-tabular" if obj_type in ("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport") else "tabular" fragment_xml = build_attribute_fragment(parsed, ts_attr_context, ts_attr_indent) nodes = import_fragment(fragment_xml) saved_co = child_objects_el child_objects_el = ts_child_obj_el ref_node = find_insertion_point("Attribute", parsed) child_objects_el = saved_co for node in nodes: insert_before_element(ts_child_obj_el, node, ref_node, ts_attr_indent) info(f"Added attribute to TS '{elem_name}': {parsed['name']}") add_count += 1 elif change_prop == "remove": if ts_child_obj_el is None: warn(f"TS '{elem_name}' has no ChildObjects, cannot remove attributes") continue attr_names = change_value if isinstance(change_value, list) else [change_value] for attr_name in attr_names: attr_el = find_element_by_name(ts_child_obj_el, "Attribute", str(attr_name)) if attr_el is None: warn(f"Attribute '{attr_name}' not found in TS '{elem_name}', skipping") continue remove_node_with_whitespace(attr_el) info(f"Removed attribute from TS '{elem_name}': {attr_name}") remove_count += 1 elif change_prop == "modify": if ts_child_obj_el is None: warn(f"TS '{elem_name}' has no ChildObjects, cannot modify attributes") continue # Temporarily swap childObjectsEl and recurse saved_child_obj_el = child_objects_el child_objects_el = ts_child_obj_el modify_child_elements(change_value, "attributes") child_objects_el = saved_child_obj_el continue # Skip normal property modification if change_prop == "name": # Rename name_el = None for gc in props_el: if localname(gc) == "Name": name_el = gc break if name_el is not None: old_name = (name_el.text or "").strip() new_name = str(change_value) name_el.text = new_name # Update Synonym if it was auto-generated old_synonym = split_camel_case(old_name) syn_el = None for gc in props_el: if localname(gc) == "Synonym": syn_el = gc break if syn_el is not None: # Check if current synonym matches auto-generated from old name current_syn = "" for item_el in syn_el: if localname(item_el) == "item": for gc in item_el: if localname(gc) == "content": current_syn = (gc.text or "").strip() if current_syn == old_synonym or not current_syn: new_synonym = split_camel_case(new_name) syn_indent = get_child_indent(props_el) new_syn_xml = build_mltext_xml(syn_indent, "Synonym", new_synonym) new_syn_nodes = import_fragment(new_syn_xml) if new_syn_nodes: # Insert new synonym after old, then remove old syn_idx = list(props_el).index(syn_el) new_syn_nodes[0].tail = syn_el.tail props_el.insert(syn_idx + 1, new_syn_nodes[0]) remove_node_with_whitespace(syn_el) info(f"Renamed {xml_tag}: {old_name} -> {new_name}") modify_count += 1 elif change_prop == "type": # Change type type_el = None for gc in props_el: if localname(gc) == "Type": type_el = gc break new_type_str = str(change_value) type_indent = get_child_indent(props_el) new_type_xml = build_value_type_xml(type_indent, new_type_str) new_type_nodes = import_fragment(new_type_xml) if type_el is not None and new_type_nodes: type_idx = list(props_el).index(type_el) new_type_nodes[0].tail = type_el.tail props_el.insert(type_idx + 1, new_type_nodes[0]) remove_node_with_whitespace(type_el) elif new_type_nodes: # No existing Type -- insert after Comment comment_el = None for gc in props_el: if localname(gc) == "Comment": comment_el = gc break if comment_el is not None: comment_idx = list(props_el).index(comment_el) nxt = comment_el.getnext() insert_before_element(props_el, new_type_nodes[0], nxt, type_indent) # Also update FillValue if present fill_val_el = None for gc in props_el: if localname(gc) == "FillValue": fill_val_el = gc break if fill_val_el is not None: fill_indent = get_child_indent(props_el) new_fill_xml = build_fill_value_xml(fill_indent, new_type_str) new_fill_nodes = import_fragment(new_fill_xml) if new_fill_nodes: fill_idx = list(props_el).index(fill_val_el) new_fill_nodes[0].tail = fill_val_el.tail props_el.insert(fill_idx + 1, new_fill_nodes[0]) remove_node_with_whitespace(fill_val_el) info(f"Changed type of {xml_tag} '{elem_name}': {new_type_str}") modify_count += 1 elif change_prop == "synonym": syn_el = None for gc in props_el: if localname(gc) == "Synonym": syn_el = gc break syn_indent = get_child_indent(props_el) new_syn_xml = build_mltext_xml(syn_indent, "Synonym", str(change_value)) new_syn_nodes = import_fragment(new_syn_xml) if syn_el is not None and new_syn_nodes: syn_idx = list(props_el).index(syn_el) new_syn_nodes[0].tail = syn_el.tail props_el.insert(syn_idx + 1, new_syn_nodes[0]) remove_node_with_whitespace(syn_el) info(f"Changed synonym of {xml_tag} '{elem_name}': {change_value}") modify_count += 1 else: # Scalar property change (Indexing, FillChecking, Use, etc.) scalar_el = None for gc in props_el: if localname(gc) == change_prop: scalar_el = gc break if scalar_el is not None: value_str = str(change_value) if isinstance(change_value, bool): value_str = "true" if change_value else "false" else: value_str = normalize_enum_value(change_prop, value_str) # Clear children and set text for ch in list(scalar_el): scalar_el.remove(ch) scalar_el.text = value_str info(f"Modified {xml_tag} '{elem_name}'.{change_prop} = {value_str}") modify_count += 1 else: warn(f"{xml_tag} '{elem_name}': property '{change_prop}' not found") def process_modify(modify_def): for raw_key, value in modify_def.items(): child_type = resolve_child_type_key(raw_key) if not child_type: warn(f"Unknown modify child type: {raw_key}") continue if child_type == "properties": modify_properties(value) else: modify_child_elements(value, child_type) # ============================================================ # Complex property helpers # ============================================================ complex_property_map = { "Owners": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"'}, "RegisterRecords": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"'}, "BasedOn": {"tag": "xr:Item", "attr": 'xsi:type="xr:MDObjectRef"'}, "InputByString": {"tag": "xr:Field", "attr": None}, } def find_property_element(prop_name): for child in properties_el: if localname(child) == prop_name: return child return None def get_complex_property_values(prop_el): values = [] for child in prop_el: values.append((child.text or "").strip()) return values def add_complex_property_item(property_name, values): global add_count map_entry = complex_property_map.get(property_name) if not map_entry: warn(f"Unknown complex property: {property_name}") return prop_el = find_property_element(property_name) if prop_el is None: warn(f"Property element '{property_name}' not found in Properties") return # Get existing values to check duplicates existing = get_complex_property_values(prop_el) indent = get_child_indent(properties_el) child_indent = f"{indent}\t" # Check if element is empty (self-closing) is_empty = len(list(prop_el)) == 0 # If self-closing / empty, add closing whitespace if is_empty and not (prop_el.text and prop_el.text.strip()): prop_el.text = "\r\n" + indent for val in values: if val in existing: warn(f"{property_name} already contains '{val}', skipping") continue tag = map_entry["tag"] attr_str = map_entry["attr"] if attr_str: frag_xml = f"<{tag} {attr_str}>{esc_xml(val)}" else: frag_xml = f"<{tag}>{esc_xml(val)}" nodes = import_fragment(frag_xml) for node in nodes: insert_before_element(prop_el, node, None, child_indent) info(f"Added {property_name} item: {val}") add_count += 1 def remove_complex_property_item(property_name, values): global remove_count prop_el = find_property_element(property_name) if prop_el is None: warn(f"Property element '{property_name}' not found in Properties") return for val in values: found = False for child in list(prop_el): if (child.text or "").strip() == val: remove_node_with_whitespace(child) info(f"Removed {property_name} item: {val}") remove_count += 1 found = True break if not found: warn(f"{property_name} item '{val}' not found, skipping") # Collapse if empty has_elements = any(True for _ in prop_el) if not has_elements: prop_el.text = None def set_complex_property(property_name, values): global modify_count map_entry = complex_property_map.get(property_name) if not map_entry: warn(f"Unknown complex property: {property_name}") return prop_el = find_property_element(property_name) if prop_el is None: warn(f"Property element '{property_name}' not found in Properties") return indent = get_child_indent(properties_el) child_indent = f"{indent}\t" # Remove all existing children for ch in list(prop_el): prop_el.remove(ch) prop_el.text = None if not values: # Leave self-closing info(f"Cleared {property_name}") modify_count += 1 return # Add closing whitespace prop_el.text = "\r\n" + indent # Add each value for val in values: tag = map_entry["tag"] attr_str = map_entry["attr"] if attr_str: frag_xml = f"<{tag} {attr_str}>{esc_xml(val)}" else: frag_xml = f"<{tag}>{esc_xml(val)}" nodes = import_fragment(frag_xml) for node in nodes: insert_before_element(prop_el, node, None, child_indent) count = len(values) info(f"Set {property_name}: {count} items") modify_count += 1 # ============================================================ # Save helpers # ============================================================ def save_xml(tree, path): """Save XML tree with BOM and proper encoding declaration.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") # Fix XML declaration quotes xml_bytes = xml_bytes.replace(b"", b'') # Fix d5p1 namespace declarations stripped by lxml (it treats them as unused # because d5p1: appears only in text content, not in element/attribute names) xml_bytes = re.sub( b'(d5p1:)', b'\\1 xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"\\2', xml_bytes ) if not xml_bytes.endswith(b"\n"): xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) # ============================================================ # Main # ============================================================ def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") global xml_tree, xml_root, obj_element, obj_type, md_ns global properties_el, child_objects_el, obj_name global add_count, remove_count, modify_count, warn_count valid_operations = [ "add-attribute", "add-ts", "add-dimension", "add-resource", "add-enumValue", "add-column", "add-form", "add-template", "add-command", "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", "remove-attribute", "remove-ts", "remove-dimension", "remove-resource", "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", "add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts", "modify-attribute", "modify-dimension", "modify-resource", "modify-enumValue", "modify-column", "modify-property", "set-owners", "set-registerRecords", "set-basedOn", "set-inputByString", ] parser = argparse.ArgumentParser(description="Edit existing 1C metadata object XML", allow_abbrev=False) parser.add_argument("-DefinitionFile", default=None, help="JSON definition file") parser.add_argument("-ObjectPath", "-Path", required=True, help="Path to object XML or directory") parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation") parser.add_argument("-Value", default=None, help="Inline value") parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation") args = parser.parse_args() # --- Mode validation --- if args.DefinitionFile and args.Operation: die("Cannot use both -DefinitionFile and -Operation") if not args.DefinitionFile and not args.Operation: die("Either -DefinitionFile or -Operation is required") # --- Load JSON definition (DefinitionFile mode) --- definition = None if args.DefinitionFile: if not os.path.exists(args.DefinitionFile): die(f"Definition file not found: {args.DefinitionFile}") with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f: definition = json.load(f) # --- Resolve object path --- object_path = args.ObjectPath if os.path.isdir(object_path): dir_name = os.path.basename(object_path) candidate = os.path.join(object_path, f"{dir_name}.xml") sibling = os.path.join(os.path.dirname(object_path), f"{dir_name}.xml") if os.path.exists(candidate): object_path = candidate elif os.path.exists(sibling): object_path = sibling else: die(f"Directory given but no {dir_name}.xml found inside or as sibling") # File not found -- check Dir/Name/Name.xml -> Dir/Name.xml if not os.path.exists(object_path): file_name = os.path.splitext(os.path.basename(object_path))[0] parent_dir = os.path.dirname(object_path) parent_dir_name = os.path.basename(parent_dir) if file_name == parent_dir_name: candidate = os.path.join(os.path.dirname(parent_dir), f"{file_name}.xml") if os.path.exists(candidate): object_path = candidate if not os.path.exists(object_path): die(f"Object file not found: {object_path}") resolved_path = os.path.abspath(object_path) # --- Load XML --- xml_parser = etree.XMLParser(remove_blank_text=False) xml_tree = etree.parse(resolved_path, xml_parser) xml_root = xml_tree.getroot() # --- Detect object type --- if localname(xml_root) != "MetaDataObject": die(f"Root element must be MetaDataObject, got: {localname(xml_root)}") # Find the first child element -- this is the object type element obj_element = None for child in xml_root: obj_element = child break if obj_element is None: die("No object element found under MetaDataObject") obj_type = localname(obj_element) md_ns = etree.QName(obj_element.tag).namespace or "" # Find Properties and ChildObjects properties_el = None child_objects_el = None for child in obj_element: ln = localname(child) if ln == "Properties": properties_el = child if ln == "ChildObjects": child_objects_el = child if properties_el is None: die(f"No found in {obj_type}") # Extract object name obj_name = "" for child in properties_el: if localname(child) == "Name": obj_name = (child.text or "").strip() break info(f"Object: {obj_type}.{obj_name}") # --- Inline mode conversion --- if args.Operation: definition = convert_inline_to_definition(args.Operation, args.Value or "") if definition is None: die("No definition loaded") # --- Process complex property operations --- if "_complex" in definition and definition["_complex"]: for cop in definition["_complex"]: action = cop["action"] if action == "add": add_complex_property_item(cop["property"], cop["values"]) elif action == "remove": remove_complex_property_item(cop["property"], cop["values"]) elif action == "set": set_complex_property(cop["property"], cop["values"]) # --- Process standard operations --- for prop_name, prop_value in definition.items(): if prop_name == "_complex": continue op_key = resolve_operation_key(prop_name) if not op_key: warn(f"Unknown operation: {prop_name}") continue if op_key == "add": process_add(prop_value) elif op_key == "remove": process_remove(prop_value) elif op_key == "modify": process_modify(prop_value) # --- Save XML --- save_xml(xml_tree, resolved_path) info(f"Saved: {resolved_path}") # --- Auto-validate --- if not args.NoValidate: script_dir = os.path.dirname(os.path.abspath(__file__)) validate_script = os.path.normpath(os.path.join(script_dir, "..", "..", "meta-validate", "scripts", "meta-validate.py")) if os.path.exists(validate_script): print() print("--- Running meta-validate ---") python_exe = sys.executable subprocess.run([python_exe, validate_script, "-ObjectPath", "-Path", resolved_path]) else: print() print(f"[SKIP] meta-validate not found at: {validate_script}") # --- Summary --- print() print("=== meta-edit summary ===") print(f" Object: {obj_type}.{obj_name}") print(f" Added: {add_count}") print(f" Removed: {remove_count}") print(f" Modified: {modify_count}") if warn_count > 0: print(f" Warnings: {warn_count}") total_changes = add_count + remove_count + modify_count if total_changes == 0: print(" No changes applied.") sys.exit(0) if __name__ == "__main__": main()