feat(crossplatform): add Python 3 ports for all 58 PS1 skill scripts

Add cross-platform Python alternatives alongside existing PowerShell
scripts. PS1 remains the default runtime; Python is opt-in via switch
scripts. All parameters are identical between runtimes.

New files:
- 58 Python scripts in .claude/skills/*/scripts/*.py
- scripts/switch-to-python.py and switch-to-powershell.py
- docs/python-porting-guide.md
- __pycache__/ added to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-25 16:16:07 +03:00
co-authored by Claude Opus 4.6
parent 6b5992de34
commit 86a959a354
63 changed files with 31223 additions and 2 deletions
+514
View File
@@ -0,0 +1,514 @@
#!/usr/bin/env python3
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from html import escape as html_escape
from lxml import etree
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XS_NS = "http://www.w3.org/2001/XMLSchema"
# Canonical type order for ChildObjects (44 types)
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
]
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
REF_PROPS = ["DefaultLanguage"]
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
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
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
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
children = list(container)
if len(children) == 0:
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def insert_before_ref(container, new_el, ref_el, child_indent):
"""Insert new_el before ref_el inside container."""
idx = list(container).index(ref_el)
prev = ref_el.getprevious()
if prev is not None:
new_el.tail = prev.tail
prev.tail = "\r\n" + child_indent
else:
new_el.tail = container.text
container.text = "\r\n" + child_indent
container.insert(idx, new_el)
def remove_with_indent(el):
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
if el.tail:
prev.tail = el.tail
else:
if el.tail:
parent.text = el.tail
parent.remove(el)
def expand_self_closing(container, parent_indent):
if len(container) == 0 and not (container.text and container.text.strip()):
container.text = "\r\n" + parent_indent
def import_fragment(xml_string):
wrapper = (
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
return list(frag)
def parse_batch_value(val):
items = []
for part in val.split(";;"):
trimmed = part.strip()
if trimmed:
items.append(trimmed)
return items
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.isfile(candidate):
config_path = candidate
else:
print("No Configuration.xml in directory", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(config_path):
print(f"File not found: {config_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(config_path)
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
cfg_el = None
for child in xml_root:
if isinstance(child.tag, str) and localname(child) == "Configuration":
cfg_el = child
break
if cfg_el is None:
print("No <Configuration> element found", file=sys.stderr)
sys.exit(1)
props_el = None
child_objs_el = None
for child in cfg_el:
if not isinstance(child.tag, str):
continue
if localname(child) == "Properties":
props_el = child
if localname(child) == "ChildObjects":
child_objs_el = child
obj_name = ""
if props_el is not None:
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Name":
obj_name = (child.text or "").strip()
break
info(f"Configuration: {obj_name}")
# --- Operations ---
def do_modify_property(batch_val):
nonlocal modify_count
items = parse_batch_value(batch_val)
for item in items:
eq_idx = item.find("=")
if eq_idx < 1:
print(f"Invalid property format '{item}', expected 'Key=Value'", file=sys.stderr)
sys.exit(1)
prop_name = item[:eq_idx].strip()
prop_value = item[eq_idx + 1:].strip()
prop_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == prop_name:
prop_el = child
break
if prop_el is None:
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
sys.exit(1)
if prop_name in ML_PROPS:
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
indent = get_child_indent(props_el)
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
lang_el.text = "ru"
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
content_el.text = prop_value
prop_el.text = "\r\n" + indent + "\t"
item_el.text = "\r\n" + indent + "\t\t"
lang_el.tail = "\r\n" + indent + "\t\t"
content_el.tail = "\r\n" + indent + "\t"
item_el.tail = "\r\n" + indent
elif prop_name in SCALAR_PROPS or prop_name in REF_PROPS:
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
prop_el.text = prop_value
else:
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = prop_value
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
def do_add_child_object(batch_val):
nonlocal add_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
items = parse_batch_value(batch_val)
cfg_indent = get_child_indent(cfg_el)
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
expand_self_closing(child_objs_el, cfg_indent)
child_indent = get_child_indent(child_objs_el)
for item in items:
dot_idx = item.find(".")
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
obj_name_val = item[dot_idx + 1:]
if type_name not in TYPE_ORDER:
print(f"Unknown type '{type_name}'", file=sys.stderr)
sys.exit(1)
type_idx = TYPE_ORDER.index(type_name)
# Dedup
exists = False
for child in child_objs_el:
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
exists = True
break
if exists:
warn(f"Already exists: {type_name}.{obj_name_val}")
continue
# Find insertion point
insert_before = None
for child in child_objs_el:
if not isinstance(child.tag, str):
continue
child_type_name = localname(child)
if child_type_name not in TYPE_ORDER:
continue
child_type_idx = TYPE_ORDER.index(child_type_name)
if child_type_name == type_name:
if (child.text or "") > obj_name_val and insert_before is None:
insert_before = child
elif child_type_idx > type_idx and insert_before is None:
insert_before = child
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
new_el.text = obj_name_val
if insert_before is not None:
insert_before_ref(child_objs_el, new_el, insert_before, child_indent)
else:
insert_before_closing(child_objs_el, new_el, child_indent)
add_count += 1
info(f"Added: {type_name}.{obj_name_val}")
def do_remove_child_object(batch_val):
nonlocal remove_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
items = parse_batch_value(batch_val)
for item in items:
dot_idx = item.find(".")
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
obj_name_val = item[dot_idx + 1:]
found = False
for child in list(child_objs_el):
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
remove_with_indent(child)
remove_count += 1
info(f"Removed: {type_name}.{obj_name_val}")
found = True
break
if not found:
warn(f"Not found: {type_name}.{obj_name_val}")
def do_add_default_role(batch_val):
nonlocal add_count
items = parse_batch_value(batch_val)
roles_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
roles_el = child
break
if roles_el is None:
print("No <DefaultRoles> element found in Properties", file=sys.stderr)
sys.exit(1)
props_indent = get_child_indent(props_el)
if len(roles_el) == 0 and not (roles_el.text and roles_el.text.strip()):
expand_self_closing(roles_el, props_indent)
role_indent = get_child_indent(roles_el)
for item in items:
role_name = item
if not role_name.startswith("Role."):
role_name = f"Role.{role_name}"
exists = False
for child in roles_el:
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
exists = True
break
if exists:
warn(f"DefaultRole already exists: {role_name}")
continue
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
nodes = import_fragment(frag_xml)
if nodes:
insert_before_closing(roles_el, nodes[0], role_indent)
add_count += 1
info(f"Added DefaultRole: {role_name}")
def do_remove_default_role(batch_val):
nonlocal remove_count
items = parse_batch_value(batch_val)
roles_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
roles_el = child
break
if roles_el is None:
print("No <DefaultRoles> element found", file=sys.stderr)
sys.exit(1)
for item in items:
role_name = item
if not role_name.startswith("Role."):
role_name = f"Role.{role_name}"
found = False
for child in list(roles_el):
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
remove_with_indent(child)
remove_count += 1
info(f"Removed DefaultRole: {role_name}")
found = True
break
if not found:
warn(f"DefaultRole not found: {role_name}")
def do_set_default_roles(batch_val):
nonlocal modify_count
items = parse_batch_value(batch_val)
roles_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
roles_el = child
break
if roles_el is None:
print("No <DefaultRoles> element found", file=sys.stderr)
sys.exit(1)
# Clear all existing children
for ch in list(roles_el):
roles_el.remove(ch)
roles_el.text = None
if not items:
modify_count += 1
info("Cleared DefaultRoles")
return
props_indent = get_child_indent(props_el)
role_indent = props_indent + "\t"
roles_el.text = "\r\n" + props_indent
for item in items:
role_name = item
if not role_name.startswith("Role."):
role_name = f"Role.{role_name}"
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
nodes = import_fragment(frag_xml)
if nodes:
insert_before_closing(roles_el, nodes[0], role_indent)
modify_count += 1
info(f"Set DefaultRoles: {len(items)} roles")
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "modify-property":
do_modify_property(op_value)
elif op_name == "add-childObject":
do_add_child_object(op_value)
elif op_name == "remove-childObject":
do_remove_child_object(op_value)
elif op_name == "add-defaultRole":
do_add_default_role(op_value)
elif op_name == "remove-defaultRole":
do_remove_default_role(op_value)
elif op_name == "set-defaultRoles":
do_set_default_roles(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "cf-validate", "scripts", "cf-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
# --- Summary ---
print()
print("=== cf-edit summary ===")
print(f" Configuration: {obj_name}")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
+399
View File
@@ -0,0 +1,399 @@
#!/usr/bin/env python3
# cf-info v1.0 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from collections import OrderedDict
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve path ---
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
# Directory -> find Configuration.xml
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.isfile(candidate):
config_path = candidate
else:
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(config_path):
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(config_path, etree.XMLParser(remove_blank_text=False))
xml_root = tree.getroot()
NS = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xs": "http://www.w3.org/2001/XMLSchema",
"app": "http://v8.1c.ru/8.2/managed-application/core",
}
md_root = xml_root # root is MetaDataObject itself
if etree.QName(md_root.tag).localname != "MetaDataObject":
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
sys.exit(1)
cfg_node = md_root.find("md:Configuration", NS)
if cfg_node is None:
print("[ERROR] No <Configuration> element found", file=sys.stderr)
sys.exit(1)
version = md_root.get("version", "")
props_node = cfg_node.find("md:Properties", NS)
child_obj_node = cfg_node.find("md:ChildObjects", NS)
# --- Helpers ---
def get_ml_text(node):
if node is None:
return ""
item = node.find("v8:item/v8:content", NS)
if item is not None and item.text:
return item.text
return ""
def get_prop_text(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
if n is not None and n.text:
return n.text
return ""
def get_prop_ml(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
return get_ml_text(n)
# --- Type name maps (canonical order, 44 types) ---
type_order = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
]
type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
"SettingsStorage": "Хранилища настроек", "FunctionalOption": "Функциональные опции",
"FunctionalOptionsParameter": "Параметры ФО", "DefinedType": "Определяемые типы",
"CommonCommand": "Общие команды", "CommandGroup": "Группы команд", "Constant": "Константы",
"CommonForm": "Общие формы", "Catalog": "Справочники", "Document": "Документы",
"DocumentNumerator": "Нумераторы", "Sequence": "Последовательности", "DocumentJournal": "Журналы документов",
"Enum": "Перечисления", "Report": "Отчёты", "DataProcessor": "Обработки",
"InformationRegister": "Регистры сведений", "AccumulationRegister": "Регистры накопления",
"ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов",
"AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР",
"CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы",
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
}
# --- Count objects in ChildObjects ---
object_counts = OrderedDict()
total_objects = 0
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue # skip comments/PIs
type_name = etree.QName(child.tag).localname
if type_name not in object_counts:
object_counts[type_name] = 0
object_counts[type_name] += 1
total_objects += 1
# --- Read key properties ---
cfg_name = get_prop_text("Name")
cfg_synonym = get_prop_ml("Synonym")
cfg_version = get_prop_text("Version")
cfg_vendor = get_prop_text("Vendor")
cfg_compat = get_prop_text("CompatibilityMode")
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
cfg_default_run = get_prop_text("DefaultRunMode")
cfg_script = get_prop_text("ScriptVariant")
cfg_default_lang = get_prop_text("DefaultLanguage")
cfg_data_lock = get_prop_text("DataLockControlMode")
dash = "\u2014"
cfg_modality = get_prop_text("ModalityUseMode")
cfg_intf_compat = get_prop_text("InterfaceCompatibilityMode")
cfg_auto_num = get_prop_text("ObjectAutonumerationMode")
cfg_sync_calls = get_prop_text("SynchronousPlatformExtensionAndAddInCallUseMode")
cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode ---
if args.Mode == "brief":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
compat_part = f" | {cfg_compat}" if cfg_compat else ""
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
# --- OVERVIEW mode ---
if args.Mode == "overview":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
out()
# Key properties
out(f"Формат: {version}")
if cfg_vendor:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
out(f"Совместимость: {cfg_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
out(f"Язык: {cfg_default_lang}")
out(f"Блокировки: {cfg_data_lock}")
out(f"Модальность: {cfg_modality}")
out(f"Интерфейс: {cfg_intf_compat}")
out()
# Object counts table
out(f"--- Состав ({total_objects} объектов) ---")
out()
max_type_len = 0
for type_name in type_order:
if type_name in object_counts:
ru_name = type_ru_names.get(type_name, type_name)
if len(ru_name) > max_type_len:
max_type_len = len(ru_name)
if max_type_len < 10:
max_type_len = 10
for type_name in type_order:
if type_name in object_counts:
count = object_counts[type_name]
ru_name = type_ru_names.get(type_name, type_name)
padded = ru_name.ljust(max_type_len)
out(f" {padded} {count}")
# --- FULL mode ---
if args.Mode == "full":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
out()
# --- Section: Identification ---
out("--- Идентификация ---")
out(f"UUID: {cfg_node.get('uuid', '')}")
out(f"Имя: {cfg_name}")
if cfg_synonym:
out(f"Синоним: {cfg_synonym}")
cfg_comment = get_prop_text("Comment")
if cfg_comment:
out(f"Комментарий: {cfg_comment}")
cfg_prefix = get_prop_text("NamePrefix")
if cfg_prefix:
out(f"Префикс: {cfg_prefix}")
if cfg_vendor:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
if cfg_update_addr:
out(f"Каталог обн.: {cfg_update_addr}")
out()
# --- Section: Modes ---
out("--- Режимы работы ---")
out(f"Формат: {version}")
out(f"Совместимость: {cfg_compat}")
out(f"Совм. расширений: {cfg_ext_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
out(f"Блокировки: {cfg_data_lock}")
out(f"Автонумерация: {cfg_auto_num}")
out(f"Модальность: {cfg_modality}")
out(f"Синхр. вызовы: {cfg_sync_calls}")
out(f"Интерфейс: {cfg_intf_compat}")
out(f"Табл. пространства: {cfg_db_spaces}")
out(f"Режим окна: {cfg_window_mode}")
out()
# --- Section: Language, roles, purposes ---
out("--- Назначение ---")
out(f"Язык по умолч.: {cfg_default_lang}")
# UsePurposes
purpose_node = props_node.find("md:UsePurposes", NS)
if purpose_node is not None:
purposes = []
for val in purpose_node.findall("v8:Value", NS):
if val.text:
purposes.append(val.text)
if purposes:
out(f"Назначения: {', '.join(purposes)}")
# DefaultRoles
roles_node = props_node.find("md:DefaultRoles", NS)
if roles_node is not None:
roles = []
for item in roles_node.findall("xr:Item", NS):
if item.text:
roles.append(item.text)
if roles:
out(f"Роли по умолч.: {len(roles)}")
for r in roles:
out(f" - {r}")
# Booleans
use_mf = get_prop_text("UseManagedFormInOrdinaryApplication")
use_of = get_prop_text("UseOrdinaryFormInManagedApplication")
out(f"Управл.формы в обычн.: {use_mf}")
out(f"Обычн.формы в управл.: {use_of}")
out()
# --- Section: Storages & default forms ---
out("--- Хранилища и формы по умолчанию ---")
storage_props = [
"CommonSettingsStorage", "ReportsUserSettingsStorage", "ReportsVariantsStorage",
"FormDataSettingsStorage", "DynamicListsUserSettingsStorage", "URLExternalDataStorage",
]
for sp in storage_props:
val = get_prop_text(sp)
if val:
out(f" {sp}: {val}")
form_props = [
"DefaultReportForm", "DefaultReportVariantForm", "DefaultReportSettingsForm",
"DefaultReportAppearanceTemplate", "DefaultDynamicListSettingsForm", "DefaultSearchForm",
"DefaultDataHistoryChangeHistoryForm", "DefaultDataHistoryVersionDataForm",
"DefaultDataHistoryVersionDifferencesForm", "DefaultCollaborationSystemUsersChoiceForm",
"DefaultConstantsForm", "DefaultInterface", "DefaultStyle",
]
for fp in form_props:
val = get_prop_text(fp)
if val:
out(f" {fp}: {val}")
out()
# --- Section: Info ---
cfg_brief = get_prop_ml("BriefInformation")
cfg_detail = get_prop_ml("DetailedInformation")
cfg_copyright = get_prop_ml("Copyright")
cfg_vendor_addr = get_prop_ml("VendorInformationAddress")
cfg_info_addr = get_prop_ml("ConfigurationInformationAddress")
if cfg_brief or cfg_detail or cfg_copyright or cfg_vendor_addr or cfg_info_addr:
out("--- Информация ---")
if cfg_brief:
out(f"Краткая: {cfg_brief}")
if cfg_detail:
out(f"Подробная: {cfg_detail}")
if cfg_copyright:
out(f"Copyright: {cfg_copyright}")
if cfg_vendor_addr:
out(f"Сайт поставщика: {cfg_vendor_addr}")
if cfg_info_addr:
out(f"Адрес информ.: {cfg_info_addr}")
out()
# --- Section: Mobile functionalities ---
mobile_func = props_node.find("md:UsedMobileApplicationFunctionalities", NS)
if mobile_func is not None:
enabled_funcs = []
disabled_funcs = []
for func in mobile_func.findall("app:functionality", NS):
f_name = func.find("app:functionality", NS)
f_use = func.find("app:use", NS)
if f_name is not None and f_use is not None:
if f_use.text == "true":
enabled_funcs.append(f_name.text or "")
else:
disabled_funcs.append(f_name.text or "")
total_func = len(enabled_funcs) + len(disabled_funcs)
out(f"--- Мобильные функциональности ({total_func}, включено: {len(enabled_funcs)}) ---")
for f in enabled_funcs:
out(f" [+] {f}")
for f in disabled_funcs:
out(f" [-] {f}")
out()
# --- Section: InternalInfo ---
internal_info = cfg_node.find("md:InternalInfo", NS)
if internal_info is not None:
contained = internal_info.findall("xr:ContainedObject", NS)
out(f"--- InternalInfo ({len(contained)} ContainedObject) ---")
for co in contained:
class_id_node = co.find("xr:ClassId", NS)
object_id_node = co.find("xr:ObjectId", NS)
class_id = class_id_node.text if class_id_node is not None else ""
object_id = object_id_node.text if object_id_node is not None else ""
out(f" {class_id} -> {object_id}")
out()
# --- Section: ChildObjects (full list) ---
out(f"--- Состав ({total_objects} объектов) ---")
out()
for type_name in type_order:
if type_name not in object_counts:
continue
count = object_counts[type_name]
ru_name = type_ru_names.get(type_name, type_name)
out(f" {ru_name} ({type_name}): {count}")
# Collect names for this type
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == type_name:
out(f" {child.text or ''}")
# --- Pagination and output ---
total = len(lines_buf)
if args.Offset > 0 or args.Limit < total:
start = min(args.Offset, total)
end = min(start + args.Limit, total)
page = lines_buf[start:end]
result = "\n".join(page)
if end < total:
result += f"\n\n... ({end} of {total} lines, use -Offset {end} to continue)"
else:
result = "\n".join(lines_buf)
print(result)
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write(result)
print(f"\nWritten to: {out_file}")
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
# cf-init v1.0 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
output_dir = args.OutputDir
version = args.Version
vendor = args.Vendor
compat = args.CompatibilityMode
# --- Resolve output dir ---
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- Check existing ---
cfg_file = os.path.join(output_dir, "Configuration.xml")
if os.path.exists(cfg_file):
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# --- Generate UUIDs ---
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities ---
mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
("Calendars","false"), ("PushNotifications","false"), ("LocalNotifications","false"),
("InAppPurchases","false"), ("PersonalComputerFileExchange","false"), ("Ads","false"),
("NumberDialing","false"), ("CallProcessing","false"), ("CallLog","false"),
("AutoSendSMS","false"), ("ReceiveSMS","false"), ("SMSLog","false"),
("Camera","false"), ("Microphone","false"), ("MusicLibrary","false"),
("PictureAndVideoLibraries","false"), ("AudioPlaybackAndVibration","false"),
("BackgroundAudioPlaybackAndVibration","false"), ("InstallPackages","false"),
("OSBackup","true"), ("ApplicationUsageStatistics","false"),
("BarcodeScanning","false"), ("BackgroundAudioRecording","false"),
("AllFilesAccess","false"), ("Videoconferences","false"), ("NFC","false"),
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
]
mobile_xml = ""
for func_name, func_use in mobile_funcs:
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<NamePrefix/>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles/>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t<UpdateCatalogAddress/>
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
\t\t\t<AdditionalFullTextSearchDictionaries/>
\t\t\t<CommonSettingsStorage/>
\t\t\t<ReportsUserSettingsStorage/>
\t\t\t<ReportsVariantsStorage/>
\t\t\t<FormDataSettingsStorage/>
\t\t\t<DynamicListsUserSettingsStorage/>
\t\t\t<URLExternalDataStorage/>
\t\t\t<Content/>
\t\t\t<DefaultReportForm/>
\t\t\t<DefaultReportVariantForm/>
\t\t\t<DefaultReportSettingsForm/>
\t\t\t<DefaultReportAppearanceTemplate/>
\t\t\t<DefaultDynamicListSettingsForm/>
\t\t\t<DefaultSearchForm/>
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
\t\t\t<DefaultDataHistoryVersionDataForm/>
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
\t\t\t<RequiredMobileApplicationPermissions/>
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
\t\t\t</UsedMobileApplicationFunctionalities>
\t\t\t<StandaloneConfigurationRestrictionRoles/>
\t\t\t<MobileApplicationURLs/>
\t\t\t<AllowedIncomingShareRequestTypes/>
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
\t\t\t<DefaultInterface/>
\t\t\t<DefaultStyle/>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/>
\t\t</Properties>
\t\t<ChildObjects>
\t\t\t<Language>Русский</Language>
\t\t</ChildObjects>
\t</Configuration>
</MetaDataObject>'''
# --- Languages/Русский.xml ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<Language uuid="{uuid_lang}">
\t\t<Properties>
\t\t\t<Name>Русский</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>Русский</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<LanguageCode>ru</LanguageCode>
\t\t</Properties>
\t</Language>
</MetaDataObject>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
if __name__ == '__main__':
main()
@@ -0,0 +1,530 @@
#!/usr/bin/env python3
# cf-validate v1.0 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re
from lxml import etree
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xs': 'http://www.w3.org/2001/XMLSchema',
'app': 'http://v8.1c.ru/8.2/managed-application/core',
}
GUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
IDENT_PATTERN = re.compile(
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
)
# 7 fixed ClassIds for Configuration
VALID_CLASS_IDS = [
'9cd510cd-abfc-11d4-9434-004095e12fc7', # managed application module
'9fcd25a0-4822-11d4-9414-008048da11f9', # ordinary application module
'e3687481-0a87-462c-a166-9f34594f9bba', # session module
'9de14907-ec23-4a07-96f0-85521cb6b53b', # external connection module
'51f2d5d8-ea4d-4064-8892-82951750031e', # command interface
'e68182ea-4237-4383-967f-90c1e3370bc7', # main section command interface
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
]
# 44 types in canonical order
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
'Constant', 'CommonForm', 'Catalog', 'Document',
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
'ChartOfCalculationTypes', 'CalculationRegister',
'BusinessProcess', 'Task', 'IntegrationService',
]
# Type -> directory mapping
CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
'SettingsStorage': 'SettingsStorages', 'FunctionalOption': 'FunctionalOptions',
'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'DefinedType': 'DefinedTypes',
'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'Constant': 'Constants',
'CommonForm': 'CommonForms', 'Catalog': 'Catalogs', 'Document': 'Documents',
'DocumentNumerator': 'DocumentNumerators', 'Sequence': 'Sequences',
'DocumentJournal': 'DocumentJournals', 'Enum': 'Enums', 'Report': 'Reports',
'DataProcessor': 'DataProcessors', 'InformationRegister': 'InformationRegisters',
'AccumulationRegister': 'AccumulationRegisters',
'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes',
'ChartOfAccounts': 'ChartsOfAccounts', 'AccountingRegister': 'AccountingRegisters',
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
'CalculationRegister': 'CalculationRegisters',
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
'IntegrationService': 'IntegrationServices',
}
# Valid enum values for Configuration properties
VALID_ENUM_VALUES = {
'ConfigurationExtensionCompatibilityMode': [
'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16',
'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5',
'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10',
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'],
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
'CompatibilityMode': [
'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16',
'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5',
'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10',
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
],
}
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def finalize(self, out_file):
self.out('')
self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ===')
result = self.text()
print(result, end='')
if out_file:
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration XML structure', allow_abbrev=False
)
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
config_path = args.ConfigPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isdir(config_path):
candidate = os.path.join(config_path, 'Configuration.xml')
if os.path.exists(candidate):
config_path = candidate
else:
print(f'[ERROR] No Configuration.xml found in directory: {config_path}')
sys.exit(1)
if not os.path.exists(config_path):
print(f'[ERROR] File not found: {config_path}')
sys.exit(1)
resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path)
if out_file and not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
r = Reporter(max_errors)
r.out('')
# --- 1. Parse XML ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.lines.insert(0, '=== Validation: Configuration (parse failed) ===')
r.out('')
r.error(f'1. XML parse failed: {e}')
r.finalize(out_file)
sys.exit(1)
root = xml_doc.getroot()
# --- Check 1: Root structure ---
check1_ok = True
root_local = etree.QName(root.tag).localname
root_ns = etree.QName(root.tag).namespace or ''
if root_local != 'MetaDataObject':
r.error(f"1. Root element is '{root_local}', expected 'MetaDataObject'")
r.finalize(out_file)
sys.exit(1)
if root_ns != EXPECTED_NS:
r.error(f"1. Root namespace is '{root_ns}', expected '{EXPECTED_NS}'")
check1_ok = False
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
# Must have Configuration child
cfg_node = None
for child in root:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Configuration' and etree.QName(child.tag).namespace == EXPECTED_NS:
cfg_node = child
break
if cfg_node is None:
r.error('1. No <Configuration> element found inside MetaDataObject')
r.finalize(out_file)
sys.exit(1)
# UUID
cfg_uuid = cfg_node.get('uuid', '')
if not cfg_uuid:
r.error('1. Missing uuid on <Configuration>')
check1_ok = False
elif not GUID_PATTERN.match(cfg_uuid):
r.error(f"1. Invalid uuid '{cfg_uuid}' on <Configuration>")
check1_ok = False
# Get name early for header
props_node = cfg_node.find('md:Properties', NS)
name_node = props_node.find('md:Name', NS) if props_node is not None else None
obj_name = (name_node.text or '') if name_node is not None and name_node.text else '(unknown)'
r.lines.insert(0, f'=== Validation: Configuration.{obj_name} ===')
if check1_ok:
r.ok(f'1. Root structure: MetaDataObject/Configuration, version {version}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 2: InternalInfo ---
internal_info = cfg_node.find('md:InternalInfo', NS)
check2_ok = True
if internal_info is None:
r.error('2. InternalInfo: missing')
else:
contained = internal_info.findall('xr:ContainedObject', NS)
if len(contained) != 7:
r.warn(f'2. InternalInfo: expected 7 ContainedObject, found {len(contained)}')
found_class_ids = {}
for co in contained:
class_id_el = co.find('xr:ClassId', NS)
object_id_el = co.find('xr:ObjectId', NS)
if class_id_el is None or not (class_id_el.text or ''):
r.error('2. ContainedObject missing ClassId')
check2_ok = False
continue
cid = class_id_el.text
if cid not in VALID_CLASS_IDS:
r.error(f'2. Unknown ClassId: {cid}')
check2_ok = False
if cid in found_class_ids:
r.error(f'2. Duplicate ClassId: {cid}')
check2_ok = False
found_class_ids[cid] = True
if object_id_el is None or not (object_id_el.text or ''):
r.error(f'2. ContainedObject missing ObjectId for ClassId {cid}')
check2_ok = False
elif not GUID_PATTERN.match(object_id_el.text):
r.error(f"2. Invalid ObjectId '{object_id_el.text}' for ClassId {cid}")
check2_ok = False
# Check missing ClassIds
missing_ids = [cid for cid in VALID_CLASS_IDS if cid not in found_class_ids]
if len(missing_ids) > 0:
r.warn(f'2. Missing ClassIds: {len(missing_ids)} of 7')
if check2_ok:
r.ok(f'2. InternalInfo: {len(contained)} ContainedObject, all ClassIds valid')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 3: Properties -- Name, Synonym, DefaultLanguage, DefaultRunMode ---
def_lang = ''
syn_present = False
if props_node is None:
r.error('3. Properties block missing')
else:
check3_ok = True
# Name
if name_node is None or not (name_node.text or ''):
r.error('3. Properties: Name is missing or empty')
check3_ok = False
else:
name_val = name_node.text
if not IDENT_PATTERN.match(name_val):
r.error(f"3. Properties: Name '{name_val}' is not a valid 1C identifier")
check3_ok = False
# Synonym
syn_node = props_node.find('md:Synonym', NS)
if syn_node is not None:
syn_item = syn_node.find('v8:item', NS)
if syn_item is not None:
syn_content = syn_item.find('v8:content', NS)
if syn_content is not None and syn_content.text:
syn_present = True
# DefaultLanguage
def_lang_node = props_node.find('md:DefaultLanguage', NS)
def_lang = (def_lang_node.text or '') if def_lang_node is not None else ''
if not def_lang:
r.error('3. Properties: DefaultLanguage is missing or empty')
check3_ok = False
# DefaultRunMode
def_run_node = props_node.find('md:DefaultRunMode', NS)
if def_run_node is None or not (def_run_node.text or ''):
r.warn('3. Properties: DefaultRunMode is missing or empty')
if check3_ok:
syn_info = 'Synonym present' if syn_present else 'no Synonym'
r.ok(f'3. Properties: Name="{obj_name}", {syn_info}, DefaultLanguage={def_lang}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 4: Property values -- enum properties ---
if props_node is not None:
enum_checked = 0
check4_ok = True
for prop_name, allowed in VALID_ENUM_VALUES.items():
prop_node = props_node.find(f'md:{prop_name}', NS)
if prop_node is not None and prop_node.text:
val = prop_node.text
if val not in allowed:
r.error(f"4. Property '{prop_name}' has invalid value '{val}'")
check4_ok = False
enum_checked += 1
if check4_ok:
r.ok(f'4. Property values: {enum_checked} enum properties checked')
else:
r.warn('4. No Properties block to check')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 5: ChildObjects -- valid types, no duplicates, order ---
child_obj_node = cfg_node.find('md:ChildObjects', NS)
if child_obj_node is None:
r.error('5. ChildObjects block missing')
else:
check5_ok = True
total_count = 0
type_counts = {} # type_name -> {obj_name: True}
duplicates = {}
type_first_index = {}
last_type_order = -1
order_ok = True
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
obj_name_val = child.text or ''
# Valid type?
if type_name in CHILD_OBJECT_TYPES:
type_idx = CHILD_OBJECT_TYPES.index(type_name)
else:
type_idx = -1
if type_idx < 0:
r.error(f"5. Unknown type '{type_name}' in ChildObjects")
check5_ok = False
else:
# Check order
if type_name not in type_first_index:
type_first_index[type_name] = type_idx
if type_idx < last_type_order:
r.warn(f"5. Type '{type_name}' is out of canonical order (after type at position {last_type_order})")
order_ok = False
last_type_order = type_idx
# Count and dedup
if type_name not in type_counts:
type_counts[type_name] = {}
if obj_name_val in type_counts[type_name]:
dup_key = f'{type_name}.{obj_name_val}'
if dup_key not in duplicates:
r.error(f'5. Duplicate: {dup_key}')
duplicates[dup_key] = True
check5_ok = False
else:
type_counts[type_name][obj_name_val] = True
total_count += 1
type_count = len(type_counts)
if check5_ok:
order_info = ', order correct' if order_ok else ''
r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
if def_lang and child_obj_node is not None:
lang_name = def_lang
if lang_name.startswith('Language.'):
lang_name = lang_name[9:]
found = False
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Language' and (child.text or '') == lang_name:
found = True
break
if found:
r.ok(f'6. DefaultLanguage "{def_lang}" found in ChildObjects')
else:
r.error(f'6. DefaultLanguage "{def_lang}" not found in ChildObjects')
else:
if not def_lang:
r.warn('6. Cannot check DefaultLanguage (empty)')
else:
r.warn('6. Cannot check DefaultLanguage (no ChildObjects)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 7: Language files exist ---
if child_obj_node is not None:
lang_names = []
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Language':
lang_names.append(child.text or '')
if len(lang_names) > 0:
exist_count = 0
for ln in lang_names:
lang_file = os.path.join(config_dir, 'Languages', ln + '.xml')
if os.path.exists(lang_file):
exist_count += 1
else:
r.warn(f'7. Language file missing: Languages/{ln}.xml')
if exist_count == len(lang_names):
r.ok(f'7. Language files: {exist_count}/{len(lang_names)} exist')
else:
r.warn('7. No Language entries in ChildObjects')
else:
r.warn('7. Cannot check language files (no ChildObjects)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 8: Object directories exist (spot-check) ---
if child_obj_node is not None:
dirs_to_check = {}
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
if type_name == 'Language':
continue
if type_name in CHILD_TYPE_DIR_MAP:
dir_name = CHILD_TYPE_DIR_MAP[type_name]
dirs_to_check[dir_name] = dirs_to_check.get(dir_name, 0) + 1
missing_dirs = []
for dir_name, count in dirs_to_check.items():
dir_path = os.path.join(config_dir, dir_name)
if not os.path.isdir(dir_path):
missing_dirs.append(f'{dir_name} ({count} objects)')
if len(missing_dirs) == 0:
r.ok(f'8. Object directories: {len(dirs_to_check)} directories, all exist')
else:
for md in missing_dirs:
r.warn(f'8. Missing directory: {md}')
else:
r.ok('8. Object directories: N/A')
# --- Final output ---
r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
@@ -0,0 +1,844 @@
#!/usr/bin/env python3
# cfe-borrow v1.0 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
import uuid
from lxml import etree
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
# --- Type mappings ---
CHILD_TYPE_DIR_MAP = {
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
"CommonModule": "CommonModules", "CommonPicture": "CommonPictures",
"CommonCommand": "CommonCommands", "CommonTemplate": "CommonTemplates",
"ExchangePlan": "ExchangePlans", "Report": "Reports", "DataProcessor": "DataProcessors",
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
"BusinessProcess": "BusinessProcesses", "Task": "Tasks",
"Subsystem": "Subsystems", "Role": "Roles", "Constant": "Constants",
"FunctionalOption": "FunctionalOptions", "DefinedType": "DefinedTypes",
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
"CommonForm": "CommonForms", "DocumentJournal": "DocumentJournals",
"SessionParameter": "SessionParameters", "StyleItem": "StyleItems",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs",
"SettingsStorage": "SettingsStorages", "FilterCriterion": "FilterCriteria",
"CommandGroup": "CommandGroups", "DocumentNumerator": "DocumentNumerators",
"Sequence": "Sequences", "IntegrationService": "IntegrationServices",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
"CommonAttribute": "CommonAttributes", "Style": "Styles",
}
SYNONYM_MAP = {
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
"\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435": "Enum",
"\u041e\u0431\u0449\u0438\u0439\u041c\u043e\u0434\u0443\u043b\u044c": "CommonModule",
"\u041e\u0431\u0449\u0430\u044f\u041a\u0430\u0440\u0442\u0438\u043d\u043a\u0430": "CommonPicture",
"\u041e\u0431\u0449\u0430\u044f\u041a\u043e\u043c\u0430\u043d\u0434\u0430": "CommonCommand",
"\u041e\u0431\u0449\u0438\u0439\u041c\u0430\u043a\u0435\u0442": "CommonTemplate",
"\u041f\u043b\u0430\u043d\u041e\u0431\u043c\u0435\u043d\u0430": "ExchangePlan",
"\u041e\u0442\u0447\u0435\u0442": "Report",
"\u041e\u0442\u0447\u0451\u0442": "Report",
"\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430": "DataProcessor",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439": "InformationRegister",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f": "AccumulationRegister",
"\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a": "ChartOfCharacteristicTypes",
"\u041f\u043b\u0430\u043d\u0421\u0447\u0435\u0442\u043e\u0432": "ChartOfAccounts",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438": "AccountingRegister",
"\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430": "ChartOfCalculationTypes",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0420\u0430\u0441\u0447\u0435\u0442\u0430": "CalculationRegister",
"\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441": "BusinessProcess",
"\u0417\u0430\u0434\u0430\u0447\u0430": "Task",
"\u041f\u043e\u0434\u0441\u0438\u0441\u0442\u0435\u043c\u0430": "Subsystem",
"\u0420\u043e\u043b\u044c": "Role",
"\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u0430": "Constant",
"\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0430\u044f\u041e\u043f\u0446\u0438\u044f": "FunctionalOption",
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439\u0422\u0438\u043f": "DefinedType",
"\u041e\u0431\u0449\u0430\u044f\u0424\u043e\u0440\u043c\u0430": "CommonForm",
"\u0416\u0443\u0440\u043d\u0430\u043b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432": "DocumentJournal",
"\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0421\u0435\u0430\u043d\u0441\u0430": "SessionParameter",
"\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u043c\u0430\u043d\u0434": "CommandGroup",
"\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430\u041d\u0430\u0421\u043e\u0431\u044b\u0442\u0438\u0435": "EventSubscription",
"\u0420\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442\u043d\u043e\u0435\u0417\u0430\u0434\u0430\u043d\u0438\u0435": "ScheduledJob",
"\u041e\u0431\u0449\u0438\u0439\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442": "CommonAttribute",
"\u041f\u0430\u043a\u0435\u0442XDTO": "XDTOPackage",
"HTTP\u0421\u0435\u0440\u0432\u0438\u0441": "HTTPService",
"\u0421\u0435\u0440\u0432\u0438\u0441\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438": "IntegrationService",
}
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
]
GENERATED_TYPES = {
"Catalog": [
{"prefix": "CatalogObject", "category": "Object"},
{"prefix": "CatalogRef", "category": "Ref"},
{"prefix": "CatalogSelection", "category": "Selection"},
{"prefix": "CatalogList", "category": "List"},
{"prefix": "CatalogManager", "category": "Manager"},
],
"Document": [
{"prefix": "DocumentObject", "category": "Object"},
{"prefix": "DocumentRef", "category": "Ref"},
{"prefix": "DocumentSelection", "category": "Selection"},
{"prefix": "DocumentList", "category": "List"},
{"prefix": "DocumentManager", "category": "Manager"},
],
"Enum": [
{"prefix": "EnumRef", "category": "Ref"},
{"prefix": "EnumManager", "category": "Manager"},
{"prefix": "EnumList", "category": "List"},
],
"Constant": [
{"prefix": "ConstantManager", "category": "Manager"},
{"prefix": "ConstantValueManager", "category": "ValueManager"},
{"prefix": "ConstantValueKey", "category": "ValueKey"},
],
"InformationRegister": [
{"prefix": "InformationRegisterRecord", "category": "Record"},
{"prefix": "InformationRegisterManager", "category": "Manager"},
{"prefix": "InformationRegisterSelection", "category": "Selection"},
{"prefix": "InformationRegisterList", "category": "List"},
{"prefix": "InformationRegisterRecordSet", "category": "RecordSet"},
{"prefix": "InformationRegisterRecordKey", "category": "RecordKey"},
{"prefix": "InformationRegisterRecordManager", "category": "RecordManager"},
],
"AccumulationRegister": [
{"prefix": "AccumulationRegisterRecord", "category": "Record"},
{"prefix": "AccumulationRegisterManager", "category": "Manager"},
{"prefix": "AccumulationRegisterSelection", "category": "Selection"},
{"prefix": "AccumulationRegisterList", "category": "List"},
{"prefix": "AccumulationRegisterRecordSet", "category": "RecordSet"},
{"prefix": "AccumulationRegisterRecordKey", "category": "RecordKey"},
],
"AccountingRegister": [
{"prefix": "AccountingRegisterRecord", "category": "Record"},
{"prefix": "AccountingRegisterManager", "category": "Manager"},
{"prefix": "AccountingRegisterSelection", "category": "Selection"},
{"prefix": "AccountingRegisterList", "category": "List"},
{"prefix": "AccountingRegisterRecordSet", "category": "RecordSet"},
{"prefix": "AccountingRegisterRecordKey", "category": "RecordKey"},
],
"CalculationRegister": [
{"prefix": "CalculationRegisterRecord", "category": "Record"},
{"prefix": "CalculationRegisterManager", "category": "Manager"},
{"prefix": "CalculationRegisterSelection", "category": "Selection"},
{"prefix": "CalculationRegisterList", "category": "List"},
{"prefix": "CalculationRegisterRecordSet", "category": "RecordSet"},
{"prefix": "CalculationRegisterRecordKey", "category": "RecordKey"},
],
"ChartOfAccounts": [
{"prefix": "ChartOfAccountsObject", "category": "Object"},
{"prefix": "ChartOfAccountsRef", "category": "Ref"},
{"prefix": "ChartOfAccountsSelection", "category": "Selection"},
{"prefix": "ChartOfAccountsList", "category": "List"},
{"prefix": "ChartOfAccountsManager", "category": "Manager"},
],
"ChartOfCharacteristicTypes": [
{"prefix": "ChartOfCharacteristicTypesObject", "category": "Object"},
{"prefix": "ChartOfCharacteristicTypesRef", "category": "Ref"},
{"prefix": "ChartOfCharacteristicTypesSelection", "category": "Selection"},
{"prefix": "ChartOfCharacteristicTypesList", "category": "List"},
{"prefix": "ChartOfCharacteristicTypesManager", "category": "Manager"},
],
"ChartOfCalculationTypes": [
{"prefix": "ChartOfCalculationTypesObject", "category": "Object"},
{"prefix": "ChartOfCalculationTypesRef", "category": "Ref"},
{"prefix": "ChartOfCalculationTypesSelection", "category": "Selection"},
{"prefix": "ChartOfCalculationTypesList", "category": "List"},
{"prefix": "ChartOfCalculationTypesManager", "category": "Manager"},
{"prefix": "DisplacingCalculationTypes", "category": "DisplacingCalculationTypes"},
{"prefix": "BaseCalculationTypes", "category": "BaseCalculationTypes"},
{"prefix": "LeadingCalculationTypes", "category": "LeadingCalculationTypes"},
],
"BusinessProcess": [
{"prefix": "BusinessProcessObject", "category": "Object"},
{"prefix": "BusinessProcessRef", "category": "Ref"},
{"prefix": "BusinessProcessSelection", "category": "Selection"},
{"prefix": "BusinessProcessList", "category": "List"},
{"prefix": "BusinessProcessManager", "category": "Manager"},
],
"Task": [
{"prefix": "TaskObject", "category": "Object"},
{"prefix": "TaskRef", "category": "Ref"},
{"prefix": "TaskSelection", "category": "Selection"},
{"prefix": "TaskList", "category": "List"},
{"prefix": "TaskManager", "category": "Manager"},
],
"ExchangePlan": [
{"prefix": "ExchangePlanObject", "category": "Object"},
{"prefix": "ExchangePlanRef", "category": "Ref"},
{"prefix": "ExchangePlanSelection", "category": "Selection"},
{"prefix": "ExchangePlanList", "category": "List"},
{"prefix": "ExchangePlanManager", "category": "Manager"},
],
"DocumentJournal": [
{"prefix": "DocumentJournalSelection", "category": "Selection"},
{"prefix": "DocumentJournalList", "category": "List"},
{"prefix": "DocumentJournalManager", "category": "Manager"},
],
"Report": [
{"prefix": "ReportObject", "category": "Object"},
{"prefix": "ReportManager", "category": "Manager"},
],
"DataProcessor": [
{"prefix": "DataProcessorObject", "category": "Object"},
{"prefix": "DataProcessorManager", "category": "Manager"},
],
}
TYPES_WITH_CHILD_OBJECTS = [
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"BusinessProcess", "Task", "Enum",
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
]
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
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"'
)
def get_child_indent(container):
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
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
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
children = list(container)
if len(children) == 0:
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def insert_before_ref(container, new_el, ref_el, child_indent):
idx = list(container).index(ref_el)
prev = ref_el.getprevious()
if prev is not None:
new_el.tail = prev.tail
prev.tail = "\r\n" + child_indent
else:
new_el.tail = container.text
container.text = "\r\n" + child_indent
container.insert(idx, new_el)
def expand_self_closing(container, parent_indent):
if len(container) == 0 and not (container.text and container.text.strip()):
container.text = "\r\n" + parent_indent
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def save_text_bom(path, text):
with open(path, "w", encoding="utf-8-sig") as fh:
fh.write(text)
def new_guid():
return str(uuid.uuid4())
def main():
parser = argparse.ArgumentParser(description="Borrow objects from configuration into extension", allow_abbrev=False)
parser.add_argument("-ExtensionPath", required=True)
parser.add_argument("-ConfigPath", required=True)
parser.add_argument("-Object", required=True)
args = parser.parse_args()
# --- 1. Resolve paths ---
ext_path = args.ExtensionPath
if not os.path.isabs(ext_path):
ext_path = os.path.join(os.getcwd(), ext_path)
if os.path.isdir(ext_path):
candidate = os.path.join(ext_path, "Configuration.xml")
if os.path.isfile(candidate):
ext_path = candidate
else:
print(f"No Configuration.xml in extension directory: {ext_path}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(ext_path):
print(f"Extension file not found: {ext_path}", file=sys.stderr)
sys.exit(1)
ext_resolved = os.path.abspath(ext_path)
ext_dir = os.path.dirname(ext_resolved)
cfg_path = args.ConfigPath
if not os.path.isabs(cfg_path):
cfg_path = os.path.join(os.getcwd(), cfg_path)
if os.path.isdir(cfg_path):
candidate = os.path.join(cfg_path, "Configuration.xml")
if os.path.isfile(candidate):
cfg_path = candidate
else:
print(f"No Configuration.xml in config directory: {cfg_path}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(cfg_path):
print(f"Config file not found: {cfg_path}", file=sys.stderr)
sys.exit(1)
cfg_resolved = os.path.abspath(cfg_path)
cfg_dir = os.path.dirname(cfg_resolved)
# --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(ext_resolved, xml_parser)
xml_root = tree.getroot()
cfg_el = None
for child in xml_root:
if isinstance(child.tag, str) and localname(child) == "Configuration":
cfg_el = child
break
if cfg_el is None:
print("No <Configuration> element found in extension", file=sys.stderr)
sys.exit(1)
props_el = None
child_objs_el = None
for child in cfg_el:
if not isinstance(child.tag, str):
continue
if localname(child) == "Properties":
props_el = child
if localname(child) == "ChildObjects":
child_objs_el = child
if props_el is None:
print("No <Properties> element found in extension", file=sys.stderr)
sys.exit(1)
if child_objs_el is None:
print("No <ChildObjects> element found in extension", file=sys.stderr)
sys.exit(1)
# --- 3. Extract NamePrefix ---
name_prefix = ""
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "NamePrefix":
name_prefix = (child.text or "").strip()
break
info(f"Extension NamePrefix: {name_prefix}")
# --- Helper functions ---
def read_source_object(type_name, obj_name):
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
if not dir_name:
print(f"Unknown type '{type_name}'", file=sys.stderr)
sys.exit(1)
src_file = os.path.join(cfg_dir, dir_name, f"{obj_name}.xml")
if not os.path.isfile(src_file):
print(f"Source object not found: {src_file}", file=sys.stderr)
sys.exit(1)
src_parser = etree.XMLParser(remove_blank_text=True)
src_tree = etree.parse(src_file, src_parser)
src_root = src_tree.getroot()
src_el = None
for c in src_root:
if isinstance(c.tag, str):
src_el = c
break
if src_el is None:
print(f"No metadata element found in {dir_name}/{obj_name}.xml", file=sys.stderr)
sys.exit(1)
src_uuid = src_el.get("uuid", "")
if not src_uuid:
print(f"No uuid attribute on source element in {dir_name}/{obj_name}.xml", file=sys.stderr)
sys.exit(1)
src_props = {}
props_node = src_el.find(f"{{{MD_NS}}}Properties")
if props_node is not None:
for prop_name in COMMON_MODULE_PROPS:
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
if prop_node is not None:
src_props[prop_name] = (prop_node.text or "").strip()
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
def read_source_form_uuid(type_name, obj_name, form_name):
dir_name = CHILD_TYPE_DIR_MAP[type_name]
src_file = os.path.join(cfg_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
if not os.path.isfile(src_file):
print(f"Source form not found: {src_file}", file=sys.stderr)
sys.exit(1)
src_parser = etree.XMLParser(remove_blank_text=True)
src_tree = etree.parse(src_file, src_parser)
src_el = None
for c in src_tree.getroot():
if isinstance(c.tag, str):
src_el = c
break
if src_el is None:
print(f"No metadata element found in source form: {src_file}", file=sys.stderr)
sys.exit(1)
src_uuid = src_el.get("uuid", "")
if not src_uuid:
print(f"No uuid attribute on source form element: {src_file}", file=sys.stderr)
sys.exit(1)
return src_uuid
def build_internal_info_xml(type_name, obj_name, indent):
types = GENERATED_TYPES.get(type_name)
if not types:
return f"{indent}<InternalInfo/>"
lines = [f"{indent}<InternalInfo>"]
if type_name == "ExchangePlan":
this_node_uuid = new_guid()
lines.append(f"{indent}\t<xr:ThisNode>{this_node_uuid}</xr:ThisNode>")
for gt in types:
full_name = f"{gt['prefix']}.{obj_name}"
type_id = new_guid()
value_id = new_guid()
lines.append(f'{indent}\t<xr:GeneratedType name="{full_name}" category="{gt["category"]}">')
lines.append(f"{indent}\t\t<xr:TypeId>{type_id}</xr:TypeId>")
lines.append(f"{indent}\t\t<xr:ValueId>{value_id}</xr:ValueId>")
lines.append(f"{indent}\t</xr:GeneratedType>")
lines.append(f"{indent}</InternalInfo>")
return "\n".join(lines)
def build_borrowed_object_xml(type_name, obj_name, source_uuid, source_props):
new_uuid_val = new_guid()
internal_info_xml = build_internal_info_xml(type_name, obj_name, "\t\t")
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">')
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
lines.append(internal_info_xml)
lines.append("\t\t<Properties>")
lines.append("\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>")
lines.append(f"\t\t\t<Name>{obj_name}</Name>")
lines.append("\t\t\t<Comment/>")
lines.append(f"\t\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>")
if type_name == "CommonModule":
for prop_name in COMMON_MODULE_PROPS:
prop_val = source_props.get(prop_name, "false")
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS:
lines.append("\t\t<ChildObjects/>")
lines.append(f"\t</{type_name}>")
lines.append("</MetaDataObject>")
return "\n".join(lines)
def add_to_child_objects(type_name, obj_name):
cfg_indent = get_child_indent(cfg_el)
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
expand_self_closing(child_objs_el, cfg_indent)
ci = get_child_indent(child_objs_el)
if type_name not in TYPE_ORDER:
print(f"Unknown type '{type_name}' for ChildObjects ordering", file=sys.stderr)
sys.exit(1)
type_idx = TYPE_ORDER.index(type_name)
# Dedup
for child in child_objs_el:
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name:
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
return
insert_before = None
for child in child_objs_el:
if not isinstance(child.tag, str):
continue
child_type_name = localname(child)
if child_type_name not in TYPE_ORDER:
continue
child_type_idx = TYPE_ORDER.index(child_type_name)
if child_type_name == type_name:
if (child.text or "") > obj_name and insert_before is None:
insert_before = child
elif child_type_idx > type_idx and insert_before is None:
insert_before = child
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
new_el.text = obj_name
if insert_before is not None:
insert_before_ref(child_objs_el, new_el, insert_before, ci)
else:
insert_before_closing(child_objs_el, new_el, ci)
info(f"Added to ChildObjects: {type_name}.{obj_name}")
def test_object_borrowed(type_name, obj_name):
dir_name = CHILD_TYPE_DIR_MAP[type_name]
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
return os.path.isfile(obj_file)
def register_form_in_object(type_name, obj_name, form_name):
dir_name = CHILD_TYPE_DIR_MAP[type_name]
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
if not os.path.isfile(obj_file):
warn(f"Parent object file not found: {obj_file} \u2014 form not registered in ChildObjects")
return
obj_parser = etree.XMLParser(remove_blank_text=False)
obj_tree = etree.parse(obj_file, obj_parser)
obj_root = obj_tree.getroot()
obj_el = None
for c in obj_root:
if isinstance(c.tag, str):
obj_el = c
break
if obj_el is None:
warn(f"No type element in {obj_file} \u2014 form not registered")
return
child_objs = obj_el.find(f"{{{MD_NS}}}ChildObjects")
if child_objs is None:
child_objs = etree.SubElement(obj_el, f"{{{MD_NS}}}ChildObjects")
# Set proper whitespace
prev = child_objs.getprevious()
if prev is not None:
child_objs.tail = "\r\n\t"
prev_tail = prev.tail or ""
if not prev_tail.endswith("\t\t"):
prev.tail = "\r\n\t\t"
# Dedup
for c in child_objs:
if isinstance(c.tag, str) and localname(c) == "Form" and (c.text or "") == form_name:
warn(f"Form '{form_name}' already in ChildObjects of {type_name}.{obj_name}")
return
if len(child_objs) == 0 and not (child_objs.text and child_objs.text.strip()):
child_objs.text = "\r\n\t\t"
form_el = etree.Element(f"{{{MD_NS}}}Form")
form_el.text = form_name
insert_before_closing(child_objs, form_el, "\t\t\t")
save_xml_bom(obj_tree, obj_file)
info(f" Registered form in: {obj_file}")
def borrow_form(type_name, obj_name, form_name):
dir_name = CHILD_TYPE_DIR_MAP[type_name]
# 1. Read source form UUID
form_uuid = read_source_form_uuid(type_name, obj_name, form_name)
info(f" Source form UUID: {form_uuid}")
# 2. Read source Form.xml
src_form_xml_path = os.path.join(cfg_dir, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml")
if not os.path.isfile(src_form_xml_path):
print(f"Source Form.xml not found: {src_form_xml_path}", file=sys.stderr)
sys.exit(1)
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
src_form_content = fh.read()
# 3. Generate form metadata XML
new_form_uuid = new_guid()
form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="2.17">',
f'\t<Form uuid="{new_form_uuid}">',
'\t\t<InternalInfo/>',
'\t\t<Properties>',
'\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>',
f'\t\t\t<Name>{form_name}</Name>',
'\t\t\t<Comment/>',
f'\t\t\t<ExtendedConfigurationObject>{form_uuid}</ExtendedConfigurationObject>',
'\t\t\t<FormType>Managed</FormType>',
'\t\t</Properties>',
'\t</Form>',
'</MetaDataObject>',
]
# 4. Create directories
form_meta_dir = os.path.join(ext_dir, dir_name, obj_name, "Forms")
os.makedirs(form_meta_dir, exist_ok=True)
form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml")
save_text_bom(form_meta_file, "\n".join(form_meta_lines))
info(f" Created: {form_meta_file}")
# 5. Generate Form.xml with BaseForm
src_form_parser = etree.XMLParser(remove_blank_text=False)
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", "2.17")
src_auto_cmd = None
src_child_items = None
for fc in src_form_el:
if not isinstance(fc.tag, str):
continue
ln = localname(fc)
if ln == "AutoCommandBar" and src_auto_cmd is None:
src_auto_cmd = fc
elif ln == "ChildItems" and src_child_items is None:
src_child_items = fc
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
auto_cmd_xml = ""
if src_auto_cmd is not None:
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode")
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
child_items_xml = ""
if src_child_items is not None:
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
else:
child_items_xml = "<ChildItems/>"
# Extract source form opening tag
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
form_tag = f'<Form version="{form_version}">'
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
if m_decl:
xml_decl = m_decl.group(1)
m_tag = re.search(r'(<Form[^>]*>)', src_form_content)
if m_tag:
form_tag = m_tag.group(1)
# Build output
parts = []
parts.append(xml_decl)
parts.append("\r\n")
parts.append(form_tag)
parts.append("\r\n")
if auto_cmd_xml:
parts.append(f"\t{auto_cmd_xml}\r\n")
parts.append(f"\t{child_items_xml}\r\n")
parts.append("\t<Attributes/>\r\n")
# BaseForm
parts.append(f'\t<BaseForm version="{form_version}">\r\n')
if auto_cmd_xml:
ac_lines = auto_cmd_xml.split("\n")
for li, line in enumerate(ac_lines):
if li == 0:
parts.append(f"\t\t{line}")
else:
parts.append(f"\t{line}")
parts.append("\r\n")
ci_lines = child_items_xml.split("\n")
for li, line in enumerate(ci_lines):
if li == 0:
parts.append(f"\t\t{line}")
else:
parts.append(f"\t{line}")
parts.append("\r\n")
parts.append("\t\t<Attributes/>\r\n")
parts.append("\t</BaseForm>\r\n")
parts.append("</Form>")
form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext")
os.makedirs(form_xml_dir, exist_ok=True)
form_xml_file = os.path.join(form_xml_dir, "Form.xml")
save_text_bom(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl
module_dir = os.path.join(form_xml_dir, "Form")
os.makedirs(module_dir, exist_ok=True)
module_bsl_file = os.path.join(module_dir, "Module.bsl")
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects
register_form_in_object(type_name, obj_name, form_name)
return [form_meta_file, form_xml_file, module_bsl_file]
# --- 9. Parse -Object into items ---
items = []
for part in args.Object.split(";;"):
trimmed = part.strip()
if trimmed:
items.append(trimmed)
if not items:
print("No objects specified in -Object", file=sys.stderr)
sys.exit(1)
# --- 10. Process each item ---
borrowed_files = []
borrowed_count = 0
for item in items:
dot_idx = item.find(".")
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name' or 'Type.Name.Form.FormName'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
remainder = item[dot_idx + 1:]
if type_name in SYNONYM_MAP:
type_name = SYNONYM_MAP[type_name]
if type_name not in CHILD_TYPE_DIR_MAP:
print(f"Unknown type '{type_name}'", file=sys.stderr)
sys.exit(1)
form_name = None
form_idx = remainder.find(".Form.")
if form_idx > 0:
obj_name = remainder[:form_idx]
form_name = remainder[form_idx + 6:]
else:
obj_name = remainder
dir_name = CHILD_TYPE_DIR_MAP[type_name]
if form_name:
# --- Form borrowing ---
info(f"Borrowing form {type_name}.{obj_name}.Form.{form_name}...")
if not test_object_borrowed(type_name, obj_name):
info(f" Parent object {type_name}.{obj_name} not yet borrowed \u2014 borrowing first...")
src = read_source_object(type_name, obj_name)
info(f" Source UUID: {src['Uuid']}")
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
target_dir = os.path.join(ext_dir, dir_name)
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml)
info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name)
borrowed_files.append(target_file)
form_files = borrow_form(type_name, obj_name, form_name)
borrowed_files.extend(form_files)
borrowed_count += 1
else:
# --- Object borrowing ---
info(f"Borrowing {type_name}.{obj_name}...")
src = read_source_object(type_name, obj_name)
info(f" Source UUID: {src['Uuid']}")
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
target_dir = os.path.join(ext_dir, dir_name)
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml)
info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name)
borrowed_files.append(target_file)
borrowed_count += 1
# --- Save modified Configuration.xml ---
save_xml_bom(tree, ext_resolved)
info(f"Saved: {ext_resolved}")
# --- Summary ---
print()
print("=== cfe-borrow summary ===")
print(f" Extension: {ext_dir}")
print(f" Config: {cfg_dir}")
print(f" Borrowed: {borrowed_count} object(s)")
for f in borrowed_files:
print(f" - {f}")
sys.exit(0)
if __name__ == "__main__":
main()
+538
View File
@@ -0,0 +1,538 @@
#!/usr/bin/env python3
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from lxml import etree
# --- Namespace maps ---
MD_NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
}
FORM_NSMAP = {
"f": "http://v8.1c.ru/8.3/xcf/logform",
}
# --- Type -> directory mapping ---
CHILD_TYPE_DIR_MAP = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"CommonModule": "CommonModules",
"CommonPicture": "CommonPictures",
"CommonCommand": "CommonCommands",
"CommonTemplate": "CommonTemplates",
"ExchangePlan": "ExchangePlans",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfAccounts": "ChartsOfAccounts",
"AccountingRegister": "AccountingRegisters",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"CalculationRegister": "CalculationRegisters",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"Subsystem": "Subsystems",
"Role": "Roles",
"Constant": "Constants",
"FunctionalOption": "FunctionalOptions",
"DefinedType": "DefinedTypes",
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
"CommonForm": "CommonForms",
"DocumentJournal": "DocumentJournals",
"SessionParameter": "SessionParameters",
"StyleItem": "StyleItems",
"EventSubscription": "EventSubscriptions",
"ScheduledJob": "ScheduledJobs",
"SettingsStorage": "SettingsStorages",
"FilterCriterion": "FilterCriteria",
"CommandGroup": "CommandGroups",
"DocumentNumerator": "DocumentNumerators",
"Sequence": "Sequences",
"IntegrationService": "IntegrationServices",
"CommonAttribute": "CommonAttributes",
}
# --- Helper: check if object is borrowed ---
def get_object_info(obj_type, obj_name, extension_path):
if obj_type not in CHILD_TYPE_DIR_MAP:
return None
dir_name = CHILD_TYPE_DIR_MAP[obj_type]
obj_file = os.path.join(extension_path, dir_name, f"{obj_name}.xml")
if not os.path.isfile(obj_file):
return {"Borrowed": False, "File": obj_file, "Exists": False}
parser_xml = etree.XMLParser(remove_blank_text=False)
doc = etree.parse(obj_file, parser_xml)
doc_root = doc.getroot()
# Find first element child
obj_el = None
for c in doc_root:
if isinstance(c.tag, str):
obj_el = c
break
if obj_el is None:
return {"Borrowed": False, "File": obj_file, "Exists": True}
props_el = obj_el.find("md:Properties", MD_NSMAP)
ob_node = None
if props_el is not None:
ob_node = props_el.find("md:ObjectBelonging", MD_NSMAP)
borrowed = ob_node is not None and ob_node.text == "Adopted"
return {
"Borrowed": borrowed,
"File": obj_file,
"Exists": True,
"Type": obj_type,
"Name": obj_name,
"DirName": dir_name,
"ObjElement": obj_el,
}
# --- Helper: find .bsl files for object ---
def get_bsl_files(obj_type, obj_name, extension_path):
if obj_type not in CHILD_TYPE_DIR_MAP:
return []
dir_name = CHILD_TYPE_DIR_MAP[obj_type]
obj_dir = os.path.join(extension_path, dir_name, obj_name)
if not os.path.isdir(obj_dir):
return []
bsl_files = []
ext_dir = os.path.join(obj_dir, "Ext")
if os.path.isdir(ext_dir):
for item in os.listdir(ext_dir):
if item.lower().endswith(".bsl"):
bsl_files.append(os.path.join(ext_dir, item))
# Forms
forms_dir = os.path.join(obj_dir, "Forms")
if os.path.isdir(forms_dir):
for dirpath, dirnames, filenames in os.walk(forms_dir):
for fn in filenames:
if fn == "Module.bsl":
bsl_files.append(os.path.join(dirpath, fn))
return bsl_files
# --- Helper: parse interceptors from .bsl ---
def get_interceptors(bsl_path):
if not os.path.isfile(bsl_path):
return []
with open(bsl_path, "r", encoding="utf-8-sig") as fh:
lines = fh.readlines()
interceptors = []
pattern = re.compile(r'^&(\u041f\u0435\u0440\u0435\u0434|\u041f\u043e\u0441\u043b\u0435|\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c|\u0412\u043c\u0435\u0441\u0442\u043e)\("([^"]+)"\)')
# The above is: ^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)
for i, line in enumerate(lines):
stripped = line.strip()
m = pattern.match(stripped)
if m:
interceptors.append({
"Type": m.group(1),
"Method": m.group(2),
"Line": i + 1,
"File": bsl_path,
})
return interceptors
# --- Helper: extract #Вставка blocks from .bsl ---
def get_insertion_blocks(bsl_path):
if not os.path.isfile(bsl_path):
return []
with open(bsl_path, "r", encoding="utf-8-sig") as fh:
lines = fh.readlines()
blocks = []
in_block = False
block_lines = []
start_line = 0
for i, line in enumerate(lines):
stripped = line.strip()
if stripped == "\u0023\u0412\u0441\u0442\u0430\u0432\u043a\u0430":
# #Вставка
in_block = True
block_lines = []
start_line = i + 1
elif stripped == "\u0023\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438" and in_block:
# #КонецВставки
in_block = False
blocks.append({
"StartLine": start_line,
"EndLine": i + 1,
"Code": "\n".join(block_lines).strip(),
"File": bsl_path,
})
elif in_block:
block_lines.append(line.rstrip("\n").rstrip("\r"))
return blocks
# --- Helper: analyze form for callType events and commands ---
def get_form_interceptors(form_xml_path):
if not os.path.isfile(form_xml_path):
return None
parser_xml = etree.XMLParser(remove_blank_text=False)
try:
doc = etree.parse(form_xml_path, parser_xml)
except Exception:
return None
f_root = doc.getroot()
base_form = f_root.find("f:BaseForm", FORM_NSMAP)
is_borrowed = base_form is not None
interceptors = []
# Form-level events with callType
events_node = f_root.find("f:Events", FORM_NSMAP)
if events_node is not None:
for evt in events_node.findall("f:Event", FORM_NSMAP):
ct = evt.get("callType", "")
if ct:
evt_name = evt.get("name", "")
evt_text = evt.text or ""
interceptors.append(f"Event:{evt_name} [{ct}] -> {evt_text}")
# Element-level events with callType (scan all elements recursively)
child_items = f_root.find("f:ChildItems", FORM_NSMAP)
if child_items is not None:
# Walk all descendant elements looking for Events/Event[@callType]
f_ns = FORM_NSMAP["f"]
for el in child_items.iter():
if not isinstance(el.tag, str):
continue
el_name = el.get("name", "")
if not el_name:
continue
events_sub = el.find(f"{{{f_ns}}}Events")
if events_sub is None:
continue
for evt in events_sub.findall(f"{{{f_ns}}}Event"):
ct = evt.get("callType", "")
if ct:
evt_name = evt.get("name", "")
evt_text = evt.text or ""
interceptors.append(f"Element:{el_name}.{evt_name} [{ct}] -> {evt_text}")
# Commands with callType on Action
f_ns = FORM_NSMAP["f"]
cmds_node = f_root.find(f"{{{f_ns}}}Commands")
if cmds_node is not None:
for cmd in cmds_node.findall(f"{{{f_ns}}}Command"):
cmd_name = cmd.get("name", "")
for action in cmd.findall(f"{{{f_ns}}}Action"):
ct = action.get("callType", "")
if ct:
action_text = action.text or ""
interceptors.append(f"Command:{cmd_name} [{ct}] -> {action_text}")
return {
"IsBorrowed": is_borrowed,
"Interceptors": interceptors,
}
# --- Mode A: Extension overview ---
def mode_a(objects, extension_path):
borrowed_list = []
own_list = []
for obj in objects:
info = get_object_info(obj["Type"], obj["Name"], extension_path)
if info is None:
print(f" [?] {obj['Type']}.{obj['Name']} \u2014 unknown type")
continue
if not info["Exists"]:
print(f" [?] {obj['Type']}.{obj['Name']} \u2014 file not found")
continue
if info["Borrowed"]:
borrowed_list.append(obj)
print(f" [BORROWED] {obj['Type']}.{obj['Name']}")
# Find .bsl files and interceptors
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
for bsl in bsl_files:
rel_path = bsl.replace(extension_path, "").lstrip("\\/")
interceptor_list = get_interceptors(bsl)
if len(interceptor_list) > 0:
for ic in interceptor_list:
print(f' &{ic["Type"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}')
else:
print(f" {rel_path} (no interceptors)")
# Check for own attributes/forms in ChildObjects
obj_el = info.get("ObjElement")
if obj_el is not None:
child_obj = obj_el.find("md:ChildObjects", MD_NSMAP)
if child_obj is not None:
own_attrs = 0
own_forms = 0
own_ts = 0
borrowed_items = 0
form_names = []
for c in child_obj:
if not isinstance(c.tag, str):
continue
ln = etree.QName(c.tag).localname
c_props = c.find("md:Properties", MD_NSMAP)
if c_props is not None:
c_ob = c_props.find("md:ObjectBelonging", MD_NSMAP)
if c_ob is not None and c_ob.text == "Adopted":
borrowed_items += 1
continue
if ln == "Attribute":
own_attrs += 1
elif ln == "TabularSection":
own_ts += 1
elif ln == "Form":
form_names.append(c.text or "")
own_forms += 1
parts = []
if own_attrs > 0:
parts.append(f"{own_attrs} own attrs")
if own_ts > 0:
parts.append(f"{own_ts} own TS")
if own_forms > 0:
parts.append(f"{own_forms} own forms")
if borrowed_items > 0:
parts.append(f"{borrowed_items} borrowed items")
if len(parts) > 0:
print(f" ChildObjects: {', '.join(parts)}")
# Analyze forms
for fn in form_names:
form_xml_path = os.path.join(
extension_path, info["DirName"], info["Name"],
"Forms", fn, "Ext", "Form.xml"
)
fi = get_form_interceptors(form_xml_path)
if fi is None:
print(f" Form.{fn} (?)")
continue
form_tag = "borrowed" if fi["IsBorrowed"] else "own"
if len(fi["Interceptors"]) > 0:
print(f" Form.{fn} ({form_tag}):")
for ic in fi["Interceptors"]:
print(f" {ic}")
else:
print(f" Form.{fn} ({form_tag})")
else:
own_list.append(obj)
print(f" [OWN] {obj['Type']}.{obj['Name']}")
# Brief info for own objects
obj_el = info.get("ObjElement")
if obj_el is not None:
child_obj = obj_el.find("md:ChildObjects", MD_NSMAP)
if child_obj is not None:
attrs = 0
forms = 0
ts = 0
for c in child_obj:
if not isinstance(c.tag, str):
continue
ln = etree.QName(c.tag).localname
if ln == "Attribute":
attrs += 1
elif ln == "TabularSection":
ts += 1
elif ln == "Form":
forms += 1
parts = []
if attrs > 0:
parts.append(f"{attrs} attrs")
if ts > 0:
parts.append(f"{ts} TS")
if forms > 0:
parts.append(f"{forms} forms")
if len(parts) > 0:
print(f" {', '.join(parts)}")
print("")
print(f"=== Summary: {len(borrowed_list)} borrowed, {len(own_list)} own objects ===")
# --- Mode B: Transfer check ---
def mode_b(objects, extension_path, config_path):
transferred = 0
not_transferred = 0
needs_review = 0
for obj in objects:
info = get_object_info(obj["Type"], obj["Name"], extension_path)
if info is None or not info["Exists"] or not info["Borrowed"]:
continue
# Find .bsl files with &ИзменениеИКонтроль
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
for bsl in bsl_files:
interceptor_list = get_interceptors(bsl)
mac_interceptors = [ic for ic in interceptor_list if ic["Type"] == "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c"]
if len(mac_interceptors) == 0:
continue
for ic in mac_interceptors:
method_name = ic["Method"]
rel_bsl = bsl.replace(extension_path, "").lstrip("\\/")
# Find #Вставка blocks in this file
insert_blocks = get_insertion_blocks(bsl)
if len(insert_blocks) == 0:
print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 no #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 blocks')
needs_review += 1
continue
# Find corresponding module in config
if obj["Type"] not in CHILD_TYPE_DIR_MAP:
continue
config_bsl = bsl.replace(extension_path, config_path)
if not os.path.isfile(config_bsl):
print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 config module not found')
needs_review += 1
continue
with open(config_bsl, "r", encoding="utf-8-sig") as fh:
config_content = fh.read()
all_transferred = True
for block in insert_blocks:
code = block["Code"]
if not code:
continue
# Normalize whitespace for comparison
code_norm = re.sub(r'\s+', ' ', code)
config_norm = re.sub(r'\s+', ' ', config_content)
if code_norm not in config_norm:
all_transferred = False
if all_transferred:
print(f' [TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 {len(insert_blocks)} block(s)')
transferred += 1
else:
print(f' [NOT_TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 some blocks not found in config')
not_transferred += 1
print("")
print(f"=== Transfer check: {transferred} transferred, {not_transferred} not transferred, {needs_review} needs review ===")
# --- Main ---
def main():
parser = argparse.ArgumentParser(description="Analyze and compare 1C configuration extension (CFE)", allow_abbrev=False)
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
args = parser.parse_args()
extension_path = args.ExtensionPath
config_path = args.ConfigPath
mode = args.Mode
# --- Resolve paths ---
if not os.path.isabs(extension_path):
extension_path = os.path.join(os.getcwd(), extension_path)
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isfile(extension_path):
extension_path = os.path.dirname(extension_path)
if os.path.isfile(config_path):
config_path = os.path.dirname(config_path)
ext_cfg = os.path.join(extension_path, "Configuration.xml")
src_cfg = os.path.join(config_path, "Configuration.xml")
if not os.path.isfile(ext_cfg):
print(f"Extension Configuration.xml not found: {ext_cfg}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(src_cfg):
print(f"Config Configuration.xml not found: {src_cfg}", file=sys.stderr)
sys.exit(1)
# --- Parse extension Configuration.xml ---
parser_xml = etree.XMLParser(remove_blank_text=False)
ext_doc = etree.parse(ext_cfg, parser_xml)
ext_root = ext_doc.getroot()
ext_props = ext_root.find(".//md:Configuration/md:Properties", MD_NSMAP)
ext_name_node = ext_props.find("md:Name", MD_NSMAP) if ext_props is not None else None
ext_name = ext_name_node.text if ext_name_node is not None and ext_name_node.text else "?"
prefix_node = ext_props.find("md:NamePrefix", MD_NSMAP) if ext_props is not None else None
name_prefix = prefix_node.text if prefix_node is not None and prefix_node.text else ""
purpose_node = ext_props.find("md:ConfigurationExtensionPurpose", MD_NSMAP) if ext_props is not None else None
purpose = purpose_node.text if purpose_node is not None and purpose_node.text else "?"
print(f"=== cfe-diff Mode {mode}: {ext_name} ({purpose}) ===")
print(f" NamePrefix: {name_prefix}")
print("")
# --- Collect ChildObjects ---
child_obj_node = ext_root.find(".//md:Configuration/md:ChildObjects", MD_NSMAP)
if child_obj_node is None:
print("[WARN] No ChildObjects in extension")
sys.exit(0)
objects = []
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
ln = etree.QName(child.tag).localname
if ln == "Language":
continue
objects.append({"Type": ln, "Name": child.text or ""})
if len(objects) == 0:
print("No objects (besides Language) in extension.")
sys.exit(0)
# --- Run selected mode ---
if mode == "A":
mode_a(objects, extension_path)
elif mode == "B":
mode_b(objects, extension_path, config_path)
if __name__ == "__main__":
main()
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid
from xml.etree import ElementTree as ET
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Create 1C configuration extension scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-NamePrefix', dest='NamePrefix', default=None)
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
parser.add_argument('-Purpose', dest='Purpose', default='Customization', choices=['Patch','Customization','AddOn'])
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
name_prefix = args.NamePrefix if args.NamePrefix else f"{name}_"
output_dir = args.OutputDir
purpose = args.Purpose
version = args.Version
vendor = args.Vendor
compat = args.CompatibilityMode
# --- Resolve output dir ---
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- Check existing ---
cfg_file = os.path.join(output_dir, "Configuration.xml")
if os.path.exists(cfg_file):
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath:
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.exists(candidate):
config_path = candidate
else:
print(f"No Configuration.xml in config directory: {config_path}", file=sys.stderr)
sys.exit(1)
if not os.path.exists(config_path):
print(f"Config file not found: {config_path}", file=sys.stderr)
sys.exit(1)
cfg_dir = os.path.dirname(os.path.abspath(config_path))
# Read Language UUID from base config
base_lang_file = os.path.join(cfg_dir, "Languages", "Русский.xml")
if os.path.exists(base_lang_file):
try:
base_tree = ET.parse(base_lang_file)
base_root = base_tree.getroot()
for child in base_root:
if child.tag.endswith('}Language') or child.tag == 'Language':
base_lang_uuid = child.get('uuid', base_lang_uuid)
print(f"[INFO] Base config Language UUID: {base_lang_uuid}")
break
except Exception:
print(f"[WARN] Could not parse {base_lang_file}")
else:
print(f"[WARN] Base config language not found: {base_lang_file}")
# Read CompatibilityMode from base config
try:
base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot()
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text:
compat = compat_node.text.strip()
print(f"[INFO] Base config CompatibilityMode: {compat}")
else:
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
except Exception:
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
else:
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
# --- Generate UUIDs ---
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
uuid_role = new_uuid()
co = [new_uuid() for _ in range(7)]
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
# --- Role name ---
role_name = f"{name_prefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
default_roles_xml = ""
if not args.NoRole:
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
# --- ChildObjects ---
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
if not args.NoRole:
child_objects_xml += f"\r\n\t\t\t<Role>{role_name}</Role>"
child_objects_xml += "\r\n\t\t"
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
\t\t</Properties>
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
\t</Configuration>
</MetaDataObject>'''
# --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>Русский</Name>
\t\t\t<Comment/>
\t\t\t<ExtendedConfigurationObject>{base_lang_uuid}</ExtendedConfigurationObject>
\t\t\t<LanguageCode>ru</LanguageCode>
\t\t</Properties>
\t</Language>
</MetaDataObject>'''
# --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<Role uuid="{uuid_role}">
\t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name>
\t\t\t<Synonym/>
\t\t\t<Comment/>
\t\t</Properties>
\t</Role>
</MetaDataObject>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
# --- Role ---
role_file = None
if not args.NoRole:
role_dir = os.path.join(output_dir, "Roles")
os.makedirs(role_dir, exist_ok=True)
role_file = os.path.join(role_dir, f"{role_name}.xml")
write_utf8_bom(role_file, role_xml)
# --- Output ---
print(f"[OK] Создано расширение: {name}")
print(f" Каталог: {output_dir}")
print(f" Назначение: {purpose}")
print(f" Префикс: {name_prefix}")
print(f" Совместимость: {compat}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
if role_file:
print(f" Role: {role_file}")
if __name__ == '__main__':
main()
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import xml.etree.ElementTree as ET
def main():
parser = argparse.ArgumentParser(
description="Generate method interceptor for 1C extension (CFE)",
allow_abbrev=False,
)
parser.add_argument("-ExtensionPath", required=True)
parser.add_argument("-ModulePath", required=True)
parser.add_argument("-MethodName", required=True)
parser.add_argument(
"-InterceptorType",
required=True,
choices=["Before", "After", "ModificationAndControl"],
)
parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере
parser.add_argument("-IsFunction", action="store_true")
args = parser.parse_args()
extension_path = args.ExtensionPath
module_path = args.ModulePath
method_name = args.MethodName
interceptor_type = args.InterceptorType
context = args.Context
is_function = args.IsFunction
# --- Resolve extension path ---
if not os.path.isabs(extension_path):
extension_path = os.path.join(os.getcwd(), extension_path)
if os.path.isfile(extension_path):
extension_path = os.path.dirname(extension_path)
cfg_file = os.path.join(extension_path, "Configuration.xml")
if not os.path.isfile(cfg_file):
print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr)
sys.exit(1)
# --- Read NamePrefix from Configuration.xml ---
tree = ET.parse(cfg_file)
root = tree.getroot()
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
props_node = root.find(".//md:Configuration/md:Properties", ns)
name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_
if props_node is not None:
prefix_node = props_node.find("md:NamePrefix", ns)
if prefix_node is not None and prefix_node.text:
name_prefix = prefix_node.text
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
type_dir_map = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"CommonModule": "CommonModules",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"ExchangePlan": "ExchangePlans",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
}
parts = module_path.split(".")
if len(parts) < 2:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in type_dir_map:
print(f"Unknown object type: {obj_type}", file=sys.stderr)
sys.exit(1)
dir_name = type_dir_map[obj_type]
bsl_file = None
if obj_type == "CommonModule":
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl")
elif len(parts) >= 4 and parts[2] == "Form":
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
form_name = parts[3]
bsl_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"
)
elif len(parts) >= 3:
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
module_name = parts[2]
module_file_map = {
"ObjectModule": "ObjectModule.bsl",
"ManagerModule": "ManagerModule.bsl",
"RecordSetModule": "RecordSetModule.bsl",
"CommandModule": "CommandModule.bsl",
}
module_file_name = module_file_map.get(module_name, f"{module_name}.bsl")
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name)
else:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
# --- Map InterceptorType to decorator ---
decorator_map = {
"Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед
"After": "&\u041f\u043e\u0441\u043b\u0435", # &После
"ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль
}
decorator = decorator_map[interceptor_type]
# --- Map Context to annotation ---
context_map = {
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере
"\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста
}
context_annotation = context_map.get(context, f"&{context}")
# --- Procedure name ---
proc_name = f"{name_prefix}{method_name}"
# --- Generate BSL code ---
keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура
end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры
body_lines = []
if interceptor_type == "Before":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода
elif interceptor_type == "After":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода
elif interceptor_type == "ModificationAndControl":
body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения,
body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки
if is_function:
body_lines.append("\t")
body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение
bsl_code = [
context_annotation,
f'{decorator}("{method_name}")',
f"{keyword} {proc_name}()",
]
bsl_code.extend(body_lines)
bsl_code.append(end_keyword)
bsl_text = "\r\n".join(bsl_code) + "\r\n"
# --- Check form borrowing for .Form. paths ---
if len(parts) >= 4 and parts[2] == "Form":
form_name = parts[3]
form_meta_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml"
)
form_xml_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml"
)
if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file):
print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.")
print(" Run /cfe-borrow first:")
print(
f" /cfe-borrow -ExtensionPath {extension_path} "
f'-ConfigPath <ConfigPath> -Object "{obj_type}.{obj_name}.Form.{form_name}"'
)
print()
# --- Check if file exists and append ---
bsl_dir = os.path.dirname(bsl_file)
if not os.path.isdir(bsl_dir):
os.makedirs(bsl_dir, exist_ok=True)
if os.path.isfile(bsl_file):
# Append to existing file
with open(bsl_file, "r", encoding="utf-8-sig") as f:
existing = f.read()
separator = "\r\n"
if existing and not existing.endswith("\n"):
separator = "\r\n\r\n"
new_content = existing + separator + bsl_text
with open(bsl_file, "w", encoding="utf-8-sig") as f:
f.write(new_content)
print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл
else:
with open(bsl_file, "w", encoding="utf-8-sig") as f:
f.write(bsl_text)
print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля
print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл:
print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор:
print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура:
print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст:
if __name__ == "__main__":
main()
@@ -0,0 +1,594 @@
#!/usr/bin/env python3
# cfe-validate v1.0 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re
from lxml import etree
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xs': 'http://www.w3.org/2001/XMLSchema',
'app': 'http://v8.1c.ru/8.2/managed-application/core',
}
GUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
IDENT_PATTERN = re.compile(
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
)
# 7 fixed ClassIds for Configuration
VALID_CLASS_IDS = [
'9cd510cd-abfc-11d4-9434-004095e12fc7',
'9fcd25a0-4822-11d4-9414-008048da11f9',
'e3687481-0a87-462c-a166-9f34594f9bba',
'9de14907-ec23-4a07-96f0-85521cb6b53b',
'51f2d5d8-ea4d-4064-8892-82951750031e',
'e68182ea-4237-4383-967f-90c1e3370bc7',
'fb282519-d103-4dd3-bc12-cb271d631dfc',
]
# 44 types in canonical order
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
'Constant', 'CommonForm', 'Catalog', 'Document',
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
'ChartOfCalculationTypes', 'CalculationRegister',
'BusinessProcess', 'Task', 'IntegrationService',
]
# Type -> directory mapping
CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
'SettingsStorage': 'SettingsStorages', 'FunctionalOption': 'FunctionalOptions',
'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'DefinedType': 'DefinedTypes',
'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'Constant': 'Constants',
'CommonForm': 'CommonForms', 'Catalog': 'Catalogs', 'Document': 'Documents',
'DocumentNumerator': 'DocumentNumerators', 'Sequence': 'Sequences',
'DocumentJournal': 'DocumentJournals', 'Enum': 'Enums', 'Report': 'Reports',
'DataProcessor': 'DataProcessors', 'InformationRegister': 'InformationRegisters',
'AccumulationRegister': 'AccumulationRegisters',
'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes',
'ChartOfAccounts': 'ChartsOfAccounts', 'AccountingRegister': 'AccountingRegisters',
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
'CalculationRegister': 'CalculationRegisters',
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
'IntegrationService': 'IntegrationServices',
}
# Valid enum values for extension properties
VALID_ENUM_VALUES = {
'ConfigurationExtensionCompatibilityMode': [
'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16',
'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5',
'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10',
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
}
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def finalize(self, out_file):
self.out('')
self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ===')
result = self.text()
print(result, end='')
if out_file:
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
)
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
extension_path = args.ExtensionPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(extension_path):
extension_path = os.path.join(os.getcwd(), extension_path)
if os.path.isdir(extension_path):
candidate = os.path.join(extension_path, 'Configuration.xml')
if os.path.exists(candidate):
extension_path = candidate
else:
print(f'[ERROR] No Configuration.xml found in directory: {extension_path}')
sys.exit(1)
if not os.path.exists(extension_path):
print(f'[ERROR] File not found: {extension_path}')
sys.exit(1)
resolved_path = os.path.abspath(extension_path)
config_dir = os.path.dirname(resolved_path)
if out_file and not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
r = Reporter(max_errors)
r.out('')
# --- 1. Parse XML ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.lines.insert(0, '=== Validation: Extension (parse failed) ===')
r.out('')
r.error(f'1. XML parse failed: {e}')
r.finalize(out_file)
sys.exit(1)
root = xml_doc.getroot()
# --- Check 1: Root structure ---
check1_ok = True
root_local = etree.QName(root.tag).localname
root_ns = etree.QName(root.tag).namespace or ''
if root_local != 'MetaDataObject':
r.error(f"1. Root element is '{root_local}', expected 'MetaDataObject'")
r.finalize(out_file)
sys.exit(1)
if root_ns != EXPECTED_NS:
r.error(f"1. Root namespace is '{root_ns}', expected '{EXPECTED_NS}'")
check1_ok = False
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
# Must have Configuration child
cfg_node = None
for child in root:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Configuration' and etree.QName(child.tag).namespace == EXPECTED_NS:
cfg_node = child
break
if cfg_node is None:
r.error('1. No <Configuration> element found inside MetaDataObject')
r.finalize(out_file)
sys.exit(1)
# UUID
cfg_uuid = cfg_node.get('uuid', '')
if not cfg_uuid:
r.error('1. Missing uuid on <Configuration>')
check1_ok = False
elif not GUID_PATTERN.match(cfg_uuid):
r.error(f"1. Invalid uuid '{cfg_uuid}' on <Configuration>")
check1_ok = False
# Get name early for header
props_node = cfg_node.find('md:Properties', NS)
name_node = props_node.find('md:Name', NS) if props_node is not None else None
obj_name = (name_node.text or '') if name_node is not None and name_node.text else '(unknown)'
r.lines.insert(0, f'=== Validation: Extension.{obj_name} ===')
if check1_ok:
r.ok(f'1. Root structure: MetaDataObject/Configuration, version {version}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 2: InternalInfo ---
internal_info = cfg_node.find('md:InternalInfo', NS)
check2_ok = True
if internal_info is None:
r.error('2. InternalInfo: missing')
else:
contained = internal_info.findall('xr:ContainedObject', NS)
if len(contained) != 7:
r.warn(f'2. InternalInfo: expected 7 ContainedObject, found {len(contained)}')
found_class_ids = {}
for co in contained:
class_id_el = co.find('xr:ClassId', NS)
object_id_el = co.find('xr:ObjectId', NS)
if class_id_el is None or not (class_id_el.text or ''):
r.error('2. ContainedObject missing ClassId')
check2_ok = False
continue
cid = class_id_el.text
if cid not in VALID_CLASS_IDS:
r.error(f'2. Unknown ClassId: {cid}')
check2_ok = False
if cid in found_class_ids:
r.error(f'2. Duplicate ClassId: {cid}')
check2_ok = False
found_class_ids[cid] = True
if object_id_el is None or not (object_id_el.text or ''):
r.error(f'2. ContainedObject missing ObjectId for ClassId {cid}')
check2_ok = False
elif not GUID_PATTERN.match(object_id_el.text):
r.error(f"2. Invalid ObjectId '{object_id_el.text}' for ClassId {cid}")
check2_ok = False
missing_ids = [cid for cid in VALID_CLASS_IDS if cid not in found_class_ids]
if len(missing_ids) > 0:
r.warn(f'2. Missing ClassIds: {len(missing_ids)} of 7')
if check2_ok:
r.ok(f'2. InternalInfo: {len(contained)} ContainedObject, all ClassIds valid')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 3: Extension-specific properties ---
def_lang = ''
if props_node is None:
r.error('3. Properties block missing')
else:
check3_ok = True
# ObjectBelonging = Adopted
ob_node = props_node.find('md:ObjectBelonging', NS)
ob_val = (ob_node.text or '') if ob_node is not None else ''
if ob_val != 'Adopted':
r.error(f"3. ObjectBelonging must be 'Adopted', got '{ob_val}'")
check3_ok = False
# Name
if name_node is None or not (name_node.text or ''):
r.error('3. Name is missing or empty')
check3_ok = False
else:
name_val = name_node.text
if not IDENT_PATTERN.match(name_val):
r.error(f"3. Name '{name_val}' is not a valid 1C identifier")
check3_ok = False
# ConfigurationExtensionPurpose
purpose_node = props_node.find('md:ConfigurationExtensionPurpose', NS)
valid_purposes = ['Patch', 'Customization', 'AddOn']
if purpose_node is None or not (purpose_node.text or ''):
r.error('3. ConfigurationExtensionPurpose is missing')
check3_ok = False
elif purpose_node.text not in valid_purposes:
r.error(f"3. ConfigurationExtensionPurpose '{purpose_node.text}' invalid (expected: Patch, Customization, AddOn)")
check3_ok = False
# NamePrefix
prefix_node = props_node.find('md:NamePrefix', NS)
if prefix_node is None or not (prefix_node.text or ''):
r.warn('3. NamePrefix is empty')
# KeepMappingToExtendedConfigurationObjectsByIDs
keep_map_node = props_node.find('md:KeepMappingToExtendedConfigurationObjectsByIDs', NS)
if keep_map_node is None:
r.warn('3. KeepMappingToExtendedConfigurationObjectsByIDs is missing')
# DefaultLanguage
def_lang_node = props_node.find('md:DefaultLanguage', NS)
def_lang = (def_lang_node.text or '') if def_lang_node is not None else ''
if check3_ok:
purpose_val = purpose_node.text if purpose_node is not None and purpose_node.text else '?'
prefix_val = (prefix_node.text or '') if prefix_node is not None and prefix_node.text else '(empty)'
r.ok(f'3. Extension properties: Name="{obj_name}", Purpose={purpose_val}, Prefix={prefix_val}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 4: Enum property values ---
if props_node is not None:
enum_checked = 0
check4_ok = True
for prop_name, allowed in VALID_ENUM_VALUES.items():
prop_node = props_node.find(f'md:{prop_name}', NS)
if prop_node is not None and prop_node.text:
val = prop_node.text
if val not in allowed:
r.error(f"4. Property '{prop_name}' has invalid value '{val}'")
check4_ok = False
enum_checked += 1
if check4_ok:
r.ok(f'4. Property values: {enum_checked} enum properties checked')
else:
r.warn('4. No Properties block to check')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 5: ChildObjects -- valid types, no duplicates, order ---
child_obj_node = cfg_node.find('md:ChildObjects', NS)
if child_obj_node is None:
r.error('5. ChildObjects block missing')
else:
check5_ok = True
total_count = 0
type_counts = {}
duplicates = {}
type_first_index = {}
last_type_order = -1
order_ok = True
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
obj_name_val = child.text or ''
if type_name in CHILD_OBJECT_TYPES:
type_idx = CHILD_OBJECT_TYPES.index(type_name)
else:
type_idx = -1
if type_idx < 0:
r.error(f"5. Unknown type '{type_name}' in ChildObjects")
check5_ok = False
else:
if type_name not in type_first_index:
type_first_index[type_name] = type_idx
if type_idx < last_type_order:
r.warn(f"5. Type '{type_name}' is out of canonical order (after type at position {last_type_order})")
order_ok = False
last_type_order = type_idx
if type_name not in type_counts:
type_counts[type_name] = {}
if obj_name_val in type_counts[type_name]:
dup_key = f'{type_name}.{obj_name_val}'
if dup_key not in duplicates:
r.error(f'5. Duplicate: {dup_key}')
duplicates[dup_key] = True
check5_ok = False
else:
type_counts[type_name][obj_name_val] = True
total_count += 1
type_count = len(type_counts)
if check5_ok:
order_info = ', order correct' if order_ok else ''
r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 6: DefaultLanguage references existing Language in ChildObjects ---
if def_lang and child_obj_node is not None:
lang_name = def_lang
if lang_name.startswith('Language.'):
lang_name = lang_name[9:]
found = False
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Language' and (child.text or '') == lang_name:
found = True
break
if found:
r.ok(f'6. DefaultLanguage "{def_lang}" found in ChildObjects')
else:
r.error(f'6. DefaultLanguage "{def_lang}" not found in ChildObjects')
else:
if not def_lang:
r.warn('6. Cannot check DefaultLanguage (empty)')
else:
r.warn('6. Cannot check DefaultLanguage (no ChildObjects)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 7: Language files exist ---
if child_obj_node is not None:
lang_names = []
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'Language':
lang_names.append(child.text or '')
if len(lang_names) > 0:
exist_count = 0
for ln in lang_names:
lang_file = os.path.join(config_dir, 'Languages', ln + '.xml')
if os.path.exists(lang_file):
exist_count += 1
else:
r.warn(f'7. Language file missing: Languages/{ln}.xml')
if exist_count == len(lang_names):
r.ok(f'7. Language files: {exist_count}/{len(lang_names)} exist')
else:
r.warn('7. No Language entries in ChildObjects')
else:
r.warn('7. Cannot check language files (no ChildObjects)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 8: Object directories exist ---
if child_obj_node is not None:
dirs_to_check = {}
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
if type_name == 'Language':
continue
if type_name in CHILD_TYPE_DIR_MAP:
dir_name = CHILD_TYPE_DIR_MAP[type_name]
dirs_to_check[dir_name] = dirs_to_check.get(dir_name, 0) + 1
missing_dirs = []
for dir_name, count in dirs_to_check.items():
dir_path = os.path.join(config_dir, dir_name)
if not os.path.isdir(dir_path):
missing_dirs.append(f'{dir_name} ({count} objects)')
if len(missing_dirs) == 0:
r.ok(f'8. Object directories: {len(dirs_to_check)} directories, all exist')
else:
for md in missing_dirs:
r.warn(f'8. Missing directory: {md}')
else:
r.ok('8. Object directories: N/A')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 9: Borrowed objects validation ---
if child_obj_node is not None:
borrowed_count = 0
borrowed_ok_count = 0
check9_ok = True
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
child_name = child.text or ''
if type_name == 'Language':
continue
if type_name not in CHILD_TYPE_DIR_MAP:
continue
dir_name = CHILD_TYPE_DIR_MAP[type_name]
obj_file = os.path.join(config_dir, dir_name, child_name + '.xml')
if not os.path.exists(obj_file):
continue
# Parse object XML
obj_doc = None
try:
obj_parser = etree.XMLParser(remove_blank_text=False)
obj_doc = etree.parse(obj_file, obj_parser)
except etree.XMLSyntaxError as e:
r.warn(f'9. Cannot parse {dir_name}/{child_name}.xml: {e}')
continue
obj_root = obj_doc.getroot()
# Find the object element (Catalog, Document, etc.)
obj_el = None
for c in obj_root:
if isinstance(c.tag, str):
obj_el = c
break
if obj_el is None:
continue
obj_props = obj_el.find(f'{{{NS["md"]}}}Properties')
if obj_props is None:
continue
ob_node = obj_props.find(f'{{{NS["md"]}}}ObjectBelonging')
if ob_node is not None and (ob_node.text or '') == 'Adopted':
borrowed_count += 1
# Check ExtendedConfigurationObject
ext_obj = obj_props.find(f'{{{NS["md"]}}}ExtendedConfigurationObject')
if ext_obj is None or not (ext_obj.text or ''):
r.error(f'9. Borrowed {type_name}.{child_name}: missing ExtendedConfigurationObject')
check9_ok = False
elif not GUID_PATTERN.match(ext_obj.text):
r.error(f"9. Borrowed {type_name}.{child_name}: invalid ExtendedConfigurationObject UUID '{ext_obj.text}'")
check9_ok = False
else:
borrowed_ok_count += 1
if r.stopped:
break
if borrowed_count == 0:
r.ok('9. Borrowed objects: none found')
elif check9_ok:
r.ok(f'9. Borrowed objects: {borrowed_ok_count}/{borrowed_count} validated')
# --- Final output ---
r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
# db-create v1.0 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Create 1C information base",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UseTemplate", default="")
parser.add_argument("-AddToList", action="store_true")
parser.add_argument("-ListName", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate template ---
if args.UseTemplate and not os.path.exists(args.UseTemplate):
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["CREATEINFOBASE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
else:
arguments.append(f'File="{args.InfoBasePath}"')
# --- Template ---
if args.UseTemplate:
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
# --- Add to list ---
if args.AddToList:
if args.ListName:
arguments.extend(["/AddToList", f'"{args.ListName}"'])
else:
arguments.append("/AddToList")
# --- Output ---
out_file = os.path.join(temp_dir, "create_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else:
print(f"Information base created successfully: {args.InfoBasePath}")
else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Dump 1C configuration to CF file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Dump 1C configuration to XML files",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
parser.add_argument(
"-Mode",
default="Changes",
choices=["Full", "Changes", "Partial", "UpdateInfo"],
help="Dump mode (default: Changes)",
)
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
parser.add_argument("-Extension", default="", help="Extension name to dump")
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate Partial mode ---
if args.Mode == "Partial" and not args.Objects:
print("Error: -Objects required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- Create output dir if needed ---
if not os.path.exists(args.ConfigDir):
os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}")
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
arguments += ["-Format", args.Format]
if args.Mode == "Full":
print("Executing full configuration dump...")
elif args.Mode == "Changes":
print("Executing incremental configuration dump...")
arguments.append("-update")
arguments.append("-force")
elif args.Mode == "Partial":
print("Executing partial configuration dump...")
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
list_file = os.path.join(temp_dir, "dump_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(object_list))
arguments += ["-listFile", f'"{list_file}"']
print(f"Objects to dump: {len(object_list)}")
for obj in object_list:
print(f" {obj}")
elif args.Mode == "UpdateInfo":
print("Updating ConfigDumpInfo.xml...")
arguments.append("-configDumpInfoOnly")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# db-load-cf v1.0 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Load 1C configuration from CF file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_cf_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
# db-load-git v1.0 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def get_object_xml_from_bsl(relative_path):
"""Map BSL path to object XML path."""
parts = re.split(r"[\\/]", relative_path)
if len(parts) >= 2:
return f"{parts[0]}/{parts[1]}.xml"
return None
def run_git(config_dir, git_args):
"""Run a git command in config_dir and return output lines on success."""
result = subprocess.run(
["git"] + git_args,
capture_output=True,
text=True,
cwd=config_dir,
)
if result.returncode == 0:
return [line for line in result.stdout.splitlines() if line.strip()]
return []
def main():
parser = argparse.ArgumentParser(
description="Load Git changes into 1C database",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
parser.add_argument(
"-Source",
default="All",
choices=["All", "Staged", "Unstaged", "Commit"],
help="Change source (default: All)",
)
parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD")
parser.add_argument("-Extension", default="", help="Extension name to load")
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="File format (default: Hierarchical)",
)
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
args = parser.parse_args()
# --- Resolve V8Path (skip if DryRun) ---
v8path = None
if not args.DryRun:
v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) ---
if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
sys.exit(1)
# --- Validate Commit mode ---
if args.Source == "Commit" and not args.CommitRange:
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
sys.exit(1)
# --- Check git ---
try:
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: git not found in PATH", file=sys.stderr)
sys.exit(1)
# --- Get changed files from Git ---
changed_files = []
if args.Source == "Staged":
print("Getting staged changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"])
elif args.Source == "Unstaged":
print("Getting unstaged changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--name-only"])
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
elif args.Source == "Commit":
print(f"Getting changes from {args.CommitRange}...")
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", args.CommitRange])
elif args.Source == "All":
print("Getting all uncommitted changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"])
changed_files += run_git(args.ConfigDir, ["diff", "--name-only"])
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
# Deduplicate and filter blanks
changed_files = list(dict.fromkeys(f for f in changed_files if f.strip()))
if len(changed_files) == 0:
print("No changes found")
sys.exit(0)
print(f"Git changes detected: {len(changed_files)} files")
# --- Filter and map to config files ---
config_files = []
for file in changed_files:
file = file.strip().replace("\\", "/")
if not file:
continue
# Skip service files
if file == "ConfigDumpInfo.xml":
continue
# Only process .xml and .bsl files
if not re.search(r"\.(xml|bsl)$", file):
continue
# Check file exists in config dir
full_path = os.path.join(args.ConfigDir, file)
if file.endswith(".xml"):
if os.path.exists(full_path):
if file not in config_files:
config_files.append(file)
elif file.endswith(".bsl"):
# For BSL: add the BSL itself + parent object XML + all Ext/ files
object_xml = get_object_xml_from_bsl(file)
if object_xml:
full_xml_path = os.path.join(args.ConfigDir, object_xml)
if os.path.exists(full_xml_path):
if object_xml not in config_files:
config_files.append(object_xml)
if file not in config_files:
config_files.append(file)
# Add all files from Ext/ directory of the object
parts = re.split(r"[\\/]", file)
if len(parts) >= 2:
ext_dir = os.path.join(args.ConfigDir, parts[0], parts[1], "Ext")
if os.path.isdir(ext_dir):
for root, dirs, files in os.walk(ext_dir):
for fname in files:
abs_path = os.path.join(root, fname)
# Build relative path from ConfigDir
rel_path = os.path.relpath(abs_path, args.ConfigDir).replace("\\", "/")
if rel_path not in config_files:
config_files.append(rel_path)
if len(config_files) == 0:
print("No configuration files found in changes")
sys.exit(0)
print(f"Files for loading: {len(config_files)}")
for f in config_files:
print(f" {f}")
# --- DryRun: stop here ---
if args.DryRun:
print("")
print("DryRun mode - no changes applied")
sys.exit(0)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_git_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(config_files))
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", f'"{list_file}"']
arguments += ["-Format", args.Format]
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print("")
print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
print("")
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
# db-load-xml v1.0 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Load 1C configuration from XML files",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
parser.add_argument(
"-Mode",
default="Full",
choices=["Full", "Partial"],
help="Load mode (default: Full)",
)
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)")
parser.add_argument("-Extension", default="", help="Extension name to load")
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="File format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
sys.exit(1)
# --- Validate Partial mode ---
if args.Mode == "Partial" and not args.Files and not args.ListFile:
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
if args.Mode == "Full":
print("Executing full configuration load...")
else:
print("Executing partial configuration load...")
# Build list file
generated_list_file = None
if args.ListFile:
# Use provided list file
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
generated_list_file = args.ListFile
else:
# Generate from -Files parameter
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", f'"{generated_list_file}"']
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
arguments += ["-Format", args.Format]
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# db-run v1.0 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import subprocess
import sys
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Launch 1C:Enterprise",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-Execute", default="")
parser.add_argument("-CParam", default="")
parser.add_argument("-URL", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Build arguments ---
arguments = ["ENTERPRISE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
# --- Optional params ---
execute = args.Execute
if execute:
ext = os.path.splitext(execute)[1].lower()
if ext == ".erf":
print("[WARN] /Execute does not support ERF files (external reports).")
print(f" Open the report via File -> Open: {execute}")
print(" Launching database without /Execute.")
execute = ""
if execute:
arguments.extend(["/Execute", f'"{execute}"'])
if args.CParam:
arguments.extend(["/C", f'"{args.CParam}"'])
if args.URL:
arguments.extend(["/URL", f'"{args.URL}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute (background, no wait) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
subprocess.Popen([v8path] + arguments)
print("1C:Enterprise launched")
if __name__ == "__main__":
main()
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Update 1C database configuration",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.append("/UpdateDBCfg")
# --- Options ---
if args.Dynamic:
arguments.append(f"-Dynamic{args.Dynamic}")
if args.Server:
arguments.append("-Server")
if args.WarningsAsErrors:
arguments.append("-WarningsAsErrors")
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "update_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
# add-form v1.0 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
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."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
parser.add_argument("-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Main", action="store_true")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
processor_name = args.ProcessorName
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
is_main = args.Main
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject 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"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_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'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' 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: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"'
' version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. 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 <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
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(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Build external data processor or report (EPF/ERF) from XML sources",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate source file ---
if not os.path.isfile(args.SourceFile):
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_build_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
# --- Output ---
out_file = os.path.join(temp_dir, "build_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}")
else:
print(f"Error building (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Dump external data processor or report (EPF/ERF) to XML sources",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
if not os.path.exists(args.OutputDir):
os.makedirs(args.OutputDir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
arguments += ["-Format", args.Format]
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}")
else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
# epf-init v1.0 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Init 1C external data processor scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir
uuid1 = new_uuid()
uuid2 = new_uuid()
uuid3 = new_uuid()
uuid4 = new_uuid()
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<ExternalDataProcessor uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
\t\t\t</xr:ContainedObject>
\t\t\t<xr:GeneratedType name="ExternalDataProcessorObject.{name}" category="Object">
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<DefaultForm/>
\t\t\t<AuxiliaryForm/>
\t\t</Properties>
\t\t<ChildObjects/>
\t</ExternalDataProcessor>
</MetaDataObject>'''
root_file = os.path.join(src_dir, f"{name}.xml")
processor_dir = os.path.join(src_dir, name)
if os.path.exists(root_file):
print(f"Файл уже существует: {root_file}", file=sys.stderr)
sys.exit(1)
os.makedirs(src_dir, exist_ok=True)
ext_dir = os.path.join(processor_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
print(f"[OK] Создана обработка: {root_file}")
print(f" Каталог: {processor_dir}")
print(f" Модуль: {module_path}")
if __name__ == '__main__':
main()
@@ -0,0 +1,696 @@
#!/usr/bin/env python3
# epf-validate v1.0 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
import argparse
import os
import re
import sys
from io import StringIO
from lxml import etree
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
XS_NS = "http://www.w3.org/2001/XMLSchema"
APP_NS = "http://v8.1c.ru/8.2/managed-application/core"
NSMAP = {"md": MD_NS, "v8": V8_NS, "xr": XR_NS, "xsi": XSI_NS, "xs": XS_NS, "app": APP_NS}
GUID_PATTERN = re.compile(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
IDENT_PATTERN = re.compile(r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$')
CLASS_IDS = {
"ExternalDataProcessor": "c3831ec8-d8d5-4f93-8a22-f9bfae07327f",
"ExternalReport": "e41aff26-25cf-4bb6-b6c1-3f478a75f374",
}
ALLOWED_CHILD_TYPES = {"Attribute", "TabularSection", "Form", "Template", "Command"}
CHILD_TYPE_ORDER = {
"Attribute": 0,
"TabularSection": 1,
"Form": 2,
"Template": 3,
"Command": 4,
}
def localname(el):
return etree.QName(el.tag).localname
def main():
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default=None)
args = parser.parse_args()
max_errors = args.MaxErrors
# --- Resolve path ---
object_path = args.ObjectPath
if not os.path.isabs(object_path):
object_path = os.path.join(os.getcwd(), object_path)
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.isfile(candidate):
object_path = candidate
elif os.path.isfile(sibling):
object_path = sibling
else:
xml_files = [f for f in os.listdir(object_path) if f.lower().endswith(".xml")]
if xml_files:
object_path = os.path.join(object_path, xml_files[0])
else:
print(f"[ERROR] No XML file found in directory: {object_path}")
sys.exit(1)
if not os.path.isfile(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.isfile(candidate):
object_path = candidate
if not os.path.isfile(object_path):
print(f"[ERROR] File not found: {object_path}")
sys.exit(1)
resolved_path = os.path.abspath(object_path)
src_dir = os.path.dirname(resolved_path)
# --- Output infrastructure ---
errors = 0
warnings = 0
stopped = False
output_lines = []
def out_line(msg):
output_lines.append(msg)
def report_ok(msg):
out_line(f"[OK] {msg}")
def report_error(msg):
nonlocal errors, stopped
errors += 1
out_line(f"[ERROR] {msg}")
if errors >= max_errors:
stopped = True
def report_warn(msg):
nonlocal warnings
warnings += 1
out_line(f"[WARN] {msg}")
def finalize():
out_line("")
out_line(f"=== Result: {errors} errors, {warnings} warnings ===")
result = "\n".join(output_lines)
print(result)
if args.OutFile:
with open(args.OutFile, "w", encoding="utf-8-sig") as fh:
fh.write(result)
print(f"Written to: {args.OutFile}")
# --- 1. Parse XML ---
out_line("")
try:
xml_parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(resolved_path, xml_parser)
except Exception as e:
out_line("=== Validation: (parse failed) ===")
out_line("")
report_error(f"1. XML parse failed: {e}")
finalize()
sys.exit(1)
root = tree.getroot()
# --- Check 1: Root structure ---
check1_ok = True
if localname(root) != "MetaDataObject":
report_error(f"1. Root element is '{localname(root)}', expected 'MetaDataObject'")
finalize()
sys.exit(1)
expected_ns = MD_NS
if root.tag.split("}")[0].lstrip("{") != expected_ns:
report_error(f"1. Root namespace is '{root.tag.split('}')[0].lstrip('{')}', expected '{expected_ns}'")
check1_ok = False
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20"):
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
# Detect type
child_elements = []
for child in root:
if isinstance(child.tag, str) and child.tag.startswith(f"{{{expected_ns}}}"):
child_elements.append(child)
if not child_elements:
report_error("1. No metadata type element found inside MetaDataObject")
finalize()
sys.exit(1)
elif len(child_elements) > 1:
report_error(f"1. Multiple type elements found: {[localname(c) for c in child_elements]}")
check1_ok = False
type_node = child_elements[0]
md_type = localname(type_node)
if md_type not in ("ExternalDataProcessor", "ExternalReport"):
report_error(f"1. Unexpected type '{md_type}' (expected ExternalDataProcessor or ExternalReport)")
finalize()
sys.exit(1)
type_uuid = type_node.get("uuid", "")
if not type_uuid:
report_error(f"1. Missing uuid on <{md_type}>")
check1_ok = False
elif not GUID_PATTERN.match(type_uuid):
report_error(f"1. Invalid uuid '{type_uuid}' on <{md_type}>")
check1_ok = False
props_node = type_node.find(f"{{{MD_NS}}}Properties")
name_node = props_node.find(f"{{{MD_NS}}}Name") if props_node is not None else None
obj_name = name_node.text if name_node is not None and name_node.text else "(unknown)"
short_type = "EPF" if md_type == "ExternalDataProcessor" else "ERF"
output_lines.insert(0, f"=== Validation: {short_type}.{obj_name} ===")
if check1_ok:
report_ok(f"1. Root structure: MetaDataObject/{md_type}, version {version}")
if stopped:
finalize()
sys.exit(1)
# --- Check 2: InternalInfo ---
internal_info = type_node.find(f"{{{MD_NS}}}InternalInfo")
if internal_info is None:
report_error("2. InternalInfo block missing")
else:
check2_ok = True
contained_obj = internal_info.find(f"{{{XR_NS}}}ContainedObject")
if contained_obj is None:
report_error("2. InternalInfo: missing xr:ContainedObject")
check2_ok = False
else:
class_id_node = contained_obj.find(f"{{{XR_NS}}}ClassId")
object_id_node = contained_obj.find(f"{{{XR_NS}}}ObjectId")
expected_class_id = CLASS_IDS[md_type]
if class_id_node is None or not class_id_node.text:
report_error("2. Missing ClassId in ContainedObject")
check2_ok = False
elif class_id_node.text != expected_class_id:
report_error(f"2. ClassId is '{class_id_node.text}', expected '{expected_class_id}' for {md_type}")
check2_ok = False
if object_id_node is not None and object_id_node.text and not GUID_PATTERN.match(object_id_node.text):
report_error("2. Invalid ObjectId UUID")
check2_ok = False
gen_types = internal_info.findall(f"{{{XR_NS}}}GeneratedType")
if not gen_types:
report_error("2. No GeneratedType entries found")
check2_ok = False
else:
for gt in gen_types:
gt_name = gt.get("name", "")
gt_category = gt.get("category", "")
if gt_category != "Object":
report_warn(f"2. Unexpected GeneratedType category '{gt_category}' (expected 'Object')")
expected_prefix = f"{md_type}Object."
if gt_name and obj_name != "(unknown)" and not gt_name.startswith(expected_prefix):
report_warn(f"2. GeneratedType name '{gt_name}' does not start with '{expected_prefix}'")
type_id = gt.find(f"{{{XR_NS}}}TypeId")
value_id = gt.find(f"{{{XR_NS}}}ValueId")
if type_id is not None and type_id.text and not GUID_PATTERN.match(type_id.text):
report_error("2. Invalid TypeId UUID in GeneratedType")
check2_ok = False
if value_id is not None and value_id.text and not GUID_PATTERN.match(value_id.text):
report_error("2. Invalid ValueId UUID in GeneratedType")
check2_ok = False
if check2_ok:
report_ok(f"2. InternalInfo: ClassId correct, {len(gen_types)} GeneratedType")
if stopped:
finalize()
sys.exit(1)
# --- Check 3: Properties ---
if props_node is None:
report_error("3. Properties block missing")
else:
check3_ok = True
if name_node is None or not name_node.text:
report_error("3. Properties: Name is missing or empty")
check3_ok = False
else:
name_val = name_node.text
if not IDENT_PATTERN.match(name_val):
report_error(f"3. Properties: Name '{name_val}' is not a valid 1C identifier")
check3_ok = False
if len(name_val) > 80:
report_warn(f"3. Properties: Name '{name_val}' exceeds 80 characters ({len(name_val)})")
syn_node = props_node.find(f"{{{MD_NS}}}Synonym")
syn_present = False
if syn_node is not None:
syn_item = syn_node.find(f"{{{V8_NS}}}item")
if syn_item is not None:
syn_content = syn_item.find(f"{{{V8_NS}}}content")
if syn_content is not None and syn_content.text:
syn_present = True
default_form_node = props_node.find(f"{{{MD_NS}}}DefaultForm")
default_form_val = (default_form_node.text or "").strip() if default_form_node is not None else ""
aux_form_node = props_node.find(f"{{{MD_NS}}}AuxiliaryForm")
aux_form_val = (aux_form_node.text or "").strip() if aux_form_node is not None else ""
main_dcs_val = ""
if md_type == "ExternalReport":
main_dcs_node = props_node.find(f"{{{MD_NS}}}MainDataCompositionSchema")
main_dcs_val = (main_dcs_node.text or "").strip() if main_dcs_node is not None else ""
if check3_ok:
syn_info = "Synonym present" if syn_present else "no Synonym"
extras = ""
if default_form_val:
extras += ", DefaultForm set"
if main_dcs_val:
extras += ", MainDCS set"
report_ok(f'3. Properties: Name="{obj_name}", {syn_info}{extras}')
if stopped:
finalize()
sys.exit(1)
# --- Check 4: ChildObjects ---
child_obj_node = type_node.find(f"{{{MD_NS}}}ChildObjects")
form_names = []
template_names = []
if child_obj_node is not None:
check4_ok = True
child_counts = {}
last_order = -1
order_ok = True
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
child_tag = localname(child)
if child_tag not in ALLOWED_CHILD_TYPES:
report_error(f"4. ChildObjects: disallowed element '{child_tag}'")
check4_ok = False
continue
child_counts[child_tag] = child_counts.get(child_tag, 0) + 1
this_order = CHILD_TYPE_ORDER.get(child_tag, -1)
if this_order < last_order and order_ok:
report_warn(f"4. ChildObjects: '{child_tag}' appears after higher-order elements (expected: Attribute, TabularSection, Form, Template, Command)")
order_ok = False
last_order = this_order
if child_tag == "Form":
form_names.append((child.text or "").strip())
elif child_tag == "Template":
template_names.append((child.text or "").strip())
if check4_ok:
summary = ", ".join(f"{k}({v})" for k, v in sorted(child_counts.items(), key=lambda x: CHILD_TYPE_ORDER.get(x[0], 99)))
if summary:
report_ok(f"4. ChildObjects: {summary}")
else:
report_ok("4. ChildObjects: empty")
else:
report_ok("4. ChildObjects: absent")
if stopped:
finalize()
sys.exit(1)
# --- Check 5: DefaultForm / MainDCS cross-references ---
check5_ok = True
if default_form_val:
expected_prefix = f"{md_type}.{obj_name}.Form."
if default_form_val.startswith(expected_prefix):
ref_form_name = default_form_val[len(expected_prefix):]
if ref_form_name not in form_names:
report_error(f"5. DefaultForm references '{ref_form_name}', but no such Form in ChildObjects")
check5_ok = False
else:
report_warn(f"5. DefaultForm value '{default_form_val}' has unexpected prefix (expected '{expected_prefix}...')")
if aux_form_val:
expected_prefix = f"{md_type}.{obj_name}.Form."
if aux_form_val.startswith(expected_prefix):
ref_form_name = aux_form_val[len(expected_prefix):]
if ref_form_name not in form_names:
report_error(f"5. AuxiliaryForm references '{ref_form_name}', but no such Form in ChildObjects")
check5_ok = False
if main_dcs_val and md_type == "ExternalReport":
expected_prefix = f"ExternalReport.{obj_name}.Template."
if main_dcs_val.startswith(expected_prefix):
ref_tpl_name = main_dcs_val[len(expected_prefix):]
if ref_tpl_name not in template_names:
report_error(f"5. MainDataCompositionSchema references '{ref_tpl_name}', but no such Template in ChildObjects")
check5_ok = False
else:
report_warn(f"5. MainDataCompositionSchema value '{main_dcs_val}' has unexpected prefix")
if check5_ok:
refs = []
if default_form_val:
refs.append("DefaultForm")
if aux_form_val:
refs.append("AuxiliaryForm")
if main_dcs_val:
refs.append("MainDCS")
if refs:
report_ok(f"5. Cross-references: {', '.join(refs)} valid")
else:
report_ok("5. Cross-references: none to check")
if stopped:
finalize()
sys.exit(1)
# --- Check 6: Attributes ---
def check_attribute(node, context):
uuid = node.get("uuid", "")
if not uuid:
report_error(f"6. {context}Attribute missing uuid")
return False
if not GUID_PATTERN.match(uuid):
report_error(f"6. {context}Attribute has invalid uuid '{uuid}'")
return False
el_props = node.find(f"{{{MD_NS}}}Properties")
if el_props is None:
report_error(f"6. {context}Attribute (uuid={uuid}) missing Properties")
return False
el_name = el_props.find(f"{{{MD_NS}}}Name")
if el_name is None or not el_name.text:
report_error(f"6. {context}Attribute (uuid={uuid}) missing or empty Name")
return False
name_val = el_name.text
if not IDENT_PATTERN.match(name_val):
report_error(f"6. {context}Attribute '{name_val}' has invalid identifier")
return False
type_el = el_props.find(f"{{{MD_NS}}}Type")
if type_el is None:
report_error(f"6. {context}Attribute '{name_val}' missing Type block")
return False
v8_types = type_el.findall(f"{{{V8_NS}}}Type")
v8_type_sets = type_el.findall(f"{{{V8_NS}}}TypeSet")
if not v8_types and not v8_type_sets:
report_error(f"6. {context}Attribute '{name_val}' Type block has no v8:Type or v8:TypeSet")
return False
return True
if child_obj_node is not None:
attrs = child_obj_node.findall(f"{{{MD_NS}}}Attribute")
check6_ok = True
attr_count = 0
for attr in attrs:
if stopped:
break
ok = check_attribute(attr, "")
if not ok:
check6_ok = False
attr_count += 1
if attr_count > 0:
if check6_ok:
report_ok(f"6. Attributes: {attr_count} checked (UUID, Name, Type)")
else:
report_ok("6. Attributes: none")
else:
report_ok("6. Attributes: N/A")
if stopped:
finalize()
sys.exit(1)
# --- Check 7: TabularSections ---
if child_obj_node is not None:
ts_sections = child_obj_node.findall(f"{{{MD_NS}}}TabularSection")
if ts_sections:
check7_ok = True
ts_count = 0
ts_attr_total = 0
for ts in ts_sections:
if stopped:
break
ts_count += 1
ts_uuid = ts.get("uuid", "")
if not ts_uuid or not GUID_PATTERN.match(ts_uuid):
report_error(f"7. TabularSection #{ts_count}: invalid or missing uuid")
check7_ok = False
ts_props = ts.find(f"{{{MD_NS}}}Properties")
ts_name_node = ts_props.find(f"{{{MD_NS}}}Name") if ts_props is not None else None
ts_name = ts_name_node.text if ts_name_node is not None and ts_name_node.text else "(unnamed)"
if ts_name_node is None or not ts_name_node.text:
report_error(f"7. TabularSection #{ts_count}: missing or empty Name")
check7_ok = False
elif not IDENT_PATTERN.match(ts_name):
report_error(f"7. TabularSection '{ts_name}': invalid identifier")
check7_ok = False
ts_int_info = ts.find(f"{{{MD_NS}}}InternalInfo")
if ts_int_info is not None:
ts_gens = ts_int_info.findall(f"{{{XR_NS}}}GeneratedType")
if len(ts_gens) < 2:
report_warn(f"7. TabularSection '{ts_name}': expected 2 GeneratedType, found {len(ts_gens)}")
ts_child_obj = ts.find(f"{{{MD_NS}}}ChildObjects")
if ts_child_obj is not None:
ts_attrs = ts_child_obj.findall(f"{{{MD_NS}}}Attribute")
ts_attr_names = {}
for ta in ts_attrs:
ta_ok = check_attribute(ta, f"TabularSection '{ts_name}'.")
if not ta_ok:
check7_ok = False
ts_attr_total += 1
ta_props = ta.find(f"{{{MD_NS}}}Properties")
if ta_props is not None:
ta_name_node = ta_props.find(f"{{{MD_NS}}}Name")
if ta_name_node is not None and ta_name_node.text:
if ta_name_node.text in ts_attr_names:
report_error(f"7. Duplicate attribute '{ta_name_node.text}' in TabularSection '{ts_name}'")
check7_ok = False
else:
ts_attr_names[ta_name_node.text] = True
if check7_ok:
report_ok(f"7. TabularSections: {ts_count} sections, {ts_attr_total} inner attributes")
else:
report_ok("7. TabularSections: none")
else:
report_ok("7. TabularSections: N/A")
if stopped:
finalize()
sys.exit(1)
# --- Check 8: Name uniqueness ---
check8_ok = True
all_names = {}
if child_obj_node is not None:
name_kinds = [
("Attribute", f"{{{MD_NS}}}Attribute"),
("TabularSection", f"{{{MD_NS}}}TabularSection"),
("Command", f"{{{MD_NS}}}Command"),
]
for kind, xpath in name_kinds:
nodes = child_obj_node.findall(xpath)
for node in nodes:
np = node.find(f"{{{MD_NS}}}Properties")
if np is not None:
nn = np.find(f"{{{MD_NS}}}Name")
if nn is not None and nn.text:
nv = nn.text
if nv in all_names:
report_error(f"8. Duplicate name '{nv}' ({kind} conflicts with {all_names[nv]})")
check8_ok = False
else:
all_names[nv] = kind
for fn in form_names:
if fn in all_names:
report_error(f"8. Duplicate name '{fn}' (Form conflicts with {all_names[fn]})")
check8_ok = False
else:
all_names[fn] = "Form"
for tn in template_names:
if tn in all_names:
report_error(f"8. Duplicate name '{tn}' (Template conflicts with {all_names[tn]})")
check8_ok = False
else:
all_names[tn] = "Template"
if check8_ok:
report_ok(f"8. Name uniqueness: {len(all_names)} names, all unique")
if stopped:
finalize()
sys.exit(1)
# --- Check 9: File existence ---
check9_ok = True
files_checked = 0
obj_dir = os.path.join(src_dir, obj_name)
for fn in form_names:
form_meta_xml = os.path.join(obj_dir, "Forms", f"{fn}.xml")
if not os.path.isfile(form_meta_xml):
report_error(f"9. Missing form descriptor: Forms/{fn}.xml")
check9_ok = False
else:
files_checked += 1
form_xml = os.path.join(obj_dir, "Forms", fn, "Ext", "Form.xml")
if not os.path.isfile(form_xml):
report_error(f"9. Missing form layout: Forms/{fn}/Ext/Form.xml")
check9_ok = False
else:
files_checked += 1
for tn in template_names:
tpl_meta_xml = os.path.join(obj_dir, "Templates", f"{tn}.xml")
if not os.path.isfile(tpl_meta_xml):
report_error(f"9. Missing template descriptor: Templates/{tn}.xml")
check9_ok = False
else:
files_checked += 1
tpl_ext_dir = os.path.join(obj_dir, "Templates", tn, "Ext")
if os.path.isdir(tpl_ext_dir):
tpl_files = [f for f in os.listdir(tpl_ext_dir) if f.startswith("Template.")]
if not tpl_files:
report_error(f"9. Missing template content: Templates/{tn}/Ext/Template.*")
check9_ok = False
else:
files_checked += 1
else:
report_error(f"9. Missing template Ext directory: Templates/{tn}/Ext/")
check9_ok = False
obj_module = os.path.join(obj_dir, "Ext", "ObjectModule.bsl")
if os.path.isfile(obj_module):
files_checked += 1
if check9_ok:
if files_checked > 0:
report_ok(f"9. File existence: {files_checked} files verified")
else:
report_ok("9. File existence: no forms/templates to check")
if stopped:
finalize()
sys.exit(1)
# --- Check 10: Form descriptors structure ---
check10_ok = True
forms_checked = 0
for fn in form_names:
form_meta_xml = os.path.join(obj_dir, "Forms", f"{fn}.xml")
if not os.path.isfile(form_meta_xml):
continue
try:
f_parser = etree.XMLParser(remove_blank_text=True)
f_tree = etree.parse(form_meta_xml, f_parser)
f_root = f_tree.getroot()
if localname(f_root) != "MetaDataObject":
report_error(f"10. Form '{fn}': root element is '{localname(f_root)}', expected 'MetaDataObject'")
check10_ok = False
continue
f_type_node = f_root.find(f"{{{MD_NS}}}Form")
if f_type_node is None:
report_error(f"10. Form '{fn}': missing <Form> element")
check10_ok = False
continue
f_uuid = f_type_node.get("uuid", "")
if not f_uuid or not GUID_PATTERN.match(f_uuid):
report_error(f"10. Form '{fn}': invalid or missing uuid")
check10_ok = False
f_props = f_type_node.find(f"{{{MD_NS}}}Properties")
if f_props is not None:
f_name = f_props.find(f"{{{MD_NS}}}Name")
if f_name is not None and f_name.text != fn:
report_error(f"10. Form '{fn}': Name in descriptor is '{f_name.text}', expected '{fn}'")
check10_ok = False
f_type = f_props.find(f"{{{MD_NS}}}FormType")
if f_type is not None and f_type.text != "Managed":
report_warn(f"10. Form '{fn}': FormType is '{f_type.text}' (expected 'Managed')")
forms_checked += 1
except Exception as e:
report_error(f"10. Form '{fn}': XML parse error: {e}")
check10_ok = False
if check10_ok:
if forms_checked > 0:
report_ok(f"10. Form descriptors: {forms_checked} checked")
else:
report_ok("10. Form descriptors: none to check")
# --- Final output ---
finalize()
if errors > 0:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# erf-init v1.0 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Init 1C external report scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir
uuid1 = new_uuid()
uuid2 = new_uuid()
uuid3 = new_uuid()
uuid4 = new_uuid()
# --- Properties ---
main_dcs_value = ""
child_objects_content = ""
if args.WithSKD:
main_dcs_value = f"ExternalReport.{name}.Template.ОсновнаяСхемаКомпоновкиДанных"
child_objects_content = f"\n\t\t\t<Template>ОсновнаяСхемаКомпоновкиДанных</Template>\n"
main_dcs_element = f"<MainDataCompositionSchema>{main_dcs_value}</MainDataCompositionSchema>" if main_dcs_value else "<MainDataCompositionSchema/>"
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<ExternalReport uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
\t\t\t</xr:ContainedObject>
\t\t\t<xr:GeneratedType name="ExternalReportObject.{name}" category="Object">
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<DefaultForm/>
\t\t\t<AuxiliaryForm/>
\t\t\t{main_dcs_element}
\t\t\t<DefaultSettingsForm/>
\t\t\t<AuxiliarySettingsForm/>
\t\t\t<DefaultVariantForm/>
\t\t\t<VariantsStorage/>
\t\t\t<SettingsStorage/>
\t\t</Properties>
\t\t{child_objects_xml}
\t</ExternalReport>
</MetaDataObject>'''
root_file = os.path.join(src_dir, f"{name}.xml")
report_dir = os.path.join(src_dir, name)
if os.path.exists(root_file):
print(f"Файл уже существует: {root_file}", file=sys.stderr)
sys.exit(1)
os.makedirs(src_dir, exist_ok=True)
ext_dir = os.path.join(report_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
print(f"[OK] Создан отчёт: {root_file}")
print(f" Каталог: {report_dir}")
print(f" Модуль: {module_path}")
# --- СКД-макет ---
if args.WithSKD:
templates_dir = os.path.join(report_dir, "Templates")
skd_name = "ОсновнаяСхемаКомпоновкиДанных"
skd_meta_path = os.path.join(templates_dir, f"{skd_name}.xml")
skd_ext_dir = os.path.join(templates_dir, skd_name, "Ext")
os.makedirs(skd_ext_dir, exist_ok=True)
skd_uuid = new_uuid()
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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" version="2.17">
\t<Template uuid="{skd_uuid}">
\t\t<Properties>
\t\t\t<Name>{skd_name}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>Основная схема компоновки данных</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<TemplateType>DataCompositionSchema</TemplateType>
\t\t</Properties>
\t</Template>
</MetaDataObject>'''
write_utf8_bom(skd_meta_path, skd_meta_xml)
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"
\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"
\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
\t<dataSource>
\t\t<name>ИсточникДанных1</name>
\t\t<dataSourceType>Local</dataSourceType>
\t</dataSource>
</DataCompositionSchema>'''
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
write_utf8_bom(skd_file_path, skd_content)
print(f" СКД: {skd_meta_path}")
print(f" Тело: {skd_file_path}")
if __name__ == '__main__':
main()
+446
View File
@@ -0,0 +1,446 @@
#!/usr/bin/env python3
# form-add v1.0 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
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."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add managed form to 1C config object", allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Purpose", default="Object")
parser.add_argument("-SetDefault", action="store_true")
args = parser.parse_args()
object_path = args.ObjectPath
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
purpose = args.Purpose
set_default = args.SetDefault
# --- Phase 1: Determine object type ---
if not os.path.exists(object_path):
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
sys.exit(1)
object_xml_full = os.path.abspath(object_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml)
root = tree.getroot()
supported_types = [
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
]
object_type = None
object_node = None
for t in supported_types:
node = root.find(f".//md:{t}", NSMAP)
if node is not None:
object_type = t
object_node = node
break
if object_type is None:
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
sys.exit(1)
# Object name from Properties/Name
name_node = root.find(f".//md:{object_type}/md:Properties/md:Name", NSMAP)
if name_node is None or not name_node.text:
print("Не удалось определить имя объекта из Properties/Name", file=sys.stderr)
sys.exit(1)
object_name = name_node.text
print()
print("=== form-add ===")
print()
print(f"Object: {object_type}.{object_name}")
# --- Phase 2: Validate Purpose ---
# Normalize: capitalize first letter, lowercase rest
purpose = purpose[0].upper() + purpose[1:].lower()
valid_purposes = ["Object", "List", "Choice", "Record"]
if purpose not in valid_purposes:
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
sys.exit(1)
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"]
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
if purpose == "List":
if object_type == "DataProcessor":
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
sys.exit(1)
elif purpose == "Choice":
if object_type in processor_like_types or object_type == "InformationRegister":
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
sys.exit(1)
elif purpose == "Record":
if object_type != "InformationRegister":
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
sys.exit(1)
# --- Phase 3: Create files ---
object_dir = os.path.splitext(object_xml_full)[0]
forms_dir = os.path.join(object_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 3a. Form metadata ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject 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"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_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'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 3b. Form.xml ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' 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: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"'
)
if purpose in ("List", "Choice"):
# Dynamic list
main_table = f"{object_type}.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
'\t\t\t<Type>\n'
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<Settings xsi:type="DynamicList">\n'
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
'\t\t\t</Settings>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
elif purpose == "Record":
# Information register record
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
else:
# Object — object form
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
attr_type_map = {
"Document": "DocumentObject",
"Catalog": "CatalogObject",
"DataProcessor": "DataProcessorObject",
"Report": "ReportObject",
"ExternalDataProcessor": "ExternalDataProcessorObject",
"ExternalReport": "ExternalReportObject",
"ChartOfAccounts": "ChartOfAccountsObject",
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
"ExchangePlan": "ExchangePlanObject",
"BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager",
}
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
else:
write_text_with_bom(form_xml_path, form_xml)
# --- 3c. Module.bsl ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
'\n'
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
if os.path.exists(module_path):
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
else:
write_text_with_bom(module_path, module_bsl)
# --- Phase 4: Register in parent object ---
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(f".//md:{object_type}/md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1)
# Add <Form>$FormName</Form>
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
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(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault ---
is_first_form_for_purpose = False
default_prop_name = None
default_value = f"{object_type}.{object_name}.Form.{form_name}"
# Determine property name for DefaultForm
if purpose == "Object":
if object_type in processor_like_types:
default_prop_name = "DefaultForm"
else:
default_prop_name = "DefaultObjectForm"
elif purpose == "List":
default_prop_name = "DefaultListForm"
elif purpose == "Choice":
default_prop_name = "DefaultChoiceForm"
elif purpose == "Record":
default_prop_name = "DefaultRecordForm"
# Check if value is already set
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
if default_node is not None:
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
default_updated = False
if set_default or is_first_form_for_purpose:
if default_node is not None:
default_node.text = default_value
default_updated = True
# Save with BOM
save_xml_with_bom(tree, object_xml_full)
# --- Phase 5: Output ---
obj_dir_name = os.path.dirname(object_path)
obj_base_name = os.path.splitext(os.path.basename(object_path))[0]
print("Created:")
print(f" Metadata: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}.xml")
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated:
print(f"{default_prop_name}: {default_value}")
print()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,599 @@
#!/usr/bin/env python3
# form-info v1.0 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from lxml import etree
# --- Namespace map ---
NSMAP = {
"d": "http://v8.1c.ru/8.3/xcf/logform",
"v8": "http://v8.1c.ru/8.1/data/core",
"v8ui": "http://v8.1c.ru/8.1/data/ui",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
"xs": "http://www.w3.org/2001/XMLSchema",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"cfg": "http://v8.1c.ru/8.1/data/enterprise/current-config",
"dcsset": "http://v8.1c.ru/8.1/data-composition-system/settings",
}
# --- Skip elements ---
SKIP_ELEMENTS = {
"ExtendedTooltip",
"ContextMenu",
"AutoCommandBar",
"SearchStringAddition",
"ViewStatusAddition",
"SearchControlAddition",
"ColumnGroup",
}
# --- Helper: extract multilang text ---
def get_ml_text(node):
if node is None:
return ""
content = node.find("v8:item/v8:content", NSMAP)
if content is not None and content.text:
return content.text
# Fallback: concatenate all text
text = "".join(node.itertext()).strip()
if text:
return text
return ""
# --- Helper: format type compactly ---
def format_type(type_node):
if type_node is None or len(type_node) == 0:
return ""
type_set = type_node.find("v8:TypeSet", NSMAP)
if type_set is not None:
val = type_set.text or ""
if val.startswith("cfg:"):
val = val[4:]
return val
types = type_node.findall("v8:Type", NSMAP)
if len(types) == 0:
return ""
parts = []
for t in types:
raw = t.text or ""
if raw == "xs:string":
sq = type_node.find("v8:StringQualifiers/v8:Length", NSMAP)
length = int(sq.text) if sq is not None and sq.text else 0
if length > 0:
parts.append(f"string({length})")
else:
parts.append("string")
elif raw == "xs:decimal":
nq = type_node.find("v8:NumberQualifiers", NSMAP)
if nq is not None:
d = nq.find("v8:Digits", NSMAP)
f = nq.find("v8:FractionDigits", NSMAP)
digits = d.text if d is not None and d.text else "0"
frac = f.text if f is not None and f.text else "0"
parts.append(f"decimal({digits},{frac})")
else:
parts.append("decimal")
elif raw == "xs:boolean":
parts.append("boolean")
elif raw == "xs:dateTime":
dq = type_node.find("v8:DateQualifiers/v8:DateFractions", NSMAP)
if dq is not None:
frac_text = dq.text or ""
if frac_text == "Date":
parts.append("date")
elif frac_text == "Time":
parts.append("time")
else:
parts.append("dateTime")
else:
parts.append("dateTime")
elif raw == "xs:binary":
parts.append("binary")
elif raw.startswith("cfg:"):
parts.append(raw[4:])
elif raw == "v8:ValueTable":
parts.append("ValueTable")
elif raw == "v8:ValueTree":
parts.append("ValueTree")
elif raw == "v8:ValueListType":
parts.append("ValueList")
elif raw == "v8:TypeDescription":
parts.append("TypeDescription")
elif raw == "v8:Universal":
parts.append("Universal")
elif raw == "v8:FixedArray":
parts.append("FixedArray")
elif raw == "v8:FixedStructure":
parts.append("FixedStructure")
elif raw == "v8ui:FormattedString":
parts.append("FormattedString")
elif raw == "v8ui:Picture":
parts.append("Picture")
elif raw == "v8ui:Color":
parts.append("Color")
elif raw == "v8ui:Font":
parts.append("Font")
elif raw.startswith("dcsset:"):
parts.append(raw.replace("dcsset:", "DCS."))
elif raw.startswith("dcssch:"):
parts.append(raw.replace("dcssch:", "DCS."))
elif raw.startswith("dcscor:"):
parts.append(raw.replace("dcscor:", "DCS."))
else:
parts.append(raw)
return " | ".join(parts)
# --- Helper: check if title differs from name ---
def test_title_differs(node, name):
title_node = node.find("d:Title", NSMAP)
if title_node is None:
return None
title_text = get_ml_text(title_node)
if not title_text:
return None
# Normalize: remove spaces, lowercase
norm_title = title_text.replace(" ", "").lower()
norm_name = name.lower()
if norm_title == norm_name:
return None
return title_text
# --- Helper: get events as compact string ---
def get_events_str(node):
events_node = node.find("d:Events", NSMAP)
if events_node is None:
return ""
evts = []
for e in events_node.findall("d:Event", NSMAP):
e_name = e.get("name", "")
ct = e.get("callType", "")
if ct:
evts.append(f"{e_name}[{ct}]")
else:
evts.append(e_name)
if len(evts) == 0:
return ""
return " {" + ", ".join(evts) + "}"
# --- Helper: get flags ---
def get_flags(node):
flags = []
vis = node.find("d:Visible", NSMAP)
if vis is not None and vis.text == "false":
flags.append("visible:false")
en = node.find("d:Enabled", NSMAP)
if en is not None and en.text == "false":
flags.append("enabled:false")
ro = node.find("d:ReadOnly", NSMAP)
if ro is not None and ro.text == "true":
flags.append("ro")
if len(flags) == 0:
return ""
return " [" + ",".join(flags) + "]"
# --- Element type abbreviations ---
def get_element_tag(node):
local_name = etree.QName(node.tag).localname
if local_name == "UsualGroup":
group_node = node.find("d:Group", NSMAP)
orient = ""
if group_node is not None:
g_text = group_node.text or ""
if g_text == "Vertical":
orient = ":V"
elif g_text == "Horizontal":
orient = ":H"
elif g_text == "AlwaysHorizontal":
orient = ":AH"
elif g_text == "AlwaysVertical":
orient = ":AV"
beh = node.find("d:Behavior", NSMAP)
collapse = ""
if beh is not None and beh.text == "Collapsible":
collapse = ",collapse"
return f"[Group{orient}{collapse}]"
elif local_name == "InputField":
return "[Input]"
elif local_name == "CheckBoxField":
return "[Check]"
elif local_name == "LabelDecoration":
return "[Label]"
elif local_name == "LabelField":
return "[LabelField]"
elif local_name == "PictureDecoration":
return "[Picture]"
elif local_name == "PictureField":
return "[PicField]"
elif local_name == "CalendarField":
return "[Calendar]"
elif local_name == "Table":
return "[Table]"
elif local_name == "Button":
return "[Button]"
elif local_name == "CommandBar":
return "[CmdBar]"
elif local_name == "Pages":
return "[Pages]"
elif local_name == "Page":
return "[Page]"
elif local_name == "Popup":
return "[Popup]"
elif local_name == "ButtonGroup":
return "[BtnGroup]"
else:
return f"[{local_name}]"
# --- Count significant children (for Page summary) ---
def count_significant_children(child_items_node):
if child_items_node is None:
return 0
count = 0
for child in child_items_node:
if not isinstance(child.tag, str):
continue
ln = etree.QName(child.tag).localname
if ln in SKIP_ELEMENTS:
continue
count += 1
return count
# --- Build element tree recursively ---
def build_tree(child_items_node, prefix, tree_lines):
if child_items_node is None:
return
# Collect significant children
children = []
for child in child_items_node:
if not isinstance(child.tag, str):
continue
ln = etree.QName(child.tag).localname
if ln in SKIP_ELEMENTS:
continue
children.append(child)
for i, child in enumerate(children):
last = (i == len(children) - 1)
connector = "\u2514\u2500" if last else "\u251C\u2500"
continuation = " " if last else "\u2502 "
tag = get_element_tag(child)
name = child.get("name", "")
flags = get_flags(child)
events = get_events_str(child)
# DataPath or CommandName
binding = ""
dp = child.find("d:DataPath", NSMAP)
if dp is not None and dp.text:
binding = f" -> {dp.text}"
else:
cn = child.find("d:CommandName", NSMAP)
if cn is not None and cn.text:
cn_val = cn.text
m = re.match(r'^Form\.StandardCommand\.(.+)$', cn_val)
if m:
binding = f" -> {m.group(1)} [std]"
else:
m = re.match(r'^Form\.Command\.(.+)$', cn_val)
if m:
binding = f" -> {m.group(1)} [cmd]"
else:
binding = f" -> {cn_val}"
# Title differs?
title_str = ""
diff_title = test_title_differs(child, name)
if diff_title:
title_str = f" [title:{diff_title}]"
line = f"{prefix}{connector} {tag} {name}{binding}{flags}{title_str}{events}"
tree_lines.append(line)
# Recurse into containers (but not Page -- show summary)
local_name = etree.QName(child.tag).localname
if local_name == "Page":
ci = child.find("d:ChildItems", NSMAP)
cnt = count_significant_children(ci)
# Append count to last line
tree_lines[-1] = tree_lines[-1] + f" ({cnt} items)"
elif local_name in ("UsualGroup", "Pages", "Table", "CommandBar", "ButtonGroup", "Popup"):
ci = child.find("d:ChildItems", NSMAP)
if ci is not None:
build_tree(ci, prefix + continuation, tree_lines)
# --- Main ---
def main():
parser = argparse.ArgumentParser(description="Analyze 1C managed form structure", allow_abbrev=False)
parser.add_argument("-FormPath", required=True, help="Path to Form.xml")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
args = parser.parse_args()
form_path = args.FormPath
limit = args.Limit
offset = args.Offset
# --- Validate path ---
if not os.path.isfile(form_path):
print(f"File not found: {form_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(form_path, parser_xml)
root = tree.getroot()
# --- Detect extension (BaseForm) ---
base_form_node = root.find("d:BaseForm", NSMAP)
is_extension = base_form_node is not None
# --- Determine form name and object from path ---
resolved_path = os.path.abspath(form_path)
parts = resolved_path.replace("\\", "/").split("/")
form_name = ""
object_context = ""
# Look for /Forms/<FormName>/Ext/Form.xml pattern
forms_idx = -1
for i in range(len(parts) - 1, -1, -1):
if parts[i] == "Forms":
forms_idx = i
break
if forms_idx >= 0 and (forms_idx + 1) < len(parts):
form_name = parts[forms_idx + 1]
# Object is 2 levels up: .../<ObjectType>/<ObjectName>/Forms/...
if forms_idx >= 2:
obj_type = parts[forms_idx - 2]
obj_name = parts[forms_idx - 1]
object_context = f"{obj_type}.{obj_name}"
else:
# CommonForms pattern: .../<ObjectType>/<FormName>/Ext/Form.xml
ext_idx = -1
for i in range(len(parts) - 1, -1, -1):
if parts[i] == "Ext":
ext_idx = i
break
if ext_idx >= 2:
form_name = parts[ext_idx - 1]
obj_type = parts[ext_idx - 2]
object_context = obj_type
else:
form_name = os.path.splitext(os.path.basename(form_path))[0]
# --- Collect output ---
lines = []
# Header -- include Title if present
title_node = root.find("d:Title", NSMAP)
form_title = None
if title_node is not None:
form_title = get_ml_text(title_node)
if not form_title:
form_title = "".join(title_node.itertext()).strip() or None
ext_marker = " [EXTENSION]" if is_extension else ""
header = f"=== Form: {form_name}{ext_marker}"
if form_title:
header += f'"{form_title}"'
if object_context:
header += f" ({object_context})"
header += " ==="
lines.append(header)
# --- Form properties (Title excluded -- shown in header) ---
prop_names = [
"Width", "Height", "Group",
"WindowOpeningMode", "EnterKeyBehavior", "AutoTitle", "AutoURL",
"AutoFillCheck", "Customizable", "CommandBarLocation",
"SaveDataInSettings", "AutoSaveDataInSettings",
"AutoTime", "UsePostingMode", "RepostOnWrite",
"UseForFoldersAndItems",
"ReportResult", "DetailsData", "ReportFormType",
"VerticalScroll", "ScalingMode",
]
props = []
for pn in prop_names:
p_node = root.find(f"d:{pn}", NSMAP)
if p_node is not None:
val = get_ml_text(p_node)
if not val:
val = "".join(p_node.itertext()).strip()
props.append(f"{pn}={val}")
if len(props) > 0:
lines.append("")
lines.append("Properties: " + ", ".join(props))
# --- Excluded commands ---
excluded_cmds = []
for ec in root.findall("d:CommandSet/d:ExcludedCommand", NSMAP):
excluded_cmds.append(ec.text or "")
# --- Form events ---
form_events = root.find("d:Events", NSMAP)
if form_events is not None and len(form_events) > 0:
lines.append("")
lines.append("Events:")
for e in form_events.findall("d:Event", NSMAP):
e_name = e.get("name", "")
e_handler = e.text or ""
ct = e.get("callType", "")
ct_str = f"[{ct}]" if ct else ""
lines.append(f" {e_name}{ct_str} -> {e_handler}")
# --- Element tree ---
child_items = root.find("d:ChildItems", NSMAP)
if child_items is not None:
lines.append("")
lines.append("Elements:")
tree_lines = []
build_tree(child_items, " ", tree_lines)
lines.extend(tree_lines)
# --- Attributes ---
attrs_node = root.find("d:Attributes", NSMAP)
if attrs_node is not None:
attr_lines = []
for attr in attrs_node.findall("d:Attribute", NSMAP):
a_name = attr.get("name", "")
type_node = attr.find("d:Type", NSMAP)
type_str = format_type(type_node)
main_attr = attr.find("d:MainAttribute", NSMAP)
is_main = main_attr is not None and main_attr.text == "true"
prefix_char = "*" if is_main else " "
main_suffix = " (main)" if is_main else ""
# DynamicList: show MainTable
settings = attr.find("d:Settings", NSMAP)
dyn_table = ""
if settings is not None and type_str == "DynamicList":
mt = settings.find("d:MainTable", NSMAP)
if mt is not None and mt.text:
dyn_table = f" -> {mt.text}"
# ValueTable/ValueTree columns
col_str = ""
columns = attr.find("d:Columns", NSMAP)
if columns is not None and type_str in ("ValueTable", "ValueTree"):
cols = []
for col in columns.findall("d:Column", NSMAP):
c_name = col.get("name", "")
c_type_node = col.find("d:Type", NSMAP)
c_type = format_type(c_type_node)
if c_type:
cols.append(f"{c_name}: {c_type}")
else:
cols.append(c_name)
if len(cols) > 0:
col_str = " [" + ", ".join(cols) + "]"
if type_str or col_str or dyn_table:
line = f" {prefix_char}{a_name}: {type_str}{col_str}{dyn_table}{main_suffix}"
else:
line = f" {prefix_char}{a_name}{main_suffix}"
attr_lines.append(line)
if len(attr_lines) > 0:
lines.append("")
lines.append("Attributes:")
lines.extend(attr_lines)
# --- Parameters ---
params_node = root.find("d:Parameters", NSMAP)
if params_node is not None:
param_lines = []
for param in params_node.findall("d:Parameter", NSMAP):
p_name = param.get("name", "")
type_node = param.find("d:Type", NSMAP)
type_str = format_type(type_node)
key_param = param.find("d:KeyParameter", NSMAP)
is_key = key_param is not None and key_param.text == "true"
key_suffix = " (key)" if is_key else ""
if type_str:
param_lines.append(f" {p_name}: {type_str}{key_suffix}")
else:
param_lines.append(f" {p_name}{key_suffix}")
if len(param_lines) > 0:
lines.append("")
lines.append("Parameters:")
lines.extend(param_lines)
# --- Commands ---
cmds_node = root.find("d:Commands", NSMAP)
if cmds_node is not None:
cmd_lines = []
for cmd in cmds_node.findall("d:Command", NSMAP):
c_name = cmd.get("name", "")
shortcut = cmd.find("d:Shortcut", NSMAP)
sc_str = f" [{shortcut.text}]" if shortcut is not None and shortcut.text else ""
# Collect all Action elements (may have multiple with callType)
actions = cmd.findall("d:Action", NSMAP)
if len(actions) > 1:
act_parts = []
for a in actions:
ct = a.get("callType", "")
ct_str = f"[{ct}]" if ct else ""
act_parts.append(f"{a.text or ''}{ct_str}")
action_str = " -> " + ", ".join(act_parts)
elif len(actions) == 1:
ct = actions[0].get("callType", "")
ct_str = f"[{ct}]" if ct else ""
action_str = f" -> {actions[0].text or ''}{ct_str}"
else:
action_str = ""
cmd_lines.append(f" {c_name}{action_str}{sc_str}")
if len(cmd_lines) > 0:
lines.append("")
lines.append("Commands:")
lines.extend(cmd_lines)
# --- BaseForm footer ---
if is_extension:
bf_version = base_form_node.get("version", "")
bf_str = f"present (version {bf_version})" if bf_version else "present"
lines.append("")
lines.append(f"BaseForm: {bf_str}")
# --- Truncation protection ---
total_lines = len(lines)
if offset > 0:
if offset >= total_lines:
print(f"[INFO] Offset {offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
lines = lines[offset:]
if len(lines) > limit:
shown = lines[:limit]
for l in shown:
print(l)
remaining = total_lines - offset - limit
print("")
print(f"[TRUNCATED] Shown {limit} of {total_lines} lines. Use -Offset {offset + limit} to continue.")
else:
for l in lines:
print(l)
if __name__ == "__main__":
main()
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
# remove-form v1.0 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
form_name = args.FormName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
form_dir = os.path.join(forms_dir, form_name)
if not os.path.exists(form_meta_path):
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(form_dir):
shutil.rmtree(form_dir)
print(f"[OK] Удалён каталог: {form_dir}")
os.remove(form_meta_path)
print(f"[OK] Удалён файл: {form_meta_path}")
# --- 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()
# Remove <Form>FormName</Form> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
if node.text and node.text.strip() == form_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
break
# Clear DefaultForm if it pointed to removed form
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None and default_form.text:
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
default_form.text = ""
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
if __name__ == "__main__":
main()
@@ -0,0 +1,589 @@
#!/usr/bin/env python3
# form-validate v1.0 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from lxml import etree
F_NS = "http://v8.1c.ru/8.3/xcf/logform"
V8_NS = "http://v8.1c.ru/8.1/data/core"
NSMAP = {"f": F_NS, "v8": V8_NS}
def localname(el):
return etree.QName(el.tag).localname
def main():
parser = argparse.ArgumentParser(description="Validate 1C managed form", allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-MaxErrors", type=int, default=30)
args = parser.parse_args()
form_path = args.FormPath
max_errors = args.MaxErrors
if not os.path.isfile(form_path):
print(f"File not found: {form_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
try:
xml_parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(form_path, xml_parser)
except Exception as e:
print(f"[ERROR] XML parse error: {e}")
print()
print("---")
print("Errors: 1, Warnings: 0")
sys.exit(1)
root = tree.getroot()
errors = 0
warnings = 0
stopped = False
def report_ok(msg):
print(f"[OK] {msg}")
def report_error(msg):
nonlocal errors, stopped
errors += 1
print(f"[ERROR] {msg}")
if errors >= max_errors:
stopped = True
def report_warn(msg):
nonlocal warnings
warnings += 1
print(f"[WARN] {msg}")
# --- Form name from path ---
form_name = os.path.splitext(os.path.basename(form_path))[0]
parent_dir = os.path.dirname(form_path)
if parent_dir:
ext_dir = os.path.basename(parent_dir)
if ext_dir == "Ext":
form_dir = os.path.dirname(parent_dir)
if form_dir:
form_name = os.path.basename(form_dir)
print(f"=== Validation: {form_name} ===")
print()
# Early BaseForm detection
has_base_form = root.find(f"{{{F_NS}}}BaseForm") is not None
# --- Check 1: Root element and version ---
if localname(root) != "Form":
report_error(f"Root element is '{localname(root)}', expected 'Form'")
else:
version = root.get("version", "")
if version == "2.17":
report_ok(f"Root element: Form version={version}")
elif version:
report_warn(f"Form version='{version}' (expected 2.17)")
else:
report_warn("Form version attribute missing")
# --- Check 2: AutoCommandBar ---
if not stopped:
acb = root.find(f"{{{F_NS}}}AutoCommandBar")
if acb is not None:
acb_name = acb.get("name", "")
acb_id = acb.get("id", "")
if acb_id == "-1":
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
else:
report_error(f"AutoCommandBar id='{acb_id}', expected '-1'")
else:
report_error("AutoCommandBar element missing")
# --- Collect all elements with IDs ---
element_ids = {} # id -> name
all_elements = [] # list of dicts {Name, Tag, Id, ParentName, Node}
def collect_elements(node, parent_name):
nonlocal stopped
for child in node:
if not isinstance(child.tag, str):
continue
name = child.get("name", "")
eid = child.get("id", "")
if name and eid:
tag = localname(child)
all_elements.append({
"Name": name,
"Tag": tag,
"Id": eid,
"ParentName": parent_name,
"Node": child,
})
if eid != "-1":
if eid in element_ids:
report_error(f"Duplicate element id={eid}: '{name}' and '{element_ids[eid]}'")
else:
element_ids[eid] = name
child_items = child.find(f"{{{F_NS}}}ChildItems")
if child_items is not None:
collect_elements(child_items, name)
child_items_root = root.find(f"{{{F_NS}}}ChildItems")
if child_items_root is not None:
collect_elements(child_items_root, "(root)")
acb = root.find(f"{{{F_NS}}}AutoCommandBar")
if acb is not None:
acb_children = acb.find(f"{{{F_NS}}}ChildItems")
if acb_children is not None:
collect_elements(acb_children, "\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c")
# --- Check 3: Unique element IDs ---
if not stopped:
# Duplicates already reported during collection
dup_count = 0
id_counts = {}
for el in all_elements:
eid = el["Id"]
if eid == "-1":
continue
id_counts[eid] = id_counts.get(eid, 0) + 1
dup_count = sum(1 for v in id_counts.values() if v > 1)
if dup_count == 0:
report_ok(f"Unique element IDs: {len(element_ids)} elements")
# --- Collect attributes (separate ID pool) ---
attr_map = {} # name -> node
attr_ids = {} # id -> name
attr_nodes_parent = root.find(f"{{{F_NS}}}Attributes")
attr_nodes = []
if attr_nodes_parent is not None:
attr_nodes = attr_nodes_parent.findall(f"{{{F_NS}}}Attribute")
for attr in attr_nodes:
attr_name = attr.get("name", "")
attr_id = attr.get("id", "")
if attr_name:
attr_map[attr_name] = attr
if attr_id:
if attr_id in attr_ids:
report_error(f"Duplicate attribute id={attr_id}: '{attr_name}' and '{attr_ids[attr_id]}'")
else:
attr_ids[attr_id] = attr_name
# Column IDs uniqueness within parent
col_ids = {}
columns = attr.find(f"{{{F_NS}}}Columns")
if columns is not None:
for col in columns.findall(f"{{{F_NS}}}Column"):
col_id = col.get("id", "")
col_name = col.get("name", "")
if col_id:
if col_id in col_ids:
report_error(f"Duplicate column id={col_id} in '{attr_name}': '{col_name}' and '{col_ids[col_id]}'")
else:
col_ids[col_id] = col_name
if not stopped:
if attr_ids:
report_ok(f"Unique attribute IDs: {len(attr_ids)} entries")
# --- Collect commands (separate ID pool) ---
cmd_map = {} # name -> node
cmd_ids = {} # id -> name
cmd_nodes_parent = root.find(f"{{{F_NS}}}Commands")
cmd_nodes = []
if cmd_nodes_parent is not None:
cmd_nodes = cmd_nodes_parent.findall(f"{{{F_NS}}}Command")
for cmd in cmd_nodes:
cmd_name = cmd.get("name", "")
cmd_id = cmd.get("id", "")
if cmd_name:
cmd_map[cmd_name] = cmd
if cmd_id:
if cmd_id in cmd_ids:
report_error(f"Duplicate command id={cmd_id}: '{cmd_name}' and '{cmd_ids[cmd_id]}'")
else:
cmd_ids[cmd_id] = cmd_name
if not stopped:
if cmd_ids:
report_ok(f"Unique command IDs: {len(cmd_ids)} entries")
# --- Check 4: Companion elements ---
companion_rules = {
"InputField": ["ContextMenu", "ExtendedTooltip"],
"CheckBoxField": ["ContextMenu", "ExtendedTooltip"],
"LabelDecoration": ["ContextMenu", "ExtendedTooltip"],
"LabelField": ["ContextMenu", "ExtendedTooltip"],
"PictureDecoration": ["ContextMenu", "ExtendedTooltip"],
"PictureField": ["ContextMenu", "ExtendedTooltip"],
"CalendarField": ["ContextMenu", "ExtendedTooltip"],
"UsualGroup": ["ExtendedTooltip"],
"Pages": ["ExtendedTooltip"],
"Page": ["ExtendedTooltip"],
"Button": ["ExtendedTooltip"],
"Table": ["ContextMenu", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"],
}
if not stopped:
companion_errors = 0
companion_checked = 0
for el in all_elements:
if stopped:
break
tag = el["Tag"]
el_name = el["Name"]
node = el["Node"]
if tag not in companion_rules:
continue
required = companion_rules[tag]
companion_checked += 1
for comp_tag in required:
comp_node = node.find(f"{{{F_NS}}}{comp_tag}")
if comp_node is None:
report_error(f"[{tag}] '{el_name}': missing companion <{comp_tag}>")
companion_errors += 1
if companion_errors == 0 and companion_checked > 0:
report_ok(f"Companion elements: {companion_checked} elements checked")
# --- Check 5: DataPath -> Attribute references ---
if not stopped:
path_errors = 0
path_checked = 0
path_base_skipped = 0
skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"}
for el in all_elements:
if stopped:
break
tag = el["Tag"]
el_name = el["Name"]
node = el["Node"]
if tag in skip_tags:
continue
if has_base_form and el["Id"]:
try:
if int(el["Id"]) < 1000000:
path_base_skipped += 1
continue
except (ValueError, TypeError):
pass
dp_node = node.find(f"{{{F_NS}}}DataPath")
if dp_node is None:
continue
data_path = (dp_node.text or "").strip()
if not data_path:
continue
path_checked += 1
clean_path = re.sub(r'\[\d+\]', '', data_path)
segments = clean_path.split(".")
root_attr = segments[0]
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
path_errors += 1
path_msg = ""
if path_checked > 0:
path_msg = f"{path_checked} paths checked"
if path_base_skipped > 0:
skip_note = f"{path_base_skipped} base skipped"
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
if path_errors == 0 and path_msg:
report_ok(f"DataPath references: {path_msg}")
elif path_errors == 0:
report_ok("DataPath references: none")
# --- Check 6: Button command references ---
if not stopped:
cmd_errors = 0
cmd_checked = 0
for el in all_elements:
if stopped:
break
tag = el["Tag"]
el_name = el["Name"]
node = el["Node"]
if tag != "Button":
continue
cmd_node = node.find(f"{{{F_NS}}}CommandName")
if cmd_node is None:
continue
cmd_ref = (cmd_node.text or "").strip()
if not cmd_ref:
continue
m = re.match(r'^Form\.Command\.(.+)$', cmd_ref)
if m:
cmd_name_ref = m.group(1)
cmd_checked += 1
if cmd_name_ref not in cmd_map:
report_error(f"[Button] '{el_name}': CommandName='{cmd_ref}' \u2014 command '{cmd_name_ref}' not found in Commands")
cmd_errors += 1
if cmd_errors == 0 and cmd_checked > 0:
report_ok(f"Command references: {cmd_checked} buttons checked")
elif cmd_checked == 0:
report_ok("Command references: none")
# --- Check 7: Events have handler names ---
if not stopped:
event_errors = 0
event_checked = 0
# Form-level events
form_events = root.find(f"{{{F_NS}}}Events")
if form_events is not None:
for evt in form_events.findall(f"{{{F_NS}}}Event"):
evt_name = evt.get("name", "")
handler = (evt.text or "").strip()
event_checked += 1
if not handler:
report_error(f"Form event '{evt_name}': empty handler name")
event_errors += 1
# Element-level events
for el in all_elements:
if stopped:
break
tag = el["Tag"]
el_name = el["Name"]
node = el["Node"]
events_node = node.find(f"{{{F_NS}}}Events")
if events_node is None:
continue
for evt in events_node.findall(f"{{{F_NS}}}Event"):
evt_name = evt.get("name", "")
handler = (evt.text or "").strip()
event_checked += 1
if not handler:
report_error(f"[{tag}] '{el_name}' event '{evt_name}': empty handler name")
event_errors += 1
if event_errors == 0 and event_checked > 0:
report_ok(f"Event handlers: {event_checked} events checked")
elif event_checked == 0:
report_ok("Event handlers: none")
# --- Check 8: Command actions ---
if not stopped:
action_errors = 0
action_checked = 0
for cmd in cmd_nodes:
if stopped:
break
cmd_name = cmd.get("name", "")
action_node = cmd.find(f"{{{F_NS}}}Action")
action_checked += 1
if action_node is None or not (action_node.text or "").strip():
report_error(f"Command '{cmd_name}': missing or empty Action")
action_errors += 1
if action_errors == 0 and action_checked > 0:
report_ok(f"Command actions: {action_checked} commands checked")
elif action_checked == 0:
report_ok("Command actions: none")
# --- Check 9: MainAttribute count ---
if not stopped:
main_count = 0
for attr in attr_nodes:
main_node = attr.find(f"{{{F_NS}}}MainAttribute")
if main_node is not None and (main_node.text or "") == "true":
main_count += 1
if main_count <= 1:
main_info = "1 main attribute" if main_count == 1 else "no main attribute"
report_ok(f"MainAttribute: {main_info}")
else:
report_error(f"Multiple MainAttribute=true ({main_count} found, expected 0 or 1)")
# --- Check 10: Title must be multilingual XML ---
if not stopped:
title_node = root.find(f"{{{F_NS}}}Title")
if title_node is not None:
v8_items = title_node.findall(f"{{{V8_NS}}}item")
if len(v8_items) == 0 and (title_node.text or "").strip():
report_error(f"Form Title is plain text ('{(title_node.text or '').strip()}') \u2014 must be multilingual XML (<v8:item>). Use top-level 'title' key in form-compile DSL.")
else:
report_ok("Title: multilingual XML")
# --- Check 11: Extension-specific validations ---
base_form_node = root.find(f"{{{F_NS}}}BaseForm")
is_extension = base_form_node is not None
if not stopped and is_extension:
# 11a. BaseForm version
bf_version = base_form_node.get("version", "")
if bf_version:
report_ok(f"BaseForm: version={bf_version}")
else:
report_warn("BaseForm: version attribute missing")
# 11b. callType values validation
valid_call_types = {"Before", "After", "Override"}
ct_errors = 0
ct_checked = 0
form_events_node = root.find(f"{{{F_NS}}}Events")
if form_events_node is not None:
for evt in form_events_node.findall(f"{{{F_NS}}}Event"):
ct = evt.get("callType", "")
if ct:
ct_checked += 1
if ct not in valid_call_types:
report_error(f"Form event '{evt.get('name', '')}': invalid callType='{ct}' (expected: Before, After, Override)")
ct_errors += 1
for el in all_elements:
if stopped:
break
events_node = el["Node"].find(f"{{{F_NS}}}Events")
if events_node is None:
continue
for evt in events_node.findall(f"{{{F_NS}}}Event"):
ct = evt.get("callType", "")
if ct:
ct_checked += 1
if ct not in valid_call_types:
report_error(f"[{el['Tag']}] '{el['Name']}' event '{evt.get('name', '')}': invalid callType='{ct}'")
ct_errors += 1
for cmd in cmd_nodes:
if stopped:
break
cmd_name = cmd.get("name", "")
for action in cmd.findall(f"{{{F_NS}}}Action"):
ct = action.get("callType", "")
if ct:
ct_checked += 1
if ct not in valid_call_types:
report_error(f"Command '{cmd_name}' Action: invalid callType='{ct}'")
ct_errors += 1
if not stopped and ct_errors == 0 and ct_checked > 0:
report_ok(f"callType values: {ct_checked} checked")
# 11c. Extension ID ranges
base_attr_names = set()
base_cmd_names = set()
bf_attrs = base_form_node.find(f"{{{F_NS}}}Attributes")
if bf_attrs is not None:
for b_attr in bf_attrs.findall(f"{{{F_NS}}}Attribute"):
ba_name = b_attr.get("name", "")
if ba_name:
base_attr_names.add(ba_name)
bf_cmds = base_form_node.find(f"{{{F_NS}}}Commands")
if bf_cmds is not None:
for b_cmd in bf_cmds.findall(f"{{{F_NS}}}Command"):
bc_name = b_cmd.get("name", "")
if bc_name:
base_cmd_names.add(bc_name)
id_warn_count = 0
for attr in attr_nodes:
a_name = attr.get("name", "")
a_id = attr.get("id", "")
if a_name and a_name not in base_attr_names and a_id:
try:
int_id = int(a_id)
if int_id < 1000000:
report_warn(f"Attribute '{a_name}' (id={a_id}): extension-added attribute has id < 1000000")
id_warn_count += 1
except (ValueError, TypeError):
pass
for cmd in cmd_nodes:
c_name = cmd.get("name", "")
c_id = cmd.get("id", "")
if c_name and c_name not in base_cmd_names and c_id:
try:
int_id = int(c_id)
if int_id < 1000000:
report_warn(f"Command '{c_name}' (id={c_id}): extension-added command has id < 1000000")
id_warn_count += 1
except (ValueError, TypeError):
pass
if not stopped and id_warn_count == 0:
ext_attr_count = sum(1 for a in attr_nodes if a.get("name", "") not in base_attr_names)
ext_cmd_count = sum(1 for c in cmd_nodes if c.get("name", "") not in base_cmd_names)
if (ext_attr_count + ext_cmd_count) > 0:
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
# Check callType without BaseForm
if not stopped and not is_extension:
call_type_without_base = False
fe_node = root.find(f"{{{F_NS}}}Events")
if fe_node is not None:
for evt in fe_node.findall(f"{{{F_NS}}}Event"):
if evt.get("callType"):
call_type_without_base = True
break
if not call_type_without_base:
for cmd in cmd_nodes:
for action in cmd.findall(f"{{{F_NS}}}Action"):
if action.get("callType"):
call_type_without_base = True
break
if call_type_without_base:
break
if call_type_without_base:
report_warn("callType attributes found but no BaseForm \u2014 possible incorrect structure")
# --- Summary ---
print()
print("---")
print(f"Total: {len(all_elements)} elements, {len(attr_nodes)} attributes, {len(cmd_nodes)} commands")
if stopped:
print(f"Stopped after {max_errors} errors. Fix and re-run.")
if errors == 0 and warnings == 0:
print("All checks passed.")
else:
print(f"Errors: {errors}, Warnings: {warnings}")
if errors > 0:
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# add-help v1.0 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
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."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-Lang", default="ru")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
lang = args.Lang
src_dir = args.SrcDir
# --- Checks ---
processor_dir = os.path.join(src_dir, object_name)
ext_dir = os.path.join(processor_dir, "Ext")
if not os.path.isdir(ext_dir):
print(f"Каталог обработки не найден: {ext_dir}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
help_xml_path = os.path.join(ext_dir, "Help.xml")
if os.path.exists(help_xml_path):
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
sys.exit(1)
# --- 1. Help.xml ---
help_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Page>{lang}</Page>\n'
'</Help>'
)
write_text_with_bom(help_xml_path, help_xml)
# --- 2. Help/<lang>.html ---
help_dir = os.path.join(ext_dir, "Help")
os.makedirs(help_dir, exist_ok=True)
help_html_path = os.path.join(help_dir, f"{lang}.html")
help_html = (
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
'<html>\n'
'<head>\n'
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
'</head>\n'
'<body>\n'
f' <h1>{object_name}</h1>\n'
' <p>Описание обработки.</p>\n'
'</body>\n'
'</html>'
)
write_text_with_bom(help_html_path, help_html)
# --- 3. Check IncludeHelpInContents in form metadata ---
forms_dir = os.path.join(processor_dir, "Forms")
if os.path.isdir(forms_dir):
for entry in os.listdir(forms_dir):
if not entry.endswith(".xml"):
continue
form_meta_full = os.path.join(forms_dir, entry)
if not os.path.isfile(form_meta_full):
continue
parser_xml = etree.XMLParser(remove_blank_text=False)
form_tree = etree.parse(form_meta_full, parser_xml)
form_root = form_tree.getroot()
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
if include_help is not None:
continue
# Add after <FormType>
form_type = form_root.find(".//md:FormType", NSMAP)
if form_type is None:
continue
parent = form_type.getparent()
ns = "http://v8.1c.ru/8.3/MDClasses"
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
new_elem.text = "false"
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
parent.remove(new_elem)
# Find index of FormType in parent
form_type_idx = list(parent).index(form_type)
# Insert after FormType
parent.insert(form_type_idx + 1, new_elem)
# Whitespace handling: copy FormType's tail as new_elem's tail,
# and set FormType's tail to include newline + indent
new_elem.tail = form_type.tail
form_type.tail = "\n\t\t\t"
save_xml_with_bom(form_tree, form_meta_full)
print(f" IncludeHelpInContents добавлен: {entry}")
print(f"[OK] Создана справка: {object_name}")
print(f" Метаданные: {help_xml_path}")
print(f" Страница: {help_html_path}")
if __name__ == "__main__":
main()
@@ -0,0 +1,442 @@
#!/usr/bin/env python3
# interface-edit v1.0 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from lxml import etree
CI_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
XS_NS = "http://www.w3.org/2001/XMLSchema"
SECTION_ORDER = ["CommandsVisibility", "CommandsPlacement", "CommandsOrder", "SubsystemsOrder", "GroupsOrder"]
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
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
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
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
children = list(container)
if len(children) == 0:
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def remove_with_indent(el):
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
if el.tail:
prev.tail = el.tail
else:
if el.tail:
parent.text = el.tail
parent.remove(el)
def import_ci_fragment(xml_string):
wrapper = (
f'<_W xmlns="{CI_NS}" xmlns:xr="{XR_NS}" '
f'xmlns:xsi="{XSI_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
nodes = []
for child in frag:
nodes.append(child)
return nodes
def parse_value_list(val):
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def find_command_by_name(section, cmd_name):
for child in section:
if isinstance(child.tag, str) and localname(child) == "Command":
if child.get("name") == cmd_name:
return child
return None
def main():
parser = argparse.ArgumentParser(description="Edit 1C CommandInterface.xml", allow_abbrev=False)
parser.add_argument("-CIPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["hide", "show", "place", "order", "subsystem-order", "group-order"])
parser.add_argument("-Value", default=None)
parser.add_argument("-CreateIfMissing", action="store_true")
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Resolve path ---
ci_path = args.CIPath
if not os.path.isabs(ci_path):
ci_path = os.path.join(os.getcwd(), ci_path)
resolved_path = ci_path
# --- Create if missing ---
if not os.path.isfile(ci_path):
if args.CreateIfMissing:
parent_dir = os.path.dirname(ci_path)
if parent_dir and not os.path.isdir(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
empty_ci = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<CommandInterface xmlns="{CI_NS}"\n'
f'\txmlns:xr="{XR_NS}"\n'
f'\txmlns:xs="{XS_NS}"\n'
f'\txmlns:xsi="{XSI_NS}"\n'
f'\tversion="2.17">\n'
f'</CommandInterface>'
)
with open(ci_path, "w", encoding="utf-8-sig") as fh:
fh.write(empty_ci)
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
else:
print(f"File not found: {ci_path} (use -CreateIfMissing to create)", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(ci_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
if localname(root) != "CommandInterface":
print(f"Expected <CommandInterface> root element, got <{localname(root)}>", file=sys.stderr)
sys.exit(1)
def ensure_section(section_name):
# Find existing
for child in root:
if isinstance(child.tag, str) and localname(child) == section_name:
return child
# Create new section
new_section = etree.Element(f"{{{CI_NS}}}{section_name}")
my_idx = SECTION_ORDER.index(section_name) if section_name in SECTION_ORDER else -1
ref_node = None
for child in root:
if not isinstance(child.tag, str):
continue
child_idx = SECTION_ORDER.index(localname(child)) if localname(child) in SECTION_ORDER else -1
if child_idx > my_idx:
ref_node = child
break
root_indent = get_child_indent(root)
new_section.text = "\r\n" + root_indent
if ref_node is not None:
# Insert before ref_node
idx = list(root).index(ref_node)
new_section.tail = "\r\n" + root_indent
root.insert(idx, new_section)
else:
insert_before_closing(root, new_section, root_indent)
return new_section
def do_hide(commands):
nonlocal add_count, modify_count
section = ensure_section("CommandsVisibility")
section_indent = get_child_indent(section)
for cmd in commands:
existing = find_command_by_name(section, cmd)
if existing is not None:
common_el = None
for vis in existing:
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
for c in vis:
if isinstance(c.tag, str) and localname(c) == "Common":
common_el = c
break
if common_el is not None and (common_el.text or "").strip() == "false":
warn(f"Already hidden: {cmd}")
continue
if common_el is not None:
common_el.text = "false"
modify_count += 1
info(f"Changed to hidden: {cmd}")
continue
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>false</xr:Common></Visibility></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Hidden: {cmd}")
def do_show(commands):
nonlocal add_count, modify_count
section = None
for child in root:
if isinstance(child.tag, str) and localname(child) == "CommandsVisibility":
section = child
break
for cmd in commands:
if section is None:
section = ensure_section("CommandsVisibility")
existing = find_command_by_name(section, cmd)
if existing is not None:
common_el = None
for vis in existing:
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
for c in vis:
if isinstance(c.tag, str) and localname(c) == "Common":
common_el = c
break
if common_el is not None and (common_el.text or "").strip() == "true":
warn(f"Already shown: {cmd}")
continue
if common_el is not None and (common_el.text or "").strip() == "false":
common_el.text = "true"
modify_count += 1
info(f"Changed to shown: {cmd}")
continue
section_indent = get_child_indent(section)
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>true</xr:Common></Visibility></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Shown: {cmd}")
def do_place(json_val):
nonlocal add_count, modify_count
defn = json.loads(json_val)
cmd_name = str(defn["command"])
group_name = str(defn["group"])
if not cmd_name or not group_name:
print("place requires {command, group}", file=sys.stderr)
sys.exit(1)
section = ensure_section("CommandsPlacement")
section_indent = get_child_indent(section)
existing = find_command_by_name(section, cmd_name)
if existing is not None:
for child in existing:
if isinstance(child.tag, str) and localname(child) == "CommandGroup":
child.text = group_name
modify_count += 1
info(f"Updated placement: {cmd_name} -> {group_name}")
return
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup><Placement>Auto</Placement></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Placed: {cmd_name} -> {group_name}")
def do_order(json_val):
nonlocal add_count, remove_count
defn = json.loads(json_val)
group_name = str(defn["group"])
commands = [str(c) for c in defn["commands"]]
if not group_name or not commands:
print("order requires {group, commands:[...]}", file=sys.stderr)
sys.exit(1)
section = ensure_section("CommandsOrder")
section_indent = get_child_indent(section)
# Remove existing entries for this group
to_remove = []
for child in section:
if not isinstance(child.tag, str) or localname(child) != "Command":
continue
for gc in child:
if isinstance(gc.tag, str) and localname(gc) == "CommandGroup" and (gc.text or "").strip() == group_name:
to_remove.append(child)
break
for node in to_remove:
remove_with_indent(node)
remove_count += 1
# Add new entries
for cmd_name in commands:
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Set order for {group_name} : {len(commands)} commands")
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
sys.exit(1)
section = ensure_section("SubsystemsOrder")
section_indent = get_child_indent(section)
# Clear existing
for child in list(section):
if isinstance(child.tag, str):
remove_with_indent(child)
remove_count += 1
# Add new entries
for sub in subsystems:
new_el = etree.Element(f"{{{CI_NS}}}Subsystem")
new_el.text = sub
insert_before_closing(section, new_el, section_indent)
add_count += 1
info(f"Set subsystem order: {len(subsystems)} entries")
def do_group_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
sys.exit(1)
section = ensure_section("GroupsOrder")
section_indent = get_child_indent(section)
# Clear existing
for child in list(section):
if isinstance(child.tag, str):
remove_with_indent(child)
remove_count += 1
# Add new entries
for grp in groups:
new_el = etree.Element(f"{{{CI_NS}}}Group")
new_el.text = grp
insert_before_closing(section, new_el, section_indent)
add_count += 1
info(f"Set group order: {len(groups)} entries")
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "hide":
do_hide(parse_value_list(op_value))
elif op_name == "show":
do_show(parse_value_list(op_value))
elif op_name == "place":
do_place(op_value)
elif op_name == "order":
do_order(op_value)
elif op_name == "subsystem-order":
do_subsystem_order(op_value)
elif op_name == "group-order":
do_group_order(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "interface-validate", "scripts", "interface-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
# --- Summary ---
print()
print("=== interface-edit summary ===")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
@@ -0,0 +1,390 @@
#!/usr/bin/env python3
# interface-validate v1.0 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
import sys, os, argparse, re
from lxml import etree
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS_XS = 'http://www.w3.org/2001/XMLSchema'
NS = {
'ci': NS_CI,
'xr': NS_XR,
'xsi': NS_XSI,
'xs': NS_XS,
}
VALID_SECTIONS = [
'CommandsVisibility', 'CommandsPlacement', 'CommandsOrder',
'SubsystemsOrder', 'GroupsOrder'
]
STD_CMD_PATTERN = re.compile(r'^[A-Za-z]+\.[^\s\.]+\.StandardCommand\.\w+$')
CUSTOM_CMD_PATTERN = re.compile(r'^[A-Za-z]+\.[^\s\.]+\.Command\.\w+$')
COMMON_CMD_PATTERN = re.compile(r'^CommonCommand\.\w+$')
UUID_CMD_PATTERN = re.compile(
r'^0:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def find_duplicates(items):
seen = {}
dupes = []
for item in items:
seen[item] = seen.get(item, 0) + 1
for item, count in seen.items():
if count > 1 and item not in dupes:
dupes.append(item)
return dupes
def main():
parser = argparse.ArgumentParser(
description='Validate 1C CommandInterface.xml structure', allow_abbrev=False
)
parser.add_argument('-CIPath', dest='CIPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
ci_path = args.CIPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(ci_path):
ci_path = os.path.join(os.getcwd(), ci_path)
if not os.path.exists(ci_path):
print(f'[ERROR] File not found: {ci_path}')
sys.exit(1)
resolved_path = os.path.abspath(ci_path)
# --- Derive context name from path ---
context_name = ''
parts = re.split(r'[/\\]', resolved_path)
for i in range(len(parts)):
if parts[i] == 'Subsystems' and (i + 1) < len(parts):
context_name = parts[i + 1]
if not context_name:
context_name = 'Root'
r = Reporter(max_errors)
all_command_names = []
r.out(f'=== Validation: CommandInterface ({context_name}) ===')
r.out('')
# --- 1. XML well-formedness + root structure ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.error(f'1. XML parse error: {e}')
r.stopped = True
root = None
if not r.stopped:
root = xml_doc.getroot()
root_local = etree.QName(root.tag).localname
if root_local != 'CommandInterface':
r.error(f'1. Root element: expected <CommandInterface>, got <{root_local}>')
r.stopped = True
else:
ns_uri = etree.QName(root.tag).namespace or ''
version = root.get('version', '')
expected_ns = NS_CI
if ns_uri != expected_ns:
r.error(f'1. Root namespace: expected {expected_ns}, got {ns_uri}')
elif not version:
r.warn('1. Root structure: CommandInterface, namespace valid, but no version attribute')
else:
r.ok(f'1. Root structure: CommandInterface, version {version}, namespace valid')
# --- 2. Valid child elements ---
found_sections = []
if not r.stopped:
invalid_elements = []
for child in root:
if not isinstance(child.tag, str):
continue
local_name = etree.QName(child.tag).localname
if local_name in VALID_SECTIONS:
found_sections.append(local_name)
else:
invalid_elements.append(local_name)
if len(invalid_elements) > 0:
r.error(f'2. Invalid child elements: {", ".join(invalid_elements)}')
else:
r.ok(f'2. Child elements: {len(found_sections)} valid sections')
# --- 3. Section order ---
if not r.stopped:
order_ok = True
last_idx = -1
for sec in found_sections:
idx = VALID_SECTIONS.index(sec) if sec in VALID_SECTIONS else -1
if idx < last_idx:
r.error(f"3. Section order: '{sec}' appears after a later section (expected: CommandsVisibility -> CommandsPlacement -> CommandsOrder -> SubsystemsOrder -> GroupsOrder)")
order_ok = False
break
last_idx = idx
if order_ok:
r.ok('3. Section order: correct')
# --- 4. No duplicate sections ---
if not r.stopped:
dupes = find_duplicates(found_sections)
if dupes:
r.error(f'4. Duplicate sections: {", ".join(dupes)}')
else:
r.ok('4. No duplicate sections')
# --- 5. CommandsVisibility ---
vis_names = []
if not r.stopped:
vis_section = root.find(f'{{{NS_CI}}}CommandsVisibility')
if vis_section is not None:
vis_ok = True
vis_count = 0
for cmd in vis_section:
if not isinstance(cmd.tag, str):
continue
vis_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("5. CommandsVisibility: Command element without 'name' attribute")
vis_ok = False
continue
vis_names.append(cmd_name)
all_command_names.append(cmd_name)
visibility = cmd.find(f'{{{NS_CI}}}Visibility')
if visibility is None:
r.error(f'5. CommandsVisibility[{cmd_name}]: missing <Visibility>')
vis_ok = False
continue
common = visibility.find(f'{{{NS_XR}}}Common')
if common is None:
r.error(f'5. CommandsVisibility[{cmd_name}]: missing <xr:Common>')
vis_ok = False
continue
val = (common.text or '').strip()
if val not in ('true', 'false'):
r.error(f"5. CommandsVisibility[{cmd_name}]: xr:Common='{val}' (expected true/false)")
vis_ok = False
if vis_ok:
r.ok(f'5. CommandsVisibility: {vis_count} entries, all valid')
else:
r.ok('5. CommandsVisibility: not present')
# --- 6. CommandsVisibility duplicates ---
if not r.stopped:
if len(vis_names) > 0:
dupes = find_duplicates(vis_names)
if dupes:
r.warn(f'6. CommandsVisibility: duplicates: {", ".join(dupes)}')
else:
r.ok('6. CommandsVisibility: no duplicates')
else:
r.ok('6. CommandsVisibility: no duplicates (empty)')
# --- 7. CommandsPlacement ---
if not r.stopped:
plc_section = root.find(f'{{{NS_CI}}}CommandsPlacement')
if plc_section is not None:
plc_ok = True
plc_count = 0
for cmd in plc_section:
if not isinstance(cmd.tag, str):
continue
plc_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("7. CommandsPlacement: Command without 'name' attribute")
plc_ok = False
continue
all_command_names.append(cmd_name)
grp_el = cmd.find(f'{{{NS_CI}}}CommandGroup')
if grp_el is None or not (grp_el.text or '').strip():
r.error(f'7. CommandsPlacement[{cmd_name}]: missing or empty <CommandGroup>')
plc_ok = False
continue
placement_el = cmd.find(f'{{{NS_CI}}}Placement')
if placement_el is None:
r.error(f'7. CommandsPlacement[{cmd_name}]: missing <Placement>')
plc_ok = False
elif (placement_el.text or '').strip() != 'Auto':
r.warn(f"7. CommandsPlacement[{cmd_name}]: Placement='{(placement_el.text or '').strip()}' (expected Auto)")
if plc_ok:
r.ok(f'7. CommandsPlacement: {plc_count} entries, all valid')
else:
r.ok('7. CommandsPlacement: not present')
# --- 8. CommandsOrder ---
if not r.stopped:
ord_section = root.find(f'{{{NS_CI}}}CommandsOrder')
if ord_section is not None:
ord_ok = True
ord_count = 0
for cmd in ord_section:
if not isinstance(cmd.tag, str):
continue
ord_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("8. CommandsOrder: Command without 'name' attribute")
ord_ok = False
continue
all_command_names.append(cmd_name)
grp_el = cmd.find(f'{{{NS_CI}}}CommandGroup')
if grp_el is None or not (grp_el.text or '').strip():
r.error(f'8. CommandsOrder[{cmd_name}]: missing or empty <CommandGroup>')
ord_ok = False
if ord_ok:
r.ok(f'8. CommandsOrder: {ord_count} entries, all valid')
else:
r.ok('8. CommandsOrder: not present')
# --- 9. SubsystemsOrder format ---
sub_names = []
if not r.stopped:
sub_section = root.find(f'{{{NS_CI}}}SubsystemsOrder')
if sub_section is not None:
sub_ok = True
sub_count = 0
for sub_el in sub_section:
if not isinstance(sub_el.tag, str):
continue
sub_count += 1
text = (sub_el.text or '').strip()
sub_names.append(text)
if not text:
r.error('9. SubsystemsOrder: empty <Subsystem> element')
sub_ok = False
elif not text.startswith('Subsystem.'):
r.error(f"9. SubsystemsOrder: '{text}' - expected format Subsystem.X...")
sub_ok = False
if sub_ok:
r.ok(f'9. SubsystemsOrder: {sub_count} entries, all valid format')
else:
r.ok('9. SubsystemsOrder: not present')
# --- 10. SubsystemsOrder duplicates ---
if not r.stopped:
if len(sub_names) > 0:
dupes = find_duplicates(sub_names)
if dupes:
r.warn(f'10. SubsystemsOrder: duplicates: {", ".join(dupes)}')
else:
r.ok('10. SubsystemsOrder: no duplicates')
else:
r.ok('10. SubsystemsOrder: no duplicates (empty)')
# --- 11. GroupsOrder entries ---
grp_names = []
if not r.stopped:
grp_section = root.find(f'{{{NS_CI}}}GroupsOrder')
if grp_section is not None:
grp_ok = True
grp_count = 0
for grp in grp_section:
if not isinstance(grp.tag, str):
continue
grp_count += 1
text = (grp.text or '').strip()
grp_names.append(text)
if not text:
r.error('11. GroupsOrder: empty <Group> element')
grp_ok = False
if grp_ok:
r.ok(f'11. GroupsOrder: {grp_count} entries, all valid')
else:
r.ok('11. GroupsOrder: not present')
# --- 12. GroupsOrder duplicates ---
if not r.stopped:
if len(grp_names) > 0:
dupes = find_duplicates(grp_names)
if dupes:
r.warn(f'12. GroupsOrder: duplicates: {", ".join(dupes)}')
else:
r.ok('12. GroupsOrder: no duplicates')
else:
r.ok('12. GroupsOrder: no duplicates (empty)')
# --- 13. Command reference format ---
if not r.stopped:
if len(all_command_names) > 0:
bad_refs = []
for ref in all_command_names:
if STD_CMD_PATTERN.match(ref):
continue
if CUSTOM_CMD_PATTERN.match(ref):
continue
if COMMON_CMD_PATTERN.match(ref):
continue
if UUID_CMD_PATTERN.match(ref):
continue
bad_refs.append(ref)
if len(bad_refs) == 0:
r.ok(f'13. Command reference format: all {len(all_command_names)} valid')
else:
shown = bad_refs[:5]
suffix = ' ...' if len(bad_refs) > 5 else ''
r.warn(f'13. Command reference format: {len(bad_refs)} unrecognized: {", ".join(shown)}{suffix}')
else:
r.ok('13. Command reference format: n/a (no commands)')
# --- Finalize ---
r.out('---')
r.out(f'Errors: {r.errors}, Warnings: {r.warnings}')
result = r.text()
print(result, end='')
if out_file:
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,468 @@
#!/usr/bin/env python3
# meta-remove v1.0 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import shutil
from lxml import etree
# --- Type -> plural directory mapping ---
TYPE_PLURAL_MAP = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"Constant": "Constants",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"ExchangePlan": "ExchangePlans",
"DocumentJournal": "DocumentJournals",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"CommonModule": "CommonModules",
"ScheduledJob": "ScheduledJobs",
"EventSubscription": "EventSubscriptions",
"HTTPService": "HTTPServices",
"WebService": "WebServices",
"DefinedType": "DefinedTypes",
"Role": "Roles",
"Subsystem": "Subsystems",
"CommonForm": "CommonForms",
"CommonTemplate": "CommonTemplates",
"CommonPicture": "CommonPictures",
"CommonAttribute": "CommonAttributes",
"SessionParameter": "SessionParameters",
"FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
"Sequence": "Sequences",
"FilterCriterion": "FilterCriteria",
"SettingsStorage": "SettingsStorages",
"XDTOPackage": "XDTOPackages",
"WSReference": "WSReferences",
"StyleItem": "StyleItems",
"Language": "Languages",
}
# Type -> reference type names (used in XML <v8:Type> elements)
TYPE_REF_NAMES = {
"Catalog": ["CatalogRef", "CatalogObject"],
"Document": ["DocumentRef", "DocumentObject"],
"Enum": ["EnumRef"],
"ExchangePlan": ["ExchangePlanRef", "ExchangePlanObject"],
"ChartOfAccounts": ["ChartOfAccountsRef", "ChartOfAccountsObject"],
"ChartOfCharacteristicTypes": ["ChartOfCharacteristicTypesRef", "ChartOfCharacteristicTypesObject"],
"ChartOfCalculationTypes": ["ChartOfCalculationTypesRef", "ChartOfCalculationTypesObject"],
"BusinessProcess": ["BusinessProcessRef", "BusinessProcessObject"],
"Task": ["TaskRef", "TaskObject"],
}
# Type -> Russian manager name (used in BSL code)
TYPE_RU_MANAGER = {
"Catalog": "\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438",
"Document": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
"Enum": "\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f",
"Constant": "\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b",
"InformationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439",
"AccumulationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f",
"AccountingRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438",
"CalculationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
"ChartOfAccounts": "\u041f\u043b\u0430\u043d\u044b\u0421\u0447\u0435\u0442\u043e\u0432",
"ChartOfCharacteristicTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a",
"ChartOfCalculationTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
"BusinessProcess": "\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u044b",
"Task": "\u0417\u0430\u0434\u0430\u0447\u0438",
"ExchangePlan": "\u041f\u043b\u0430\u043d\u044b\u041e\u0431\u043c\u0435\u043d\u0430",
"Report": "\u041e\u0442\u0447\u0435\u0442\u044b",
"DataProcessor": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438",
"DocumentJournal": "\u0416\u0443\u0440\u043d\u0430\u043b\u044b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
"CommonModule": None,
}
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core"
NSMAP = {"md": MD_NS, "v8": V8_NS}
def localname(el):
return etree.QName(el.tag).localname
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Remove metadata object from 1C configuration dump", allow_abbrev=False)
parser.add_argument("-ConfigDir", required=True)
parser.add_argument("-Object", required=True)
parser.add_argument("-DryRun", action="store_true")
parser.add_argument("-KeepFiles", action="store_true")
parser.add_argument("-Force", action="store_true")
args = parser.parse_args()
config_dir = args.ConfigDir
if not os.path.isabs(config_dir):
config_dir = os.path.join(os.getcwd(), config_dir)
if not os.path.isdir(config_dir):
print(f"[ERROR] Config directory not found: {config_dir}")
sys.exit(1)
config_xml = os.path.join(config_dir, "Configuration.xml")
if not os.path.isfile(config_xml):
print(f"[ERROR] Configuration.xml not found in: {config_dir}")
sys.exit(1)
# --- Parse object spec ---
parts = args.Object.split(".", 1)
if len(parts) != 2 or not parts[0] or not parts[1]:
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b)")
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in TYPE_PLURAL_MAP:
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
sys.exit(1)
type_plural = TYPE_PLURAL_MAP[obj_type]
print(f"=== meta-remove: {obj_type}.{obj_name} ===")
print()
if args.DryRun:
print("[DRY-RUN] No changes will be made")
print()
actions = 0
errors = 0
# --- 1. Find object files ---
type_dir = os.path.join(config_dir, type_plural)
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
obj_dir = os.path.join(type_dir, obj_name)
has_xml = os.path.isfile(obj_xml)
has_dir = os.path.isdir(obj_dir)
if not has_xml and not has_dir:
print(f"[WARN] Object files not found: {type_plural}/{obj_name}.xml")
print(" Proceeding with deregistration only...")
else:
if has_xml:
print(f"[FOUND] {type_plural}/{obj_name}.xml")
if has_dir:
file_count = sum(len(files) for _, _, files in os.walk(obj_dir))
print(f"[FOUND] {type_plural}/{obj_name}/ ({file_count} files)")
# --- 2. Reference check ---
print()
print("--- Reference check ---")
search_patterns = []
# 1) XML type references
if obj_type in TYPE_REF_NAMES:
for ref_name in TYPE_REF_NAMES[obj_type]:
search_patterns.append(f"{ref_name}.{obj_name}")
# 2) BSL code references
ru_mgr = TYPE_RU_MANAGER.get(obj_type)
if ru_mgr:
search_patterns.append(f"{ru_mgr}.{obj_name}")
search_patterns.append(f"{type_plural}.{obj_name}")
# 3) CommonModule: method calls
if obj_type == "CommonModule":
search_patterns.append(f"{obj_name}.")
# 4) ScheduledJob/EventSubscription handler references
if obj_type == "CommonModule":
search_patterns.append(f"<Handler>{obj_name}.")
search_patterns.append(f"<MethodName>{obj_name}.")
# Exclude object's own files
exclude_dirs = []
if has_dir:
exclude_dirs.append(obj_dir)
exclude_file = obj_xml if has_xml else ""
# Search all XML and BSL files
references = []
search_extensions = (".xml", ".bsl")
for root_path, dirs, files in os.walk(config_dir):
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in search_extensions:
continue
full_path = os.path.join(root_path, fname)
# Skip own files
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
continue
skip = False
for ed in exclude_dirs:
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
skip = True
break
if skip:
continue
# Get relative path
rel_path = os.path.relpath(full_path, config_dir)
rel_path_fwd = rel_path.replace("\\", "/")
# Skip auto-cleaned files
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
continue
try:
with open(full_path, "r", encoding="utf-8-sig") as fh:
content = fh.read()
except Exception:
continue
for pat in search_patterns:
if pat in content:
references.append({"File": rel_path, "Pattern": pat})
break
# Also check Type.Name references
type_name_ref = f"{obj_type}.{obj_name}"
already_found_files = {r["File"] for r in references}
for root_path, dirs, files in os.walk(config_dir):
for fname in files:
if not fname.lower().endswith(".xml"):
continue
full_path = os.path.join(root_path, fname)
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
continue
skip = False
for ed in exclude_dirs:
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
skip = True
break
if skip:
continue
rel_path = os.path.relpath(full_path, config_dir)
rel_path_fwd = rel_path.replace("\\", "/")
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
continue
if rel_path in already_found_files:
continue
try:
with open(full_path, "r", encoding="utf-8-sig") as fh:
content = fh.read()
except Exception:
continue
if type_name_ref in content:
references.append({"File": rel_path, "Pattern": type_name_ref})
if references:
print(f"[WARN] Found {len(references)} reference(s) to {obj_type}.{obj_name}:")
print()
shown = 0
for ref in references:
print(f" {ref['File']}")
print(f" pattern: {ref['Pattern']}")
shown += 1
if shown >= 20:
remaining = len(references) - shown
if remaining > 0:
print(f" ... and {remaining} more")
break
print()
if not args.Force:
print(f"[ERROR] Cannot remove: object has {len(references)} reference(s).")
print(" Use -Force to remove anyway, or fix references first.")
sys.exit(1)
else:
print("[WARN] -Force specified, proceeding despite references")
else:
print("[OK] No references found")
# --- 3. Remove from Configuration.xml ChildObjects ---
print()
print("--- Configuration.xml ---")
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(config_xml, xml_parser)
xml_root = tree.getroot()
cfg_node = xml_root.find(f"{{{MD_NS}}}Configuration")
if cfg_node is None:
print("[ERROR] Configuration element not found in Configuration.xml")
errors += 1
else:
child_objects = cfg_node.find(f"{{{MD_NS}}}ChildObjects")
if child_objects is not None:
found = False
for child in list(child_objects):
if not isinstance(child.tag, str):
continue
if localname(child) == obj_type and (child.text or "").strip() == obj_name:
found = True
if not args.DryRun:
# Remove preceding whitespace (tail of previous sibling or text of parent)
prev = child.getprevious()
if prev is not None:
if prev.tail and prev.tail.strip() == "":
prev.tail = prev.tail.rsplit("\n", 1)[0] + "\n" if "\n" in prev.tail else ""
if not prev.tail.strip():
# Keep just the last newline+indent before the next element
pass
child_objects.remove(child)
print(f"[OK] Removed <{obj_type}>{obj_name}</{obj_type}> from ChildObjects")
actions += 1
break
if not found:
print(f"[WARN] <{obj_type}>{obj_name}</{obj_type}> not found in ChildObjects")
# Save Configuration.xml
if actions > 0 and not args.DryRun:
save_xml_bom(tree, config_xml)
print("[OK] Configuration.xml saved")
# --- 4. Remove from subsystem Content ---
print()
print("--- Subsystems ---")
subsystems_dir = os.path.join(config_dir, "Subsystems")
subsystems_found = 0
subsystems_cleaned = 0
def remove_from_subsystems(dir_path):
nonlocal subsystems_found, subsystems_cleaned
if not os.path.isdir(dir_path):
return
for fname in os.listdir(dir_path):
if not fname.lower().endswith(".xml"):
continue
xml_file = os.path.join(dir_path, fname)
if not os.path.isfile(xml_file):
continue
ss_parser = etree.XMLParser(remove_blank_text=False)
try:
ss_tree = etree.parse(xml_file, ss_parser)
except Exception:
continue
ss_root = ss_tree.getroot()
ss_node = None
for child in ss_root:
if isinstance(child.tag, str) and localname(child) == "Subsystem":
ss_node = child
break
if ss_node is None:
continue
props_node = ss_node.find(f"{{{MD_NS}}}Properties")
if props_node is None:
continue
content_node = props_node.find(f"{{{MD_NS}}}Content")
if content_node is None:
continue
ss_name_node = props_node.find(f"{{{MD_NS}}}Name")
ss_name = ss_name_node.text if ss_name_node is not None and ss_name_node.text else os.path.splitext(fname)[0]
target_ref = f"{obj_type}.{obj_name}"
modified = False
for item in list(content_node):
if not isinstance(item.tag, str):
continue
val = (item.text or "").strip()
if val == target_ref:
subsystems_found += 1
if not args.DryRun:
content_node.remove(item)
modified = True
print(f"[OK] Removed from subsystem '{ss_name}'")
subsystems_cleaned += 1
if modified and not args.DryRun:
save_xml_bom(ss_tree, xml_file)
# Recurse into child subsystems
base_name = os.path.splitext(fname)[0]
child_dir = os.path.join(dir_path, base_name, "Subsystems")
if os.path.isdir(child_dir):
remove_from_subsystems(child_dir)
if os.path.isdir(subsystems_dir):
remove_from_subsystems(subsystems_dir)
if subsystems_cleaned == 0:
print("[OK] Not referenced in any subsystem")
else:
print("[OK] No Subsystems directory")
# --- 5. Delete object files ---
print()
print("--- Files ---")
if not args.KeepFiles:
if has_dir and not args.DryRun:
shutil.rmtree(obj_dir)
print(f"[OK] Deleted directory: {type_plural}/{obj_name}/")
actions += 1
elif has_dir:
print(f"[DRY] Would delete directory: {type_plural}/{obj_name}/")
actions += 1
if has_xml and not args.DryRun:
os.remove(obj_xml)
print(f"[OK] Deleted file: {type_plural}/{obj_name}.xml")
actions += 1
elif has_xml:
print(f"[DRY] Would delete file: {type_plural}/{obj_name}.xml")
actions += 1
if not has_xml and not has_dir:
print("[OK] No files to delete")
else:
print("[SKIP] File deletion skipped (-KeepFiles)")
# --- Summary ---
print()
total_actions = actions + subsystems_cleaned
if args.DryRun:
print(f"=== Dry run complete: {total_actions} actions would be performed ===")
else:
print(f"=== Done: {total_actions} actions performed ({subsystems_cleaned} subsystem references removed) ===")
if errors > 0:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
@@ -0,0 +1,935 @@
# meta-validate v1.0 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from lxml import etree
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default="")
args = parser.parse_args()
object_path = args.ObjectPath
max_errors = args.MaxErrors
out_file = args.OutFile
# ── resolve path ─────────────────────────────────────────────
if not os.path.isabs(object_path):
object_path = os.path.join(os.getcwd(), object_path)
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:
xml_files = [f for f in os.listdir(object_path) if f.endswith(".xml")]
if xml_files:
object_path = os.path.join(object_path, xml_files[0])
else:
print(f"[ERROR] No XML file found in directory: {object_path}")
sys.exit(1)
# 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):
print(f"[ERROR] File not found: {object_path}")
sys.exit(1)
resolved_path = os.path.abspath(object_path)
# ── output infrastructure ────────────────────────────────────
errors = 0
warnings = 0
stopped = False
output_lines = []
def out_line(msg):
output_lines.append(msg)
def report_ok(msg):
out_line(f"[OK] {msg}")
def report_error(msg):
global errors, stopped
errors += 1
out_line(f"[ERROR] {msg}")
if errors >= max_errors:
stopped = True
def report_warn(msg):
global warnings
warnings += 1
out_line(f"[WARN] {msg}")
def finalize():
out_line("")
out_line(f"=== Result: {errors} errors, {warnings} warnings ===")
result = "\n".join(output_lines)
print(result)
if out_file:
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write(result)
print(f"Written to: {out_file}")
# ── Reference tables ─────────────────────────────────────────
guid_pattern = re.compile(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
ident_pattern = re.compile(r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$')
valid_types = (
"Catalog", "Document", "Enum", "Constant",
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
"ChartOfAccounts", "ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"BusinessProcess", "Task", "ExchangePlan", "DocumentJournal",
"Report", "DataProcessor",
"CommonModule", "ScheduledJob", "EventSubscription",
"HTTPService", "WebService", "DefinedType",
)
# GeneratedType categories by type
generated_type_categories = {
"Catalog": ["Object", "Ref", "Selection", "List", "Manager"],
"Document": ["Object", "Ref", "Selection", "List", "Manager"],
"Enum": ["Ref", "Manager", "List"],
"Constant": ["Manager", "ValueManager", "ValueKey"],
"InformationRegister": ["Record", "Manager", "Selection", "List", "RecordSet", "RecordKey", "RecordManager"],
"AccumulationRegister": ["Record", "Manager", "Selection", "List", "RecordSet", "RecordKey"],
"AccountingRegister": ["Record", "Manager", "Selection", "List", "RecordSet", "RecordKey", "ExtDimensions"],
"CalculationRegister": ["Record", "Manager", "Selection", "List", "RecordSet", "RecordKey", "Recalcs"],
"ChartOfAccounts": ["Object", "Ref", "Selection", "List", "Manager", "ExtDimensionTypes", "ExtDimensionTypesRow"],
"ChartOfCharacteristicTypes": ["Object", "Ref", "Selection", "List", "Manager", "Characteristic"],
"ChartOfCalculationTypes": ["Object", "Ref", "Selection", "List", "Manager", "DisplacingCalculationTypes", "DisplacingCalculationTypesRow", "BaseCalculationTypes", "BaseCalculationTypesRow", "LeadingCalculationTypes", "LeadingCalculationTypesRow"],
"BusinessProcess": ["Object", "Ref", "Selection", "List", "Manager", "RoutePointRef"],
"Task": ["Object", "Ref", "Selection", "List", "Manager"],
"ExchangePlan": ["Object", "Ref", "Selection", "List", "Manager"],
"DocumentJournal": ["Selection", "List", "Manager"],
"Report": ["Object", "Manager"],
"DataProcessor": ["Object", "Manager"],
"DefinedType": ["DefinedType"],
}
# Types that have NO InternalInfo / GeneratedType
types_without_internal_info = ("CommonModule", "ScheduledJob", "EventSubscription")
# StandardAttributes by type
standard_attributes_by_type = {
"Catalog": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "IsFolder", "Owner", "Parent", "Description", "Code"],
"Document": ["Posted", "Ref", "DeletionMark", "Date", "Number"],
"Enum": ["Order", "Ref"],
"InformationRegister": ["Active", "LineNumber", "Recorder", "Period"],
"AccumulationRegister": ["Active", "LineNumber", "Recorder", "Period", "RecordType"],
"AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account"],
"CalculationRegister": ["Active", "Recorder", "LineNumber", "RegistrationPeriod", "CalculationType", "ReversingEntry", "ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod", "BegOfBasePeriod", "EndOfBasePeriod"],
"ChartOfAccounts": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "Order", "Type", "OffBalance"],
"ChartOfCharacteristicTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "IsFolder", "ValueType"],
"ChartOfCalculationTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "ActionPeriodIsBasic"],
"BusinessProcess": ["Ref", "DeletionMark", "Date", "Number", "Started", "Completed", "HeadTask"],
"Task": ["Ref", "DeletionMark", "Date", "Number", "Executed", "Description", "RoutePoint", "BusinessProcess"],
"ExchangePlan": ["Ref", "DeletionMark", "Code", "Description", "ThisNode", "SentNo", "ReceivedNo"],
"DocumentJournal": ["Type", "Ref", "Date", "Posted", "DeletionMark", "Number"],
}
# Types that have StandardAttributes block
types_with_std_attrs = (
"Catalog", "Document", "Enum",
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
"ChartOfAccounts", "ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"BusinessProcess", "Task", "ExchangePlan", "DocumentJournal",
)
# ChildObjects rules
child_object_rules = {
"Catalog": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"Document": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"ExchangePlan": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"ChartOfAccounts": ["Attribute", "TabularSection", "Form", "Template", "Command", "AccountingFlag", "ExtDimensionAccountingFlag"],
"ChartOfCharacteristicTypes": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"ChartOfCalculationTypes": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"BusinessProcess": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"Task": ["Attribute", "TabularSection", "Form", "Template", "Command", "AddressingAttribute"],
"Report": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"DataProcessor": ["Attribute", "TabularSection", "Form", "Template", "Command"],
"Enum": ["EnumValue", "Form", "Template", "Command"],
"InformationRegister": ["Dimension", "Resource", "Attribute", "Form", "Template", "Command"],
"AccumulationRegister": ["Dimension", "Resource", "Attribute", "Form", "Template", "Command"],
"AccountingRegister": ["Dimension", "Resource", "Attribute", "Form", "Template", "Command"],
"CalculationRegister": ["Dimension", "Resource", "Attribute", "Form", "Template", "Command", "Recalculation"],
"DocumentJournal": ["Column", "Form", "Template", "Command"],
"HTTPService": ["URLTemplate"],
"WebService": ["Operation"],
"Constant": ["Form"],
"DefinedType": [],
"CommonModule": [],
"ScheduledJob": [],
"EventSubscription": [],
}
# Valid enum property values
valid_property_values = {
"CodeType": ["String", "Number"],
"CodeAllowedLength": ["Variable", "Fixed"],
"NumberType": ["String", "Number"],
"NumberAllowedLength": ["Variable", "Fixed"],
"Posting": ["Allow", "Deny"],
"RealTimePosting": ["Allow", "Deny"],
"RegisterRecordsDeletion": ["AutoDelete", "AutoDeleteOnUnpost", "AutoDeleteOff"],
"RegisterRecordsWritingOnPost": ["WriteModified", "WriteSelected", "WriteAll"],
"DataLockControlMode": ["Automatic", "Managed"],
"FullTextSearch": ["Use", "DontUse"],
"DefaultPresentation": ["AsDescription", "AsCode"],
"HierarchyType": ["HierarchyFoldersAndItems", "HierarchyItemsOnly"],
"EditType": ["InDialog", "InList", "BothWays"],
"WriteMode": ["Independent", "RecorderSubordinate"],
"InformationRegisterPeriodicity": ["Nonperiodical", "Second", "Day", "Month", "Quarter", "Year", "RecorderPosition"],
"RegisterType": ["Balance", "Turnovers"],
"ReturnValuesReuse": ["DontUse", "DuringRequest", "DuringSession"],
"ReuseSessions": ["DontUse", "AutoUse"],
"FillChecking": ["DontCheck", "ShowError", "ShowWarning"],
"Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"],
"DataHistory": ["Use", "DontUse"],
}
# ── Namespaces ───────────────────────────────────────────────
NS = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xs": "http://www.w3.org/2001/XMLSchema",
"cfg": "http://v8.1c.ru/8.1/data/enterprise/current-config",
}
MD_NS = NS["md"]
def local_name(node):
return etree.QName(node.tag).localname
def find(parent, xpath):
r = parent.xpath(xpath, namespaces=NS)
return r[0] if r else None
def find_all(parent, xpath):
return parent.xpath(xpath, namespaces=NS)
def inner_text(node):
if node is None:
return ""
return node.text or ""
def text_of(node):
if node is None:
return ""
return (node.text or "").strip()
# ── 1. Parse XML ─────────────────────────────────────────────
out_line("")
tree = None
try:
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, parser_xml)
except Exception as e:
out_line("=== Validation: (parse failed) ===")
out_line("")
report_error(f"1. XML parse failed: {e}")
finalize()
sys.exit(1)
root = tree.getroot()
# ── Check 1: Root structure ──────────────────────────────────
check1_ok = True
if local_name(root) != "MetaDataObject":
report_error(f"1. Root element is '{local_name(root)}', expected 'MetaDataObject'")
finalize()
sys.exit(1)
expected_ns = "http://v8.1c.ru/8.3/MDClasses"
root_ns = etree.QName(root.tag).namespace or ""
if root_ns != expected_ns:
report_error(f"1. Root namespace is '{root_ns}', expected '{expected_ns}'")
check1_ok = False
# Version attribute
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20"):
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
# Detect type element -- exactly one child element in md namespace
type_node = None
md_type = ""
child_elements = []
for child in root:
if isinstance(child.tag, str) and etree.QName(child.tag).namespace == expected_ns:
child_elements.append(child)
if len(child_elements) == 0:
report_error("1. No metadata type element found inside MetaDataObject")
finalize()
sys.exit(1)
elif len(child_elements) > 1:
names = [local_name(c) for c in child_elements]
report_error(f"1. Multiple type elements found: {names}")
check1_ok = False
type_node = child_elements[0]
md_type = local_name(type_node)
if md_type not in valid_types:
report_error(f"1. Unrecognized metadata type: {md_type}")
finalize()
sys.exit(1)
# UUID on type element
type_uuid = type_node.get("uuid", "")
if not type_uuid:
report_error(f"1. Missing uuid on <{md_type}> element")
check1_ok = False
elif not guid_pattern.match(type_uuid):
report_error(f"1. Invalid uuid '{type_uuid}' on <{md_type}>")
check1_ok = False
# Get object name early for header
props_node = find(type_node, "md:Properties")
name_node = find(props_node, "md:Name") if props_node is not None else None
obj_name = inner_text(name_node) if name_node is not None and inner_text(name_node) else "(unknown)"
# Now emit header — insert at beginning
output_lines.insert(0, f"=== Validation: {md_type}.{obj_name} ===")
if check1_ok:
report_ok(f"1. Root structure: MetaDataObject/{md_type}, version {version}")
if stopped:
finalize()
sys.exit(1)
# ── Check 2: InternalInfo ────────────────────────────────────
internal_info = find(type_node, "md:InternalInfo")
if md_type in types_without_internal_info:
if internal_info is not None:
gen_types = find_all(internal_info, "xr:GeneratedType")
if len(gen_types) > 0:
report_warn(f"2. InternalInfo: {md_type} should not have GeneratedType entries, found {len(gen_types)}")
else:
report_ok(f"2. InternalInfo: absent or empty (correct for {md_type})")
else:
report_ok(f"2. InternalInfo: absent (correct for {md_type})")
elif md_type in generated_type_categories:
expected_categories = generated_type_categories[md_type]
if internal_info is None:
report_error(f"2. InternalInfo: missing (expected {len(expected_categories)} GeneratedType)")
else:
gen_types = find_all(internal_info, "xr:GeneratedType")
check2_ok = True
found_categories = []
for gt in gen_types:
gt_name = gt.get("name", "")
gt_category = gt.get("category", "")
found_categories.append(gt_category)
# Validate name format
if gt_name and obj_name != "(unknown)":
if not gt_name.endswith(f".{obj_name}"):
report_error(f"2. GeneratedType name '{gt_name}' does not end with '.{obj_name}'")
check2_ok = False
# Validate category
if gt_category not in expected_categories:
report_warn(f"2. Unexpected GeneratedType category '{gt_category}' for {md_type}")
# Validate TypeId and ValueId UUIDs
type_id = find(gt, "xr:TypeId")
value_id = find(gt, "xr:ValueId")
if type_id is not None and not guid_pattern.match(inner_text(type_id)):
report_error(f"2. Invalid TypeId UUID in GeneratedType '{gt_category}'")
check2_ok = False
if value_id is not None and not guid_pattern.match(inner_text(value_id)):
report_error(f"2. Invalid ValueId UUID in GeneratedType '{gt_category}'")
check2_ok = False
# ExchangePlan: check for ThisNode
if md_type == "ExchangePlan":
this_node = find(internal_info, "xr:ThisNode")
if this_node is None:
report_warn("2. ExchangePlan missing xr:ThisNode in InternalInfo")
elif not guid_pattern.match(inner_text(this_node)):
report_error("2. ExchangePlan xr:ThisNode has invalid UUID")
check2_ok = False
# Check count mismatch
missing_cats = [c for c in expected_categories if c not in found_categories]
if missing_cats:
report_warn(f"2. Missing GeneratedType categories: {', '.join(missing_cats)}")
if check2_ok:
cat_list = ", ".join(sorted(found_categories))
report_ok(f"2. InternalInfo: {len(gen_types)} GeneratedType ({cat_list})")
else:
report_ok(f"2. InternalInfo: N/A for {md_type}")
if stopped:
finalize()
sys.exit(1)
# ── Check 3: Properties -- Name, Synonym ─────────────────────
if props_node is None:
report_error("3. Properties block missing")
else:
check3_ok = True
# Name
if name_node is None or not inner_text(name_node):
report_error("3. Properties: Name is missing or empty")
check3_ok = False
else:
name_val = inner_text(name_node)
if not ident_pattern.match(name_val):
report_error(f"3. Properties: Name '{name_val}' is not a valid 1C identifier")
check3_ok = False
if len(name_val) > 80:
report_warn(f"3. Properties: Name '{name_val}' is longer than 80 characters ({len(name_val)})")
# Synonym
syn_node = find(props_node, "md:Synonym")
syn_present = False
if syn_node is not None:
syn_item = find(syn_node, "v8:item")
if syn_item is not None:
syn_content = find(syn_item, "v8:content")
if syn_content is not None and inner_text(syn_content):
syn_present = True
if check3_ok:
syn_info = "Synonym present" if syn_present else "no Synonym"
report_ok(f'3. Properties: Name="{obj_name}", {syn_info}')
if stopped:
finalize()
sys.exit(1)
# ── Check 4: Property values -- enum properties ──────────────
if props_node is not None:
enum_checked = 0
check4_ok = True
for prop_name, allowed in valid_property_values.items():
prop_node = find(props_node, f"md:{prop_name}")
if prop_node is not None and inner_text(prop_node):
val = inner_text(prop_node)
if val not in allowed:
report_error(f"4. Property '{prop_name}' has invalid value '{val}' (allowed: {', '.join(allowed)})")
check4_ok = False
enum_checked += 1
if check4_ok:
report_ok(f"4. Property values: {enum_checked} enum properties checked")
else:
report_warn("4. No Properties block to check")
if stopped:
finalize()
sys.exit(1)
# ── Check 5: StandardAttributes ──────────────────────────────
if md_type in types_with_std_attrs:
std_attr_node = find(props_node, "md:StandardAttributes")
if std_attr_node is None:
report_ok(f"5. StandardAttributes: absent (optional for {md_type})")
else:
std_attrs = find_all(std_attr_node, "xr:StandardAttribute")
expected_std_attrs = standard_attributes_by_type.get(md_type, [])
check5_ok = True
found_names = []
for sa in std_attrs:
sa_name = sa.get("name", "")
if sa_name:
found_names.append(sa_name)
if sa_name not in expected_std_attrs:
# AccountingRegister has dynamic attrs
is_dynamic = (md_type == "AccountingRegister" and
(re.match(r'^ExtDimension\d+$', sa_name) or
re.match(r'^ExtDimensionType\d+$', sa_name) or
sa_name == "PeriodAdjustment"))
# CalculationRegister has conditional period attrs
is_calc_dynamic = (md_type == "CalculationRegister" and
sa_name in ("ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod",
"BegOfBasePeriod", "EndOfBasePeriod"))
if not is_dynamic and not is_calc_dynamic:
report_warn(f"5. Unexpected StandardAttribute '{sa_name}' for {md_type}")
else:
report_error("5. StandardAttribute without 'name' attribute")
check5_ok = False
if expected_std_attrs:
missing_attrs = [a for a in expected_std_attrs if a not in found_names]
if missing_attrs:
report_warn(f"5. Missing StandardAttributes: {', '.join(missing_attrs)}")
if check5_ok:
report_ok(f"5. StandardAttributes: {len(std_attrs)} entries")
else:
report_ok(f"5. StandardAttributes: N/A for {md_type}")
if stopped:
finalize()
sys.exit(1)
# ── Check 6: ChildObjects -- allowed element types ───────────
child_obj_node = find(type_node, "md:ChildObjects")
allowed_children = child_object_rules.get(md_type, [])
if child_obj_node is not None:
check6_ok = True
child_counts = {}
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
child_tag = local_name(child)
if child_tag not in allowed_children:
report_error(f"6. ChildObjects: disallowed element '{child_tag}' for {md_type}")
check6_ok = False
child_counts[child_tag] = child_counts.get(child_tag, 0) + 1
if check6_ok:
summary = ", ".join(f"{k}({v})" for k, v in sorted(child_counts.items()))
if summary:
report_ok(f"6. ChildObjects types: {summary}")
else:
report_ok(f"6. ChildObjects: empty (valid for {md_type})")
elif len(allowed_children) == 0:
report_ok(f"6. ChildObjects: absent (correct for {md_type})")
else:
report_ok("6. ChildObjects: absent")
if stopped:
finalize()
sys.exit(1)
# ── Check 7: Child elements -- UUID, Name, Type ──────────────
def check_child_element(node, kind, require_type):
uuid = node.get("uuid", "")
if not uuid:
report_error(f"7. {kind} missing uuid")
return False
if not guid_pattern.match(uuid):
report_error(f"7. {kind} has invalid uuid '{uuid}'")
return False
el_props = find(node, "md:Properties")
if el_props is None:
report_error(f"7. {kind} (uuid={uuid}) missing Properties")
return False
el_name = find(el_props, "md:Name")
if el_name is None or not inner_text(el_name):
report_error(f"7. {kind} (uuid={uuid}) missing or empty Name")
return False
name_val = inner_text(el_name)
if not ident_pattern.match(name_val):
report_error(f"7. {kind} '{name_val}' has invalid identifier")
return False
if require_type:
type_el = find(el_props, "md:Type")
if type_el is None:
report_error(f"7. {kind} '{name_val}' missing Type block")
return False
v8_types = find_all(type_el, "v8:Type")
v8_type_sets = find_all(type_el, "v8:TypeSet")
if len(v8_types) == 0 and len(v8_type_sets) == 0:
report_error(f"7. {kind} '{name_val}' Type block has no v8:Type or v8:TypeSet")
return False
return True
if child_obj_node is not None:
check7_ok = True
check7_count = 0
element_kinds = ("Attribute", "Dimension", "Resource", "EnumValue", "Column")
for kind in element_kinds:
elements = find_all(child_obj_node, f"md:{kind}")
require_type = kind not in ("EnumValue", "Column")
for el in elements:
if stopped:
break
ok = check_child_element(el, kind, require_type)
if not ok:
check7_ok = False
check7_count += 1
if check7_ok and check7_count > 0:
report_ok(f"7. Child elements: {check7_count} items checked (UUID, Name, Type)")
elif check7_count == 0:
report_ok("7. Child elements: none to check")
else:
report_ok("7. Child elements: N/A (no ChildObjects)")
if stopped:
finalize()
sys.exit(1)
# ── Check 8: Name uniqueness ─────────────────────────────────
def check_uniqueness(nodes, kind):
names = {}
has_dupes = False
for node in nodes:
el_props = find(node, "md:Properties")
if el_props is None:
continue
el_name = find(el_props, "md:Name")
if el_name is None or not inner_text(el_name):
continue
name_val = inner_text(el_name)
if name_val in names:
report_error(f"8. Duplicate {kind} name: '{name_val}'")
has_dupes = True
else:
names[name_val] = True
return not has_dupes
if child_obj_node is not None:
check8_ok = True
# Attributes
attrs = find_all(child_obj_node, "md:Attribute")
if len(attrs) > 0:
if not check_uniqueness(attrs, "Attribute"):
check8_ok = False
# TabularSections
tss = find_all(child_obj_node, "md:TabularSection")
if len(tss) > 0:
if not check_uniqueness(tss, "TabularSection"):
check8_ok = False
# Dimensions
dims = find_all(child_obj_node, "md:Dimension")
if len(dims) > 0:
if not check_uniqueness(dims, "Dimension"):
check8_ok = False
# Resources
ress = find_all(child_obj_node, "md:Resource")
if len(ress) > 0:
if not check_uniqueness(ress, "Resource"):
check8_ok = False
# EnumValues
evs = find_all(child_obj_node, "md:EnumValue")
if len(evs) > 0:
if not check_uniqueness(evs, "EnumValue"):
check8_ok = False
# Columns (DocumentJournal)
cols = find_all(child_obj_node, "md:Column")
if len(cols) > 0:
if not check_uniqueness(cols, "Column"):
check8_ok = False
# URLTemplates (HTTPService)
url_ts = find_all(child_obj_node, "md:URLTemplate")
if len(url_ts) > 0:
if not check_uniqueness(url_ts, "URLTemplate"):
check8_ok = False
# Operations (WebService)
ops = find_all(child_obj_node, "md:Operation")
if len(ops) > 0:
if not check_uniqueness(ops, "Operation"):
check8_ok = False
if check8_ok:
report_ok("8. Name uniqueness: all names unique")
else:
report_ok("8. Name uniqueness: N/A")
if stopped:
finalize()
sys.exit(1)
# ── Check 9: TabularSections -- internal structure ───────────
if child_obj_node is not None:
ts_sections = find_all(child_obj_node, "md:TabularSection")
if len(ts_sections) > 0:
check9_ok = True
ts_count = 0
for ts in ts_sections:
if stopped:
break
ts_count += 1
# UUID
ts_uuid = ts.get("uuid", "")
if not ts_uuid or not guid_pattern.match(ts_uuid):
report_error(f"9. TabularSection #{ts_count}: invalid or missing uuid")
check9_ok = False
# Name
ts_props = find(ts, "md:Properties")
ts_name_node = find(ts_props, "md:Name") if ts_props is not None else None
ts_name = inner_text(ts_name_node) if ts_name_node is not None else "(unnamed)"
if ts_name_node is None or not inner_text(ts_name_node):
report_error(f"9. TabularSection #{ts_count}: missing or empty Name")
check9_ok = False
# InternalInfo with 2 GeneratedType
ts_int_info = find(ts, "md:InternalInfo")
if ts_int_info is not None:
ts_gens = find_all(ts_int_info, "xr:GeneratedType")
if len(ts_gens) < 2:
report_warn(f"9. TabularSection '{ts_name}': expected 2 GeneratedType, found {len(ts_gens)}")
# Attributes inside TS
ts_child_obj = find(ts, "md:ChildObjects")
if ts_child_obj is not None:
ts_attrs = find_all(ts_child_obj, "md:Attribute")
ts_attr_names = {}
for ta in ts_attrs:
ta_ok = check_child_element(ta, f"TabularSection '{ts_name}'.Attribute", True)
if not ta_ok:
check9_ok = False
# Check name uniqueness within TS
ta_props = find(ta, "md:Properties")
ta_name = find(ta_props, "md:Name") if ta_props is not None else None
if ta_name is not None and inner_text(ta_name):
if inner_text(ta_name) in ts_attr_names:
report_error(f"9. Duplicate attribute '{inner_text(ta_name)}' in TabularSection '{ts_name}'")
check9_ok = False
else:
ts_attr_names[inner_text(ta_name)] = True
# StandardAttributes of TS: expect LineNumber
if ts_props is not None:
ts_std_attr = find(ts_props, "md:StandardAttributes")
if ts_std_attr is not None:
ts_std_attrs = find_all(ts_std_attr, "xr:StandardAttribute")
has_line_number = False
for tsa in ts_std_attrs:
if tsa.get("name") == "LineNumber":
has_line_number = True
if not has_line_number:
report_warn(f"9. TabularSection '{ts_name}': missing LineNumber StandardAttribute")
if check9_ok:
report_ok(f"9. TabularSections: {ts_count} sections, structure valid")
else:
report_ok("9. TabularSections: none present")
else:
report_ok("9. TabularSections: N/A")
if stopped:
finalize()
sys.exit(1)
# ── Check 10: Cross-property consistency ─────────────────────
check10_ok = True
check10_issues = 0
if props_node is not None:
# HierarchyType set but Hierarchical = false
hierarchical = find(props_node, "md:Hierarchical")
hierarchy_type = find(props_node, "md:HierarchyType")
if (hierarchical is not None and hierarchy_type is not None and
inner_text(hierarchical) == "false" and inner_text(hierarchy_type)):
report_warn(f"10. HierarchyType='{inner_text(hierarchy_type)}' but Hierarchical=false")
check10_issues += 1
# CommonModule: no context enabled
if md_type == "CommonModule":
contexts = ("Server", "ClientManagedApplication", "ClientOrdinaryApplication",
"ExternalConnection", "ServerCall", "Global")
any_enabled = False
for ctx in contexts:
ctx_node = find(props_node, f"md:{ctx}")
if ctx_node is not None and inner_text(ctx_node) == "true":
any_enabled = True
break
if not any_enabled:
report_warn("10. CommonModule: no execution context enabled")
check10_issues += 1
# EventSubscription: empty Handler
if md_type == "EventSubscription":
handler = find(props_node, "md:Handler")
if handler is None or not text_of(handler):
report_error("10. EventSubscription: empty Handler")
check10_ok = False
check10_issues += 1
# Empty Source
source = find(props_node, "md:Source")
has_source = False
if source is not None:
source_types = find_all(source, "v8:Type")
if len(source_types) > 0:
has_source = True
if not has_source:
report_warn("10. EventSubscription: no Source types specified")
check10_issues += 1
# ScheduledJob: empty MethodName
if md_type == "ScheduledJob":
method = find(props_node, "md:MethodName")
if method is None or not text_of(method):
report_error("10. ScheduledJob: empty MethodName")
check10_ok = False
check10_issues += 1
if check10_ok and check10_issues == 0:
report_ok("10. Cross-property consistency")
if stopped:
finalize()
sys.exit(1)
# ── Check 11: HTTPService/WebService nested structure ────────
if md_type == "HTTPService" and child_obj_node is not None:
url_templates = find_all(child_obj_node, "md:URLTemplate")
check11_ok = True
method_count = 0
valid_http_methods = ("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "MERGE", "CONNECT")
for ut in url_templates:
if stopped:
break
ut_props = find(ut, "md:Properties")
ut_name_node = find(ut_props, "md:Name") if ut_props is not None else None
ut_name = inner_text(ut_name_node) if ut_name_node is not None else "(unnamed)"
# Template property
tpl = find(ut_props, "md:Template") if ut_props is not None else None
if tpl is None or not text_of(tpl):
report_error(f"11. HTTPService URLTemplate '{ut_name}': empty Template")
check11_ok = False
# Methods inside URLTemplate
ut_child_obj = find(ut, "md:ChildObjects")
if ut_child_obj is not None:
methods = find_all(ut_child_obj, "md:Method")
for m in methods:
method_count += 1
m_props = find(m, "md:Properties")
if m_props is not None:
http_method = find(m_props, "md:HTTPMethod")
if http_method is not None and inner_text(http_method):
if inner_text(http_method) not in valid_http_methods:
report_error(f"11. HTTPService URLTemplate '{ut_name}': invalid HTTPMethod '{inner_text(http_method)}'")
check11_ok = False
else:
report_error(f"11. HTTPService URLTemplate '{ut_name}': Method missing HTTPMethod")
check11_ok = False
if check11_ok:
report_ok(f"11. HTTPService: {len(url_templates)} URLTemplate(s), {method_count} method(s)")
elif md_type == "WebService" and child_obj_node is not None:
operations = find_all(child_obj_node, "md:Operation")
check11_ok = True
param_count = 0
valid_directions = ("In", "Out", "InOut")
for op in operations:
if stopped:
break
op_props = find(op, "md:Properties")
op_name_node = find(op_props, "md:Name") if op_props is not None else None
op_name = inner_text(op_name_node) if op_name_node is not None else "(unnamed)"
# ReturnType
ret_type = find(op_props, "md:XDTOReturningValueType") if op_props is not None else None
if ret_type is None or not text_of(ret_type):
report_warn(f"11. WebService Operation '{op_name}': no XDTOReturningValueType")
# Parameters inside Operation
op_child_obj = find(op, "md:ChildObjects")
if op_child_obj is not None:
params = find_all(op_child_obj, "md:Parameter")
for p in params:
param_count += 1
p_props = find(p, "md:Properties")
if p_props is not None:
direction = find(p_props, "md:TransferDirection")
if direction is not None and inner_text(direction) and inner_text(direction) not in valid_directions:
report_error(f"11. WebService Operation '{op_name}': Parameter has invalid TransferDirection '{inner_text(direction)}'")
check11_ok = False
if check11_ok:
report_ok(f"11. WebService: {len(operations)} operation(s), {param_count} parameter(s)")
else:
report_ok("11. HTTPService/WebService: N/A")
# ── Final output ──────────────────────────────────────────────
finalize()
if errors > 0:
sys.exit(1)
sys.exit(0)
@@ -0,0 +1,628 @@
#!/usr/bin/env python3
# mxl-compile v1.0 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import math
import os
import re
import sys
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True)
parser.add_argument('-OutputPath', type=str, required=True)
args = parser.parse_args()
# --- 1. Load and validate JSON ---
json_path = args.JsonPath
if not os.path.exists(json_path):
print(f"File not found: {json_path}", file=sys.stderr)
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
if not defn.get('columns'):
print("Required field 'columns' is missing", file=sys.stderr)
sys.exit(1)
if not defn.get('areas'):
print("Required field 'areas' is missing", file=sys.stderr)
sys.exit(1)
total_columns = int(defn['columns'])
default_width = int(defn['defaultWidth']) if defn.get('defaultWidth') else 10
# --- 2. Build font palette ---
font_map = {} # name -> 0-based index
font_entries = [] # list of dicts
def add_font(name, font_def):
face = font_def.get('face', 'Arial') if font_def else 'Arial'
size = int(font_def.get('size', 10)) if font_def else 10
bold = 'true' if font_def and font_def.get('bold') is True else 'false'
italic = 'true' if font_def and font_def.get('italic') is True else 'false'
underline = 'true' if font_def and font_def.get('underline') is True else 'false'
strikeout = 'true' if font_def and font_def.get('strikeout') is True else 'false'
idx = len(font_entries)
font_map[name] = idx
font_entries.append({
'Face': face,
'Size': size,
'Bold': bold,
'Italic': italic,
'Underline': underline,
'Strikeout': strikeout,
})
# Add user-defined fonts
has_default = False
if defn.get('fonts'):
for fname, fdef in defn['fonts'].items():
if fname == 'default':
has_default = True
add_font(fname, fdef)
# Ensure default font exists
if not has_default:
add_font('default', {'face': 'Arial', 'size': 10})
# --- 3. Determine line palette ---
has_thin_borders = False
has_thick_borders = False
if defn.get('styles'):
for sname, sval in defn['styles'].items():
if sval.get('border') and sval['border'] != 'none':
if sval.get('borderWidth') == 'thick':
has_thick_borders = True
else:
has_thin_borders = True
thin_line_index = -1
thick_line_index = -1
line_count = 0
if has_thin_borders:
thin_line_index = line_count
line_count += 1
if has_thick_borders:
thick_line_index = line_count
line_count += 1
# --- 4. Parse column width specs ---
def parse_column_spec(spec):
cols = []
for part in spec.split(','):
part = part.strip()
m = re.match(r'^(\d+)-(\d+)$', part)
if m:
from_col = int(m.group(1))
to_col = int(m.group(2))
for i in range(from_col, to_col + 1):
cols.append(i)
else:
cols.append(int(part))
return cols
# --- 4a. Auto-calculate defaultWidth from page format ---
page_targets = {
'A4-landscape': 780,
'A4-portrait': 540,
}
page_name = None
target_width = None
if defn.get('page'):
page_name = str(defn['page'])
if re.match(r'^\d+$', page_name):
target_width = int(page_name)
elif page_name in page_targets:
target_width = page_targets[page_name]
else:
print(f"WARNING: Unknown page format '{page_name}'. Known: {', '.join(page_targets.keys())}, or a number.", file=sys.stderr)
if target_width:
total_units = 0.0
absolute_sum = 0
specified_cols = {}
if defn.get('columnWidths'):
for prop_name, prop_value in defn['columnWidths'].items():
val = str(prop_value)
cols = parse_column_spec(prop_name)
for c in cols:
specified_cols[int(c)] = True
m = re.match(r'^([0-9.]+)x$', val)
if m:
total_units += float(m.group(1))
else:
absolute_sum += int(val)
for c in range(1, total_columns + 1):
if c not in specified_cols:
total_units += 1.0
if total_units > 0:
default_width = round((target_width - absolute_sum) / total_units)
# Build column width map: 1-based col -> width
col_width_map = {}
if defn.get('columnWidths'):
for prop_name, prop_value in defn['columnWidths'].items():
val = str(prop_value)
m = re.match(r'^([0-9.]+)x$', val)
if m:
width = round(float(m.group(1)) * default_width)
else:
width = int(val)
columns = parse_column_spec(prop_name)
for c in columns:
col_width_map[c] = width
# --- 5. Style resolver ---
def resolve_style(style_name, fill_type):
font_idx = font_map.get('default', 0)
lb = -1; tb = -1; rb = -1; bb = -1
ha = ''; va = ''; nf = ''
wrap = False
if style_name and defn.get('styles'):
style = defn['styles'].get(style_name)
if style:
# Font
if style.get('font') and style['font'] in font_map:
font_idx = font_map[style['font']]
# Borders
if style.get('border') and style['border'] != 'none':
line_idx = thick_line_index if style.get('borderWidth') == 'thick' else thin_line_index
for side in style['border'].split(','):
side = side.strip()
if side == 'all':
lb = line_idx; tb = line_idx; rb = line_idx; bb = line_idx
elif side == 'left':
lb = line_idx
elif side == 'top':
tb = line_idx
elif side == 'right':
rb = line_idx
elif side == 'bottom':
bb = line_idx
# Alignment
if style.get('align'):
align_map = {'left': 'Left', 'center': 'Center', 'right': 'Right'}
ha = align_map.get(style['align'], '')
if style.get('valign'):
valign_map = {'top': 'Top', 'center': 'Center'}
va = valign_map.get(style['valign'], '')
# Wrap
if style.get('wrap') is True:
wrap = True
# Number format
if style.get('format'):
nf = style['format']
return {
'FontIdx': font_idx,
'LB': lb, 'TB': tb, 'RB': rb, 'BB': bb,
'HA': ha, 'VA': va,
'Wrap': wrap,
'FillType': fill_type,
'NumberFormat': nf,
}
# --- 6. Format palette builder ---
format_registry = {} # key -> props
format_order = [] # ordered keys for index assignment
def get_format_key(font_idx=-1, lb=-1, tb=-1, rb=-1, bb=-1, ha='', va='',
wrap=False, fill_type='', number_format='', width=-1, height=-1):
return f'f={font_idx}|lb={lb}|tb={tb}|rb={rb}|bb={bb}|ha={ha}|va={va}|wr={wrap}|ft={fill_type}|nf={number_format}|w={width}|h={height}'
def register_format(key, props):
if key not in format_registry:
format_registry[key] = props
format_order.append(key)
# Return 1-based index
return format_order.index(key) + 1
# 6a. Default width format
default_format_key = get_format_key(width=default_width)
default_format_index = register_format(default_format_key, {'Width': default_width})
# 6b. Column width formats
col_format_map = {} # 1-based col -> format index
for col in col_width_map:
w = col_width_map[col]
key = get_format_key(width=w)
idx = register_format(key, {'Width': w})
col_format_map[int(col)] = idx
# 6c. Helper: determine fillType from cell content
def get_fill_type(cell):
if cell.get('param'):
return 'Parameter'
if cell.get('template'):
return 'Template'
if cell.get('text'):
return 'Text'
return ''
# Helper: register a cell format and return its index
def register_cell_format(style_name, fill_type):
resolved = resolve_style(style_name, fill_type)
key = get_format_key(
font_idx=resolved['FontIdx'],
lb=resolved['LB'], tb=resolved['TB'], rb=resolved['RB'], bb=resolved['BB'],
ha=resolved['HA'], va=resolved['VA'],
wrap=resolved['Wrap'], fill_type=resolved['FillType'],
number_format=resolved['NumberFormat'])
props = {
'FontIdx': resolved['FontIdx'],
'LB': resolved['LB'], 'TB': resolved['TB'],
'RB': resolved['RB'], 'BB': resolved['BB'],
'HA': resolved['HA'], 'VA': resolved['VA'],
'Wrap': resolved['Wrap'],
'FillType': resolved['FillType'],
'NumberFormat': resolved['NumberFormat'],
}
return register_format(key, props)
# Pre-register all formats from areas
for area in defn['areas']:
for row in area.get('rows', []):
# Skip empty row placeholder
if row.get('empty'):
continue
# Row height format
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
register_format(h_key, {'Height': int(row['height'])})
# rowStyle gap-fill format
if row.get('rowStyle'):
register_cell_format(row['rowStyle'], '')
# Explicit cell formats
if row.get('cells'):
for cell in row['cells']:
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
ft = get_fill_type(cell)
register_cell_format(cell_style, ft)
# --- 7. Generate XML ---
lines = []
# 7a. Header
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">')
# 7b. Language settings
lines.append('\t<languageSettings>')
lines.append('\t\t<currentLanguage>ru</currentLanguage>')
lines.append('\t\t<defaultLanguage>ru</defaultLanguage>')
lines.append('\t\t<languageInfo>')
lines.append('\t\t\t<id>ru</id>')
lines.append('\t\t\t<code>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</code>')
lines.append('\t\t\t<description>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</description>')
lines.append('\t\t</languageInfo>')
lines.append('\t</languageSettings>')
# 7c. Columns
lines.append('\t<columns>')
lines.append(f'\t\t<size>{total_columns}</size>')
# Emit columnsItem for columns with non-default widths
for col in sorted(col_format_map.keys()):
fmt_idx = col_format_map[col]
col_idx = col - 1 # Convert to 0-based
lines.append('\t\t<columnsItem>')
lines.append(f'\t\t\t<index>{col_idx}</index>')
lines.append('\t\t\t<column>')
lines.append(f'\t\t\t\t<formatIndex>{fmt_idx}</formatIndex>')
lines.append('\t\t\t</column>')
lines.append('\t\t</columnsItem>')
lines.append('\t</columns>')
# 7d. Rows -- main generation loop
global_row = 0
merges = []
named_items = []
active_rowspans = [] # list of {ColStart, ColEnd, StartLocalRow, EndLocalRow}
for area in defn['areas']:
area_start_row = global_row
area_name = area.get('name', '')
active_rowspans = []
local_row = 0
for row in area.get('rows', []):
# Empty row placeholder: emit N empty rows
if row.get('empty'):
count = int(row['empty'])
for ei in range(count):
lines.append('\t<rowsItem>')
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
lines.append('\t\t\t<empty>true</empty>')
lines.append('\t\t</row>')
lines.append('\t</rowsItem>')
global_row += 1
local_row += 1
continue
# Build set of columns occupied by rowspans from previous rows
rowspan_occupied = {}
for rs in active_rowspans:
if local_row > rs['StartLocalRow'] and local_row <= rs['EndLocalRow']:
for c in range(rs['ColStart'], rs['ColEnd'] + 1):
rowspan_occupied[c] = True
row_has_content = False
row_cells = []
# Determine row height format
row_format_idx = 0
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
if h_key in format_registry:
row_format_idx = format_order.index(h_key) + 1
if row.get('cells') and len(row['cells']) > 0:
row_has_content = True
# Build set of occupied columns (1-based)
occupied_cols = dict(rowspan_occupied)
for cell in row['cells']:
col_start = int(cell['col'])
col_span = int(cell.get('span', 1))
for c in range(col_start, col_start + col_span):
occupied_cols[c] = True
# Generate explicit cells
for cell in row['cells']:
col_start = int(cell['col'])
col_span = int(cell.get('span', 1))
rowspan = int(cell.get('rowspan', 1))
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
ft = get_fill_type(cell)
fmt_idx = register_cell_format(cell_style, ft)
cell_info = {
'Col': col_start - 1, # 0-based
'FormatIdx': fmt_idx,
'Param': cell.get('param'),
'Detail': cell.get('detail'),
'Text': cell.get('text'),
'Template': cell.get('template'),
}
row_cells.append(cell_info)
# Track rowspan for subsequent rows
if rowspan > 1:
active_rowspans.append({
'ColStart': col_start,
'ColEnd': col_start + col_span - 1,
'StartLocalRow': local_row,
'EndLocalRow': local_row + rowspan - 1,
})
# Collect merge
if col_span > 1 or rowspan > 1:
merge = {'R': global_row, 'C': col_start - 1, 'W': col_span - 1}
if rowspan > 1:
merge['H'] = rowspan - 1
merges.append(merge)
# Generate gap-fill cells for rowStyle
if row.get('rowStyle'):
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
for c in range(1, total_columns + 1):
if c not in occupied_cols:
row_cells.append({
'Col': c - 1,
'FormatIdx': gap_fmt_idx,
'Param': None,
'Detail': None,
'Text': None,
'Template': None,
})
# Sort cells by column
row_cells.sort(key=lambda x: x['Col'])
elif row.get('rowStyle'):
# Row with only rowStyle, no explicit cells
row_has_content = True
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
for c in range(1, total_columns + 1):
if c in rowspan_occupied:
continue
row_cells.append({
'Col': c - 1,
'FormatIdx': gap_fmt_idx,
'Param': None,
'Detail': None,
'Text': None,
'Template': None,
})
# Emit rowsItem
lines.append('\t<rowsItem>')
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
if row_format_idx > 0:
lines.append(f'\t\t\t<formatIndex>{row_format_idx}</formatIndex>')
if not row_has_content:
lines.append('\t\t\t<empty>true</empty>')
else:
for cell_info in row_cells:
lines.append('\t\t\t<c>')
lines.append(f'\t\t\t\t<i>{cell_info["Col"]}</i>')
lines.append('\t\t\t\t<c>')
lines.append(f'\t\t\t\t\t<f>{cell_info["FormatIdx"]}</f>')
if cell_info['Param']:
lines.append(f'\t\t\t\t\t<parameter>{cell_info["Param"]}</parameter>')
if cell_info['Detail']:
lines.append(f'\t\t\t\t\t<detailParameter>{cell_info["Detail"]}</detailParameter>')
if cell_info['Text']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Text"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
if cell_info['Template']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Template"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
lines.append('\t\t\t\t</c>')
lines.append('\t\t\t</c>')
lines.append('\t\t</row>')
lines.append('\t</rowsItem>')
local_row += 1
global_row += 1
area_end_row = global_row - 1
named_items.append({
'Name': area_name,
'BeginRow': area_start_row,
'EndRow': area_end_row,
})
total_row_count = global_row
# 7e. Scalar metadata
lines.append(f'\t<templateMode>true</templateMode>')
lines.append(f'\t<defaultFormatIndex>{default_format_index}</defaultFormatIndex>')
lines.append(f'\t<height>{total_row_count}</height>')
lines.append(f'\t<vgRows>{total_row_count}</vgRows>')
# 7f. Merges
for m in merges:
lines.append('\t<merge>')
lines.append(f'\t\t<r>{m["R"]}</r>')
lines.append(f'\t\t<c>{m["C"]}</c>')
if m.get('H'):
lines.append(f'\t\t<h>{m["H"]}</h>')
lines.append(f'\t\t<w>{m["W"]}</w>')
lines.append('\t</merge>')
# 7g. Named items
for ni in named_items:
lines.append('\t<namedItem xsi:type="NamedItemCells">')
lines.append(f'\t\t<name>{ni["Name"]}</name>')
lines.append('\t\t<area>')
lines.append('\t\t\t<type>Rows</type>')
lines.append(f'\t\t\t<beginRow>{ni["BeginRow"]}</beginRow>')
lines.append(f'\t\t\t<endRow>{ni["EndRow"]}</endRow>')
lines.append('\t\t\t<beginColumn>-1</beginColumn>')
lines.append('\t\t\t<endColumn>-1</endColumn>')
lines.append('\t\t</area>')
lines.append('\t</namedItem>')
# 7h. Line palette
if has_thin_borders:
lines.append('\t<line width="1" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
if has_thick_borders:
lines.append('\t<line width="2" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
# 7i. Font palette
for fe in font_entries:
lines.append(f'\t<font faceName="{fe["Face"]}" height="{fe["Size"]}" bold="{fe["Bold"]}" italic="{fe["Italic"]}" underline="{fe["Underline"]}" strikeout="{fe["Strikeout"]}" kind="Absolute" scale="100"/>')
# 7j. Format palette
for key in format_order:
fmt = format_registry[key]
lines.append('\t<format>')
if fmt.get('FontIdx') is not None and fmt.get('FontIdx', -1) >= 0:
lines.append(f'\t\t<font>{fmt["FontIdx"]}</font>')
if fmt.get('LB') is not None and fmt.get('LB', -1) >= 0:
lines.append(f'\t\t<leftBorder>{fmt["LB"]}</leftBorder>')
if fmt.get('TB') is not None and fmt.get('TB', -1) >= 0:
lines.append(f'\t\t<topBorder>{fmt["TB"]}</topBorder>')
if fmt.get('RB') is not None and fmt.get('RB', -1) >= 0:
lines.append(f'\t\t<rightBorder>{fmt["RB"]}</rightBorder>')
if fmt.get('BB') is not None and fmt.get('BB', -1) >= 0:
lines.append(f'\t\t<bottomBorder>{fmt["BB"]}</bottomBorder>')
if fmt.get('Width'):
lines.append(f'\t\t<width>{fmt["Width"]}</width>')
if fmt.get('Height'):
lines.append(f'\t\t<height>{fmt["Height"]}</height>')
if fmt.get('HA'):
lines.append(f'\t\t<horizontalAlignment>{fmt["HA"]}</horizontalAlignment>')
if fmt.get('VA'):
lines.append(f'\t\t<verticalAlignment>{fmt["VA"]}</verticalAlignment>')
if fmt.get('Wrap') is True:
lines.append('\t\t<textPlacement>Wrap</textPlacement>')
if fmt.get('FillType'):
lines.append(f'\t\t<fillType>{fmt["FillType"]}</fillType>')
if fmt.get('NumberFormat'):
lines.append('\t\t<format>')
lines.append('\t\t\t<v8:item>')
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t<v8:content>{esc_xml(fmt["NumberFormat"])}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append('\t\t</format>')
lines.append('\t</format>')
# 7k. Close document
lines.append('</document>')
# --- 8. Write output ---
out_path = args.OutputPath
if not os.path.isabs(out_path):
out_path = os.path.join(os.getcwd(), out_path)
out_dir = os.path.dirname(out_path)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
content = '\n'.join(lines) + '\n'
write_utf8_bom(out_path, content)
# --- 9. Summary ---
print(f"[OK] Compiled: {args.OutputPath}")
if defn.get('page'):
print(f" Page: {page_name} -> target {target_width}, defaultWidth={default_width}")
print(f" Areas: {len(named_items)}, Rows: {total_row_count}, Columns: {total_columns}")
print(f" Fonts: {len(font_entries)}, Lines: {line_count}, Formats: {len(format_registry)}")
print(f" Merges: {len(merges)}")
if __name__ == '__main__':
main()
@@ -0,0 +1,703 @@
#!/usr/bin/env python3
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import sys
from collections import OrderedDict
from lxml import etree
# --- Namespace map ---
NSMAP = {
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
"v8": "http://v8.1c.ru/8.1/data/core",
"v8ui": "http://v8.1c.ru/8.1/data/ui",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
def find(node, xpath):
return node.find(xpath, NSMAP)
def findall(node, xpath):
return node.findall(xpath, NSMAP)
def text_of(node):
if node is not None and node.text:
return node.text
return None
def int_of(node, default=0):
if node is not None and node.text:
return int(node.text)
return default
# --- Main ---
def main():
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True, help="Path to Template.xml")
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
args = parser.parse_args()
template_path = args.TemplatePath
output_path = args.OutputPath
# --- 1. Load and parse XML ---
if not os.path.isfile(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(template_path, parser_xml)
root = tree.getroot()
# --- 2. Extract font palette ---
raw_fonts = []
for f_node in findall(root, "d:font"):
raw_fonts.append({
"Face": f_node.get("faceName", ""),
"Size": int(f_node.get("height", "0")),
"Bold": f_node.get("bold") == "true",
"Italic": f_node.get("italic") == "true",
"Underline": f_node.get("underline") == "true",
"Strikeout": f_node.get("strikeout") == "true",
})
# --- 3. Extract line palette ---
raw_lines = []
for l_node in findall(root, "d:line"):
raw_lines.append({"Width": int(l_node.get("width", "0"))})
# --- 4. Extract format palette ---
raw_formats = []
for fmt_node in findall(root, "d:format"):
fmt = {
"FontIdx": -1,
"LB": -1, "TB": -1, "RB": -1, "BB": -1,
"Width": 0, "Height": 0,
"HA": "", "VA": "",
"Wrap": False, "FillType": "", "DataFormat": "",
}
n = find(fmt_node, "d:font")
if n is not None and n.text:
fmt["FontIdx"] = int(n.text)
n = find(fmt_node, "d:leftBorder")
if n is not None and n.text:
fmt["LB"] = int(n.text)
n = find(fmt_node, "d:topBorder")
if n is not None and n.text:
fmt["TB"] = int(n.text)
n = find(fmt_node, "d:rightBorder")
if n is not None and n.text:
fmt["RB"] = int(n.text)
n = find(fmt_node, "d:bottomBorder")
if n is not None and n.text:
fmt["BB"] = int(n.text)
n = find(fmt_node, "d:width")
if n is not None and n.text:
fmt["Width"] = int(n.text)
n = find(fmt_node, "d:height")
if n is not None and n.text:
fmt["Height"] = int(n.text)
n = find(fmt_node, "d:horizontalAlignment")
if n is not None and n.text:
fmt["HA"] = n.text
n = find(fmt_node, "d:verticalAlignment")
if n is not None and n.text:
fmt["VA"] = n.text
n = find(fmt_node, "d:textPlacement")
if n is not None and n.text == "Wrap":
fmt["Wrap"] = True
n = find(fmt_node, "d:fillType")
if n is not None and n.text:
fmt["FillType"] = n.text
n = find(fmt_node, "d:format/v8:item/v8:content")
if n is not None and n.text:
fmt["DataFormat"] = n.text
raw_formats.append(fmt)
def get_format(idx):
if idx <= 0 or idx > len(raw_formats):
return None
return raw_formats[idx - 1]
# --- 5. Extract columns and default width ---
col_node = find(root, "d:columns")
total_columns = int_of(find(col_node, "d:size"))
col_format_indices = {}
for ci in findall(col_node, "d:columnsItem"):
col_idx = int_of(find(ci, "d:index"))
fmt_idx = int_of(find(ci, "d:column/d:formatIndex"))
col_format_indices[col_idx] = fmt_idx
default_fmt_idx = 0
n = find(root, "d:defaultFormatIndex")
if n is not None and n.text:
default_fmt_idx = int(n.text)
default_width = 10
if default_fmt_idx > 0:
def_fmt = get_format(default_fmt_idx)
if def_fmt and def_fmt["Width"] > 0:
default_width = def_fmt["Width"]
# Build column width map (1-based col -> width), only non-default
col_width_map = OrderedDict()
for col0 in sorted(col_format_indices.keys()):
fmt = get_format(col_format_indices[col0])
if fmt and fmt["Width"] > 0 and fmt["Width"] != default_width:
col1 = str(col0 + 1)
col_width_map[col1] = fmt["Width"]
# --- 6. Extract merges ---
merge_map = {}
for m_node in findall(root, "d:merge"):
r = int_of(find(m_node, "d:r"))
c = int_of(find(m_node, "d:c"))
w = int_of(find(m_node, "d:w"))
h_node = find(m_node, "d:h")
h = int_of(h_node) if h_node is not None else 0
merge_map[f"{r},{c}"] = {"W": w, "H": h}
# --- 7. Extract named items ---
named_areas = []
for ni_node in findall(root, "d:namedItem"):
xsi_type = ni_node.get(f"{{{XSI_NS}}}type", "")
if xsi_type != "NamedItemCells":
continue
area_node = find(ni_node, "d:area")
area_type_node = find(area_node, "d:type")
area_type = text_of(area_type_node) or ""
if area_type != "Rows":
continue
named_areas.append({
"Name": text_of(find(ni_node, "d:name")) or "",
"BeginRow": int_of(find(area_node, "d:beginRow")),
"EndRow": int_of(find(area_node, "d:endRow")),
})
# --- 8. Extract rows ---
row_data = {}
for ri_node in findall(root, "d:rowsItem"):
row_idx = int_of(find(ri_node, "d:index"))
row_node = find(ri_node, "d:row")
index_to = row_idx
it_node = find(ri_node, "d:indexTo")
if it_node is not None and it_node.text:
index_to = int(it_node.text)
row_fmt_idx = 0
fmt_node = find(row_node, "d:formatIndex")
if fmt_node is not None and fmt_node.text:
row_fmt_idx = int(fmt_node.text)
is_empty = False
empty_node = find(row_node, "d:empty")
if empty_node is not None and empty_node.text == "true":
is_empty = True
cells = []
if not is_empty:
col = -1
for c_group in findall(row_node, "d:c"):
i_node = find(c_group, "d:i")
if i_node is not None and i_node.text:
col = int(i_node.text)
else:
col += 1
c_content = find(c_group, "d:c")
if c_content is None:
continue
cell_fmt_idx = 0
f_node = find(c_content, "d:f")
if f_node is not None and f_node.text:
cell_fmt_idx = int(f_node.text)
param = None
p_node = find(c_content, "d:parameter")
if p_node is not None and p_node.text:
param = p_node.text
detail = None
d_node = find(c_content, "d:detailParameter")
if d_node is not None and d_node.text:
detail = d_node.text
text = None
t_node = find(c_content, "d:tl/v8:item/v8:content")
if t_node is not None and t_node.text:
text = t_node.text
cells.append({
"Col": col,
"FormatIdx": cell_fmt_idx,
"Param": param,
"Detail": detail,
"Text": text,
})
for r in range(row_idx, index_to + 1):
row_data[r] = {
"FormatIdx": row_fmt_idx,
"Cells": cells,
"Empty": is_empty,
}
# --- 9. Build style key (ignoring fillType) ---
def get_border_desc(fmt):
if not fmt:
return {"Border": "none", "Thick": False}
lb = fmt["LB"] >= 0
tb = fmt["TB"] >= 0
rb = fmt["RB"] >= 0
bb = fmt["BB"] >= 0
if not lb and not tb and not rb and not bb:
return {"Border": "none", "Thick": False}
thick = False
for b_idx in [fmt["LB"], fmt["TB"], fmt["RB"], fmt["BB"]]:
if b_idx >= 0 and b_idx < len(raw_lines) and raw_lines[b_idx]["Width"] >= 2:
thick = True
break
if lb and tb and rb and bb:
return {"Border": "all", "Thick": thick}
sides = []
if tb:
sides.append("top")
if bb:
sides.append("bottom")
if lb:
sides.append("left")
if rb:
sides.append("right")
return {"Border": ",".join(sides), "Thick": thick}
def get_style_key(fmt):
if not fmt:
return "empty"
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
bd = get_border_desc(fmt)
return f"f={fi}|b={bd['Border']}|bw={bd['Thick']}|ha={fmt['HA']}|va={fmt['VA']}|wr={fmt['Wrap']}|df={fmt['DataFormat']}"
# --- 10. Name fonts ---
font_names = {}
font_defs = OrderedDict()
if len(raw_fonts) > 0:
font_names[0] = "default"
font_defs["default"] = raw_fonts[0]
def get_font_key(f):
return f"{f['Face']}|{f['Size']}|{f['Bold']}|{f['Italic']}|{f['Underline']}|{f['Strikeout']}"
font_key_map = {}
if len(raw_fonts) > 0:
font_key_map[get_font_key(raw_fonts[0])] = "default"
for i in range(1, len(raw_fonts)):
f = raw_fonts[i]
df = raw_fonts[0]
# Dedup: if identical font already named, reuse
f_key = get_font_key(f)
if f_key in font_key_map:
font_names[i] = font_key_map[f_key]
continue
name = None
if f["Face"] == df["Face"] and f["Size"] == df["Size"]:
if f["Bold"] and not df["Bold"] and not f["Italic"] and not f["Underline"] and not f["Strikeout"]:
name = "bold"
elif f["Italic"] and not df["Italic"] and not f["Bold"]:
name = "italic"
elif f["Underline"] and not df["Underline"] and not f["Bold"] and not f["Italic"]:
name = "underline"
elif f["Face"] == df["Face"] and f["Size"] > df["Size"] and f["Bold"]:
name = "header"
elif f["Face"] == df["Face"] and f["Size"] < df["Size"]:
name = "small"
if not name:
parts = []
if f["Face"] and f["Face"] != df["Face"]:
parts.append(f["Face"].lower())
parts.append(str(f["Size"]))
if f["Bold"]:
parts.append("bold")
if f["Italic"]:
parts.append("italic")
if f["Underline"]:
parts.append("underline")
if f["Strikeout"]:
parts.append("strikeout")
name = "-".join(parts)
base_name = name
suffix = 2
while name in font_defs:
name = f"{base_name}{suffix}"
suffix += 1
font_names[i] = name
font_defs[name] = f
font_key_map[f_key] = name
# --- 11. Collect and name styles ---
style_keys = OrderedDict()
format_to_style_key = {}
for rd in row_data.values():
for cell in rd["Cells"]:
fmt = get_format(cell["FormatIdx"])
if not fmt:
continue
key = get_style_key(fmt)
if key not in style_keys:
style_keys[key] = fmt
format_to_style_key[cell["FormatIdx"]] = key
def name_style(fmt):
if not fmt:
return "default"
parts = []
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
if fi in font_names and font_names[fi] != "default":
parts.append(font_names[fi])
bd = get_border_desc(fmt)
if bd["Border"] != "none":
if bd["Border"] == "all":
parts.append("bordered")
else:
parts.append(f"border-{bd['Border']}")
if fmt["HA"] == "Center":
parts.append("center")
elif fmt["HA"] == "Right":
parts.append("right")
if fmt["VA"] == "Center":
parts.append("vcenter")
elif fmt["VA"] == "Top":
parts.append("vtop")
if fmt["Wrap"]:
parts.append("wrap")
if fmt["DataFormat"]:
parts.append("fmt")
if len(parts) == 0:
return "default"
return "-".join(parts)
style_names = OrderedDict()
style_defs = OrderedDict()
for key in style_keys:
fmt = style_keys[key]
name = name_style(fmt)
base_name = name
suffix = 2
while name in style_defs:
name = f"{base_name}{suffix}"
suffix += 1
style_names[key] = name
s_def = OrderedDict()
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
if fi in font_names and font_names[fi] != "default":
s_def["font"] = font_names[fi]
if fmt["HA"]:
a_map = {"Left": "left", "Center": "center", "Right": "right"}
a = a_map.get(fmt["HA"])
if a:
s_def["align"] = a
if fmt["VA"]:
va_map = {"Top": "top", "Center": "center"}
a = va_map.get(fmt["VA"])
if a:
s_def["valign"] = a
bd = get_border_desc(fmt)
if bd["Border"] != "none":
s_def["border"] = bd["Border"]
if bd["Thick"]:
s_def["borderWidth"] = "thick"
if fmt["Wrap"]:
s_def["wrap"] = True
if fmt["DataFormat"]:
s_def["format"] = fmt["DataFormat"]
style_defs[name] = s_def
def get_style_name(fmt_idx):
key = format_to_style_key.get(fmt_idx)
if key and key in style_names:
return style_names[key]
return "default"
# --- 12. Build areas ---
dsl_areas = []
for area in named_areas:
area_rows = []
for global_row in range(area["BeginRow"], area["EndRow"] + 1):
rd = row_data.get(global_row)
if not rd or rd["Empty"]:
area_rows.append(OrderedDict())
continue
dsl_row = OrderedDict()
# Row height
if rd["FormatIdx"] > 0:
row_fmt = get_format(rd["FormatIdx"])
if row_fmt and row_fmt["Height"] > 0:
dsl_row["height"] = row_fmt["Height"]
# Separate content cells from gap-fill cells
content_cells = []
gap_cells = []
for cell in rd["Cells"]:
has_content = cell["Param"] or cell["Text"]
has_merge = f"{global_row},{cell['Col']}" in merge_map
if has_content or has_merge:
content_cells.append(cell)
else:
gap_cells.append(cell)
# Detect rowStyle
row_style_name = None
row_style_key = None
if len(gap_cells) > 0:
gap_keys = {}
for gc in gap_cells:
fmt = get_format(gc["FormatIdx"])
gap_keys[get_style_key(fmt)] = True
if len(gap_keys) == 1:
row_style_key = list(gap_keys.keys())[0]
if row_style_key in style_names:
row_style_name = style_names[row_style_key]
if row_style_name and row_style_name != "default":
dsl_row["rowStyle"] = row_style_name
# Build cell list
dsl_cells = []
for cell in sorted(content_cells, key=lambda c: c["Col"]):
dsl_cell = OrderedDict()
dsl_cell["col"] = cell["Col"] + 1
# Span/rowspan from merge
mk = f"{global_row},{cell['Col']}"
if mk in merge_map:
m = merge_map[mk]
if m["W"] > 0:
dsl_cell["span"] = m["W"] + 1
if m["H"] > 0:
dsl_cell["rowspan"] = m["H"] + 1
# Style
cell_fmt = get_format(cell["FormatIdx"])
cell_style_key = get_style_key(cell_fmt)
if row_style_key and cell_style_key == row_style_key:
pass # Inherits rowStyle
else:
sn = get_style_name(cell["FormatIdx"])
if sn != "default" or not row_style_name:
dsl_cell["style"] = sn
# Content
fill_type = cell_fmt["FillType"] if cell_fmt else ""
if cell["Param"]:
dsl_cell["param"] = cell["Param"]
if cell["Detail"]:
dsl_cell["detail"] = cell["Detail"]
elif fill_type == "Template" and cell["Text"]:
dsl_cell["template"] = cell["Text"]
elif cell["Text"]:
dsl_cell["text"] = cell["Text"]
dsl_cells.append(dsl_cell)
if len(dsl_cells) > 0:
dsl_row["cells"] = dsl_cells
area_rows.append(dsl_row)
# Compress consecutive empty rows ({}) into { empty = N }
compressed_rows = []
empty_run = 0
for r in area_rows:
if len(r) == 0:
empty_run += 1
else:
if empty_run > 0:
if empty_run == 1:
compressed_rows.append(OrderedDict())
else:
compressed_rows.append(OrderedDict([("empty", empty_run)]))
empty_run = 0
compressed_rows.append(r)
if empty_run > 0:
if empty_run == 1:
compressed_rows.append(OrderedDict())
else:
compressed_rows.append(OrderedDict([("empty", empty_run)]))
dsl_areas.append(OrderedDict([
("name", area["Name"]),
("rows", compressed_rows),
]))
# --- 13. Compress columnWidths ---
compressed_widths = OrderedDict()
if len(col_width_map) > 0:
# Group columns by width
width_to_cols = {}
for col_str, width in col_width_map.items():
width_to_cols.setdefault(width, []).append(col_str)
for width, cols in width_to_cols.items():
cols_sorted = sorted(cols, key=lambda x: int(x))
ranges = []
range_start = cols_sorted[0]
range_prev = cols_sorted[0]
for i in range(1, len(cols_sorted)):
if int(cols_sorted[i]) == int(range_prev) + 1:
range_prev = cols_sorted[i]
else:
if range_start == range_prev:
ranges.append(range_start)
else:
ranges.append(f"{range_start}-{range_prev}")
range_start = cols_sorted[i]
range_prev = cols_sorted[i]
if range_start == range_prev:
ranges.append(range_start)
else:
ranges.append(f"{range_start}-{range_prev}")
for rng in ranges:
compressed_widths[rng] = width
# --- 14. Build fonts output ---
fonts_out = OrderedDict()
for name, f in font_defs.items():
f_out = OrderedDict()
f_out["face"] = f["Face"]
f_out["size"] = f["Size"]
if f["Bold"]:
f_out["bold"] = True
if f["Italic"]:
f_out["italic"] = True
if f["Underline"]:
f_out["underline"] = True
if f["Strikeout"]:
f_out["strikeout"] = True
fonts_out[name] = f_out
# --- 15. Assemble result ---
result = OrderedDict()
result["columns"] = total_columns
result["defaultWidth"] = default_width
if len(compressed_widths) > 0:
result["columnWidths"] = compressed_widths
# Remove empty "default" style
if "default" in style_defs and len(style_defs["default"]) == 0:
del style_defs["default"]
# Remove unused styles
used_styles = set()
for a in dsl_areas:
for r in a["rows"]:
if "rowStyle" in r:
used_styles.add(r["rowStyle"])
if "cells" in r:
for c in r["cells"]:
if "style" in c:
used_styles.add(c["style"])
to_remove = [s for s in style_defs if s not in used_styles]
for s in to_remove:
del style_defs[s]
result["fonts"] = fonts_out
result["styles"] = style_defs
result["areas"] = dsl_areas
# --- 16. Convert to JSON ---
json_str = json.dumps(result, ensure_ascii=False, indent=2)
# --- 17. Output ---
if output_path:
abs_path = os.path.join(os.getcwd(), output_path) if not os.path.isabs(output_path) else output_path
with open(abs_path, "w", encoding="utf-8") as fh:
fh.write(json_str)
print(f"[OK] Decompiled: {output_path}")
else:
print(json_str)
print(f" Areas: {len(named_areas)}, Rows: {len(row_data)}, Columns: {total_columns}", file=sys.stderr)
print(f" Fonts: {len(font_defs)}, Styles: {len(style_defs)}, Merges: {len(merge_map)}", file=sys.stderr)
if __name__ == "__main__":
main()
+442
View File
@@ -0,0 +1,442 @@
#!/usr/bin/env python3
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C spreadsheet (MXL) structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", default="", help="Path to Template.xml")
parser.add_argument("-ProcessorName", default="", help="Processor name (used with -TemplateName)")
parser.add_argument("-TemplateName", default="", help="Template name (used with -ProcessorName)")
parser.add_argument("-SrcDir", default="src", help="Source directory (default: src)")
parser.add_argument("-Format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("-WithText", action="store_true", default=False, help="Include text content")
parser.add_argument("-MaxParams", type=int, default=10, help="Max parameters to show per area")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
args = parser.parse_args()
# --- Resolve template path ---
template_path = args.TemplatePath
if not template_path:
if not args.ProcessorName or not args.TemplateName:
print("Specify -TemplatePath or both -ProcessorName and -TemplateName", file=sys.stderr)
sys.exit(1)
template_path = os.path.join(args.SrcDir, args.ProcessorName, "Templates", args.TemplateName, "Ext", "Template.xml")
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.isfile(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(template_path, etree.XMLParser(remove_blank_text=True))
root = tree.getroot()
NS = {
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
"v8": "http://v8.1c.ru/8.1/data/core",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
# --- Column sets ---
column_sets = []
default_col_count = 0
for cols in root.findall("d:columns", NS):
size_node = cols.find("d:size", NS)
id_node = cols.find("d:id", NS)
size = int(size_node.text) if size_node is not None and size_node.text else 0
if id_node is not None:
column_sets.append({"Id": id_node.text or "", "Size": size})
else:
default_col_count = size
# --- Rows: collect row data ---
row_nodes = root.findall("d:rowsItem", NS)
total_rows = len(row_nodes)
height_node = root.find("d:height", NS)
doc_height = int(height_node.text) if height_node is not None and height_node.text else total_rows
# --- Named items ---
named_areas = []
named_drawings = []
for ni in root.findall("d:namedItem", NS):
ni_type = ni.get(f"{{{XSI_NS}}}type", "")
name_node = ni.find("d:name", NS)
name = name_node.text if name_node is not None else ""
if "NamedItemCells" in ni_type:
area = ni.find("d:area", NS)
area_type_node = area.find("d:type", NS)
area_type = area_type_node.text if area_type_node is not None else ""
begin_row = int(area.find("d:beginRow", NS).text)
end_row = int(area.find("d:endRow", NS).text)
begin_col = int(area.find("d:beginColumn", NS).text)
end_col = int(area.find("d:endColumn", NS).text)
cols_id = None
cols_id_node = area.find("d:columnsID", NS)
if cols_id_node is not None:
cols_id = cols_id_node.text
named_areas.append({
"Name": name,
"AreaType": area_type,
"BeginRow": begin_row,
"EndRow": end_row,
"BeginCol": begin_col,
"EndCol": end_col,
"ColumnsID": cols_id,
})
elif "NamedItemDrawing" in ni_type:
draw_id_node = ni.find("d:drawingID", NS)
draw_id = draw_id_node.text if draw_id_node is not None else ""
named_drawings.append({"Name": name, "DrawingID": draw_id})
# --- Scan rows for parameters and text ---
# Build row index map: rowIndex -> XmlNode
row_map = {}
for ri in row_nodes:
idx_node = ri.find("d:index", NS)
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
row_map[idx] = ri
def get_cell_data(row_node, include_text):
row = row_node.find("d:row", NS)
if row is None:
return []
results = []
for c_group in row.findall("d:c", NS):
cell = c_group.find("d:c", NS)
if cell is None:
continue
param = cell.find("d:parameter", NS)
detail = cell.find("d:detailParameter", NS)
tl = cell.find("d:tl", NS)
if param is not None:
entry = {"Kind": "Parameter", "Value": param.text or ""}
if detail is not None:
entry["Detail"] = detail.text or ""
results.append(entry)
if tl is not None:
content = tl.find("v8:item/v8:content", NS)
if content is not None and content.text:
text = content.text
is_template = bool(re.search(r'\[.+\]', text))
if is_template:
# Extract parameter names from [Param] placeholders
# Skip numeric-only like [5]
for m in re.finditer(r'\[([^\]]+)\]', text):
val = m.group(1)
if not re.match(r'^\d+$', val):
results.append({"Kind": "TemplateParam", "Value": val})
# Full template text only with -WithText
if include_text:
results.append({"Kind": "Template", "Value": text})
elif include_text:
results.append({"Kind": "Text", "Value": text})
return results
def get_area_cell_data(area, row_map_ref, include_text):
params = []
details = []
texts = []
templates = []
start_row = area["BeginRow"]
end_row = area["EndRow"]
if start_row == -1:
start_row = 0
if end_row == -1:
end_row = doc_height - 1
for r in range(start_row, end_row + 1):
if r in row_map_ref:
cells = get_cell_data(row_map_ref[r], include_text)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
params.append(c["Value"])
if "Detail" in c:
details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
texts.append(c["Value"])
elif kind == "Template":
templates.append(c["Value"])
return {"Params": params, "Details": details, "Texts": texts, "Templates": templates}
# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow
def area_sort_key(a):
if a["AreaType"] == "Columns":
return (a["BeginCol"], a["Name"])
return (a["BeginRow"], a["Name"])
named_areas.sort(key=area_sort_key)
# Collect data for each area
area_data = []
covered_rows = set()
for area in named_areas:
data = get_area_cell_data(area, row_map, args.WithText)
area_data.append({
"Area": area,
"Params": data["Params"],
"Details": data["Details"],
"Texts": data["Texts"],
"Templates": data["Templates"],
})
# Track covered rows
sr = area["BeginRow"]
er = area["EndRow"]
if sr != -1 and er != -1:
for r in range(sr, er + 1):
covered_rows.add(r)
# Find parameters outside named areas
outside_params = []
outside_details = []
outside_texts = []
outside_templates = []
for r in sorted(row_map.keys()):
if r not in covered_rows:
cells = get_cell_data(row_map[r], args.WithText)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
outside_params.append(c["Value"])
if "Detail" in c:
outside_details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
outside_params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
outside_texts.append(c["Value"])
elif kind == "Template":
outside_templates.append(c["Value"])
# --- Counts ---
merge_count = len(root.findall("d:merge", NS))
drawing_nodes = root.findall("d:drawing", NS)
drawing_count = len(drawing_nodes)
# --- Output ---
def truncate_list(items, max_count):
if len(items) <= max_count:
return ", ".join(items)
shown = ", ".join(items[:max_count])
remaining = len(items) - max_count
return f"{shown}, ... (+{remaining})"
# Determine template name from path
template_name = os.path.basename(os.path.dirname(os.path.dirname(template_path)))
if args.Format == "json":
result = {
"name": template_name,
"rows": doc_height,
"columns": default_col_count,
"columnSets": [{"id": cs["Id"], "size": cs["Size"]} for cs in column_sets],
"areas": [],
"outsideParams": list(outside_params),
"mergeCount": merge_count,
"drawingCount": drawing_count,
}
for ad in area_data:
area_obj = {
"name": ad["Area"]["Name"],
"type": ad["Area"]["AreaType"],
"beginRow": ad["Area"]["BeginRow"],
"endRow": ad["Area"]["EndRow"],
"beginCol": ad["Area"]["BeginCol"],
"endCol": ad["Area"]["EndCol"],
"params": list(ad["Params"]),
}
if ad["Area"]["ColumnsID"]:
area_obj["columnsID"] = ad["Area"]["ColumnsID"]
if args.WithText:
area_obj["texts"] = list(ad["Texts"])
area_obj["templates"] = list(ad["Templates"])
result["areas"].append(area_obj)
if args.WithText:
result["outsideTexts"] = list(outside_texts)
result["outsideTemplates"] = list(outside_templates)
for nd in named_drawings:
result["areas"].append({
"name": nd["Name"],
"type": "Drawing",
"drawingID": nd["DrawingID"],
})
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
# --- Text format output ---
lines = []
lines.append(f"=== {template_name} ===")
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
if len(column_sets) == 0:
lines.append(" Column sets: 1 (default only)")
else:
lines.append(f" Column sets: {len(column_sets) + 1} (default={default_col_count} cols + {len(column_sets)} additional)")
for cs in column_sets:
lines.append(f" {cs['Id'][:8]}...: {cs['Size']} cols")
lines.append("")
lines.append("--- Named areas ---")
for ad in area_data:
a = ad["Area"]
param_count = len(ad["Params"])
row_range = ""
if a["AreaType"] == "Rows":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}"
elif a["AreaType"] == "Columns":
row_range = f"cols {a['BeginCol']}-{a['EndCol']}"
elif a["AreaType"] == "Rectangle":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}, cols {a['BeginCol']}-{a['EndCol']}"
cols_info = ""
if a["ColumnsID"]:
cs_size = ""
for cs in column_sets:
if cs["Id"] == a["ColumnsID"]:
cs_size = f" {cs['Size']}cols"
break
cols_info = f" [colset{cs_size}]"
param_info = f"({param_count} params)"
name_str = a["Name"].ljust(25)
type_str = a["AreaType"].ljust(12)
lines.append(f" {name_str} {type_str} {row_range} {param_info}{cols_info}")
for nd in named_drawings:
name_str = nd["Name"].ljust(25)
lines.append(f" {name_str} Drawing drawingID={nd['DrawingID']}")
# Detect intersection pairs (Rows + Columns areas that overlap)
rows_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Rows"]
cols_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Columns"]
intersections = []
if rows_areas and cols_areas:
for ra in rows_areas:
for ca in cols_areas:
intersections.append(f"{ra['Area']['Name']}|{ca['Area']['Name']}")
if intersections:
lines.append("")
lines.append("--- Intersections (use with GetArea) ---")
for pair in intersections:
lines.append(f" {pair}")
# Parameters by area
has_params = any(len(ad["Params"]) > 0 for ad in area_data) or len(outside_params) > 0
if has_params:
lines.append("")
lines.append("--- Parameters by area ---")
for ad in area_data:
if len(ad["Params"]) > 0:
param_str = truncate_list(ad["Params"], args.MaxParams)
lines.append(f" {ad['Area']['Name']}: {param_str}")
# Show detailParameters if any
if len(ad["Details"]) > 0:
detail_str = truncate_list(ad["Details"], args.MaxParams)
lines.append(f" detail: {detail_str}")
if len(outside_params) > 0:
param_str = truncate_list(outside_params, args.MaxParams)
lines.append(f" (outside areas): {param_str}")
if len(outside_details) > 0:
detail_str = truncate_list(outside_details, args.MaxParams)
lines.append(f" detail: {detail_str}")
# WithText sections
if args.WithText:
has_text = any(len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0 for ad in area_data) or len(outside_texts) > 0 or len(outside_templates) > 0
if has_text:
lines.append("")
lines.append("--- Text content ---")
for ad in area_data:
if len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0:
lines.append(f" {ad['Area']['Name']}:")
if len(ad["Texts"]) > 0:
text_items = [f'"{t}"' for t in ad["Texts"]]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(ad["Templates"]) > 0:
tpl_items = [f'"{t}"' for t in ad["Templates"]]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
if len(outside_texts) > 0 or len(outside_templates) > 0:
lines.append(" (outside areas):")
if len(outside_texts) > 0:
text_items = [f'"{t}"' for t in outside_texts]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(outside_templates) > 0:
tpl_items = [f'"{t}"' for t in outside_templates]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
lines.append("")
lines.append("--- Stats ---")
lines.append(f" Merges: {merge_count}")
lines.append(f" Drawings: {drawing_count}")
# --- Truncation protection ---
total_lines = len(lines)
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
lines = lines[args.Offset:]
if len(lines) > args.Limit:
shown = lines[:args.Limit]
for l in shown:
print(l)
remaining = total_lines - args.Offset - args.Limit
print("")
print(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
else:
for l in lines:
print(l)
@@ -0,0 +1,365 @@
#!/usr/bin/env python3
# mxl-validate v1.0 — Validate 1C spreadsheet document Template.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges."""
import sys, os, argparse
from lxml import etree
NS_D = 'http://v8.1c.ru/8.2/data/spreadsheet'
NS_V8 = 'http://v8.1c.ru/8.1/data/core'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS = {
'd': NS_D,
'v8': NS_V8,
'xsi': NS_XSI,
}
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
def ok(self, msg):
print(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
print(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
print(f'[WARN] {msg}')
def int_text(node):
"""Return int from node text, or 0 if None."""
if node is not None and node.text:
return int(node.text)
return 0
def main():
parser = argparse.ArgumentParser(
description='Validate 1C spreadsheet document Template.xml', allow_abbrev=False
)
parser.add_argument('-TemplatePath', dest='TemplatePath', default='')
parser.add_argument('-ProcessorName', dest='ProcessorName', default='')
parser.add_argument('-TemplateName', dest='TemplateName', default='')
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=20)
args = parser.parse_args()
template_path = args.TemplatePath
processor_name = args.ProcessorName
template_name_arg = args.TemplateName
src_dir = args.SrcDir
max_errors = args.MaxErrors
# --- Resolve template path ---
if not template_path:
if not processor_name or not template_name_arg:
print('Specify -TemplatePath or both -ProcessorName and -TemplateName', file=sys.stderr)
sys.exit(1)
template_path = os.path.join(src_dir, processor_name, 'Templates',
template_name_arg, 'Ext', 'Template.xml')
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.exists(template_path):
print(f'File not found: {template_path}', file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(template_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
root = xml_doc.getroot()
r = Reporter(max_errors)
# Derive template name from path: .../Templates/<Name>/Ext/Template.xml
# Go up 2 levels from Template.xml -> Ext -> <Name>
template_display_name = os.path.basename(os.path.dirname(os.path.dirname(resolved_path)))
print(f'=== Validation: {template_display_name} ===')
print()
# --- Collect palettes ---
line_nodes = root.findall(f'{{{NS_D}}}line')
line_count = len(line_nodes)
font_nodes = [node for node in root if isinstance(node.tag, str) and etree.QName(node.tag).localname == 'font']
font_count = len(font_nodes)
format_nodes = [node for node in root if isinstance(node.tag, str) and etree.QName(node.tag).localname == 'format']
format_count = len(format_nodes)
picture_nodes = root.findall(f'{{{NS_D}}}picture')
picture_count = len(picture_nodes)
# --- Collect column sets ---
column_sets = {} # id -> size
default_col_count = 0
for cols in root.findall(f'{{{NS_D}}}columns'):
size_node = cols.find(f'{{{NS_D}}}size')
id_node = cols.find(f'{{{NS_D}}}id')
size = int_text(size_node)
if id_node is not None and id_node.text:
column_sets[id_node.text] = size
else:
default_col_count = size
# --- Check 1: height vs actual rows ---
row_nodes = root.findall(f'{{{NS_D}}}rowsItem')
height_node = root.find(f'{{{NS_D}}}height')
doc_height = int_text(height_node)
max_row_index = -1
for ri in row_nodes:
idx_node = ri.find(f'{{{NS_D}}}index')
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
if idx > max_row_index:
max_row_index = idx
expected_min_height = max_row_index + 1
if doc_height >= expected_min_height:
r.ok(f'height ({doc_height}) >= max row index + 1 ({expected_min_height}), rowsItem count={len(row_nodes)}')
else:
r.error(f'height={doc_height} but max row index={max_row_index} (need at least {expected_min_height})')
# --- Check 2: vgRows <= height ---
vg_rows_node = root.find(f'{{{NS_D}}}vgRows')
if vg_rows_node is not None:
vg_rows = int_text(vg_rows_node)
if vg_rows <= doc_height:
r.ok(f'vgRows ({vg_rows}) <= height ({doc_height})')
else:
r.warn(f'vgRows ({vg_rows}) > height ({doc_height})')
# --- Build row data for checks ---
max_format_ref = 0
max_font_ref = 0
max_line_ref = 0
# Check format palette references in formats (font, border indices)
for fmt in format_nodes:
font_idx_node = fmt.find(f'{{{NS_D}}}font')
if font_idx_node is not None and font_idx_node.text:
val = int(font_idx_node.text)
if val > max_font_ref:
max_font_ref = val
for border_name in ('leftBorder', 'topBorder', 'rightBorder', 'bottomBorder', 'drawingBorder'):
border_node = fmt.find(f'{{{NS_D}}}{border_name}')
if border_node is not None and border_node.text:
val = int(border_node.text)
if val > max_line_ref:
max_line_ref = val
# --- Check 10: font indices in formats ---
if font_count > 0:
if max_font_ref < font_count:
r.ok(f'Font refs: max={max_font_ref}, palette size={font_count}')
else:
r.error(f'Font index {max_font_ref} exceeds palette size ({font_count})')
elif max_font_ref > 0:
r.error(f'Font index {max_font_ref} referenced but no fonts defined')
else:
r.ok('No font references')
# --- Check 11: line/border indices in formats ---
if line_count > 0:
if max_line_ref < line_count:
r.ok(f'Line/border refs: max={max_line_ref}, palette size={line_count}')
else:
r.error(f'Line index {max_line_ref} exceeds palette size ({line_count})')
elif max_line_ref > 0:
r.error(f'Line index {max_line_ref} referenced but no lines defined')
else:
r.ok('No line/border references')
# --- Check 3, 4, 5, 6: row/cell checks ---
max_cell_format_ref = 0
max_row_format_ref = 0
max_default_col_idx = 0
row_index = 0
for ri in row_nodes:
if r.stopped:
break
idx_node = ri.find(f'{{{NS_D}}}index')
if idx_node is not None and idx_node.text:
row_index = int(idx_node.text)
row = ri.find(f'{{{NS_D}}}row')
if row is None:
row_index += 1
continue
# Row formatIndex
row_fmt_node = row.find(f'{{{NS_D}}}formatIndex')
if row_fmt_node is not None and row_fmt_node.text:
val = int(row_fmt_node.text)
if val > max_row_format_ref:
max_row_format_ref = val
if val > format_count:
r.error(f'Row {row_index}: formatIndex={val} > format palette size ({format_count})')
# Check columnsID
row_cols_id = None
cols_id_node = row.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
row_cols_id = cols_id_node.text
if row_cols_id not in column_sets:
r.error(f"Row {row_index}: columnsID '{row_cols_id[:8]}...' not found in column sets")
# Determine column count for this row
row_col_count = default_col_count
if row_cols_id and row_cols_id in column_sets:
row_col_count = column_sets[row_cols_id]
# Cell checks
for c_group in row.findall(f'{{{NS_D}}}c'):
i_node = c_group.find(f'{{{NS_D}}}i')
col_idx = None
if i_node is not None and i_node.text:
col_idx = int(i_node.text)
# Track max index for default column set only
if row_cols_id is None and col_idx > max_default_col_idx:
max_default_col_idx = col_idx
# Check against row's column count
if row_col_count > 0 and col_idx >= row_col_count:
r.error(f'Row {row_index}: column index {col_idx} >= column count ({row_col_count})')
cell = c_group.find(f'{{{NS_D}}}c')
if cell is not None:
f_node = cell.find(f'{{{NS_D}}}f')
if f_node is not None and f_node.text:
val = int(f_node.text)
if val > max_cell_format_ref:
max_cell_format_ref = val
if val > format_count:
r.error(f'Row {row_index}: cell format index {val} > format palette size ({format_count})')
row_index += 1
# Summary checks for format refs
if not r.stopped:
if max_cell_format_ref <= format_count and max_row_format_ref <= format_count:
r.ok(f'Format refs: max cell={max_cell_format_ref}, max row={max_row_format_ref}, palette size={format_count}')
# Check column format indices
for cols in root.findall(f'{{{NS_D}}}columns'):
if r.stopped:
break
for ci in cols.findall(f'{{{NS_D}}}columnsItem'):
col = ci.find(f'{{{NS_D}}}column')
if col is not None:
fmt_node = col.find(f'{{{NS_D}}}formatIndex')
if fmt_node is not None and fmt_node.text:
val = int(fmt_node.text)
if val > format_count:
col_idx_node = ci.find(f'{{{NS_D}}}index')
col_idx_text = col_idx_node.text if col_idx_node is not None else '?'
r.error(f'Column {col_idx_text}: formatIndex={val} > format palette size ({format_count})')
# --- Check 5: column index summary ---
if not r.stopped:
r.ok(f'Column indices: max in default set={max_default_col_idx}, default column count={default_col_count}')
# --- Check 7, 8: named areas ---
for ni in root.findall(f'{{{NS_D}}}namedItem'):
if r.stopped:
break
ni_type = ni.get(f'{{{NS_XSI}}}type', '')
name_node = ni.find(f'{{{NS_D}}}name')
name = name_node.text if name_node is not None else ''
if 'NamedItemCells' in ni_type:
area = ni.find(f'{{{NS_D}}}area')
if area is None:
continue
begin_row = int_text(area.find(f'{{{NS_D}}}beginRow'))
end_row = int_text(area.find(f'{{{NS_D}}}endRow'))
# Check row bounds (skip -1 which means "all")
if begin_row != -1 and begin_row >= doc_height:
r.error(f"Area '{name}': beginRow={begin_row} >= height={doc_height}")
if end_row != -1 and end_row >= doc_height:
r.error(f"Area '{name}': endRow={end_row} >= height={doc_height}")
# Check columnsID reference
cols_id_node = area.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
cols_id = cols_id_node.text
if cols_id not in column_sets:
r.error(f"Area '{name}': columnsID '{cols_id[:8]}...' not found")
# --- Check 9: merge bounds ---
for merge in root.findall(f'{{{NS_D}}}merge'):
if r.stopped:
break
merge_r = int_text(merge.find(f'{{{NS_D}}}r'))
merge_c = int_text(merge.find(f'{{{NS_D}}}c'))
w_node = merge.find(f'{{{NS_D}}}w')
h_node = merge.find(f'{{{NS_D}}}h')
# r=-1 means all rows, skip bound check
if merge_r != -1 and merge_r >= doc_height:
r.error(f'Merge at row={merge_r}, col={merge_c}: row >= height ({doc_height})')
if h_node is not None and merge_r != -1:
h = int_text(h_node)
if (merge_r + h) >= doc_height:
r.error(f'Merge at row={merge_r}: extends to row {merge_r + h} >= height ({doc_height})')
# Check columnsID in merge
cols_id_node = merge.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
cols_id = cols_id_node.text
if cols_id not in column_sets:
r.error(f"Merge at row={merge_r}, col={merge_c}: columnsID '{cols_id[:8]}...' not found")
# --- Check 12: drawing picture indices ---
for drawing in root.findall(f'{{{NS_D}}}drawing'):
if r.stopped:
break
pic_idx_node = drawing.find(f'{{{NS_D}}}pictureIndex')
if pic_idx_node is not None and pic_idx_node.text:
pic_idx = int(pic_idx_node.text)
if pic_idx > picture_count:
draw_id_node = drawing.find(f'{{{NS_D}}}id')
draw_id = draw_id_node.text if draw_id_node is not None else '?'
r.error(f'Drawing id={draw_id}: pictureIndex={pic_idx} > picture count ({picture_count})')
# --- Summary ---
print()
print('---')
if r.stopped:
print(f'Stopped after {max_errors} errors. Fix and re-run.')
if r.errors == 0 and r.warnings == 0:
print('All checks passed.')
else:
print(f'Errors: {r.errors}, Warnings: {r.warnings}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
@@ -0,0 +1,622 @@
#!/usr/bin/env python3
# role-compile v1.0 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import uuid
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def emit_mltext(lines, indent, tag, text):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
# --- Russian synonyms -> canonical English names ---
TYPE_ALIASES = {
"Справочник": "Catalog",
"Документ": "Document",
"РегистрСведений": "InformationRegister",
"РегистрНакопления": "AccumulationRegister",
"РегистрБухгалтерии": "AccountingRegister",
"РегистрРасчета": "CalculationRegister",
"Константа": "Constant",
"ПланСчетов": "ChartOfAccounts",
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
"ПланВидовРасчета": "ChartOfCalculationTypes",
"ПланОбмена": "ExchangePlan",
"БизнесПроцесс": "BusinessProcess",
"Задача": "Task",
"Обработка": "DataProcessor",
"Отчет": "Report",
"ОбщаяФорма": "CommonForm",
"ОбщаяКоманда": "CommonCommand",
"Подсистема": "Subsystem",
"КритерийОтбора": "FilterCriterion",
"ЖурналДокументов": "DocumentJournal",
"Последовательность": "Sequence",
"ВебСервис": "WebService",
"HTTPСервис": "HTTPService",
"СервисИнтеграции": "IntegrationService",
"ПараметрСеанса": "SessionParameter",
"ОбщийРеквизит": "CommonAttribute",
"Конфигурация": "Configuration",
"Перечисление": "Enum",
# Nested
"Реквизит": "Attribute",
"СтандартныйРеквизит": "StandardAttribute",
"ТабличнаяЧасть": "TabularSection",
"Измерение": "Dimension",
"Ресурс": "Resource",
"Команда": "Command",
"РеквизитАдресации": "AddressingAttribute",
}
RIGHT_ALIASES = {
"Чтение": "Read",
"Добавление": "Insert",
"Изменение": "Update",
"Удаление": "Delete",
"Просмотр": "View",
"Редактирование": "Edit",
"ВводПоСтроке": "InputByString",
"Проведение": "Posting",
"ОтменаПроведения": "UndoPosting",
"ИнтерактивноеДобавление": "InteractiveInsert",
"ИнтерактивнаяПометкаУдаления": "InteractiveSetDeletionMark",
"ИнтерактивноеСнятиеПометкиУдаления": "InteractiveClearDeletionMark",
"ИнтерактивноеУдаление": "InteractiveDelete",
"ИнтерактивноеУдалениеПомеченных": "InteractiveDeleteMarked",
"ИнтерактивноеПроведение": "InteractivePosting",
"ИнтерактивноеПроведениеНеоперативное": "InteractivePostingRegular",
"ИнтерактивнаяОтменаПроведения": "InteractiveUndoPosting",
"ИнтерактивноеИзменениеПроведенных": "InteractiveChangeOfPosted",
"Использование": "Use",
"Получение": "Get",
"Установка": "Set",
"Старт": "Start",
"ИнтерактивныйСтарт": "InteractiveStart",
"ИнтерактивнаяАктивация": "InteractiveActivate",
"Выполнение": "Execute",
"ИнтерактивноеВыполнение": "InteractiveExecute",
"УправлениеИтогами": "TotalsControl",
"Администрирование": "Administration",
"АдминистрированиеДанных": "DataAdministration",
"ТонкийКлиент": "ThinClient",
"ВебКлиент": "WebClient",
"ТолстыйКлиент": "ThickClient",
"ВнешнееСоединение": "ExternalConnection",
"Вывод": "Output",
"СохранениеДанныхПользователя": "SaveUserData",
"МобильныйКлиент": "MobileClient",
}
# --- Known rights per object type ---
KNOWN_RIGHTS = {
"Configuration": [
"Administration", "DataAdministration", "UpdateDataBaseConfiguration",
"ConfigurationExtensionsAdministration", "ActiveUsers", "EventLog", "ExclusiveMode",
"ThinClient", "ThickClient", "WebClient", "MobileClient", "ExternalConnection",
"Automation", "Output", "SaveUserData", "TechnicalSpecialistMode",
"InteractiveOpenExtDataProcessors", "InteractiveOpenExtReports",
"AnalyticsSystemClient", "CollaborationSystemInfoBaseRegistration",
"MainWindowModeNormal", "MainWindowModeWorkplace",
"MainWindowModeEmbeddedWorkplace", "MainWindowModeFullscreenWorkplace", "MainWindowModeKiosk",
],
"Catalog": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete", "InteractiveDeleteMarked",
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
],
"Document": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"Posting", "UndoPosting",
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete", "InteractiveDeleteMarked",
"InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting",
"InteractiveChangeOfPosted",
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
],
"InformationRegister": [
"Read", "Update", "View", "Edit", "TotalsControl",
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
],
"AccumulationRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
"AccountingRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
"CalculationRegister": ["Read", "View"],
"Constant": [
"Read", "Update", "View", "Edit",
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
],
"ChartOfAccounts": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete",
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
"ReadDataHistory", "ReadDataHistoryOfMissingData",
"UpdateDataHistory", "UpdateDataHistoryOfMissingData",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
],
"ChartOfCharacteristicTypes": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete", "InteractiveDeleteMarked",
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
],
"ChartOfCalculationTypes": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete",
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
],
"ExchangePlan": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete", "InteractiveDeleteMarked",
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
],
"BusinessProcess": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete", "InteractiveActivate", "InteractiveStart",
],
"Task": [
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
"Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
"InteractiveDelete", "InteractiveActivate", "InteractiveExecute",
],
"DataProcessor": ["Use", "View"],
"Report": ["Use", "View"],
"CommonForm": ["View"],
"CommonCommand": ["View"],
"Subsystem": ["View"],
"FilterCriterion": ["View"],
"DocumentJournal": ["Read", "View"],
"Sequence": ["Read", "Update"],
"WebService": ["Use"],
"HTTPService": ["Use"],
"IntegrationService": ["Use"],
"SessionParameter": ["Get", "Set"],
"CommonAttribute": ["View", "Edit"],
}
NESTED_RIGHTS = ["View", "Edit"]
COMMAND_RIGHTS = ["View"]
# --- Presets ---
PRESETS = {
"view": {
"Catalog": ["Read", "View", "InputByString"],
"ExchangePlan": ["Read", "View", "InputByString"],
"Document": ["Read", "View", "InputByString"],
"ChartOfAccounts": ["Read", "View", "InputByString"],
"ChartOfCharacteristicTypes": ["Read", "View", "InputByString"],
"ChartOfCalculationTypes": ["Read", "View", "InputByString"],
"BusinessProcess": ["Read", "View", "InputByString"],
"Task": ["Read", "View", "InputByString"],
"InformationRegister": ["Read", "View"],
"AccumulationRegister": ["Read", "View"],
"AccountingRegister": ["Read", "View"],
"CalculationRegister": ["Read", "View"],
"Constant": ["Read", "View"],
"DocumentJournal": ["Read", "View"],
"Sequence": ["Read"],
"CommonForm": ["View"],
"CommonCommand": ["View"],
"Subsystem": ["View"],
"FilterCriterion": ["View"],
"SessionParameter": ["Get"],
"CommonAttribute": ["View"],
"DataProcessor": ["Use", "View"],
"Report": ["Use", "View"],
"Configuration": ["ThinClient", "WebClient", "Output", "SaveUserData", "MainWindowModeNormal"],
},
"edit": {
"Catalog": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
"ExchangePlan": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
"Document": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Posting", "UndoPosting", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting", "InteractiveChangeOfPosted"],
"ChartOfAccounts": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
"ChartOfCharacteristicTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
"ChartOfCalculationTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
"BusinessProcess": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveStart"],
"Task": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveExecute"],
"InformationRegister": ["Read", "Update", "View", "Edit"],
"AccumulationRegister": ["Read", "Update", "View", "Edit"],
"AccountingRegister": ["Read", "Update", "View", "Edit"],
"Constant": ["Read", "Update", "View", "Edit"],
"DocumentJournal": ["Read", "View"],
"Sequence": ["Read", "Update"],
"SessionParameter": ["Get", "Set"],
"CommonAttribute": ["View", "Edit"],
},
}
def translate_object_name(name):
parts = name.split('.')
result = []
for p in parts:
result.append(TYPE_ALIASES.get(p, p))
return '.'.join(result)
def translate_right_name(name):
return RIGHT_ALIASES.get(name, name)
def get_object_type(object_name):
dot_idx = object_name.find('.')
if dot_idx < 0:
return object_name
return object_name[:dot_idx]
def is_nested_object(object_name):
return len(object_name.split('.')) >= 3
def resolve_preset(object_type, preset_name):
preset = preset_name.lstrip('@')
if preset not in PRESETS:
print(f"WARNING: Unknown preset '@{preset}'. Known: @view, @edit", file=sys.stderr)
return []
type_map = PRESETS[preset]
if object_type not in type_map:
available = []
for k in PRESETS:
if object_type in PRESETS[k]:
available.append(f'@{k}')
avail_str = ', '.join(available) if available else 'none'
print(f"WARNING: Preset '@{preset}' not defined for type '{object_type}'. Available: {avail_str}", file=sys.stderr)
return []
return list(type_map[object_type])
def validate_right_name(object_name, right_name):
object_type = get_object_type(object_name)
if is_nested_object(object_name):
if '.Command.' in object_name:
if right_name not in COMMAND_RIGHTS:
print(f"WARNING: {object_name}: '{right_name}' not valid for commands (only: View)", file=sys.stderr)
return False
else:
if right_name not in NESTED_RIGHTS:
print(f"WARNING: {object_name}: '{right_name}' not valid for nested objects (only: View, Edit)", file=sys.stderr)
return False
return True
if object_type not in KNOWN_RIGHTS:
print(f"WARNING: {object_name}: unknown object type '{object_type}'", file=sys.stderr)
return True
valid_rights = KNOWN_RIGHTS[object_type]
if right_name not in valid_rights:
suggestions = [r for r in valid_rights if right_name in r or r in right_name]
sug_str = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
print(f"WARNING: {object_name}: unknown right '{right_name}'.{sug_str}", file=sys.stderr)
return False
return True
def parse_object_entry(entry):
# --- String shorthand ---
if isinstance(entry, str):
colon_idx = entry.find(':')
if colon_idx < 0:
print(f"WARNING: Invalid string '{entry}' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'", file=sys.stderr)
return None
obj_name = translate_object_name(entry[:colon_idx].strip())
rights_str = entry[colon_idx + 1:].strip()
object_type = get_object_type(obj_name)
if rights_str.startswith('@'):
right_names = resolve_preset(object_type, rights_str)
else:
right_names = [translate_right_name(r.strip()) for r in rights_str.split(',') if r.strip()]
for r in right_names:
validate_right_name(obj_name, r)
rights = []
for r in right_names:
rights.append({'Name': r, 'Value': 'true', 'Condition': None})
return {'Name': obj_name, 'Rights': rights}
# --- Object form ---
obj_name = translate_object_name(str(entry.get('name', '')))
if not obj_name:
print("WARNING: Object entry missing 'name' field", file=sys.stderr)
return None
object_type = get_object_type(obj_name)
# Use a list of tuples to preserve insertion order
rights_map = {} # name -> {Value, Condition}
rights_order = [] # preserve order
# 1) Start with preset
if entry.get('preset'):
preset_rights = resolve_preset(object_type, str(entry['preset']))
for r in preset_rights:
if r not in rights_map:
rights_order.append(r)
rights_map[r] = {'Value': 'true', 'Condition': None}
# 2) Apply explicit rights
if entry.get('rights') is not None:
if isinstance(entry['rights'], list):
for r in entry['rights']:
r_name = translate_right_name(str(r))
validate_right_name(obj_name, r_name)
if r_name not in rights_map:
rights_order.append(r_name)
rights_map[r_name] = {'Value': 'true', 'Condition': None}
elif isinstance(entry['rights'], dict):
for p_name, p_value in entry['rights'].items():
r_name = translate_right_name(p_name)
validate_right_name(obj_name, r_name)
bool_val = 'true' if p_value is True or str(p_value) == 'True' else 'false'
if r_name not in rights_map:
rights_order.append(r_name)
rights_map[r_name] = {'Value': bool_val, 'Condition': None}
# 3) Apply RLS conditions
if entry.get('rls'):
for p_name, p_value in entry['rls'].items():
rls_right = translate_right_name(p_name)
if rls_right in rights_map:
rights_map[rls_right]['Condition'] = str(p_value)
else:
print(f"WARNING: {obj_name}: RLS for '{rls_right}' but this right is not in the rights list", file=sys.stderr)
# Convert to array
rights = []
for k in rights_order:
rights.append({
'Name': k,
'Value': rights_map[k]['Value'],
'Condition': rights_map[k]['Condition'],
})
return {'Name': obj_name, 'Rights': rights}
def main():
parser = argparse.ArgumentParser(description='Compile 1C role from JSON', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True)
parser.add_argument('-OutputDir', type=str, required=True)
args = parser.parse_args()
# --- 1. Load and validate JSON ---
json_path = args.JsonPath
if not os.path.exists(json_path):
print(f"File not found: {json_path}", file=sys.stderr)
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
if not defn.get('name'):
print("JSON must have 'name' field (role programmatic name)", file=sys.stderr)
sys.exit(1)
role_name = str(defn['name'])
synonym = str(defn['synonym']) if defn.get('synonym') else role_name
comment = str(defn['comment']) if defn.get('comment') else ''
# --- 2. Parse all object entries ---
parsed_objects = []
if defn.get('objects'):
for entry in defn['objects']:
parsed = parse_object_entry(entry)
if parsed:
parsed_objects.append(parsed)
# --- 3. Generate UUID ---
uid = new_uuid()
# --- 4. Emit metadata XML (Roles/Name.xml) ---
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"')
lines.append(' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"')
lines.append(' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"')
lines.append(' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"')
lines.append(' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"')
lines.append(' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"')
lines.append(' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"')
lines.append(' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"')
lines.append(' xmlns:v8="http://v8.1c.ru/8.1/data/core"')
lines.append(' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"')
lines.append(' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"')
lines.append(' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"')
lines.append(' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"')
lines.append(' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"')
lines.append(' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"')
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
lines.append(' version="2.17">')
lines.append(f' <Role uuid="{uid}">')
lines.append(' <Properties>')
lines.append(f' <Name>{role_name}</Name>')
lines.append(' <Synonym>')
lines.append(' <v8:item>')
lines.append(' <v8:lang>ru</v8:lang>')
lines.append(f' <v8:content>{esc_xml(synonym)}</v8:content>')
lines.append(' </v8:item>')
lines.append(' </Synonym>')
if comment:
lines.append(f' <Comment>{esc_xml(comment)}</Comment>')
else:
lines.append(' <Comment/>')
lines.append(' </Properties>')
lines.append(' </Role>')
lines.append('</MetaDataObject>')
metadata_xml = '\n'.join(lines) + '\n'
# --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<Rights xmlns="http://v8.1c.ru/8.2/roles"')
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
lines.append(' xsi:type="Rights" version="2.17">')
# Global flags
sfno = str(defn['setForNewObjects']).lower() if defn.get('setForNewObjects') is not None else 'false'
sfab = str(defn['setForAttributesByDefault']).lower() if defn.get('setForAttributesByDefault') is not None else 'true'
irco = str(defn['independentRightsOfChildObjects']).lower() if defn.get('independentRightsOfChildObjects') is not None else 'false'
lines.append(f' <setForNewObjects>{sfno}</setForNewObjects>')
lines.append(f' <setForAttributesByDefault>{sfab}</setForAttributesByDefault>')
lines.append(f' <independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>')
# Object blocks
total_rights = 0
for obj in parsed_objects:
lines.append(' <object>')
lines.append(f' <name>{obj["Name"]}</name>')
for right in obj['Rights']:
lines.append(' <right>')
lines.append(f' <name>{right["Name"]}</name>')
lines.append(f' <value>{right["Value"]}</value>')
if right['Condition']:
lines.append(' <restrictionByCondition>')
lines.append(f' <condition>{esc_xml(right["Condition"])}</condition>')
lines.append(' </restrictionByCondition>')
lines.append(' </right>')
total_rights += 1
lines.append(' </object>')
# RLS restriction templates
template_count = 0
if defn.get('templates'):
for tpl in defn['templates']:
lines.append(' <restrictionTemplate>')
lines.append(f' <name>{esc_xml(str(tpl["name"]))}</name>')
lines.append(f' <condition>{esc_xml(str(tpl["condition"]))}</condition>')
lines.append(' </restrictionTemplate>')
template_count += 1
lines.append('</Rights>')
rights_xml = '\n'.join(lines) + '\n'
# --- 6. Write output files ---
out_dir = args.OutputDir
if not os.path.isabs(out_dir):
out_dir = os.path.join(os.getcwd(), out_dir)
# Metadata: OutputDir/RoleName.xml
metadata_path = os.path.join(out_dir, f'{role_name}.xml')
os.makedirs(out_dir, exist_ok=True)
# Rights: OutputDir/RoleName/Ext/Rights.xml
role_sub_dir = os.path.join(out_dir, role_name)
ext_dir = os.path.join(role_sub_dir, 'Ext')
rights_path = os.path.join(ext_dir, 'Rights.xml')
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(metadata_path, metadata_xml)
write_utf8_bom(rights_path, rights_xml)
# --- 7. Register in Configuration.xml ---
config_dir = os.path.dirname(out_dir)
config_xml_path = os.path.join(config_dir, 'Configuration.xml')
reg_result = None
if os.path.exists(config_xml_path):
with open(config_xml_path, 'r', encoding='utf-8-sig') as f:
raw_text = f.read()
# Check if already registered
if f'<Role>{role_name}</Role>' in raw_text:
reg_result = 'already'
else:
# Find last <Role>...</Role> and insert after it
role_pattern = re.compile(r'(<Role>[^<]*</Role>)')
matches = list(role_pattern.finditer(raw_text))
new_role_tag = f'<Role>{role_name}</Role>'
if matches:
# Insert after last existing <Role>
last_match = matches[-1]
insert_pos = last_match.end()
raw_text = raw_text[:insert_pos] + f'\n\t\t\t{new_role_tag}' + raw_text[insert_pos:]
else:
# No existing roles — insert before </ChildObjects>
raw_text = raw_text.replace('</ChildObjects>', f'\t\t\t{new_role_tag}\n\t\t</ChildObjects>')
write_utf8_bom(config_xml_path, raw_text)
reg_result = 'added'
else:
reg_result = 'no-config'
# --- 8. Summary ---
print(f"[OK] Role '{role_name}' compiled")
print(f" UUID: {uid}")
print(f" Metadata: {metadata_path}")
print(f" Rights: {rights_path}")
print(f" Objects: {len(parsed_objects)}, Rights: {total_rights}, Templates: {template_count}")
if reg_result == 'added':
print(f" Configuration.xml: <Role>{role_name}</Role> added to ChildObjects")
elif reg_result == 'already':
print(f" Configuration.xml: <Role>{role_name}</Role> already registered")
elif reg_result == 'no-childobj':
print(f"WARNING: Configuration.xml found but <ChildObjects> not found", file=sys.stderr)
elif reg_result == 'no-config':
print(f"WARNING: Configuration.xml not found at {config_xml_path} -- register manually", file=sys.stderr)
if __name__ == '__main__':
main()
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
# role-info v1.0 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from collections import OrderedDict
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False)
parser.add_argument("-RightsPath", required=True, help="Path to Rights.xml")
parser.add_argument("-ShowDenied", action="store_true", default=False, help="Show denied rights")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve paths ---
rights_path = args.RightsPath
if not os.path.isabs(rights_path):
rights_path = os.path.join(os.getcwd(), rights_path)
if not os.path.isfile(rights_path):
print(f"[ERROR] File not found: {rights_path}", file=sys.stderr)
sys.exit(1)
# --- Try to find metadata file for role name/synonym ---
role_name = ""
role_synonym = ""
ext_dir = os.path.dirname(rights_path) # .../Ext
role_dir = os.path.dirname(ext_dir) # .../RoleName
roles_dir = os.path.dirname(role_dir) # .../Roles
role_folder_name = os.path.basename(role_dir)
meta_path = os.path.join(roles_dir, f"{role_folder_name}.xml")
if os.path.isfile(meta_path):
try:
meta_tree = etree.parse(meta_path, etree.XMLParser(remove_blank_text=False))
meta_root = meta_tree.getroot()
meta_ns = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
}
name_node = meta_root.find(".//md:Role/md:Properties/md:Name", meta_ns)
if name_node is not None and name_node.text:
role_name = name_node.text
syn_node = meta_root.find(
".//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", meta_ns
)
if syn_node is not None and syn_node.text:
role_synonym = syn_node.text
except Exception:
pass
if not role_name:
role_name = role_folder_name
# --- Parse Rights.xml ---
tree = etree.parse(rights_path, etree.XMLParser(remove_blank_text=False))
root = tree.getroot()
rights_ns = "http://v8.1c.ru/8.2/roles"
NSMAP = {"r": rights_ns}
# Global flags
set_for_new = root.get("setForNewObjects", "")
set_for_attrs = root.get("setForAttributesByDefault", "")
independent_child = root.get("independentRightsOfChildObjects", "")
# --- Collect objects ---
allowed = OrderedDict() # type -> OrderedDict { shortName -> [rights] }
denied = OrderedDict()
rls_objects = []
total_allowed = 0
total_denied = 0
for obj in root.findall("r:object", NSMAP):
obj_name = ""
rights = []
for child in obj:
local = etree.QName(child.tag).localname
if local == "name" and child.tag == f"{{{rights_ns}}}name":
obj_name = child.text or ""
if local == "right" and child.tag == f"{{{rights_ns}}}right":
r_name = ""
r_value = ""
has_rls = False
for rc in child:
rc_local = etree.QName(rc.tag).localname
if rc_local == "name":
r_name = rc.text or ""
if rc_local == "value":
r_value = rc.text or ""
if rc_local == "restrictionByCondition":
has_rls = True
if r_name and r_value:
rights.append({"name": r_name, "value": r_value, "rls": has_rls})
if not obj_name or len(rights) == 0:
continue
dot_idx = obj_name.find(".")
if dot_idx < 0:
continue
type_prefix = obj_name[:dot_idx]
short_name = obj_name[dot_idx + 1:]
for r in rights:
if r["value"] == "true":
total_allowed += 1
if type_prefix not in allowed:
allowed[type_prefix] = OrderedDict()
if short_name not in allowed[type_prefix]:
allowed[type_prefix][short_name] = []
suffix = r["name"]
if r["rls"]:
suffix += " [RLS]"
rls_objects.append(f"{type_prefix}.{short_name} ({r['name']})")
allowed[type_prefix][short_name].append(suffix)
else:
total_denied += 1
if type_prefix not in denied:
denied[type_prefix] = OrderedDict()
if short_name not in denied[type_prefix]:
denied[type_prefix][short_name] = []
denied[type_prefix][short_name].append(r["name"])
# --- Restriction templates ---
templates = []
for tpl in root.findall("r:restrictionTemplate", NSMAP):
for child in tpl:
if etree.QName(child.tag).localname == "name":
t_name = child.text or ""
paren_idx = t_name.find("(")
if paren_idx > 0:
t_name = t_name[:paren_idx]
templates.append(t_name)
# --- Output ---
header = f"=== Role: {role_name}"
if role_synonym:
header += f' --- "{role_synonym}"'
header += " ==="
out(header)
out()
out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}")
out()
# Helper: output group
def out_group(obj_map, is_denied=False):
for short_name, rights_list in obj_map.items():
if is_denied:
rights_str = ", ".join(f"-{r}" for r in rights_list)
else:
rights_str = ", ".join(rights_list)
out(f" {short_name}: {rights_str}")
# Allowed rights grouped by type
if len(allowed) > 0:
out("Allowed rights:")
out()
for type_prefix, obj_map in allowed.items():
out(f" {type_prefix} ({len(obj_map)}):")
out_group(obj_map)
out()
else:
out("(no allowed rights)")
out()
# Denied rights
if args.ShowDenied and len(denied) > 0:
out("Denied rights:")
out()
for type_prefix, obj_map in denied.items():
out(f" {type_prefix} ({len(obj_map)}):")
out_group(obj_map, is_denied=True)
out()
elif total_denied > 0:
out(f"Denied: {total_denied} rights (use -ShowDenied to list)")
out()
# RLS summary
if len(rls_objects) > 0:
out(f"RLS: {len(rls_objects)} restrictions")
# Templates
if len(templates) > 0:
out(f"Templates: {', '.join(templates)}")
out()
out("---")
out(f"Total: {total_allowed} allowed, {total_denied} denied")
# --- Pagination and output ---
total_lines = len(lines_buf)
out_lines = lines_buf[:]
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
out_lines = out_lines[args.Offset:]
if args.Limit > 0 and len(out_lines) > args.Limit:
shown = out_lines[:args.Limit]
remaining = total_lines - args.Offset - args.Limit
shown.append("")
shown.append(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
out_lines = shown
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(out_lines))
print(f"Output written to {out_file}")
else:
for line in out_lines:
print(line)
@@ -0,0 +1,500 @@
#!/usr/bin/env python3
# role-validate v1.0 — Validate 1C role Rights.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates role Rights.xml: root element, global flags, objects, rights, RLS, templates."""
import sys, os, argparse, re
from lxml import etree
GUID_PATTERN = re.compile(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
re.IGNORECASE
)
RIGHTS_NS = 'http://v8.1c.ru/8.2/roles'
# --- Known rights per object type ---
KNOWN_RIGHTS = {
'Configuration': [
'Administration', 'DataAdministration', 'UpdateDataBaseConfiguration',
'ConfigurationExtensionsAdministration', 'ActiveUsers', 'EventLog', 'ExclusiveMode',
'ThinClient', 'ThickClient', 'WebClient', 'MobileClient', 'ExternalConnection',
'Automation', 'Output', 'SaveUserData', 'TechnicalSpecialistMode',
'InteractiveOpenExtDataProcessors', 'InteractiveOpenExtReports',
'AnalyticsSystemClient', 'CollaborationSystemInfoBaseRegistration',
'MainWindowModeNormal', 'MainWindowModeWorkplace',
'MainWindowModeEmbeddedWorkplace', 'MainWindowModeFullscreenWorkplace', 'MainWindowModeKiosk',
],
'Catalog': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete', 'InteractiveDeleteMarked',
'InteractiveDeletePredefinedData', 'InteractiveSetDeletionMarkPredefinedData',
'InteractiveClearDeletionMarkPredefinedData', 'InteractiveDeleteMarkedPredefinedData',
'ReadDataHistory', 'ViewDataHistory', 'UpdateDataHistory',
'UpdateDataHistoryOfMissingData', 'ReadDataHistoryOfMissingData',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
'EditDataHistoryVersionComment', 'SwitchToDataHistoryVersion',
],
'Document': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'Posting', 'UndoPosting',
'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete', 'InteractiveDeleteMarked',
'InteractivePosting', 'InteractivePostingRegular', 'InteractiveUndoPosting',
'InteractiveChangeOfPosted',
'ReadDataHistory', 'ViewDataHistory', 'UpdateDataHistory',
'UpdateDataHistoryOfMissingData', 'ReadDataHistoryOfMissingData',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
'EditDataHistoryVersionComment', 'SwitchToDataHistoryVersion',
],
'InformationRegister': [
'Read', 'Update', 'View', 'Edit', 'TotalsControl',
'ReadDataHistory', 'ViewDataHistory', 'UpdateDataHistory',
'UpdateDataHistoryOfMissingData', 'ReadDataHistoryOfMissingData',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
'EditDataHistoryVersionComment', 'SwitchToDataHistoryVersion',
],
'AccumulationRegister': ['Read', 'Update', 'View', 'Edit', 'TotalsControl'],
'AccountingRegister': ['Read', 'Update', 'View', 'Edit', 'TotalsControl'],
'CalculationRegister': ['Read', 'View'],
'Constant': [
'Read', 'Update', 'View', 'Edit',
'ReadDataHistory', 'ViewDataHistory', 'UpdateDataHistory',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
'EditDataHistoryVersionComment', 'SwitchToDataHistoryVersion',
],
'ChartOfAccounts': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete',
'InteractiveDeletePredefinedData', 'InteractiveSetDeletionMarkPredefinedData',
'InteractiveClearDeletionMarkPredefinedData', 'InteractiveDeleteMarkedPredefinedData',
'ReadDataHistory', 'ReadDataHistoryOfMissingData',
'UpdateDataHistory', 'UpdateDataHistoryOfMissingData',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
],
'ChartOfCharacteristicTypes': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete', 'InteractiveDeleteMarked',
'InteractiveDeletePredefinedData', 'InteractiveSetDeletionMarkPredefinedData',
'InteractiveClearDeletionMarkPredefinedData', 'InteractiveDeleteMarkedPredefinedData',
'ReadDataHistory', 'ViewDataHistory', 'UpdateDataHistory',
'ReadDataHistoryOfMissingData', 'UpdateDataHistoryOfMissingData',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
'EditDataHistoryVersionComment', 'SwitchToDataHistoryVersion',
],
'ChartOfCalculationTypes': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete',
'InteractiveDeletePredefinedData', 'InteractiveSetDeletionMarkPredefinedData',
'InteractiveClearDeletionMarkPredefinedData', 'InteractiveDeleteMarkedPredefinedData',
],
'ExchangePlan': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete', 'InteractiveDeleteMarked',
'ReadDataHistory', 'ViewDataHistory', 'UpdateDataHistory',
'ReadDataHistoryOfMissingData', 'UpdateDataHistoryOfMissingData',
'UpdateDataHistorySettings', 'UpdateDataHistoryVersionComment',
'EditDataHistoryVersionComment', 'SwitchToDataHistoryVersion',
],
'BusinessProcess': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'Start', 'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete', 'InteractiveActivate', 'InteractiveStart',
],
'Task': [
'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
'Execute', 'InteractiveInsert', 'InteractiveSetDeletionMark', 'InteractiveClearDeletionMark',
'InteractiveDelete', 'InteractiveActivate', 'InteractiveExecute',
],
'DataProcessor': ['Use', 'View'],
'Report': ['Use', 'View'],
'CommonForm': ['View'],
'CommonCommand': ['View'],
'Subsystem': ['View'],
'FilterCriterion': ['View'],
'DocumentJournal': ['Read', 'View'],
'Sequence': ['Read', 'Update'],
'WebService': ['Use'],
'HTTPService': ['Use'],
'IntegrationService': ['Use'],
'SessionParameter': ['Get', 'Set'],
'CommonAttribute': ['View', 'Edit'],
}
NESTED_RIGHTS = ['View', 'Edit']
CHANNEL_RIGHTS = ['Use']
COMMAND_RIGHTS = ['View']
def get_object_type(name):
dot_idx = name.find('.')
if dot_idx < 0:
return name
return name[:dot_idx]
def is_nested_object(name):
return name.count('.') >= 2
def find_similar(needle, haystack):
result = []
needle_lower = needle.lower()
for h in haystack:
h_lower = h.lower()
if needle_lower in h_lower or h_lower in needle_lower:
result.append(h)
if len(result) >= 3:
break
return result
def get_child_text(parent, local_name, ns):
"""Get text of first child element with given local name in namespace."""
for child in parent:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == local_name and etree.QName(child.tag).namespace == ns:
return child.text or ''
return None
def get_child_el(parent, local_name, ns):
"""Get first child element with given local name in namespace."""
for child in parent:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == local_name and etree.QName(child.tag).namespace == ns:
return child
return None
def main():
parser = argparse.ArgumentParser(
description='Validate 1C role Rights.xml structure', allow_abbrev=False
)
parser.add_argument('-RightsPath', dest='RightsPath', required=True)
parser.add_argument('-MetadataPath', dest='MetadataPath', default='')
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
rights_path = args.RightsPath
metadata_path = args.MetadataPath
out_file = args.OutFile
if not os.path.isabs(rights_path):
rights_path = os.path.join(os.getcwd(), rights_path)
# --- Output helpers ---
lines = []
errors = 0
warnings = 0
def out_ok(msg):
lines.append(f' OK {msg}')
def out_warn(msg):
nonlocal warnings
warnings += 1
lines.append(f' WARN {msg}')
def out_err(msg):
nonlocal errors
errors += 1
lines.append(f' ERR {msg}')
# --- 3. Validate Rights.xml ---
lines.append(f'Validating: {rights_path}')
def finalize():
lines.append('---')
lines.append(f'Result: {errors} error(s), {warnings} warning(s)')
output = '\n'.join(lines)
if out_file:
out_path = out_file if os.path.isabs(out_file) else os.path.join(os.getcwd(), out_file)
out_dir = os.path.dirname(out_path)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
with open(out_path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(output)
print(f'[OK] Validation result written to: {out_path}')
else:
print(output)
if not os.path.exists(rights_path):
out_err(f'File not found: {rights_path}')
finalize()
sys.exit(1)
# 3a. Parse XML
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(rights_path, xml_parser)
out_ok('XML well-formed')
except etree.XMLSyntaxError as e:
out_err(f'XML parse error: {e}')
finalize()
sys.exit(1)
root = xml_doc.getroot()
root_local = etree.QName(root.tag).localname
root_ns = etree.QName(root.tag).namespace or ''
# 3b. Check root element
if root_local != 'Rights':
out_err(f"Root element is '{root_local}', expected 'Rights'")
elif root_ns != RIGHTS_NS:
out_warn(f"Namespace is '{root_ns}', expected '{RIGHTS_NS}'")
else:
out_ok('Root element: <Rights> with correct namespace')
# 3c. Global flags
flag_names = ['setForNewObjects', 'setForAttributesByDefault', 'independentRightsOfChildObjects']
flags_found = 0
for fn in flag_names:
nodes = root.findall(f'{{{RIGHTS_NS}}}{fn}')
if len(nodes) > 0:
val = nodes[0].text or ''
if val not in ('true', 'false'):
out_warn(f"{fn} = '{val}' (expected 'true' or 'false')")
flags_found += 1
else:
out_warn(f'Missing global flag: {fn}')
if flags_found == 3:
out_ok('3 global flags present')
# 3d. Objects
objects = root.findall(f'{{{RIGHTS_NS}}}object')
obj_count = len(objects)
right_count = 0
rls_count = 0
for obj in objects:
obj_name = ''
for child in obj:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == 'name' and etree.QName(child.tag).namespace == RIGHTS_NS:
obj_name = child.text or ''
break
if not obj_name:
out_err('Object without <name>')
continue
object_type = get_object_type(obj_name)
is_nested = is_nested_object(obj_name)
# Check object type is known
if not is_nested and object_type not in KNOWN_RIGHTS:
out_warn(f"{obj_name}: unknown object type '{object_type}'")
# Check rights
for child in obj:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname != 'right' or etree.QName(child.tag).namespace != RIGHTS_NS:
continue
r_name = ''
r_value = ''
has_rls = False
for rc in child:
if not isinstance(rc.tag, str):
continue
rc_local = etree.QName(rc.tag).localname
rc_ns = etree.QName(rc.tag).namespace
if rc_ns != RIGHTS_NS:
continue
if rc_local == 'name':
r_name = rc.text or ''
elif rc_local == 'value':
r_value = rc.text or ''
elif rc_local == 'restrictionByCondition':
has_rls = True
rls_count += 1
# Check condition not empty
cond_node = get_child_el(rc, 'condition', RIGHTS_NS)
if cond_node is None or not (cond_node.text or ''):
out_warn(f"{obj_name}: RLS condition for '{r_name}' is empty")
if not r_name:
out_err(f'{obj_name}: <right> without <name>')
continue
if r_value not in ('true', 'false'):
out_err(f"{obj_name}: right '{r_name}' has invalid value '{r_value}'")
continue
right_count += 1
# Validate right name
if is_nested:
if '.Command.' in obj_name:
if r_name not in COMMAND_RIGHTS:
out_warn(f"{obj_name}: '{r_name}' not valid for commands (only: View)")
elif '.IntegrationServiceChannel.' in obj_name:
if r_name not in CHANNEL_RIGHTS:
out_warn(f"{obj_name}: '{r_name}' not valid for channels (only: Use)")
else:
if r_name not in NESTED_RIGHTS:
out_warn(f"{obj_name}: '{r_name}' not valid for nested objects (only: View, Edit)")
elif object_type in KNOWN_RIGHTS:
valid_rights = KNOWN_RIGHTS[object_type]
if r_name not in valid_rights:
similar = find_similar(r_name, valid_rights)
sug_str = f' Did you mean: {", ".join(similar)}?' if similar else ''
out_warn(f"{obj_name}: unknown right '{r_name}'.{sug_str}")
out_ok(f'{obj_count} objects, {right_count} rights')
if rls_count > 0:
out_ok(f'{rls_count} RLS restrictions')
# 3e. Templates
templates = root.findall(f'{{{RIGHTS_NS}}}restrictionTemplate')
if len(templates) > 0:
tpl_names = []
for tpl in templates:
t_name = ''
t_cond = ''
for child in tpl:
if not isinstance(child.tag, str):
continue
local = etree.QName(child.tag).localname
ns = etree.QName(child.tag).namespace
if ns != RIGHTS_NS:
continue
if local == 'name':
t_name = child.text or ''
elif local == 'condition':
t_cond = child.text or ''
if not t_name:
out_warn('Restriction template without <name>')
else:
paren_idx = t_name.find('(')
short_name = t_name[:paren_idx] if paren_idx > 0 else t_name
tpl_names.append(short_name)
if not t_cond:
out_warn(f"Template '{t_name}': empty <condition>")
out_ok(f'{len(templates)} templates: {", ".join(tpl_names)}')
# --- 4. Validate metadata (optional) ---
inferred_role_name = ''
if metadata_path:
lines.append('')
if not os.path.isabs(metadata_path):
metadata_path = os.path.join(os.getcwd(), metadata_path)
if not os.path.exists(metadata_path):
out_err(f'Metadata file not found: {metadata_path}')
else:
try:
meta_parser = etree.XMLParser(remove_blank_text=False)
meta_xml = etree.parse(metadata_path, meta_parser)
meta_root = meta_xml.getroot()
# Find <Role> element anywhere
role_node = None
for el in meta_root.iter():
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Role':
role_node = el
break
if role_node is None:
out_err('Metadata: <Role> element not found')
else:
uuid_val = role_node.get('uuid', '')
if GUID_PATTERN.match(uuid_val):
out_ok(f'Metadata: UUID valid ({uuid_val})')
else:
out_err(f"Metadata: invalid UUID format '{uuid_val}'")
# Find Name
name_node = None
for el in role_node.iter():
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Name':
name_node = el
break
if name_node is not None and name_node.text:
out_ok(f'Metadata: Name = {name_node.text}')
inferred_role_name = name_node.text
else:
out_err('Metadata: <Name> is empty or missing')
# Find Synonym
syn_node = None
for el in role_node.iter():
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Synonym':
syn_node = el
break
if syn_node is not None and len(syn_node) > 0:
out_ok('Metadata: Synonym present')
else:
out_warn('Metadata: <Synonym> is empty')
except etree.XMLSyntaxError as e:
out_err(f'Metadata XML parse error: {e}')
# --- 5. Check registration in Configuration.xml ---
resolved_rights = os.path.abspath(rights_path)
ext_dir = os.path.dirname(resolved_rights) # Ext
role_dir = os.path.dirname(ext_dir) # RoleName
roles_dir = os.path.dirname(role_dir) # Roles
config_dir = os.path.dirname(roles_dir) # config root
config_xml_path = os.path.join(config_dir, 'Configuration.xml')
if not inferred_role_name:
inferred_role_name = os.path.basename(role_dir)
# Use metadata name if available (already set above if metadata was parsed)
if metadata_path and os.path.exists(metadata_path) and not inferred_role_name:
try:
meta_parser2 = etree.XMLParser(remove_blank_text=False)
meta_xml2 = etree.parse(metadata_path, meta_parser2)
for el in meta_xml2.getroot().iter():
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Role':
for el2 in el.iter():
if isinstance(el2.tag, str) and etree.QName(el2.tag).localname == 'Name':
if el2.text:
inferred_role_name = el2.text
break
break
except Exception:
pass
if os.path.exists(config_xml_path):
lines.append('')
try:
cfg_parser = etree.XMLParser(remove_blank_text=False)
cfg_xml = etree.parse(config_xml_path, cfg_parser)
cfg_ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
child_obj = cfg_xml.getroot().find('.//md:Configuration/md:ChildObjects', cfg_ns)
if child_obj is not None:
role_nodes = child_obj.findall('md:Role', cfg_ns)
found = False
for rn in role_nodes:
if (rn.text or '') == inferred_role_name:
found = True
break
if found:
out_ok(f'Configuration.xml: <Role>{inferred_role_name}</Role> registered')
else:
out_warn(f'Configuration.xml: <Role>{inferred_role_name}</Role> NOT found in ChildObjects')
except etree.XMLSyntaxError as e:
out_warn(f'Configuration.xml: parse error \u2014 {e}')
# --- 6. Summary ---
finalize()
sys.exit(1 if errors > 0 else 0)
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,668 @@
# skd-validate v1.0 — Validate 1C DCS structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from lxml import etree
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True)
parser.add_argument("-MaxErrors", type=int, default=20)
parser.add_argument("-OutFile", default="")
args = parser.parse_args()
template_path = args.TemplatePath
max_errors = args.MaxErrors
out_file = args.OutFile
# ── resolve path ─────────────────────────────────────────────
if not template_path.endswith(".xml"):
candidate = os.path.join(template_path, "Ext", "Template.xml")
if os.path.exists(candidate):
template_path = candidate
if not os.path.exists(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(template_path)
file_name = os.path.basename(resolved_path)
# ── output infrastructure ────────────────────────────────────
errors = 0
warnings = 0
stopped = False
output_lines = []
def out_line(msg):
output_lines.append(msg)
def report_ok(msg):
out_line(f"[OK] {msg}")
def report_error(msg):
global errors, stopped
errors += 1
out_line(f"[ERROR] {msg}")
if errors >= max_errors:
stopped = True
def report_warn(msg):
global warnings
warnings += 1
out_line(f"[WARN] {msg}")
def finalize():
out_line("")
out_line(f"=== Result: {errors} errors, {warnings} warnings ===")
result = "\n".join(output_lines)
print(result)
if out_file:
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write(result)
print(f"Written to: {out_file}")
out_line(f"=== Validation: {file_name} ===")
out_line("")
# ── 1. Parse XML ─────────────────────────────────────────────
NS = {
"s": "http://v8.1c.ru/8.1/data-composition-system/schema",
"dcscom": "http://v8.1c.ru/8.1/data-composition-system/common",
"dcscor": "http://v8.1c.ru/8.1/data-composition-system/core",
"dcsset": "http://v8.1c.ru/8.1/data-composition-system/settings",
"v8": "http://v8.1c.ru/8.1/data/core",
"v8ui": "http://v8.1c.ru/8.1/data/ui",
"xs": "http://www.w3.org/2001/XMLSchema",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"dcsat": "http://v8.1c.ru/8.1/data-composition-system/area-template",
}
XSI_TYPE = f"{{{NS['xsi']}}}type"
tree = None
try:
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, parser_xml)
report_ok("XML parsed successfully")
except Exception as e:
report_error(f"XML parse failed: {e}")
result = "\n".join(output_lines)
print(result)
if out_file:
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write(result)
sys.exit(1)
root = tree.getroot()
def local_name(node):
return etree.QName(node.tag).localname
def find(parent, xpath):
"""XPath find with namespaces, returns first match or None."""
r = parent.xpath(xpath, namespaces=NS)
return r[0] if r else None
def find_all(parent, xpath):
"""XPath findall with namespaces."""
return parent.xpath(xpath, namespaces=NS)
def text_of(node):
"""Return stripped text or empty string."""
if node is None:
return ""
return (node.text or "").strip()
def inner_text(node):
"""Return text (non-stripped) or empty string."""
if node is None:
return ""
return node.text or ""
# ── 3. Root element checks ───────────────────────────────────
if local_name(root) != "DataCompositionSchema":
report_error(f"Root element is '{local_name(root)}', expected 'DataCompositionSchema'")
else:
report_ok("Root element: DataCompositionSchema")
expected_ns = "http://v8.1c.ru/8.1/data-composition-system/schema"
root_ns = etree.QName(root.tag).namespace or ""
if root_ns != expected_ns:
report_error(f"Default namespace is '{root_ns}', expected '{expected_ns}'")
else:
report_ok("Default namespace correct")
if stopped:
finalize()
sys.exit(1)
# ── 4. Collect inventories ───────────────────────────────────
# DataSources
data_source_nodes = find_all(root, "s:dataSource")
data_source_names = {}
for dsn in data_source_nodes:
name = find(dsn, "s:name")
if name is not None:
data_source_names[inner_text(name)] = True
# DataSets (recursive for unions)
data_set_nodes = find_all(root, "s:dataSet")
data_set_names = {}
all_field_paths = {} # dataPath -> dataSet name
def collect_data_set_fields(ds_node, ds_name):
fields = find_all(ds_node, "s:field")
local_paths = {}
for f in fields:
dp = find(f, "s:dataPath")
if dp is not None:
path = inner_text(dp)
local_paths[path] = True
all_field_paths[path] = ds_name
# Union items
items = find_all(ds_node, "s:item")
for item in items:
item_name = find(item, "s:name")
if item_name is not None:
collect_data_set_fields(item, inner_text(item_name))
return local_paths
data_set_field_map = {}
for ds in data_set_nodes:
name_node = find(ds, "s:name")
if name_node is not None:
ds_name = inner_text(name_node)
data_set_names[ds_name] = True
data_set_field_map[ds_name] = collect_data_set_fields(ds, ds_name)
# CalculatedFields
calc_field_nodes = find_all(root, "s:calculatedField")
calc_field_paths = {}
for cf in calc_field_nodes:
dp = find(cf, "s:dataPath")
if dp is not None:
calc_field_paths[inner_text(dp)] = True
# TotalFields
total_field_nodes = find_all(root, "s:totalField")
# Parameters
param_nodes = find_all(root, "s:parameter")
param_names = {}
for p in param_nodes:
name_node = find(p, "s:name")
if name_node is not None:
param_names[inner_text(name_node)] = True
# Templates
template_nodes = find_all(root, "s:template")
template_names = {}
for t in template_nodes:
name_node = find(t, "s:name")
if name_node is not None:
template_names[inner_text(name_node)] = True
# GroupTemplates
group_template_nodes = find_all(root, "s:groupTemplate")
# SettingsVariants
variant_nodes = find_all(root, "s:settingsVariant")
# Known fields = dataset fields + calculated fields
known_fields = {}
for key in all_field_paths:
known_fields[key] = True
for key in calc_field_paths:
known_fields[key] = True
# ── 5. DataSource checks ─────────────────────────────────────
if len(data_source_nodes) == 0:
report_warn("No dataSource elements found (settings-only DCS?)")
else:
ds_names_seen = {}
ds_ok = True
for dsn in data_source_nodes:
name = find(dsn, "s:name")
typ = find(dsn, "s:dataSourceType")
if name is None or not inner_text(name):
report_error("DataSource has empty name")
ds_ok = False
elif inner_text(name) in ds_names_seen:
report_error(f"Duplicate dataSource name: {inner_text(name)}")
ds_ok = False
else:
ds_names_seen[inner_text(name)] = True
if typ is not None:
tv = inner_text(typ)
if tv not in ("Local", "External"):
report_warn(f"DataSource '{inner_text(name)}' has unusual type: {tv}")
if ds_ok:
report_ok(f"{len(data_source_nodes)} dataSource(s) found, names unique")
if stopped:
finalize()
sys.exit(1)
# ── 6. DataSet checks ────────────────────────────────────────
valid_ds_types = ("DataSetQuery", "DataSetObject", "DataSetUnion")
if len(data_set_nodes) == 0:
report_warn("No dataSet elements found (settings-only DCS?)")
else:
ds_names_seen = {}
ds_ok = True
for ds in data_set_nodes:
xsi_type = ds.get(XSI_TYPE, "")
name_node = find(ds, "s:name")
ds_name = inner_text(name_node) if name_node is not None else "(unnamed)"
if name_node is None or not inner_text(name_node):
report_error("DataSet has empty name")
ds_ok = False
elif ds_name in ds_names_seen:
report_error(f"Duplicate dataSet name: {ds_name}")
ds_ok = False
else:
ds_names_seen[ds_name] = True
if not xsi_type:
report_error(f"DataSet '{ds_name}' missing xsi:type")
ds_ok = False
elif xsi_type not in valid_ds_types:
report_warn(f"DataSet '{ds_name}' has unusual xsi:type: {xsi_type}")
# Check dataSource reference
if xsi_type != "DataSetUnion":
src_node = find(ds, "s:dataSource")
if src_node is not None and inner_text(src_node):
if inner_text(src_node) not in data_source_names:
report_error(f"DataSet '{ds_name}' references unknown dataSource: {inner_text(src_node)}")
ds_ok = False
# Check query not empty for Query type
if xsi_type == "DataSetQuery":
query_node = find(ds, "s:query")
if query_node is None or not text_of(query_node):
report_warn(f"DataSet '{ds_name}' (Query) has empty query")
# Check objectName for Object type
if xsi_type == "DataSetObject":
obj_node = find(ds, "s:objectName")
if obj_node is None or not text_of(obj_node):
report_error(f"DataSet '{ds_name}' (Object) has empty objectName")
ds_ok = False
if ds_ok:
report_ok(f"{len(data_set_nodes)} dataSet(s) found, names unique")
if stopped:
finalize()
sys.exit(1)
# ── 7. Field checks ──────────────────────────────────────────
def check_data_set_fields(ds_node, ds_name):
global stopped
fields = find_all(ds_node, "s:field")
if len(fields) == 0:
return
paths_seen = {}
field_ok = True
for f in fields:
dp = find(f, "s:dataPath")
fn = find(f, "s:field")
if dp is None or not inner_text(dp):
report_error(f"DataSet '{ds_name}': field has empty dataPath")
field_ok = False
continue
path = inner_text(dp)
if path in paths_seen:
report_warn(f"DataSet '{ds_name}': duplicate dataPath '{path}'")
else:
paths_seen[path] = True
if fn is None or not inner_text(fn):
report_warn(f"DataSet '{ds_name}': field '{path}' has empty <field> element")
if field_ok:
report_ok(f'DataSet "{ds_name}": {len(fields)} fields, dataPath unique')
# Check union items recursively
items = find_all(ds_node, "s:item")
for item in items:
item_name = find(item, "s:name")
i_name = inner_text(item_name) if item_name is not None else "(unnamed item)"
check_data_set_fields(item, i_name)
for ds in data_set_nodes:
name_node = find(ds, "s:name")
ds_name = inner_text(name_node) if name_node is not None else "(unnamed)"
check_data_set_fields(ds, ds_name)
if stopped:
finalize()
sys.exit(1)
# ── 8. DataSetLink checks ────────────────────────────────────
link_nodes = find_all(root, "s:dataSetLink")
if len(link_nodes) > 0:
link_ok = True
for link in link_nodes:
src = find(link, "s:sourceDataSet")
dst = find(link, "s:destinationDataSet")
src_expr = find(link, "s:sourceExpression")
dst_expr = find(link, "s:destinationExpression")
if src is not None and inner_text(src) and inner_text(src) not in data_set_names:
report_error(f"DataSetLink: sourceDataSet '{inner_text(src)}' not found")
link_ok = False
if dst is not None and inner_text(dst) and inner_text(dst) not in data_set_names:
report_error(f"DataSetLink: destinationDataSet '{inner_text(dst)}' not found")
link_ok = False
if src_expr is None or not text_of(src_expr):
report_error("DataSetLink: empty sourceExpression")
link_ok = False
if dst_expr is None or not text_of(dst_expr):
report_error("DataSetLink: empty destinationExpression")
link_ok = False
if link_ok:
report_ok(f"{len(link_nodes)} dataSetLink(s): references valid")
if stopped:
finalize()
sys.exit(1)
# ── 9. CalculatedField checks ────────────────────────────────
if len(calc_field_nodes) > 0:
cf_ok = True
cf_seen = {}
for cf in calc_field_nodes:
dp = find(cf, "s:dataPath")
expr = find(cf, "s:expression")
if dp is None or not inner_text(dp):
report_error("CalculatedField has empty dataPath")
cf_ok = False
continue
path = inner_text(dp)
if path in cf_seen:
report_error(f"Duplicate calculatedField dataPath: {path}")
cf_ok = False
else:
cf_seen[path] = True
if expr is None or not text_of(expr):
report_error(f"CalculatedField '{path}' has empty expression")
cf_ok = False
# Warn if collides with a dataset field
if path in all_field_paths:
report_warn(f"CalculatedField '{path}' shadows dataSet field in '{all_field_paths[path]}'")
if cf_ok:
report_ok(f"{len(calc_field_nodes)} calculatedField(s): dataPath and expression valid")
if stopped:
finalize()
sys.exit(1)
# ── 10. TotalField checks ────────────────────────────────────
if len(total_field_nodes) > 0:
tf_ok = True
for tf in total_field_nodes:
dp = find(tf, "s:dataPath")
expr = find(tf, "s:expression")
if dp is None or not inner_text(dp):
report_error("TotalField has empty dataPath")
tf_ok = False
continue
if expr is None or not text_of(expr):
report_error(f"TotalField '{inner_text(dp)}' has empty expression")
tf_ok = False
if tf_ok:
report_ok(f"{len(total_field_nodes)} totalField(s): dataPath and expression present")
if stopped:
finalize()
sys.exit(1)
# ── 11. Parameter checks ─────────────────────────────────────
if len(param_nodes) > 0:
param_ok = True
param_seen = {}
for p in param_nodes:
name_node = find(p, "s:name")
if name_node is None or not inner_text(name_node):
report_error("Parameter has empty name")
param_ok = False
continue
p_name = inner_text(name_node)
if p_name in param_seen:
report_error(f"Duplicate parameter name: {p_name}")
param_ok = False
else:
param_seen[p_name] = True
if param_ok:
report_ok(f"{len(param_nodes)} parameter(s): names unique")
if stopped:
finalize()
sys.exit(1)
# ── 12. Template checks ──────────────────────────────────────
if len(template_nodes) > 0:
tpl_ok = True
tpl_seen = {}
for t in template_nodes:
name_node = find(t, "s:name")
if name_node is None or not inner_text(name_node):
report_error("Template has empty name")
tpl_ok = False
continue
t_name = inner_text(name_node)
if t_name in tpl_seen:
report_error(f"Duplicate template name: {t_name}")
tpl_ok = False
else:
tpl_seen[t_name] = True
if tpl_ok:
report_ok(f"{len(template_nodes)} template(s): names unique")
# ── 13. GroupTemplate checks ─────────────────────────────────
if len(group_template_nodes) > 0:
gt_ok = True
valid_tpl_types = ("Header", "Footer", "Overall", "OverallHeader", "OverallFooter")
for gt in group_template_nodes:
tpl_ref = find(gt, "s:template")
tpl_type = find(gt, "s:templateType")
if tpl_ref is not None and inner_text(tpl_ref) and inner_text(tpl_ref) not in template_names:
report_error(f"GroupTemplate references unknown template: {inner_text(tpl_ref)}")
gt_ok = False
if tpl_type is not None and inner_text(tpl_type) not in valid_tpl_types:
report_warn(f"GroupTemplate has unusual templateType: {inner_text(tpl_type)}")
if gt_ok:
report_ok(f"{len(group_template_nodes)} groupTemplate(s): references valid")
if stopped:
finalize()
sys.exit(1)
# ── 14. Settings helper functions ─────────────────────────────
valid_comparison_types = (
"Equal", "NotEqual", "Greater", "GreaterOrEqual", "Less", "LessOrEqual",
"InList", "NotInList", "InHierarchy", "InListByHierarchy",
"Contains", "NotContains", "BeginsWith", "NotBeginsWith",
"Filled", "NotFilled",
)
valid_structure_types = (
"dcsset:StructureItemGroup",
"dcsset:StructureItemTable",
"dcsset:StructureItemChart",
"dcsset:StructureItemNestedObject",
)
def check_filter_items(parent_node, variant_name):
global stopped
filter_items = find_all(parent_node, "dcsset:filter/dcsset:item")
for fi in filter_items:
if stopped:
return
xsi_type = fi.get(XSI_TYPE, "")
if xsi_type == "dcsset:FilterItemComparison":
comp_type = find(fi, "dcsset:comparisonType")
if comp_type is not None and inner_text(comp_type) not in valid_comparison_types:
report_error(f"Variant '{variant_name}' filter: invalid comparisonType '{inner_text(comp_type)}'")
elif xsi_type == "dcsset:FilterItemGroup":
group_type = find(fi, "dcsset:groupType")
if group_type is not None:
valid_group_types = ("AndGroup", "OrGroup", "NotGroup")
if inner_text(group_type) not in valid_group_types:
report_warn(f"Variant '{variant_name}' filter group: unusual groupType '{inner_text(group_type)}'")
# Recurse into nested items
nested_items = find_all(fi, "dcsset:item")
for ni in nested_items:
ni_type = ni.get(XSI_TYPE, "")
if ni_type == "dcsset:FilterItemComparison":
comp_type = find(ni, "dcsset:comparisonType")
if comp_type is not None and inner_text(comp_type) not in valid_comparison_types:
report_error(f"Variant '{variant_name}' filter: invalid comparisonType '{inner_text(comp_type)}'")
def check_structure_item(item_node, variant_name):
global stopped
if stopped:
return
xsi_type = item_node.get(XSI_TYPE, "")
if not xsi_type:
report_error(f"Variant '{variant_name}': structure item missing xsi:type")
return
if xsi_type not in valid_structure_types:
report_warn(f"Variant '{variant_name}': unusual structure item type '{xsi_type}'")
# Recurse into nested items (groups can contain groups)
nested_items = find_all(item_node, "dcsset:item")
for ni in nested_items:
check_structure_item(ni, variant_name)
# Check column/row in tables
if xsi_type == "dcsset:StructureItemTable":
columns = find_all(item_node, "dcsset:column")
rows = find_all(item_node, "dcsset:row")
if len(columns) == 0:
report_warn(f"Variant '{variant_name}': table has no columns")
if len(rows) == 0:
report_warn(f"Variant '{variant_name}': table has no rows")
def check_settings(settings_node, variant_name):
global stopped
if stopped:
return
# Selection
sel_items = find_all(settings_node, "dcsset:selection/dcsset:item")
for si in sel_items:
xsi_type = si.get(XSI_TYPE, "")
if xsi_type == "dcsset:SelectedItemField":
field = find(si, "dcsset:field")
if field is not None and inner_text(field) and inner_text(field) != "SystemFields.Number":
base_path = inner_text(field).split(".")[0]
if inner_text(field) not in known_fields and base_path not in known_fields:
pass # Soft check — autoFillFields may add fields not listed explicitly
# Filter
check_filter_items(settings_node, variant_name)
# Order
order_items = find_all(settings_node, "dcsset:order/dcsset:item")
for oi in order_items:
xsi_type = oi.get(XSI_TYPE, "")
if xsi_type == "dcsset:OrderItemField":
order_type = find(oi, "dcsset:orderType")
if order_type is not None and inner_text(order_type) not in ("Asc", "Desc"):
report_warn(f"Variant '{variant_name}' order: invalid orderType '{inner_text(order_type)}'")
# Structure items
struct_items = find_all(settings_node, "dcsset:item")
for si in struct_items:
check_structure_item(si, variant_name)
# ── 15. SettingsVariant checks ────────────────────────────────
if len(variant_nodes) == 0:
report_warn("No settingsVariant elements found")
else:
v_ok = True
v_idx = 0
for v in variant_nodes:
v_idx += 1
v_name = find(v, "dcsset:name")
if v_name is None or not inner_text(v_name):
report_error(f"SettingsVariant #{v_idx} has empty name")
v_ok = False
settings = find(v, "dcsset:settings")
if settings is None:
report_error(f"SettingsVariant '{inner_text(v_name) if v_name is not None else ''}' has no settings element")
v_ok = False
continue
# Check settings internals
check_settings(settings, inner_text(v_name) if v_name is not None else "")
if v_ok:
report_ok(f"{len(variant_nodes)} settingsVariant(s) found")
# ── Final output ──────────────────────────────────────────────
finalize()
if errors > 0:
sys.exit(1)
sys.exit(0)
@@ -0,0 +1,286 @@
#!/usr/bin/env python3
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import uuid
import xml.etree.ElementTree as ET
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def emit_mltext(lines, indent, tag, text):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def split_camel_case(name):
if not name:
return name
result = re.sub(r'([a-z\u0430-\u044f\u0451])([A-Z\u0410-\u042f\u0401])', r'\1 \2', name)
if len(result) > 1:
result = result[0] + result[1:].lower()
return result
def main():
parser = argparse.ArgumentParser(description='Compile 1C subsystem from JSON definition', allow_abbrev=False)
parser.add_argument('-DefinitionFile', type=str, default=None)
parser.add_argument('-Value', type=str, default=None)
parser.add_argument('-OutputDir', type=str, required=True)
parser.add_argument('-Parent', type=str, default=None)
parser.add_argument('-NoValidate', action='store_true', default=False)
args = parser.parse_args()
# --- 1. Load JSON ---
if args.DefinitionFile and args.Value:
print("Cannot use both -DefinitionFile and -Value", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Value:
print("Either -DefinitionFile or -Value is required", file=sys.stderr)
sys.exit(1)
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
if not os.path.exists(def_file):
print(f"Definition file not found: {def_file}", file=sys.stderr)
sys.exit(1)
with open(def_file, 'r', encoding='utf-8-sig') as f:
json_text = f.read()
else:
json_text = args.Value
defn = json.loads(json_text)
if not defn.get('name'):
print("JSON must have 'name' field", file=sys.stderr)
sys.exit(1)
obj_name = str(defn['name'])
# Resolve OutputDir
output_dir = args.OutputDir
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- 2. Resolve defaults ---
synonym = str(defn['synonym']) if defn.get('synonym') else split_camel_case(obj_name)
comment = str(defn['comment']) if defn.get('comment') else ''
include_help_in_contents = 'true'
include_in_ci = str(defn['includeInCommandInterface']).lower() if defn.get('includeInCommandInterface') is not None else 'true'
use_one_command = str(defn['useOneCommand']).lower() if defn.get('useOneCommand') is not None else 'false'
explanation = str(defn['explanation']) if defn.get('explanation') else ''
picture = str(defn['picture']) if defn.get('picture') else ''
content_items = []
if defn.get('content'):
for c in defn['content']:
content_items.append(str(c))
children = []
if defn.get('children'):
for ch in defn['children']:
children.append(str(ch))
# --- 3. Build XML ---
uid = new_uuid()
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<MetaDataObject 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" version="2.17">')
lines.append(f'\t<Subsystem uuid="{uid}">')
lines.append('\t\t<Properties>')
# Name
lines.append(f'\t\t\t<Name>{esc_xml(obj_name)}</Name>')
# Synonym
emit_mltext(lines, '\t\t\t', 'Synonym', synonym)
# Comment
if comment:
lines.append(f'\t\t\t<Comment>{esc_xml(comment)}</Comment>')
else:
lines.append('\t\t\t<Comment/>')
# Boolean properties
lines.append(f'\t\t\t<IncludeHelpInContents>{include_help_in_contents}</IncludeHelpInContents>')
lines.append(f'\t\t\t<IncludeInCommandInterface>{include_in_ci}</IncludeInCommandInterface>')
lines.append(f'\t\t\t<UseOneCommand>{use_one_command}</UseOneCommand>')
# Explanation
emit_mltext(lines, '\t\t\t', 'Explanation', explanation)
# Picture
if picture:
lines.append('\t\t\t<Picture>')
lines.append(f'\t\t\t\t<xr:Ref>{picture}</xr:Ref>')
lines.append('\t\t\t\t<xr:LoadTransparent>false</xr:LoadTransparent>')
lines.append('\t\t\t</Picture>')
else:
lines.append('\t\t\t<Picture/>')
# Content
if len(content_items) > 0:
lines.append('\t\t\t<Content>')
for item in content_items:
lines.append(f'\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(item)}</xr:Item>')
lines.append('\t\t\t</Content>')
else:
lines.append('\t\t\t<Content/>')
lines.append('\t\t</Properties>')
# ChildObjects
if len(children) > 0:
lines.append('\t\t<ChildObjects>')
for ch in children:
lines.append(f'\t\t\t<Subsystem>{esc_xml(ch)}</Subsystem>')
lines.append('\t\t</ChildObjects>')
else:
lines.append('\t\t<ChildObjects/>')
lines.append('\t</Subsystem>')
lines.append('</MetaDataObject>')
# --- 4. Write files ---
parent = args.Parent
if parent:
# Nested subsystem
if not os.path.isabs(parent):
parent = os.path.join(os.getcwd(), parent)
if not os.path.exists(parent):
print(f"Parent subsystem not found: {parent}", file=sys.stderr)
sys.exit(1)
parent_dir = os.path.dirname(parent)
parent_base_name = os.path.splitext(os.path.basename(parent))[0]
subs_dir = os.path.join(parent_dir, parent_base_name, 'Subsystems')
else:
# Top-level subsystem
subs_dir = os.path.join(output_dir, 'Subsystems')
os.makedirs(subs_dir, exist_ok=True)
target_xml = os.path.join(subs_dir, f'{obj_name}.xml')
# Write XML
xml_content = '\n'.join(lines) + '\n'
write_utf8_bom(target_xml, xml_content)
print(f"[OK] Created: {target_xml}")
# Create subdirectory if children exist
if len(children) > 0:
child_subs_dir = os.path.join(subs_dir, obj_name, 'Subsystems')
if not os.path.exists(child_subs_dir):
os.makedirs(child_subs_dir, exist_ok=True)
print(f"[OK] Created directory: {child_subs_dir}")
# --- 5. Register in parent ---
parent_xml_path = None
if parent:
parent_xml_path = parent
else:
config_xml = os.path.join(output_dir, 'Configuration.xml')
if os.path.exists(config_xml):
parent_xml_path = config_xml
if parent_xml_path and os.path.exists(parent_xml_path):
with open(parent_xml_path, 'r', encoding='utf-8-sig') as f:
raw_text = f.read()
doc = ET.ElementTree(ET.fromstring(raw_text))
root = doc.getroot()
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
# Find ChildObjects
child_objects = None
if parent:
for sub in root.iter(f'{{{md_ns}}}Subsystem'):
child_objects = sub.find(f'{{{md_ns}}}ChildObjects')
break
else:
for cfg in root.iter(f'{{{md_ns}}}Configuration'):
child_objects = cfg.find(f'{{{md_ns}}}ChildObjects')
break
if child_objects is not None:
# Check if already registered
already_exists = False
for child in child_objects:
if child.tag == f'{{{md_ns}}}Subsystem' and child.text == obj_name:
already_exists = True
break
if not already_exists:
new_el = ET.SubElement(child_objects, f'{{{md_ns}}}Subsystem')
new_el.text = obj_name
# Re-serialize with whitespace preservation via raw text manipulation instead
# Since ElementTree doesn't preserve whitespace well, use regex-based insertion
# Find </ChildObjects> or <ChildObjects/> and inject
pass # Fall through to raw text approach below
if not already_exists:
# Use raw text manipulation to preserve formatting
if '<ChildObjects/>' in raw_text:
replacement = f'<ChildObjects>\n\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n\t\t</ChildObjects>'
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
elif '</ChildObjects>' in raw_text:
insert_line = f'\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n'
raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1)
write_utf8_bom(parent_xml_path, raw_text)
print(f"[OK] Registered in: {parent_xml_path}")
else:
print(f"[SKIP] Already registered in: {parent_xml_path}")
else:
print(f"[WARN] ChildObjects not found in: {parent_xml_path}")
else:
print("[INFO] No parent XML to register in")
# --- 6. 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, '..', '..', 'subsystem-validate', 'scripts', 'subsystem-validate.ps1'))
if os.path.exists(validate_script):
print()
print("--- Running subsystem-validate ---")
os.system(f'powershell.exe -NoProfile -File "{validate_script}" -SubsystemPath "{target_xml}"')
# --- 7. Summary ---
print()
print("=== subsystem-compile summary ===")
print(f" Name: {obj_name}")
print(f" UUID: {uid}")
print(f" Content: {len(content_items)} objects")
print(f" Children: {len(children)}")
print(f" File: {target_xml}")
sys.exit(0)
if __name__ == '__main__':
main()
@@ -0,0 +1,462 @@
#!/usr/bin/env python3
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from lxml import etree
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XS_NS = "http://www.w3.org/2001/XMLSchema"
NSMAP_WRAPPER = {
None: MD_NS,
"xsi": XSI_NS,
"v8": V8_NS,
"xr": XR_NS,
"xs": XS_NS,
}
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
"""Detect indentation of children inside a container element."""
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
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:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
"""Insert new_el before the closing tag of container, with proper indentation."""
children = list(container)
if len(children) == 0:
# Empty element: set text to newline+indent, tail of new_el to newline+parent_indent
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def remove_with_indent(el):
"""Remove element and clean up surrounding whitespace."""
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
# Transfer el.tail to prev.tail
if el.tail and el.tail.strip() == "":
pass # just drop extra whitespace
prev.tail = el.tail if el.tail and el.tail.strip() else (prev.tail or "")
# Actually try to keep the prev's tail as the closing indent
# Better approach: set prev.tail to what el.tail was (newline+indent of next or closing)
if el.tail:
prev.tail = el.tail
else:
# First child: adjust parent.text
if el.tail:
parent.text = el.tail
parent.remove(el)
def expand_self_closing(container, parent_indent):
"""If container is self-closing (no children, no text), add closing whitespace."""
if len(container) == 0 and not (container.text and container.text.strip()):
container.text = "\r\n" + parent_indent
def import_fragment(xml_string, doc_root):
"""Parse an XML fragment in the MD namespace context and return elements."""
wrapper = (
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
nodes = []
for child in frag:
nodes.append(child)
return nodes
def parse_value_list(val):
"""Parse a string or JSON array into a list of strings."""
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Edit existing 1C subsystem XML", allow_abbrev=False)
parser.add_argument("-SubsystemPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["add-content", "remove-content", "add-child", "remove-child", "set-property"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Resolve path ---
subsystem_path = args.SubsystemPath
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
if os.path.isfile(candidate):
subsystem_path = candidate
elif os.path.isfile(sibling):
subsystem_path = sibling
else:
print(f"No {dir_name}.xml found in directory or as sibling", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
if os.path.isfile(c):
subsystem_path = c
if not os.path.isfile(subsystem_path):
print(f"File not found: {subsystem_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(subsystem_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
# --- Detect structure ---
sub = None
for child in xml_root:
if isinstance(child.tag, str) and localname(child) == "Subsystem":
sub = child
break
if sub is None:
print("No <Subsystem> element found", file=sys.stderr)
sys.exit(1)
props_el = None
child_objs_el = None
for child in sub:
if not isinstance(child.tag, str):
continue
if localname(child) == "Properties":
props_el = child
if localname(child) == "ChildObjects":
child_objs_el = child
obj_name = ""
if props_el is not None:
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Name":
obj_name = (child.text or "").strip()
break
info(f"Subsystem: {obj_name}")
# --- Operations ---
def do_add_content(items):
nonlocal add_count
content_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Content":
content_el = child
break
if content_el is None:
print("No <Content> element found", file=sys.stderr)
sys.exit(1)
existing = set()
for child in content_el:
if isinstance(child.tag, str) and localname(child) == "Item":
existing.add((child.text or "").strip())
props_indent = get_child_indent(props_el)
if len(content_el) == 0 and not (content_el.text and content_el.text.strip()):
expand_self_closing(content_el, props_indent)
content_indent = get_child_indent(content_el)
for item in items:
if item in existing:
warn(f"Content already contains: {item}")
continue
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{item}</xr:Item>'
nodes = import_fragment(frag_xml, xml_root)
if nodes:
insert_before_closing(content_el, nodes[0], content_indent)
add_count += 1
info(f"Added content: {item}")
def do_remove_content(items):
nonlocal remove_count
content_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Content":
content_el = child
break
if content_el is None:
print("No <Content> element found", file=sys.stderr)
sys.exit(1)
for item in items:
found = False
for child in list(content_el):
if isinstance(child.tag, str) and localname(child) == "Item" and (child.text or "").strip() == item:
remove_with_indent(child)
remove_count += 1
info(f"Removed content: {item}")
found = True
break
if not found:
warn(f"Content item not found: {item}")
def do_add_child(child_name):
nonlocal add_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
for child in child_objs_el:
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
warn(f"ChildObjects already contains: {child_name}")
return
sub_indent = get_child_indent(sub)
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
expand_self_closing(child_objs_el, sub_indent)
ci = get_child_indent(child_objs_el)
new_el = etree.SubElement(child_objs_el, f"{{{MD_NS}}}Subsystem")
# Actually we need to use insert_before_closing pattern
child_objs_el.remove(new_el)
new_el = etree.Element(f"{{{MD_NS}}}Subsystem")
new_el.text = child_name
insert_before_closing(child_objs_el, new_el, ci)
add_count += 1
info(f"Added child subsystem: {child_name}")
def do_remove_child(child_name):
nonlocal remove_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
found = False
for child in list(child_objs_el):
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
remove_with_indent(child)
remove_count += 1
info(f"Removed child subsystem: {child_name}")
found = True
break
if not found:
warn(f"Child subsystem not found: {child_name}")
def do_set_property(json_val):
nonlocal modify_count
prop_def = json.loads(json_val)
prop_name = str(prop_def["name"])
prop_value = str(prop_def.get("value", ""))
prop_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == prop_name:
prop_el = child
break
if prop_el is None:
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
sys.exit(1)
bool_props = ["IncludeInCommandInterface", "UseOneCommand", "IncludeHelpInContents"]
if prop_name in bool_props:
prop_el.text = prop_value.lower()
# Clear children
for ch in list(prop_el):
prop_el.remove(ch)
modify_count += 1
info(f"Set {prop_name} = {prop_value}")
return
ml_props = ["Synonym", "Explanation"]
if prop_name in ml_props:
if not prop_value:
# Clear - make self-closing
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = None
modify_count += 1
info(f"Cleared {prop_name}")
else:
for ch in list(prop_el):
prop_el.remove(ch)
indent = get_child_indent(props_el)
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
lang_el.text = "ru"
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
content_el.text = prop_value
# Set whitespace
prop_el.text = "\r\n" + indent + "\t"
item_el.text = "\r\n" + indent + "\t\t"
lang_el.tail = "\r\n" + indent + "\t\t"
content_el.tail = "\r\n" + indent + "\t"
item_el.tail = "\r\n" + indent
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
return
if prop_name == "Comment":
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
prop_el.text = prop_value
modify_count += 1
info(f'Set Comment = "{prop_value}"')
return
if prop_name == "Picture":
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
indent = get_child_indent(props_el)
ref_el = etree.SubElement(prop_el, f"{{{XR_NS}}}Ref")
ref_el.text = prop_value
load_el = etree.SubElement(prop_el, f"{{{XR_NS}}}LoadTransparent")
load_el.text = "false"
prop_el.text = "\r\n" + indent + "\t"
ref_el.tail = "\r\n" + indent + "\t"
load_el.tail = "\r\n" + indent
modify_count += 1
info(f'Set Picture = "{prop_value}"')
return
# Generic text property
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = prop_value
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "add-content":
do_add_content(parse_value_list(op_value))
elif op_name == "remove-content":
do_remove_content(parse_value_list(op_value))
elif op_name == "add-child":
do_add_child(op_value)
elif op_name == "remove-child":
do_remove_child(op_value)
elif op_name == "set-property":
do_set_property(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "subsystem-validate", "scripts", "subsystem-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running subsystem-validate ---")
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
# --- Summary ---
print()
print("=== subsystem-edit summary ===")
print(f" Subsystem: {obj_name}")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
@@ -0,0 +1,522 @@
#!/usr/bin/env python3
# subsystem-info v1.0 — Compact summary of 1C subsystem structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from collections import OrderedDict
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C subsystem structure", allow_abbrev=False)
parser.add_argument("-SubsystemPath", required=True, help="Path to subsystem XML or Subsystems/ directory")
parser.add_argument("-Mode", choices=["overview", "content", "ci", "tree", "full"], default="overview", help="Output mode")
parser.add_argument("-Name", default="", help="Filter by name/type")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve path ---
subsystem_path = args.SubsystemPath
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
NS = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
CI_NS = {
"ci": "http://v8.1c.ru/8.3/xcf/extrnprops",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
}
# --- Helper: get LocalString text ---
def get_ml_text(node):
if node is None:
return ""
# Look for v8:item children
for item in node:
if not isinstance(item.tag, str):
continue
lang = ""
content = ""
for c in item:
if not isinstance(c.tag, str):
continue
local = etree.QName(c.tag).localname
if local == "lang":
lang = c.text or ""
if local == "content":
content = c.text or ""
if lang == "ru" and content:
return content
# fallback: first item
for item in node:
if not isinstance(item.tag, str):
continue
for c in item:
if not isinstance(c.tag, str):
continue
local = etree.QName(c.tag).localname
if local == "content" and c.text:
return c.text
return ""
# --- Helper: load subsystem XML ---
def load_subsystem_xml(xml_path):
tree = etree.parse(xml_path, etree.XMLParser(remove_blank_text=False))
doc_root = tree.getroot()
sub = doc_root.find("md:Subsystem", NS)
if sub is None:
print(f"[ERROR] Not a valid subsystem XML: {xml_path}", file=sys.stderr)
sys.exit(1)
return {"Doc": doc_root, "Sub": sub}
# --- Helper: get content items ---
def get_content_items(props):
items = []
content_node = props.find("md:Content", NS)
if content_node is None:
return items
for item in content_node.findall("xr:Item", NS):
if item.text:
items.append(item.text)
return items
# --- Helper: get child subsystem names ---
def get_child_names(sub):
names = []
co = sub.find("md:ChildObjects", NS)
if co is None:
return names
for child in co:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == "Subsystem":
names.append(child.text or "")
return names
# --- Helper: group content by type ---
def group_content_by_type(items):
groups = OrderedDict()
for item in items:
m = re.match(r'^([^.]+)\.(.+)$', item)
if m:
type_name = m.group(1)
name = m.group(2)
elif re.match(r'^[0-9a-fA-F]{8}-', item):
type_name = "[UUID]"
name = item
else:
type_name = "[Other]"
name = item
if type_name not in groups:
groups[type_name] = []
groups[type_name].append(name)
return groups
# --- Helper: find subsystem dir from XML path ---
def get_subsystem_dir(xml_path):
dir_name = os.path.dirname(xml_path)
base_name = os.path.splitext(os.path.basename(xml_path))[0]
return os.path.join(dir_name, base_name)
# --- Show functions ---
def show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
explanation, pic_text, content_items, groups, child_names, has_ci):
out(f"Подсистема: {sub_name}")
if synonym and synonym != sub_name:
out(f"Синоним: {synonym}")
if comment_text:
out(f"Комментарий: {comment_text}")
out(f"ВключатьВКомандныйИнтерфейс: {incl_ci}")
out(f"ИспользоватьОднуКоманду: {use_one_cmd}")
if explanation:
out(f"Пояснение: {explanation}")
if pic_text:
out(f"Картинка: {pic_text}")
if len(content_items) > 0:
parts = []
for type_name in groups:
parts.append(f"{type_name}: {len(groups[type_name])}")
out(f"Состав: {len(content_items)} объектов ({', '.join(parts)})")
else:
out("Состав: пусто")
if len(child_names) > 0:
out(f"Дочерние подсистемы ({len(child_names)}): {', '.join(child_names)}")
if has_ci:
out("Командный интерфейс: есть")
def show_content(sub_name, content_items, groups, name_filter):
out(f"Состав подсистемы {sub_name} ({len(content_items)} объектов):")
out()
if name_filter:
if name_filter in groups:
filtered = groups[name_filter]
out(f"{name_filter} ({len(filtered)}):")
for n in filtered:
out(f" {n}")
else:
out(f"[INFO] Тип '{name_filter}' не найден в составе.")
out(f"Доступные типы: {', '.join(groups.keys())}")
else:
for type_name in groups:
out(f"{type_name} ({len(groups[type_name])}):")
for n in groups[type_name]:
out(f" {n}")
out()
def show_ci(sub_name, subsystem_path_local):
local_sub_dir = get_subsystem_dir(subsystem_path_local)
local_ci_path = os.path.join(local_sub_dir, "Ext", "CommandInterface.xml")
if not os.path.isfile(local_ci_path):
out(f"Командный интерфейс: {sub_name}")
out()
out("Файл CommandInterface.xml не найден.")
out(f"Путь: {local_ci_path}")
else:
ci_tree = etree.parse(local_ci_path, etree.XMLParser(remove_blank_text=False))
ci_root = ci_tree.getroot()
out(f"Командный интерфейс: {sub_name}")
out()
# --- CommandsVisibility ---
vis_section = ci_root.find("ci:CommandsVisibility", CI_NS)
if vis_section is not None:
hidden = []
shown = []
for cmd in vis_section.findall("ci:Command", CI_NS):
cmd_name = cmd.get("name", "")
vis = cmd.find("ci:Visibility/xr:Common", CI_NS)
if vis is not None and vis.text == "false":
hidden.append(cmd_name)
else:
shown.append(cmd_name)
total = len(hidden) + len(shown)
if not args.Name or args.Name == "visibility":
out(f"Видимость ({total}):")
if hidden:
out(f" СКРЫТО ({len(hidden)}):")
for h in hidden:
out(f" {h}")
if shown:
out(f" ПОКАЗАНО ({len(shown)}):")
for s in shown:
out(f" {s}")
out()
# --- CommandsPlacement ---
place_section = ci_root.find("ci:CommandsPlacement", CI_NS)
if place_section is not None:
placements = []
for cmd in place_section.findall("ci:Command", CI_NS):
cmd_name = cmd.get("name", "")
grp = cmd.find("ci:CommandGroup", CI_NS)
pl = cmd.find("ci:Placement", CI_NS)
grp_text = grp.text if grp is not None and grp.text else "?"
pl_text = pl.text if pl is not None and pl.text else "?"
placements.append({"Name": cmd_name, "Group": grp_text, "Placement": pl_text})
if (not args.Name or args.Name == "placement") and placements:
arrow = "\u2192"
out(f"Размещение ({len(placements)}):")
for p in placements:
out(f" {p['Name']} {arrow} {p['Group']} ({p['Placement']})")
out()
# --- CommandsOrder ---
order_section = ci_root.find("ci:CommandsOrder", CI_NS)
if order_section is not None:
order_groups = OrderedDict()
for cmd in order_section.findall("ci:Command", CI_NS):
cmd_name = cmd.get("name", "")
grp = cmd.find("ci:CommandGroup", CI_NS)
grp_text = grp.text if grp is not None and grp.text else "?"
if grp_text not in order_groups:
order_groups[grp_text] = []
order_groups[grp_text].append(cmd_name)
total_order = sum(len(v) for v in order_groups.values())
if (not args.Name or args.Name == "order") and total_order > 0:
out(f"Порядок команд ({total_order}):")
for grp_name, cmds in order_groups.items():
out(f" [{grp_name}]:")
for c in cmds:
out(f" {c}")
out()
# --- SubsystemsOrder ---
sub_order_section = ci_root.find("ci:SubsystemsOrder", CI_NS)
if sub_order_section is not None:
sub_order = []
for s in sub_order_section.findall("ci:Subsystem", CI_NS):
if s.text:
sub_order.append(s.text)
if (not args.Name or args.Name == "subsystems") and sub_order:
out(f"Порядок подсистем ({len(sub_order)}):")
for i, s in enumerate(sub_order):
out(f" {i + 1}. {s}")
out()
# --- GroupsOrder ---
grp_order_section = ci_root.find("ci:GroupsOrder", CI_NS)
if grp_order_section is not None:
grp_order = []
for g in grp_order_section.findall("ci:Group", CI_NS):
if g.text:
grp_order.append(g.text)
if (not args.Name or args.Name == "groups") and grp_order:
out(f"Порядок групп ({len(grp_order)}):")
for g in grp_order:
out(f" {g}")
# ============================================================
# Mode: tree
# ============================================================
if args.Mode == "tree":
is_dir = os.path.isdir(subsystem_path)
root_dir = None
root_xml = None
if is_dir:
root_dir = subsystem_path
else:
if not os.path.isfile(subsystem_path):
print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr)
sys.exit(1)
root_xml = subsystem_path
# Box-drawing chars
T_BRANCH = "\u251C\u2500\u2500 " # ├──
T_LAST = "\u2514\u2500\u2500 " # └──
T_PIPE = "\u2502 " # │
T_ARROW = "\u2192" # →
def get_tree_line(xml_path):
parsed = load_subsystem_xml(xml_path)
sub = parsed["Sub"]
props = sub.find("md:Properties", NS)
name_node = props.find("md:Name", NS)
name = name_node.text if name_node is not None else ""
markers = []
sub_dir = get_subsystem_dir(xml_path)
ci_path = os.path.join(sub_dir, "Ext", "CommandInterface.xml")
if os.path.isfile(ci_path):
markers.append("CI")
use_one = props.find("md:UseOneCommand", NS)
if use_one is not None and use_one.text == "true":
markers.append("OneCmd")
incl_ci_node = props.find("md:IncludeInCommandInterface", NS)
if incl_ci_node is not None and incl_ci_node.text == "false":
markers.append("Скрыт")
marker_str = f" [{', '.join(markers)}]" if markers else ""
content_items = get_content_items(props)
child_names = get_child_names(sub)
child_str = f", {len(child_names)} дочерних" if child_names else ""
return {
"Label": f"{name}{marker_str} ({len(content_items)} объектов{child_str})",
"SubDir": sub_dir,
"ChildNames": child_names,
}
def build_tree_entry(xml_path, prefix, is_last, is_root):
info = get_tree_line(xml_path)
if is_root:
connector = ""
elif is_last:
connector = T_LAST
else:
connector = T_BRANCH
out(f"{prefix}{connector}{info['Label']}")
if info["ChildNames"]:
if is_root:
child_prefix = ""
elif is_last:
child_prefix = prefix + " "
else:
child_prefix = prefix + T_PIPE
subs_dir = os.path.join(info["SubDir"], "Subsystems")
for i, child_name in enumerate(info["ChildNames"]):
child_xml = os.path.join(subs_dir, f"{child_name}.xml")
child_is_last = (i == len(info["ChildNames"]) - 1)
if os.path.isfile(child_xml):
build_tree_entry(child_xml, child_prefix, child_is_last, False)
else:
conn2 = T_LAST if child_is_last else T_BRANCH
out(f"{child_prefix}{conn2}{child_name} [NOT FOUND]")
if root_dir:
label = os.path.basename(root_dir)
out(f"Дерево подсистем от: {label}/")
out()
xml_files = sorted(
[f for f in os.listdir(root_dir) if f.lower().endswith(".xml") and os.path.isfile(os.path.join(root_dir, f))],
key=lambda x: x.lower()
)
if args.Name:
xml_files = [f for f in xml_files if os.path.splitext(f)[0] == args.Name]
if not xml_files:
print(f"[ERROR] Subsystem '{args.Name}' not found in {root_dir}", file=sys.stderr)
sys.exit(1)
for i, fname in enumerate(xml_files):
build_tree_entry(os.path.join(root_dir, fname), "", i == len(xml_files) - 1, True)
else:
build_tree_entry(root_xml, "", True, True)
elif args.Mode == "ci":
# ============================================================
# Mode: ci -- CommandInterface.xml
# ============================================================
if os.path.isdir(subsystem_path):
print("[ERROR] ci mode requires a subsystem .xml file, not a directory", file=sys.stderr)
sys.exit(1)
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
if not os.path.isfile(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
if os.path.isfile(c):
subsystem_path = c
if not os.path.isfile(subsystem_path):
print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr)
sys.exit(1)
parsed = load_subsystem_xml(subsystem_path)
sub = parsed["Sub"]
props = sub.find("md:Properties", NS)
name_node = props.find("md:Name", NS)
sub_name = name_node.text if name_node is not None else ""
show_ci(sub_name, subsystem_path)
else:
# ============================================================
# Mode: overview / content / full -- requires a subsystem XML file
# ============================================================
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
if os.path.isfile(candidate):
subsystem_path = candidate
elif os.path.isfile(sibling):
subsystem_path = sibling
else:
print(f"[ERROR] No {dir_name}.xml found in directory. Use -Mode tree for directory listing.", file=sys.stderr)
sys.exit(1)
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
if not os.path.isfile(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
if os.path.isfile(c):
subsystem_path = c
if not os.path.isfile(subsystem_path):
print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr)
sys.exit(1)
parsed = load_subsystem_xml(subsystem_path)
sub = parsed["Sub"]
props = sub.find("md:Properties", NS)
name_node = props.find("md:Name", NS)
sub_name = name_node.text if name_node is not None else ""
synonym = get_ml_text(props.find("md:Synonym", NS))
comment_node = props.find("md:Comment", NS)
comment_text = comment_node.text if comment_node is not None and comment_node.text else ""
incl_help_node = props.find("md:IncludeHelpInContents", NS)
incl_help = incl_help_node.text if incl_help_node is not None else ""
incl_ci_node = props.find("md:IncludeInCommandInterface", NS)
incl_ci = incl_ci_node.text if incl_ci_node is not None else ""
use_one_cmd_node = props.find("md:UseOneCommand", NS)
use_one_cmd = use_one_cmd_node.text if use_one_cmd_node is not None else ""
explanation = get_ml_text(props.find("md:Explanation", NS))
# Picture
pic_node = props.find("md:Picture", NS)
pic_text = ""
if pic_node is not None and len(pic_node) > 0:
pic_ref = pic_node.find("xr:Ref", NS)
if pic_ref is not None and pic_ref.text:
pic_text = pic_ref.text
# Content
content_items = get_content_items(props)
groups = group_content_by_type(content_items)
# Children
child_names = get_child_names(sub)
# CI presence
sub_dir = get_subsystem_dir(subsystem_path)
ci_path = os.path.join(sub_dir, "Ext", "CommandInterface.xml")
has_ci = os.path.isfile(ci_path)
if args.Mode == "overview":
show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
explanation, pic_text, content_items, groups, child_names, has_ci)
elif args.Mode == "content":
show_content(sub_name, content_items, groups, args.Name)
elif args.Mode == "full":
show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
explanation, pic_text, content_items, groups, child_names, has_ci)
out()
out("--- content ---")
out()
show_content(sub_name, content_items, groups, args.Name)
out()
out("--- ci ---")
out()
show_ci(sub_name, subsystem_path)
# --- Pagination and output ---
total_lines = len(lines_buf)
out_lines = lines_buf[:]
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
out_lines = out_lines[args.Offset:]
if args.Limit > 0 and len(out_lines) > args.Limit:
shown = out_lines[:args.Limit]
remaining = total_lines - args.Offset - args.Limit
shown.append("")
shown.append(f"[ОБРЕЗАНО] Показано {args.Limit} из {total_lines} строк. Используйте -Offset {args.Offset + args.Limit} для продолжения.")
out_lines = shown
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(out_lines))
print(f"Output written to {out_file}")
else:
for line in out_lines:
print(line)
@@ -0,0 +1,349 @@
#!/usr/bin/env python3
# subsystem-validate v1.0 — Validate 1C subsystem XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates subsystem XML file structure, properties, content items, child objects."""
import sys, os, argparse, re
from lxml import etree
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
}
GUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
IDENT_PATTERN = re.compile(
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
)
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def find_duplicates(items):
seen = {}
dupes = []
for item in items:
seen[item] = seen.get(item, 0) + 1
for item, count in seen.items():
if count > 1 and item not in dupes:
dupes.append(item)
return dupes
def main():
parser = argparse.ArgumentParser(
description='Validate 1C subsystem XML structure', allow_abbrev=False
)
parser.add_argument('-SubsystemPath', dest='SubsystemPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
subsystem_path = args.SubsystemPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, dir_name + '.xml')
sibling = os.path.join(os.path.dirname(subsystem_path), dir_name + '.xml')
if os.path.exists(candidate):
subsystem_path = candidate
elif os.path.exists(sibling):
subsystem_path = sibling
else:
print(f'[ERROR] No {dir_name}.xml found in directory: {subsystem_path}')
sys.exit(1)
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
if not os.path.exists(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), fn + '.xml')
if os.path.exists(c):
subsystem_path = c
if not os.path.exists(subsystem_path):
print(f'[ERROR] File not found: {subsystem_path}')
sys.exit(1)
resolved_path = os.path.abspath(subsystem_path)
r = Reporter(max_errors)
# --- 1. XML well-formedness + root structure ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.error(f'1. XML parse error: {e}')
r.stopped = True
sub = None
version = ''
if not r.stopped:
root = xml_doc.getroot()
version = root.get('version', '')
sub_list = root.findall('md:Subsystem', NS)
sub = sub_list[0] if sub_list else None
if sub is None:
r.error('1. Root structure: expected MetaDataObject/Subsystem, not found')
r.stopped = True
else:
uuid_val = sub.get('uuid', '')
if uuid_val and GUID_PATTERN.match(uuid_val):
r.ok(f'1. Root structure: MetaDataObject/Subsystem, uuid={uuid_val}, version {version}')
else:
r.error('1. Root structure: invalid or missing uuid')
# --- Properties checks ---
props = None
if not r.stopped:
props_list = sub.findall('md:Properties', NS)
props = props_list[0] if props_list else None
if props is None:
r.error('2. Properties: <Properties> element not found')
r.stopped = True
sub_name = ''
if not r.stopped:
# --- 2. Required properties ---
required_props = [
'Name', 'Synonym', 'Comment', 'IncludeHelpInContents',
'IncludeInCommandInterface', 'UseOneCommand', 'Explanation',
'Picture', 'Content'
]
missing = []
for p in required_props:
el = props.find(f'md:{p}', NS)
if el is None:
missing.append(p)
if len(missing) == 0:
r.ok('2. Properties: all 9 required properties present')
else:
r.error(f'2. Properties: missing: {", ".join(missing)}')
# --- 3. Name ---
name_el = props.find('md:Name', NS)
sub_name = (name_el.text or '').strip() if name_el is not None else ''
r.out('')
r.out(f'=== Validation: Subsystem.{sub_name} ===')
# Re-insert header at position 0
header_line = f'=== Validation: Subsystem.{sub_name} ==='
r.lines.insert(0, '')
r.lines.insert(0, header_line)
if sub_name and IDENT_PATTERN.match(sub_name):
r.ok(f'3. Name: "{sub_name}" - valid identifier')
elif not sub_name:
r.error('3. Name: empty')
else:
r.error(f'3. Name: "{sub_name}" - invalid identifier')
# --- 4. Synonym ---
syn_el = props.find('md:Synonym', NS)
if syn_el is not None and len(syn_el) > 0:
items = syn_el.findall('v8:item', NS)
if len(items) > 0:
first_content = ''
for item in items:
c = item.find('v8:content', NS)
if c is not None and c.text:
first_content = c.text
break
r.ok(f'4. Synonym: "{first_content}" ({len(items)} lang(s))')
else:
r.warn('4. Synonym: element exists but no v8:item children')
else:
r.warn('4. Synonym: empty or missing')
# --- 5. Boolean properties ---
bool_props = ['IncludeHelpInContents', 'IncludeInCommandInterface', 'UseOneCommand']
bool_ok = True
bool_vals = {}
for bp in bool_props:
el = props.find(f'md:{bp}', NS)
if el is not None:
val = (el.text or '').strip()
bool_vals[bp] = val
if val not in ('true', 'false'):
r.error(f'5. Boolean property {bp} = "{val}" (expected true/false)')
bool_ok = False
if bool_ok:
r.ok('5. Boolean properties: valid')
# --- 6. Content items format ---
content_el = props.find('md:Content', NS)
content_items = []
if content_el is not None and len(content_el) > 0:
xr_items = content_el.findall('xr:Item', NS)
content_ok = True
for item in xr_items:
type_attr = item.get(f'{{{NS["xsi"]}}}type', '')
text = (item.text or '').strip()
content_items.append(text)
if type_attr != 'xr:MDObjectRef':
r.error(f'6. Content item "{text}": xsi:type="{type_attr}" (expected xr:MDObjectRef)')
content_ok = False
if not re.match(r'^[A-Za-z]+\..+$', text) and not GUID_PATTERN.match(text):
r.error(f'6. Content item "{text}": invalid format (expected Type.Name or UUID)')
content_ok = False
if content_ok:
r.ok(f'6. Content: {len(xr_items)} items, all valid MDObjectRef format')
else:
r.ok('6. Content: empty (no items)')
# --- 7. Content duplicates ---
if len(content_items) > 0:
dupes = find_duplicates(content_items)
if dupes:
r.warn(f'7. Content: duplicates found: {", ".join(dupes)}')
else:
r.ok('7. Content: no duplicates')
else:
r.ok('7. Content: no duplicates (empty)')
# --- 8. ChildObjects entries non-empty ---
child_objs = sub.find('md:ChildObjects', NS)
child_names = []
if child_objs is not None and len(child_objs) > 0:
child_ok = True
for child in child_objs:
if not isinstance(child.tag, str):
continue
local_name = etree.QName(child.tag).localname
if local_name != 'Subsystem':
r.error(f'8. ChildObjects: unexpected element <{local_name}>')
child_ok = False
elif not (child.text or '').strip():
r.error('8. ChildObjects: empty <Subsystem> element')
child_ok = False
else:
child_names.append((child.text or '').strip())
if child_ok:
r.ok(f'8. ChildObjects: {len(child_names)} entries, all non-empty')
else:
r.ok('8. ChildObjects: empty (leaf subsystem)')
# --- 9. ChildObjects duplicates ---
if len(child_names) > 0:
dupes = find_duplicates(child_names)
if dupes:
r.error(f'9. ChildObjects: duplicates: {", ".join(dupes)}')
else:
r.ok('9. ChildObjects: no duplicates')
else:
r.ok('9. ChildObjects: no duplicates (empty)')
# --- 10. ChildObjects files exist ---
if len(child_names) > 0:
parent_dir = os.path.dirname(resolved_path)
base_name = os.path.splitext(os.path.basename(resolved_path))[0]
subs_dir = os.path.join(parent_dir, base_name, 'Subsystems')
missing_files = []
for cn in child_names:
child_xml = os.path.join(subs_dir, cn + '.xml')
if not os.path.exists(child_xml):
missing_files.append(cn)
if len(missing_files) == 0:
r.ok(f'10. ChildObjects files: all {len(child_names)} files exist')
else:
r.warn(f'10. ChildObjects files: missing: {", ".join(missing_files)}')
else:
r.ok('10. ChildObjects files: n/a (no children)')
# --- 11. CommandInterface.xml ---
parent_dir2 = os.path.dirname(resolved_path)
base_name2 = os.path.splitext(os.path.basename(resolved_path))[0]
ci_path = os.path.join(parent_dir2, base_name2, 'Ext', 'CommandInterface.xml')
if os.path.exists(ci_path):
try:
etree.parse(ci_path, etree.XMLParser(remove_blank_text=False))
r.ok('11. CommandInterface: exists, well-formed')
except etree.XMLSyntaxError as e:
r.warn(f'11. CommandInterface: exists but NOT well-formed: {e}')
else:
r.ok('11. CommandInterface: not present')
# --- 12. Picture format ---
pic_el = props.find('md:Picture', NS)
if pic_el is not None and len(pic_el) > 0:
pic_ref = pic_el.find('xr:Ref', NS)
if pic_ref is not None and pic_ref.text:
ref_text = pic_ref.text
if ref_text.startswith('CommonPicture.'):
r.ok(f'12. Picture: {ref_text}')
else:
r.warn(f'12. Picture: "{ref_text}" (expected CommonPicture.XXX)')
else:
r.warn('12. Picture: has children but no xr:Ref content')
else:
r.ok('12. Picture: empty (not set)')
# --- 13. UseOneCommand constraint ---
use_one = bool_vals.get('UseOneCommand', '')
if use_one == 'true':
if len(content_items) == 1:
r.ok('13. UseOneCommand: true, Content has exactly 1 item')
else:
r.warn(f'13. UseOneCommand: true but Content has {len(content_items)} items (expected 1)')
else:
r.ok('13. UseOneCommand: false (no constraint)')
# --- Finalize ---
r.out('---')
r.out(f'Errors: {r.errors}, Warnings: {r.warnings}')
result = r.text()
print(result, end='')
if out_file:
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
# add-template v1.0 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
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 save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
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."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
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 ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", 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)
# --- 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'
'<MetaDataObject 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"'
' version="2.17">\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_text_with_bom(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":
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">\n'
'</SpreadsheetDocument>'
)
write_text_with_bom(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_text_with_bom(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
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})")
print(f" Метаданные: {template_meta_path}")
print(f" Содержимое: {template_file_path}")
if main_dcs_updated:
print(f" MainDataCompositionSchema: {main_dcs.text}")
if __name__ == "__main__":
main()
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
# remove-template v1.0 — Remove template from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Remove template from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-TemplateName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
template_name = args.TemplateName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", 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")
template_dir = os.path.join(templates_dir, template_name)
if not os.path.exists(template_meta_path):
print(f"Метаданные макета не найдены: {template_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(template_dir):
shutil.rmtree(template_dir)
print(f"[OK] Удалён каталог: {template_dir}")
os.remove(template_meta_path)
print(f"[OK] Удалён файл: {template_meta_path}")
# --- 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()
# Remove <Template>TemplateName</Template> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Template", NSMAP):
if node.text and node.text.strip() == template_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
break
# Clear MainDataCompositionSchema if it pointed to this template
main_dcs = root.find(".//md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None and main_dcs.text:
if re.search(rf"Template\.{re.escape(template_name)}$", main_dcs.text):
main_dcs.text = ""
print("[OK] Очищён MainDataCompositionSchema")
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Макет {template_name} удалён из {root_xml_path}")
if __name__ == "__main__":
main()
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# web-info v1.0 — Apache & 1C publication status
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Статус Apache HTTP Server и публикаций 1С.
Показывает состояние Apache, список опубликованных баз
и последние ошибки из error.log.
"""
import argparse
import os
import re
import sys
import psutil
def get_httpd_by_exe(httpd_exe_norm):
"""Get httpd processes matching our exe path."""
ours = []
foreign = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
ours.append(p)
else:
foreign.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return ours, foreign
def main():
parser = argparse.ArgumentParser(description='Apache & 1C publication status', allow_abbrev=False)
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Check Apache installation ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
print('=== Apache Web Server ===')
if not os.path.exists(httpd_exe):
print('Status: Не установлен')
print(f'Path: {apache_path} (не найден)')
print('')
print('Используйте /web-publish для установки Apache.')
sys.exit(0)
# --- Check process (only our Apache) ---
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
our_proc, foreign_proc = get_httpd_by_exe(httpd_exe_norm)
if our_proc:
pids = ', '.join(str(p.pid) for p in our_proc)
print(f'Status: Запущен (PID: {pids})')
else:
print('Status: Остановлен')
if foreign_proc:
fp = foreign_proc[0]
try:
fpath = fp.info['exe'] or '?'
except Exception:
fpath = '?'
print(f'[WARN] Обнаружен сторонний Apache (PID: {fp.pid}, {fpath})')
print(f'Path: {apache_path}')
# --- Parse httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print('Config: httpd.conf не найден')
sys.exit(0)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
# Extract port from global block
port = '\u2014'
m = re.search(r'(?m)^Listen\s+(\d+)', conf_content)
if m:
port = m.group(1)
print(f'Port: {port}')
# Extract wsap24 path
m = re.search(r'LoadModule\s+_1cws_module\s+"([^"]+)"', conf_content)
if m:
print(f'Module: {m.group(1)}')
# --- Publications ---
print('')
print('=== Опубликованные базы ===')
pub_pattern = r'# --- 1C Publication: (.+?) ---'
pub_matches = re.findall(pub_pattern, conf_content)
if not pub_matches:
print('(нет публикаций)')
else:
for app_name in pub_matches:
# Read default.vrd for this publication
vrd_path = os.path.join(apache_path, 'publish', app_name, 'default.vrd')
ib_info = '\u2014'
vrd_content = ''
if os.path.exists(vrd_path):
with open(vrd_path, 'r', encoding='utf-8-sig') as f:
vrd_content = f.read()
m = re.search(r'ib="([^"]*)"', vrd_content)
if m:
ib_info = m.group(1).replace('&quot;', '"')
# Detect published services
svc_tags = []
if vrd_content:
if re.search(r'<ws\s', vrd_content):
svc_tags.append('WS')
if re.search(r'<httpServices\s', vrd_content):
svc_tags.append('HTTP')
if re.search(r'enableStandardOdata\s*=\s*"true"', vrd_content):
svc_tags.append('OData')
svc_label = ' [' + ' '.join(svc_tags) + ']' if svc_tags else ''
url = f'http://localhost:{port}/{app_name}'
print(f' {app_name} {url} {ib_info}{svc_label}')
# --- Error log ---
print('')
print('=== Последние ошибки ===')
error_log = os.path.join(apache_path, 'logs', 'error.log')
if os.path.exists(error_log):
try:
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
all_lines = f.readlines()
tail_lines = all_lines[-5:] if len(all_lines) >= 5 else all_lines
if tail_lines:
for line in tail_lines:
print(f' {line.rstrip()}')
else:
print('(пусто)')
except Exception:
print('(ошибка чтения)')
else:
print('(нет файла)')
if __name__ == '__main__':
main()
@@ -0,0 +1,398 @@
#!/usr/bin/env python3
# web-publish v1.0 — Publish 1C infobase via Apache
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Публикация информационной базы 1С через Apache HTTP Server.
Генерирует default.vrd и настраивает httpd.conf для веб-доступа
к информационной базе 1С. При необходимости скачивает portable Apache.
Идемпотентный повторный вызов обновляет конфигурацию.
"""
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import zipfile
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def get_all_httpd():
"""Get all httpd processes."""
result = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def check_port_in_use(port):
"""Check if a port is in use and return the owning PID, or None."""
for conn in psutil.net_connections(kind='tcp'):
if conn.laddr and conn.laddr.port == port and conn.status == 'LISTEN':
return conn.pid
return None
def main():
parser = argparse.ArgumentParser(description='Publish 1C infobase via Apache', allow_abbrev=False)
parser.add_argument('-V8Path', type=str, default='', help='Path to 1C platform bin directory (for wsap24.dll)')
parser.add_argument('-InfoBasePath', type=str, default='', help='Path to file infobase')
parser.add_argument('-InfoBaseServer', type=str, default='', help='1C server (for server infobase)')
parser.add_argument('-InfoBaseRef', type=str, default='', help='Infobase name on server')
parser.add_argument('-UserName', type=str, default='', help='1C user name')
parser.add_argument('-Password', type=str, default='', help='1C password')
parser.add_argument('-AppName', type=str, default='', help='Publication name (default: from infobase folder name)')
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
parser.add_argument('-Port', type=int, default=8081, help='Port (default: 8081)')
parser.add_argument('-Manual', action='store_true', help='Do not download Apache — only check and give instructions')
args = parser.parse_args()
# --- Resolve V8Path ---
v8_path = args.V8Path
if not v8_path:
candidates = glob.glob(r'C:\Program Files\1cv8\*\bin\1cv8.exe')
candidates.sort(reverse=True)
if candidates:
v8_path = os.path.dirname(candidates[0])
else:
print('Error: платформа 1С не найдена. Укажите -V8Path', file=sys.stderr)
sys.exit(1)
elif os.path.isfile(v8_path):
v8_path = os.path.dirname(v8_path)
# Validate wsap24.dll
wsap_dll = os.path.join(v8_path, 'wsap24.dll')
if not os.path.exists(wsap_dll):
print(f'Error: wsap24.dll не найден в {v8_path}', file=sys.stderr)
sys.exit(1)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef', file=sys.stderr)
sys.exit(1)
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
port = args.Port
# --- Check / Install Apache ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if not os.path.exists(httpd_exe):
if args.Manual:
print(f'Apache не найден: {apache_path}')
print('')
print('Установите Apache вручную:')
print(' 1. Скачайте Apache Lounge (x64) с https://www.apachelounge.com/download/')
print(f' 2. Распакуйте содержимое Apache24\\ в: {apache_path}')
print(' 3. Запустите скрипт повторно')
sys.exit(1)
print('Apache не найден. Скачиваю...')
zip_url = 'https://www.apachelounge.com/download/VS18/binaries/httpd-2.4.66-260131-Win64-VS18.zip'
tmp_zip = os.path.join(tempfile.gettempdir(), 'apache24.zip')
tmp_dir = os.path.join(tempfile.gettempdir(), 'apache24_extract')
try:
urllib.request.urlretrieve(zip_url, tmp_zip)
except Exception as e:
print(f'Error: не удалось скачать Apache: {e}', file=sys.stderr)
print('Скачайте вручную: https://www.apachelounge.com/download/')
sys.exit(1)
print('Распаковка...')
if os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir, ignore_errors=True)
with zipfile.ZipFile(tmp_zip, 'r') as zf:
zf.extractall(tmp_dir)
# Move Apache24 contents up to ApachePath
inner_dir = os.path.join(tmp_dir, 'Apache24')
if not os.path.isdir(inner_dir):
# Try to find Apache24 in nested folder
found_inner = None
for root, dirs, files in os.walk(tmp_dir):
if 'Apache24' in dirs:
found_inner = os.path.join(root, 'Apache24')
break
if found_inner:
inner_dir = found_inner
else:
print('Error: каталог Apache24 не найден в архиве', file=sys.stderr)
sys.exit(1)
os.makedirs(apache_path, exist_ok=True)
# Copy contents of inner_dir to apache_path
for item in os.listdir(inner_dir):
src = os.path.join(inner_dir, item)
dst = os.path.join(apache_path, item)
if os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
# Cleanup
try:
os.remove(tmp_zip)
except OSError:
pass
try:
shutil.rmtree(tmp_dir, ignore_errors=True)
except OSError:
pass
# Patch ServerRoot in httpd.conf
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if os.path.exists(conf_file):
apache_path_fwd = apache_path.replace('\\', '/')
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
conf_content = re.sub(
r'(?m)^Define SRVROOT .*$',
f'Define SRVROOT "{apache_path_fwd}"',
conf_content,
)
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
print(f'ServerRoot обновлён: {apache_path_fwd}')
print(f'Apache установлен: {apache_path}')
# --- Derive AppName ---
app_name = args.AppName
if not app_name:
if args.InfoBasePath:
app_name = re.sub(r'[^\w]', '', os.path.basename(args.InfoBasePath))
else:
app_name = re.sub(r'[^\w]', '', args.InfoBaseRef)
app_name = app_name.lower()
app_name = app_name.lower()
if not app_name:
print('Error: не удалось определить имя публикации. Укажите -AppName', file=sys.stderr)
sys.exit(1)
print(f'Публикация: {app_name}')
# --- Create publish directory ---
publish_dir = os.path.join(apache_path, 'publish', app_name)
os.makedirs(publish_dir, exist_ok=True)
# --- Generate default.vrd ---
vrd_path = os.path.join(publish_dir, 'default.vrd')
ib_parts = []
if args.InfoBaseServer and args.InfoBaseRef:
ib_parts.append(f'Srvr=&quot;{args.InfoBaseServer}&quot;')
ib_parts.append(f'Ref=&quot;{args.InfoBaseRef}&quot;')
else:
ib_parts.append(f'File=&quot;{args.InfoBasePath}&quot;')
if args.UserName:
ib_parts.append(f'Usr=&quot;{args.UserName}&quot;')
if args.Password:
ib_parts.append(f'Pwd=&quot;{args.Password}&quot;')
ib_string = ';'.join(ib_parts) + ';'
vrd_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<point xmlns="http://v8.1c.ru/8.2/virtual-resource-system"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
base="/{app_name}"
ib="{ib_string}"
enableStandardOdata="true">
<ws pointEnableCommon="true"/>
<httpServices publishByDefault="true"/>
</point>'''
with open(vrd_path, 'wb') as f:
f.write(b'\xef\xbb\xbf')
f.write(vrd_content.encode('utf-8'))
print(f'default.vrd: {vrd_path}')
# --- Update httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
sys.exit(1)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
apache_path_fwd = apache_path.replace('\\', '/')
wsap_dll_fwd = wsap_dll.replace('\\', '/')
publish_dir_fwd = publish_dir.replace('\\', '/')
vrd_path_fwd = vrd_path.replace('\\', '/')
# --- Global block (Listen + LoadModule) ---
global_marker_start = '# --- 1C: global ---'
global_marker_end = '# --- End: global ---'
global_block = (
f'{global_marker_start}\n'
f'Listen {port}\n'
f'LoadModule _1cws_module "{wsap_dll_fwd}"\n'
f'{global_marker_end}'
)
if re.search(re.escape(global_marker_start), conf_content):
# Replace existing global block
pattern = re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end)
conf_content = re.sub(pattern, global_block, conf_content)
else:
# Comment out default Listen to avoid port conflict
conf_content = re.sub(r'(?m)^(Listen\s+\d+)', r'#\1 # commented by web-publish', conf_content)
# Append global block
conf_content = conf_content.rstrip() + '\n\n' + global_block + '\n'
# --- Publication block ---
pub_marker_start = f'# --- 1C Publication: {app_name} ---'
pub_marker_end = f'# --- End: {app_name} ---'
pub_block = (
f'{pub_marker_start}\n'
f'Alias "/{app_name}" "{publish_dir_fwd}"\n'
f'<Directory "{publish_dir_fwd}">\n'
f' AllowOverride All\n'
f' Require all granted\n'
f' SetHandler 1c-application\n'
f' ManagedApplicationDescriptor "{vrd_path_fwd}"\n'
f'</Directory>\n'
f'{pub_marker_end}'
)
if re.search(re.escape(pub_marker_start), conf_content):
# Replace existing publication block
pattern = re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end)
conf_content = re.sub(pattern, pub_block, conf_content)
else:
# Append publication block
conf_content = conf_content.rstrip() + '\n\n' + pub_block + '\n'
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
print('httpd.conf обновлён')
# --- Normalize httpd_exe for process matching ---
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Check port availability ---
holder_pid = check_port_in_use(port)
if holder_pid:
our_proc = get_our_httpd(httpd_exe_norm)
if not our_proc:
# Port is held by someone else
try:
holder_proc = psutil.Process(holder_pid)
holder_name = f'{holder_proc.name()} (PID: {holder_pid})'
except (psutil.NoSuchProcess, psutil.AccessDenied):
holder_name = f'PID {holder_pid}'
print(f'Error: порт {port} занят процессом {holder_name}', file=sys.stderr)
print('Укажите другой порт: -Port 9090')
sys.exit(1)
# --- Start Apache if not running ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if httpd_proc:
first_pid = httpd_proc[0].pid
print(f'Apache уже запущен (PID: {first_pid})')
print('Перезапуск для применения конфигурации...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
else:
# Check if a foreign httpd holds the port
foreign_httpd = get_all_httpd()
if foreign_httpd:
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign_httpd[0].pid})')
print(f' Наш Apache: {httpd_exe}')
print('Запуск Apache...')
subprocess.Popen(
[httpd_exe],
cwd=apache_path,
creationflags=subprocess.CREATE_NO_WINDOW,
)
time.sleep(2)
httpd_check = get_our_httpd(httpd_exe_norm)
if httpd_check:
print(f'Apache запущен (PID: {httpd_check[0].pid})')
else:
print('Apache не удалось запустить', file=sys.stderr)
# Run config test for diagnostics
try:
result = subprocess.run(
[httpd_exe, '-t'],
capture_output=True,
text=True,
timeout=10,
)
test_output = (result.stdout + result.stderr).strip()
if test_output:
print('--- httpd -t ---')
for line in test_output.splitlines():
print(f' {line}')
except Exception:
pass
error_log = os.path.join(apache_path, 'logs', 'error.log')
if os.path.exists(error_log):
print('--- error.log (последние 10 строк) ---')
try:
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
all_lines = f.readlines()
for line in all_lines[-10:]:
print(line.rstrip())
except Exception:
pass
sys.exit(1)
# --- Result ---
print('')
print('=== Публикация готова ===')
print(f'URL: http://localhost:{port}/{app_name}')
print(f'OData: http://localhost:{port}/{app_name}/odata/standard.odata')
print(f'HTTP-сервисы: http://localhost:{port}/{app_name}/hs/<RootUrl>/...')
print(f'Web-сервисы: http://localhost:{port}/{app_name}/ws/<Имя>?wsdl')
if __name__ == '__main__':
main()
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
# web-stop v1.0 — Stop Apache HTTP Server
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Остановка Apache HTTP Server.
Сначала пытается graceful shutdown, при неудаче принудительная остановка.
"""
import argparse
import os
import sys
import time
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def get_all_httpd():
"""Get all httpd processes."""
result = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def main():
parser = argparse.ArgumentParser(description='Stop Apache HTTP Server', allow_abbrev=False)
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Helper: normalize httpd exe path ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Check process (only our Apache) ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if not httpd_proc:
foreign = get_all_httpd()
if foreign:
print('Наш Apache не запущен')
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign[0].pid})')
else:
print('Apache не запущен')
sys.exit(0)
pids = ', '.join(str(p.pid) for p in httpd_proc)
print(f'Останавливаю Apache (PID: {pids})...')
# --- Stop our processes ---
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# --- Wait for shutdown ---
max_wait = 5
elapsed = 0
while elapsed < max_wait:
time.sleep(1)
elapsed += 1
check = get_our_httpd(httpd_exe_norm)
if not check:
print('Apache остановлен')
print('Публикации сохранены. Перезапуск: /web-publish <база> Удаление: /web-unpublish --all')
sys.exit(0)
# --- Fallback: force kill ---
remaining = get_our_httpd(httpd_exe_norm)
if remaining:
print('Принудительная остановка...')
for p in remaining:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
final = get_our_httpd(httpd_exe_norm)
if final:
print('Error: не удалось остановить Apache', file=sys.stderr)
sys.exit(1)
print('Apache остановлен')
print('Публикации сохранены. Перезапуск: /web-publish <база> Удаление: /web-unpublish --all')
if __name__ == '__main__':
main()
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
# web-unpublish v1.0 — Remove 1C web publication
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Удаление веб-публикации 1С из Apache.
Удаляет маркерный блок из httpd.conf и каталог публикации.
Если Apache запущен перезапускает для применения.
С флагом -All удаляет все публикации и останавливает Apache.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import time
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def main():
parser = argparse.ArgumentParser(description='Remove 1C web publication', allow_abbrev=False)
parser.add_argument('-AppName', type=str, default='', help='Publication name')
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
parser.add_argument('-All', action='store_true', help='Remove all publications')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Validate params ---
if not args.All and not args.AppName:
print('Error: укажите -AppName или -All', file=sys.stderr)
sys.exit(1)
# --- Read httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
sys.exit(1)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
# --- Helper: our httpd process ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Collect app names to remove ---
if args.All:
pub_pattern = r'# --- 1C Publication: (.+?) ---'
pub_matches = re.findall(pub_pattern, conf_content)
if not pub_matches:
print('Нет публикаций для удаления')
sys.exit(0)
app_names = pub_matches
print(f'Удаление всех публикаций: {", ".join(app_names)}')
else:
app_names = [args.AppName]
# --- Remove marker blocks ---
for name in app_names:
pub_marker_start = f'# --- 1C Publication: {name} ---'
pub_marker_end = f'# --- End: {name} ---'
if re.search(re.escape(pub_marker_start), conf_content):
pattern = r'\r?\n?' + re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end) + r'\r?\n?'
conf_content = re.sub(pattern, '\n', conf_content)
print(f"httpd.conf: блок публикации '{name}' удалён")
else:
print(f"Публикация '{name}' не найдена в httpd.conf")
# --- Check if any publications remain; if not, remove global block ---
remaining_pubs = re.findall(r'# --- 1C Publication: .+? ---', conf_content)
if not remaining_pubs:
global_marker_start = '# --- 1C: global ---'
global_marker_end = '# --- End: global ---'
if re.search(re.escape(global_marker_start), conf_content):
global_pattern = r'\r?\n?' + re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end) + r'\r?\n?'
conf_content = re.sub(global_pattern, '\n', conf_content)
print('httpd.conf: глобальный блок 1C удалён (нет публикаций)')
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
# --- Remove publish directories ---
for name in app_names:
publish_dir = os.path.join(apache_path, 'publish', name)
if os.path.exists(publish_dir):
shutil.rmtree(publish_dir, ignore_errors=True)
print(f'Каталог удалён: {publish_dir}')
else:
print(f'Каталог не найден: {publish_dir}')
# --- Restart/Stop Apache if running (only our instance) ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if httpd_proc:
if remaining_pubs:
print('Перезапуск Apache...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
subprocess.Popen(
[httpd_exe],
cwd=apache_path,
creationflags=subprocess.CREATE_NO_WINDOW,
)
time.sleep(2)
check = get_our_httpd(httpd_exe_norm)
if check:
print('Apache перезапущен')
else:
print('Error: Apache не удалось перезапустить', file=sys.stderr)
sys.exit(1)
else:
print('Публикаций не осталось — останавливаю Apache...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
print('Apache остановлен')
print('')
if args.All:
print(f'Все публикации удалены ({len(app_names)} шт.)')
else:
print(f"Публикация '{args.AppName}' удалена")
if __name__ == '__main__':
main()
+3
View File
@@ -16,5 +16,8 @@ test-tmp/
# Инструменты (portable Apache и т.д.)
tools/
# Python кэш
__pycache__/
# Локальный реестр баз данных 1С
.v8-project.json
+21 -2
View File
@@ -39,9 +39,24 @@
## Требования
- **Windows** с PowerShell 5.1+ (входит в Windows)
- **Windows** с PowerShell 5.1+ (входит в Windows) — рантайм по умолчанию
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF (навыки генерации XML работают без платформы)
### Кроссплатформенный режим (Python)
Для работы на **Linux/Mac** можно переключить навыки на Python 3:
```bash
python scripts/switch-to-python.py # переключить на Python
python scripts/switch-to-powershell.py # вернуть на PowerShell
```
Дополнительные зависимости Python-рантайма:
- `lxml>=4.9.0` — для навыков, работающих с DOM (edit/validate/info)
- `psutil>=5.9.0` — для web-навыков (управление Apache)
Параметры скриптов идентичны для обоих рантаймов — переключение меняет только интерпретатор в вызовах. Подробнее: [Python Porting Guide](docs/python-porting-guide.md).
## Спецификации
Полный индекс с оглавлением по всем 44 типам объектов: **[Сводный индекс спецификаций](docs/1c-specs-index.md)**
@@ -134,6 +149,9 @@
├── web-stop/ # Остановка Apache
├── web-unpublish/ # Удаление публикации
└── img-grid/ # Сетка для анализа изображений
scripts/
├── switch-to-python.py # Переключение навыков на Python-рантайм
└── switch-to-powershell.py # Возврат на PowerShell-рантайм
docs/
├── epf-guide.md # Гайд: внешние обработки и отчёты
├── mxl-guide.md # Гайд: табличный документ
@@ -162,5 +180,6 @@ docs/
├── role-dsl-spec.md # Спецификация Role DSL
├── 1c-extension-spec.md # Спецификация расширений конфигурации (CFE)
├── 1c-subsystem-spec.md # Спецификация подсистем и командного интерфейса
── web-spec.md # Спецификация веб-публикации (VRD, httpd.conf, Apache)
── web-spec.md # Спецификация веб-публикации (VRD, httpd.conf, Apache)
└── python-porting-guide.md # Руководство по Python-портам навыков
```
+191
View File
@@ -0,0 +1,191 @@
# Python Porting Guide
Руководство по Python-портам навыков 1С (PS1 → Python).
## Зачем Python рядом с PS1
PowerShell 5.1 доступен только на Windows. Python-порты обеспечивают кроссплатформенность (Linux, Mac). Модель opt-in: PS1 — по умолчанию, Python — переключается скриптами.
## Переключение рантайма
```bash
# Переключить все .md в навыках на Python
python scripts/switch-to-python.py
# Вернуть на PowerShell
python scripts/switch-to-powershell.py
```
Скрипты обрабатывают все `.md` файлы в `.claude/skills/*/` (SKILL.md, json-dsl.md и др.). Идемпотентны — повторный запуск безопасен. Python-only навыки (img-grid) пропускаются при переключении на PowerShell.
## Принцип самодостаточности
Каждый `.py` — полностью автономен, как и его `.ps1`-аналог. Нет общих модулей. Это соответствует [рекомендациям Anthropic](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) и зеркалит существующую архитектуру PS1.
Общие утилиты (5-15 строк) дублируются в каждом скрипте:
```python
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def emit_mltext(lines, indent, tag, text):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
import uuid
return str(uuid.uuid4())
def read_utf8(path):
with open(path, 'r', encoding='utf-8-sig') as f:
return f.read()
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
```
Большие словари данных (синонимы типов, карты объектов) тоже inline — как `$script:typeSynonyms` в PS1.
## Конвенция параметров
Формат `-ParamName` сохранён для минимальных различий в SKILL.md:
```python
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument('-JsonPath', dest='JsonPath', required=True)
parser.add_argument('-NoValidate', dest='NoValidate', action='store_true')
```
Switch-параметры (`-NoValidate`) → `action='store_true'`.
## Таблица маппинга PS → Python
| PS1 | Python |
|-----|--------|
| `$script:xml = New-Object StringBuilder` | `lines = []` |
| `$xml.AppendLine($text)` | `lines.append(text)` |
| `$xml.ToString()` | `'\n'.join(lines)` |
| `[System.Xml.XmlDocument] + PreserveWhitespace` | `lxml.etree.XMLParser(remove_blank_text=False)` |
| `$xmlDoc.SelectSingleNode(xpath, $ns)` | `root.find(xpath, namespaces=NSMAP)` |
| `$xmlDoc.SelectNodes(xpath, $ns)` | `root.findall(xpath, namespaces=NSMAP)` |
| `XmlWriter + MemoryStream + BOM fix` | `etree.tostring(root, xml_declaration=True, encoding='UTF-8')` |
| `[System.Guid]::NewGuid().ToString()` | `str(uuid.uuid4())` |
| `$json \| ConvertFrom-Json` | `json.loads(text)` |
| `ConvertTo-Json -Depth 10` | `json.dumps(obj, ensure_ascii=False, indent=2)` |
| `New-Object System.Text.UTF8Encoding($true)` | `encoding='utf-8-sig'` |
| `Start-Process -Wait -PassThru` | `subprocess.run([...], capture_output=True)` |
| `Start-Process` (без -Wait) | `subprocess.Popen([...])` |
| `[switch]$NoValidate` | `parser.add_argument('-NoValidate', action='store_true')` |
| `[ValidateSet("a","b")]` | `choices=["a","b"]` |
| `Get-ChildItem "path\*\..."` | `glob.glob(...)` |
| `Get-Process httpd` | `psutil.process_iter(['pid','name','exe'])` |
| `Test-Path $path` | `os.path.exists(path)` |
| `Resolve-Path` | `os.path.abspath()` |
| `Join-Path $a $b` | `os.path.join(a, b)` |
| `New-Item -ItemType Directory -Force` | `os.makedirs(path, exist_ok=True)` |
| `Remove-Item -Recurse -Force` | `shutil.rmtree(path)` |
| `Write-Host "text"` | `print("text")` |
| `Write-Error "text"` | `print("text", file=sys.stderr)` |
## lxml vs stdlib
- **Compile/init скрипты** (строковая сборка): только stdlib
- **DOM-скрипты** (edit/validate/info): `lxml` с `XMLParser(remove_blank_text=False)` для сохранения whitespace
- **Web-скрипты**: `psutil` для работы с процессами Apache
Зависимости:
- `lxml>=4.9.0` — ~25 DOM-скриптов
- `psutil>=5.9.0` — 4 web-скрипта
## Работа с BOM (UTF-8)
Кодек Python `utf-8-sig` — точный аналог `New-Object System.Text.UTF8Encoding($true)`:
- Запись: добавляет BOM (EF BB BF)
- Чтение: убирает BOM автоматически
```python
# Чтение (BOM убирается)
with open(path, 'r', encoding='utf-8-sig') as f:
content = f.read()
# Запись (BOM добавляется)
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
```
Параметр `newline=''` предотвращает двойные `\r\n` на Windows.
## Сохранение XML с lxml
```python
from lxml import etree
# Загрузка с сохранением whitespace
parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(path, parser)
root = tree.getroot()
# Сохранение с BOM
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding='UTF-8')
# Fix encoding case: lxml пишет utf-8, 1C ожидает UTF-8
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, 'wb') as f:
f.write(b'\xef\xbb\xbf') # BOM
f.write(xml_bytes)
```
## Известные подводные камни
### Namespace в XPath
lxml требует явный namespace map. В PS1 используется `XmlNamespaceManager`:
```python
NSMAP = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
node = root.find('.//md:ChildObjects/md:Form', NSMAP)
```
### d5p1: для ссылочных типов
В DCS-файлах ссылочные типы используют `d5p1:`, не `cfg:`:
```xml
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.XXX</v8:Type>
```
### encoding="UTF-8" (uppercase)
1C ожидает `encoding="UTF-8"`. lxml по умолчанию пишет `encoding='UTF-8'` с одинарными кавычками — нужна замена на двойные.
## Платформозависимые заметки
Скрипты `db-*` и `web-*` используют платформу 1С (Designer CLI, Apache) — работают только на Windows. Но синтаксических ошибок на других ОС не будет: скрипт корректно сообщит об отсутствии платформы.
## Добавление нового навыка
Чеклист:
1. Создать `.ps1` скрипт
2. Создать `.py` скрипт с идентичными параметрами
3. В SKILL.md указать `powershell.exe -NoProfile -File ... .ps1` (по умолчанию)
4. Скрипт переключения автоматически подхватит новый навык
## Обновление существующего навыка
При доработке `.ps1`:
1. Применить аналогичные изменения в `.py`
2. Если затронуты inline-утилиты — обновить во всех скриптах: `grep -r "def esc_xml" .claude/skills/`
## Inline-утилиты — полный список
| Функция | Где используется |
|---------|-----------------|
| `esc_xml()` | compile, init, edit, add скрипты |
| `emit_mltext()` | compile, init, add скрипты |
| `new_uuid()` | init, add, compile скрипты |
| `read_utf8()` | все скрипты |
| `write_utf8_bom()` | все скрипты с записью |
| `paginate()` | info скрипты |
| `split_camelcase()` | info скрипты |
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# switch-to-powershell v1.1 — Switch skill .md files back to PowerShell scripts
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Replaces python invocations with powershell.exe in all .md files under .claude/skills/."""
import os, re, glob, sys
def main():
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
skills_dir = os.path.join(repo_root, '.claude', 'skills')
# Collect all .md files in skill directories (SKILL.md, json-dsl.md, etc.)
md_files = sorted(glob.glob(os.path.join(skills_dir, '*', '*.md')))
if not md_files:
print(f"Error: no .md files found in {skills_dir}", file=sys.stderr)
sys.exit(1)
rx = re.compile(r'python\s+(\'?\.claude/skills/[^\s\']+?)\.py')
switched = 0
warnings = []
for md_path in md_files:
with open(md_path, 'r', encoding='utf-8') as f:
content = f.read()
matches = rx.findall(content)
if not matches:
continue
# Check that .ps1 files exist for all matches
all_exist = True
for m in matches:
ps1_path = m.lstrip("'") + '.ps1'
ps1_full = os.path.join(repo_root, ps1_path)
if not os.path.isfile(ps1_full):
skill_name = os.path.basename(os.path.dirname(md_path))
md_name = os.path.basename(md_path)
warnings.append(f" SKIP: {ps1_path} not found (referenced in {skill_name}/{md_name})")
all_exist = False
if not all_exist:
continue
new_content = rx.sub(r'powershell.exe -NoProfile -File \1.ps1', content)
if new_content != content:
with open(md_path, 'w', encoding='utf-8') as f:
f.write(new_content)
skill_name = os.path.basename(os.path.dirname(md_path))
md_name = os.path.basename(md_path)
print(f" [OK] {skill_name}/{md_name}")
switched += 1
print(f"\nSwitched {switched} file(s) to PowerShell.")
if warnings:
print("\nSkipped (missing .ps1 files):")
for w in warnings:
print(w)
if __name__ == '__main__':
main()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
# switch-to-python v1.1 — Switch skill .md files to use Python scripts
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Replaces powershell.exe invocations with python in all .md files under .claude/skills/."""
import os, re, glob, sys
def main():
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
skills_dir = os.path.join(repo_root, '.claude', 'skills')
# Collect all .md files in skill directories (SKILL.md, json-dsl.md, etc.)
md_files = sorted(glob.glob(os.path.join(skills_dir, '*', '*.md')))
if not md_files:
print(f"Error: no .md files found in {skills_dir}", file=sys.stderr)
sys.exit(1)
rx = re.compile(r'powershell\.exe\s+(?:-NoProfile\s+)?-File\s+(.+?)\.ps1')
switched = 0
warnings = []
for md_path in md_files:
with open(md_path, 'r', encoding='utf-8') as f:
content = f.read()
matches = rx.findall(content)
if not matches:
continue
# Check that .py files exist
for m in matches:
clean_path = m.lstrip("'")
py_path = clean_path + '.py'
py_full = os.path.join(repo_root, py_path)
if not os.path.isfile(py_full):
skill_name = os.path.basename(os.path.dirname(md_path))
md_name = os.path.basename(md_path)
warnings.append(f" WARN: {py_path} not found (referenced in {skill_name}/{md_name})")
new_content = rx.sub(r'python \1.py', content)
if new_content != content:
with open(md_path, 'w', encoding='utf-8') as f:
f.write(new_content)
skill_name = os.path.basename(os.path.dirname(md_path))
md_name = os.path.basename(md_path)
print(f" [OK] {skill_name}/{md_name}")
switched += 1
print(f"\nSwitched {switched} file(s) to Python.")
if warnings:
print("\nWarnings (missing .py files):")
for w in warnings:
print(w)
if __name__ == '__main__':
main()