#!/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'
'\t\t\n'
f'\t\t\t{template_name}\n'
'\t\t\t\n'
'\t\t\t\t\n'
'\t\t\t\t\tru\n'
f'\t\t\t\t\t{synonym}\n'
'\t\t\t\t\n'
'\t\t\t\n'
'\t\t\t\n'
f'\t\t\t{tmpl["TemplateType"]}\n'
'\t\t\n'
'\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 to end of ChildObjects — idempotent (do not duplicate already-registered template)
already_registered = child_objects.find(f"md:Template[.='{template_name}']", NSMAP) is not None
if not already_registered:
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
template_elem.text = template_name
# Remove auto-appended element to reinsert with proper whitespace
child_objects.remove(template_elem)
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
# last_child.tail is the trailing whitespace before
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = old_tail if old_tail else "\n\t\t"
else:
# Has text content but no element children
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
main_dcs_updated = False
if template_type == "DataCompositionSchema":
report_like_types = ["ExternalReport", "Report"]
object_type_node = None
object_type_name = None
for rt in report_like_types:
node = root.find(f".//md:{rt}", NSMAP)
if node is not None:
object_type_node = node
object_type_name = rt
break
if object_type_node is not None:
main_dcs = root.find(f".//md:{object_type_name}/md:Properties/md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None:
is_empty = main_dcs.text is None or main_dcs.text.strip() == ""
if is_empty or set_main_skd:
obj_name_node = root.find(f".//md:{object_type_name}/md:Properties/md:Name", NSMAP)
obj_name = obj_name_node.text if obj_name_node is not None else ""
main_dcs.text = f"{object_type_name}.{obj_name}.Template.{template_name}"
main_dcs_updated = True
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создан макет: {template_name} ({template_type})")
if already_registered:
print(f" Already registered: {template_name} in ChildObjects (skipped duplicate)")
print(f" Метаданные: {template_meta_path}")
print(f" Содержимое: {template_body_path}")
if template_type == "HTML":
print(f" Дескриптор: {template_file_path}")
if main_dcs_updated:
print(f" MainDataCompositionSchema: {main_dcs.text}")
if __name__ == "__main__":
main()