mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-10 05:23:21 +03:00
575 lines
26 KiB
Python
575 lines
26 KiB
Python
#!/usr/bin/env python3
|
||
# template-add v1.21 — Add template to 1C object
|
||
# 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
|
||
|
||
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": ".html"},
|
||
"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"<?xml version='1.0' encoding='UTF-8'?>",
|
||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + 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_text_with_bom(path, text):
|
||
"""Write text to file with UTF-8 BOM.
|
||
|
||
newline="" обязателен: в текстовом режиме Python на Windows превратил бы \\n в
|
||
\\r\\n, а на macOS оставил \\n — вывод навыка зависел бы от ОС. Через эту функцию
|
||
идёт HTML-макет, а его платформа хранит именно с LF (корпус: 399 LF из 400).
|
||
"""
|
||
with open(path, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write(text)
|
||
|
||
|
||
def write_xml_file(path, content):
|
||
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||
|
||
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||
|
||
HTML-макет сюда НЕ идёт — платформа хранит его с LF.
|
||
"""
|
||
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||
write_text_with_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'<MetaDataObject[^>]+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'<MetaDataObject[^>]+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("-SrcDir", default="src")
|
||
parser.add_argument("-SetMainSKD", action="store_true")
|
||
args = parser.parse_args()
|
||
|
||
object_name = args.ObjectName
|
||
template_name = args.TemplateName
|
||
template_type = args.TemplateType
|
||
synonym = args.Synonym if args.Synonym is not None else template_name
|
||
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"Ожидается: <SrcDir>/<ObjectName>.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 os.path.exists(template_meta_path):
|
||
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'<MetaDataObject[^>]+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) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||
# Вставляем НА МЕСТО (после 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)
|
||
|
||
# --- 1. Template metadata (Templates/<TemplateName>.xml) ---
|
||
|
||
template_uuid = str(uuid.uuid4())
|
||
|
||
template_meta_xml = (
|
||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||
f'<MetaDataObject {xmlns_decl} version="{format_version}">\n'
|
||
f'\t<Template uuid="{template_uuid}">\n'
|
||
'\t\t<Properties>\n'
|
||
f'\t\t\t<Name>{template_name}</Name>\n'
|
||
'\t\t\t<Synonym>\n'
|
||
'\t\t\t\t<v8:item>\n'
|
||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
||
'\t\t\t\t</v8:item>\n'
|
||
'\t\t\t</Synonym>\n'
|
||
'\t\t\t<Comment/>\n'
|
||
f'\t\t\t<TemplateType>{tmpl["TemplateType"]}</TemplateType>\n'
|
||
'\t\t</Properties>\n'
|
||
'\t</Template>\n'
|
||
'</MetaDataObject>'
|
||
)
|
||
|
||
write_xml_file(template_meta_path, template_meta_xml)
|
||
|
||
# --- 2. Template content (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
||
|
||
template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}")
|
||
|
||
if template_type == "HTML":
|
||
content = (
|
||
'<!DOCTYPE html>\n'
|
||
'<html>\n'
|
||
'<head>\n'
|
||
'\t<meta charset="UTF-8">\n'
|
||
'\t<title></title>\n'
|
||
'</head>\n'
|
||
'<body>\n'
|
||
'</body>\n'
|
||
'</html>'
|
||
)
|
||
write_text_with_bom(template_file_path, content)
|
||
|
||
elif template_type == "Text":
|
||
write_text_with_bom(template_file_path, "")
|
||
|
||
elif template_type == "SpreadsheetDocument":
|
||
# Пустой макет — самозакрывающимся корнем: пустых пар платформа не пишет
|
||
# ни в одной форме (0 на 65 040 XML выгрузки acc_8.3.27, включая разнесённые
|
||
# по строкам). Для XML `<A/>` и `<A></A>` тождественны по спецификации.
|
||
content = (
|
||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||
'<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document"'
|
||
' xmlns:ss="http://v8.1c.ru/spreadsheet/document"'
|
||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"/>'
|
||
)
|
||
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 = (
|
||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"\n'
|
||
'\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"\n'
|
||
'\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"\n'
|
||
'\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"\n'
|
||
'\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"\n'
|
||
'\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"\n'
|
||
'\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"\n'
|
||
'\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n'
|
||
'\t<dataSource>\n'
|
||
'\t\t<name>ИсточникДанных1</name>\n'
|
||
'\t\t<dataSourceType>Local</dataSourceType>\n'
|
||
'\t</dataSource>\n'
|
||
'</DataCompositionSchema>'
|
||
)
|
||
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 <Template> 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 </ChildObjects>
|
||
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>{template_name}</Template> in ChildObjects (skipped duplicate)")
|
||
print(f" Метаданные: {template_meta_path}")
|
||
print(f" Содержимое: {template_file_path}")
|
||
if main_dcs_updated:
|
||
print(f" MainDataCompositionSchema: {main_dcs.text}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|