mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-13 23:13:22 +03:00
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:
co-authored by
Claude Opus 4.6
parent
6b5992de34
commit
86a959a354
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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()
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
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()
|
||||
@@ -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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
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()
|
||||
@@ -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('"', '"')
|
||||
|
||||
# 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="{args.InfoBaseServer}"')
|
||||
ib_parts.append(f'Ref="{args.InfoBaseRef}"')
|
||||
else:
|
||||
ib_parts.append(f'File="{args.InfoBasePath}"')
|
||||
if args.UserName:
|
||||
ib_parts.append(f'Usr="{args.UserName}"')
|
||||
if args.Password:
|
||||
ib_parts.append(f'Pwd="{args.Password}"')
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -16,5 +16,8 @@ test-tmp/
|
||||
# Инструменты (portable Apache и т.д.)
|
||||
tools/
|
||||
|
||||
# Python кэш
|
||||
__pycache__/
|
||||
|
||||
# Локальный реестр баз данных 1С
|
||||
.v8-project.json
|
||||
|
||||
@@ -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-портам навыков
|
||||
```
|
||||
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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 скрипты |
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user