#!/usr/bin/env python3 # template-add v1.27 — Add template to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json import os import re import sys import uuid from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. def ci_parse_args(parser, argv=None): """parse_args по правилам PS: имена параметров и значения choices регистронезависимы.""" argv = list(sys.argv[1:] if argv is None else argv) names = {s.lower(): s for a in parser._actions for s in a.option_strings} for i, tok in enumerate(argv): if tok.startswith('-') and tok.lower() in names: argv[i] = names[tok.lower()] # choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр choice_map = {} for a in parser._actions: if a.choices: for s in a.option_strings: choice_map[s] = {str(c).lower(): c for c in a.choices} for i in range(len(argv) - 1): m = choice_map.get(argv[i]) if m and argv[i + 1].lower() in m: argv[i + 1] = m[argv[i + 1].lower()] return parser.parse_args(argv) NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} # ============================================================ # 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_is_external_root(xml_path): if not os.path.isfile(xml_path): return False try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str): return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport") except Exception: return False return False 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) # Autonomous external object (EPF/ERF): never part of a config on support (issue #39). if _sg_is_external_root(rp): return 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 _sg_is_external_root(d + ".xml"): return 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 TYPE_MAP = { "HTML": {"TemplateType": "HTMLDocument", "Ext": ".xml"}, "Text": {"TemplateType": "TextDocument", "Ext": ".txt"}, "SpreadsheetDocument": {"TemplateType": "SpreadsheetDocument", "Ext": ".xml"}, "BinaryData": {"TemplateType": "BinaryData", "Ext": ".bin"}, "DataCompositionSchema": {"TemplateType": "DataCompositionSchema", "Ext": ".xml"}, } def _detect_xml_style(path): """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding / финальный перенос. None → файл новый (сохранить текущее поведение).""" try: raw = open(path, "rb").read() except OSError: return None bom = raw.startswith(b"\xef\xbb\xbf") body = raw[3:] if bom else raw crlf = b"\r\n" in body m = re.search(rb'encoding="([^"]+)"', body[:200]) enc = m.group(1).decode("ascii") if m else "utf-8" final_nl = body.endswith(b"\n") return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl} def _finalize_xml_bytes(xml_bytes, style): """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце.""" enc_decl = style["enc"] if style else "UTF-8" xml_bytes = xml_bytes.replace( b"", b'') # Канонизировать переносы к LF (убирает от \r в tail'ах) xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"") .replace(b"\r\n", b"\n").replace(b"\r", b"\n")) # Финальный перенос — как в оригинале (новый файл → нет, канон #57) want_final_nl = style["final_nl"] if style else False xml_bytes = xml_bytes.rstrip(b"\n") if want_final_nl: xml_bytes += b"\n" # EOL — как в оригинале (новый файл → CRLF, канон #57) if (style["crlf"] if style else True): xml_bytes = xml_bytes.replace(b"\n", b"\r\n") return xml_bytes def save_xml_with_bom(tree, path): """Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline.""" style = _detect_xml_style(path) xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") xml_bytes = _finalize_xml_bytes(xml_bytes, style) with open(path, "wb") as f: if style is None or style["bom"]: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) def write_utf8_bom(path, content): # newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows # и LF на macOS, то есть вывод навыка зависел бы от ОС. with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) def is_valid_lang(code): """Код языка идёт и в текст XML, и в имя файла страницы. Пустое значение дало бы файл «.html» и пустой , разделитель пути — запись мимо каталога страниц, а зарезервированное имя устройства (nul, con, prn, aux, com1…, lpt1…) на Windows уводит запись в само устройство: при -Lang nul этот порт молча писал nul и пустой каталог с кодом 0. Все отказы платформы были бы тихими. fullmatch, а не match: последний с `$` допускает перевод строки в конце. Копия этой функции есть в help-add (навыки автономны, формат «дескриптор + страница» у них общий). Держать копии одинаковыми — сознательно; за дрейфом следит check-inline-drift. """ if not re.fullmatch(r"[A-Za-z0-9_-]+", code): return False if re.fullmatch(r"(?i)(con|prn|aux|nul|com[1-9]|lpt[1-9])", code): return False return True def write_xml_file(path, content): """XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце. Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл. HTML-страница макета сюда НЕ идёт — платформа хранит её с LF. """ text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n') write_utf8_bom(path, text) def detect_format_version(d): while d: # Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт # корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17. ext_path = d + ".xml" if os.path.isfile(ext_path): with open(ext_path, "r", encoding="utf-8-sig") as f: ext_head = f.read(2000) if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head): m = re.search(r']+version="(\d+\.\d+)"', ext_head) if m: return m.group(1) 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" def format_rank(ver): """"2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17").""" m = re.match(r'^(\d+)\.(\d+)$', ver or '') return int(m.group(1)) * 100 + int(m.group(2)) if m else 0 def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Add template to 1C object", allow_abbrev=False) parser.add_argument("-ObjectName", "-ProcessorName", required=True) parser.add_argument("-TemplateName", required=True) parser.add_argument("-TemplateType", required=True, choices=["HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema"]) parser.add_argument("-Synonym", default=None) parser.add_argument("-Lang", default="ru") parser.add_argument("-SrcDir", default="src") parser.add_argument("-SetMainSKD", action="store_true") args = ci_parse_args(parser) object_name = args.ObjectName template_name = args.TemplateName template_type = args.TemplateType synonym = args.Synonym if args.Synonym is not None else template_name lang = args.Lang src_dir = args.SrcDir set_main_skd = args.SetMainSKD tmpl = TYPE_MAP[template_type] # --- Checks --- object_type_folders = [ "Reports", "DataProcessors", "Documents", "Catalogs", "InformationRegisters", "AccumulationRegisters", "ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes", "BusinessProcesses", "Tasks", "ExchangePlans", ] root_xml_path = os.path.join(src_dir, f"{object_name}.xml") if not os.path.exists(root_xml_path): candidates = [] for folder in object_type_folders: probe = os.path.join(src_dir, folder, f"{object_name}.xml") if os.path.exists(probe): candidates.append(os.path.join(src_dir, folder)) if len(candidates) == 1: src_dir = candidates[0] root_xml_path = os.path.join(src_dir, f"{object_name}.xml") print(f"[INFO] SrcDir расширен до: {src_dir}") elif len(candidates) > 1: print(f"Объект '{object_name}' найден в нескольких подпапках: {', '.join(candidates)}", file=sys.stderr) print(f"Укажи SrcDir явно", file=sys.stderr) sys.exit(1) else: print(f"Корневой файл объекта не найден: {root_xml_path}", file=sys.stderr) print(f"Ожидается: /.xml", file=sys.stderr) print(f"Подсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации", file=sys.stderr) sys.exit(1) processor_dir = os.path.join(src_dir, object_name) templates_dir = os.path.join(processor_dir, "Templates") template_meta_path = os.path.join(templates_dir, f"{template_name}.xml") if not is_valid_lang(lang): print(f"Недопустимый код языка: '{lang}'", file=sys.stderr) print("Ожидается код вида ru, en (буквы, цифры, дефис, подчёркивание; имена устройств Windows недопустимы)", file=sys.stderr) sys.exit(1) # Существующий HTML-макет — не всегда повод отказать: в один макет платформа кладёт # несколько языков ( на каждый, страницы рядом), и повторный вызов с другим -Lang # добавляет страницу. Для остальных типов поведение прежнее — отказ. add_lang_mode = False if os.path.exists(template_meta_path): with open(template_meta_path, "r", encoding="utf-8-sig") as f: meta_text = f.read() m_type = re.search(r"([^<]+)", meta_text) existing_type = m_type.group(1) if m_type else None if template_type == "HTML" and existing_type == "HTMLDocument": add_lang_mode = True elif template_type == "HTML": print(f"Макет уже существует: {template_meta_path}", file=sys.stderr) print(f"Его тип — {existing_type}, страницу на языке можно добавить только к HTML-макету", file=sys.stderr) sys.exit(1) else: print(f"Макет уже существует: {template_meta_path}", file=sys.stderr) sys.exit(1) assert_edit_allowed(root_xml_path, "editable") # Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной # внешней обработки/отчёта подниматься к Configuration.xml просто некуда. format_version = None with open(root_xml_path, "r", encoding="utf-8-sig") as f: obj_head = f.read(2000) m_ver = re.search(r']+version="(\d+\.\d+)"', obj_head) if m_ver: format_version = m_ver.group(1) if not format_version: format_version = detect_format_version(os.path.abspath(src_dir)) # Объявления пространств имён — одной переменной: место эмиссии её только подставляет. # Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте. 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"' ) # 2.21 (8.5) добавила в шапку пространство палитры — ради у значений перечисления. # Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту, # дописать в конец нельзя. if format_rank(format_version) >= 221: xmlns_decl = xmlns_decl.replace( ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=') # --- Create directories --- template_ext_dir = os.path.join(templates_dir, template_name, "Ext") os.makedirs(template_ext_dir, exist_ok=True) # Скелет HTML-страницы макета — в том же виде, в каком его пишет редактор платформы # (одной строкой, парный ): первое сохранение в Конфигураторе даст минимальный # дифф. Нужен в двух местах (новый макет и добавление языка) — поэтому одной переменной. html_skeleton = ( '' '' '' '\n' '' ) # --- 1a. Добавление страницы на другом языке в существующий HTML-макет --- # Метаданные макета и ChildObjects здесь НЕ трогаем: и то и другое уже на месте, # перезапись сменила бы UUID. Язык, не объявленный в Languages/ конфигурации, платформа # принимает и возвращает при выгрузке (проверено на 8.3.27) — состав языков не проверяем. if add_lang_mode: desc_path = os.path.join(template_ext_dir, "Template.xml") page_dir = os.path.join(template_ext_dir, "Template") legacy_page_path = os.path.join(template_ext_dir, "Template.html") langs = [] desc_version = format_version # Сначала РАЗБОР состояния, и только в самом конце запись: иначе отказ на полпути # оставляет макет разобранным (страница есть, дескриптора нет) — а такую раскладку # платформа снова молча игнорирует. legacy_pending = False page_path = os.path.join(page_dir, f"{lang}.html") if os.path.exists(desc_path): with open(desc_path, "r", encoding="utf-8-sig") as f: desc_text = f.read() # Версию берём из самого дескриптора: правка чужой выгрузки не должна менять формат. m_dv = re.search(r']+version="(\d+\.\d+)"', desc_text) if m_dv: desc_version = m_dv.group(1) langs = re.findall(r"([^<]+)", desc_text) # Полумигрированное дерево: дескриптор уже есть, а старый Ext/Template.html остался рядом. # Платформа его игнорирует, поэтому текст в нём пропадёт незаметно — говорим вслух. if os.path.exists(legacy_page_path): print("[WARN] Рядом лежит старый Ext/Template.html — платформа его игнорирует.") print(" Перенесите нужное в Template/<язык>.html и удалите его.") elif os.path.exists(legacy_page_path): # Раскладка до v1.24 — одиночный Ext/Template.html, который платформа молча игнорирует. # Языка у него нет, но создать его могла только версия навыка без параметра -Lang, # то есть это страница на языке по умолчанию. if os.path.exists(os.path.join(page_dir, "ru.html")): print("Макет разобран: есть и старый Ext/Template.html, и Template/ru.html — что из них актуально, решать не навыку.", file=sys.stderr) print("Оставьте один файл и повторите.", file=sys.stderr) sys.exit(1) legacy_pending = True langs = ["ru"] # Файл страницы НИКОГДА не перезаписываем: в нём может лежать текст макета. # Ошибка — только когда добавлять нечего: язык уже в дескрипторе И страница на диске. # Сравнение без учёта регистра — зеркало -contains в PS, который регистр не различает. lang_known = lang.lower() in [x.lower() for x in langs] # Существование страницы — тоже без учёта регистра: os.path.exists на Linux различает # регистр, а PS -contains и Test-Path на Windows — нет, и порты разошлись бы на -Lang RU. page_exists = False if os.path.isdir(page_dir): want = f"{lang}.html".lower() page_exists = any(f.lower() == want for f in os.listdir(page_dir)) # При миграции проверять нечего: langs там задан нами же, а страница появится переносом. if lang_known and page_exists and not legacy_pending: print(f"Страница макета на языке '{lang}' уже существует: {page_path}", file=sys.stderr) sys.exit(1) # Порядок страниц — по коду языка (в выгрузке ERP так во всех 54 двуязычных макетах). if not lang_known: langs = langs + [lang] langs = sorted(langs) pages_xml = '\n'.join(f"\t{x}" for x in langs) desc_xml = ( '\n' '\n' f'{pages_xml}\n' '' ) # --- запись --- os.makedirs(page_dir, exist_ok=True) if legacy_pending: os.replace(legacy_page_path, os.path.join(page_dir, "ru.html")) print("[WARN] Макет был в старой раскладке (Ext/Template.html) — платформа её игнорирует.") print(" Страница считана как ru и перенесена в Template/ru.html.") write_xml_file(desc_path, desc_xml) if os.path.exists(page_path): # При миграции про сохранённое содержимое уже сказано выше — не повторяемся. if not legacy_pending: print(f"[WARN] Страница {lang}.html уже лежала на диске — содержимое сохранено, дописан только .") else: write_utf8_bom(page_path, html_skeleton) print(f"[OK] Добавлена страница макета: {template_name} ({lang})") print(f" Содержимое: {page_path}") print(f" Дескриптор: {desc_path}") sys.exit(0) # --- 1. Template metadata (Templates/.xml) --- template_uuid = str(uuid.uuid4()) template_meta_xml = ( '\n' f'\n' f'\t\n' '' ) write_xml_file(template_meta_path, template_meta_xml) # --- 2. Template content (Templates//Ext/Template.) --- template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}") # Куда класть текст макета. Совпадает с template_file_path у всех типов, кроме HTML: # там содержимое живёт в отдельной странице, а Template.xml — только дескриптор. template_body_path = template_file_path if template_type == "HTML": # HTML-макет платформа хранит парой, как справку: дескриптор Ext/Template.xml # со списком страниц и сама страница Ext/Template/<язык>.html (картинки — # рядом в _files/). Одиночный Ext/Template.html платформа молча игнорирует: # загрузка проходит без ошибок, а макет в базе пустой. page_xml = ( '\n' '\n' f'\t{lang}\n' '' ) write_xml_file(template_file_path, page_xml) page_dir = os.path.join(template_ext_dir, "Template") os.makedirs(page_dir, exist_ok=True) template_body_path = os.path.join(page_dir, f"{lang}.html") # Шапка — в том же виде, в каком её пишет редактор платформы (одной строкой, # парный ): первое сохранение в Конфигураторе даст минимальный дифф. # Страница — с LF: платформа хранит HTML именно так. write_utf8_bom(template_body_path, html_skeleton) elif template_type == "Text": write_utf8_bom(template_file_path, "") elif template_type == "SpreadsheetDocument": # Пустой макет — самозакрывающимся корнем: пустых пар платформа не пишет # ни в одной форме (0 на 65 040 XML выгрузки acc_8.3.27, включая разнесённые # по строкам). Для XML `` и `` тождественны по спецификации. content = ( '\n' '' ) write_xml_file(template_file_path, content) elif template_type == "BinaryData": with open(template_file_path, "wb") as f: pass # empty file elif template_type == "DataCompositionSchema": content = ( '\n' '\n' '\t\n' '\t\tИсточникДанных1\n' '\t\tLocal\n' '\t\n' '' ) write_xml_file(template_file_path, content) # --- 3. Modify root XML --- root_xml_full = os.path.abspath(root_xml_path) parser_xml = etree.XMLParser(remove_blank_text=False) tree = etree.parse(root_xml_full, parser_xml) root = tree.getroot() ns = "http://v8.1c.ru/8.3/MDClasses" child_objects = root.find(".//md:ChildObjects", NSMAP) if child_objects is None: print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr) sys.exit(1) # Add