Files
2026-06-04 09:28:00 +00:00

1248 lines
51 KiB
Python

# meta-validate v1.3 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import subprocess
import sys
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default="")
args = parser.parse_args()
detailed = args.Detailed
max_errors = args.MaxErrors
out_file = args.OutFile
# ── batch mode: pipe-separated paths ─────────────────────────
path_list = [p.strip() for p in args.ObjectPath.split('|') if p.strip()]
if len(path_list) > 1:
batch_ok = 0
batch_fail = 0
for single_path in path_list:
cmd = [sys.executable, __file__, "-ObjectPath", "-Path", single_path, "-MaxErrors", str(max_errors)]
if detailed:
cmd.append("-Detailed")
if out_file:
base, ext = os.path.splitext(out_file)
obj_leaf = os.path.splitext(os.path.basename(single_path))[0]
cmd += ["-OutFile", f"{base}_{obj_leaf}{ext}"]
rc = subprocess.call(cmd)
if rc == 0:
batch_ok += 1
else:
batch_fail += 1
print()
print(f"=== Batch: {len(path_list)} objects, {batch_ok} passed, {batch_fail} failed ===")
sys.exit(1 if batch_fail > 0 else 0)
object_path = path_list[0]
# ── 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)
# ── detect config directory (for cross-object checks) ────────
config_dir = None
probe = os.path.dirname(resolved_path)
for _ in range(4):
if not probe:
break
if os.path.exists(os.path.join(probe, "Configuration.xml")):
config_dir = probe
break
probe = os.path.dirname(probe)
# ── output infrastructure ────────────────────────────────────
errors = 0
warnings = 0
ok_count = 0
stopped = False
output_lines = []
def out_line(msg):
output_lines.append(msg)
def report_ok(msg):
global ok_count
ok_count += 1
if detailed:
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():
checks = ok_count + errors + warnings
if errors == 0 and warnings == 0 and not detailed:
result = f"=== Validation OK: {md_type}.{obj_name} ({checks} checks) ==="
else:
out_line("")
out_line(f"=== Result: {errors} errors, {warnings} warnings ({checks} checks) ===")
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"],
"DependenceOnCalculationTypes": ["DontUse", "OnActionPeriod"],
}
# Properties forbidden per type (would cause LoadConfigFromFiles error)
forbidden_properties = {
"ChartOfCharacteristicTypes": ["CodeType"],
"ChartOfAccounts": ["Autonumbering", "Hierarchical"],
"ChartOfCalculationTypes": ["CheckUnique", "Autonumbering"],
"ExchangePlan": ["CodeType", "CheckUnique", "Autonumbering"],
}
# ── 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})")
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")
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")
if stopped:
finalize()
sys.exit(1)
# ── Check 7b: Reserved attribute names ───────────────────────
RESERVED_ATTR_NAMES = {
'Ref', 'DeletionMark', 'Code', 'Description', 'Date', 'Number', 'Posted',
'Parent', 'Owner', 'IsFolder', 'Predefined', 'PredefinedDataName',
'Recorder', 'Period', 'LineNumber', 'Active', 'Order', 'Type', 'OffBalance',
'Started', 'Completed', 'HeadTask', 'Executed', 'RoutePoint', 'BusinessProcess',
'ThisNode', 'SentNo', 'ReceivedNo', 'CalculationType', 'RegistrationPeriod',
'ReversingEntry', 'Account', 'ValueType', 'ActionPeriodIsBasic',
}
if child_obj_node is not None:
check7b_ok = True
for attr_node in find_all(child_obj_node, 'md:Attribute'):
attr_props = find(attr_node, 'md:Properties')
if attr_props is not None:
attr_name_node = find(attr_props, 'md:Name')
if attr_name_node is not None and inner_text(attr_name_node):
an = inner_text(attr_name_node)
if an in RESERVED_ATTR_NAMES:
report_warn(f"7b. Attribute '{an}' conflicts with a standard attribute name")
check7b_ok = False
if check7b_ok:
report_ok("7b. Reserved attribute names: no conflicts")
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")
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")
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
# AccountingRegister: ChartOfAccounts must not be empty
if md_type == 'AccountingRegister':
coa = find(props_node, 'md:ChartOfAccounts')
if coa is None or not text_of(coa):
report_error('10. AccountingRegister: empty ChartOfAccounts')
check10_ok = False
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ChartOfAccounts=ChartOfAccounts.XXX"')
# CalculationRegister: ChartOfCalculationTypes must not be empty
if md_type == 'CalculationRegister':
coct = find(props_node, 'md:ChartOfCalculationTypes')
if coct is None or not text_of(coct):
report_error('10. CalculationRegister: empty ChartOfCalculationTypes')
check10_ok = False
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ChartOfCalculationTypes=ChartOfCalculationTypes.XXX"')
# BusinessProcess: Task should not be empty
if md_type == 'BusinessProcess':
task_prop = find(props_node, 'md:Task')
if task_prop is None or not text_of(task_prop):
report_warn('10. BusinessProcess: empty Task reference')
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "Task=Task.XXX"')
# CalculationRegister: ActionPeriod=true requires non-empty Schedule
if md_type == 'CalculationRegister':
action_period = find(props_node, 'md:ActionPeriod')
if action_period is not None and text_of(action_period) == 'true':
schedule = find(props_node, 'md:Schedule')
if schedule is None or not text_of(schedule):
report_warn('10. CalculationRegister: ActionPeriod=true but Schedule is empty — platform requires a schedule register')
check10_issues += 1
# DocumentJournal: RegisteredDocuments should not be empty
if md_type == 'DocumentJournal':
reg_docs = find(props_node, 'md:RegisteredDocuments')
has_reg_docs = False
if reg_docs is not None:
items = find_all(reg_docs, 'v8:Type')
if len(items) > 0:
has_reg_docs = True
if not has_reg_docs:
report_warn('10. DocumentJournal: no RegisteredDocuments specified')
check10_issues += 1
# ChartOfAccounts: ExtDimensionTypes should be set if MaxExtDimensionCount > 0
if md_type == 'ChartOfAccounts':
max_ext_dim = find(props_node, 'md:MaxExtDimensionCount')
if max_ext_dim is not None:
try:
med_val = int(inner_text(max_ext_dim) or '0')
except ValueError:
med_val = 0
if med_val > 0:
edt = find(props_node, 'md:ExtDimensionTypes')
if edt is None or not text_of(edt):
report_warn('10. ChartOfAccounts: MaxExtDimensionCount>0 but ExtDimensionTypes is empty')
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ExtDimensionTypes=ChartOfCharacteristicTypes.XXX"')
# Register: must have at least one Dimension or Resource (platform rejects empty registers)
reg_types_all = ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'InformationRegister')
if md_type in reg_types_all and child_obj_node is not None:
dims = len(find_all(child_obj_node, 'md:Dimension'))
ress = len(find_all(child_obj_node, 'md:Resource'))
attrs = len(find_all(child_obj_node, 'md:Attribute'))
if dims + ress + attrs == 0:
report_warn(f"10. {md_type}: no Dimensions, Resources, or Attributes \u2014 platform will reject")
check10_issues += 1
# Document: RegisterRecords references should point to existing objects in config
if md_type == 'Document' and config_dir:
reg_records = find(props_node, 'md:RegisterRecords')
if reg_records is not None:
rr_items = find_all(reg_records, 'xr:Item')
for item in rr_items:
ref_val = (inner_text(item) or '').strip()
if not ref_val:
continue
# Parse "AccumulationRegister.Name" -> dir AccumulationRegisters/Name
parts = ref_val.split('.', 1)
if len(parts) == 2:
ref_type, ref_name = parts
dir_map = {
'AccumulationRegister': 'AccumulationRegisters',
'InformationRegister': 'InformationRegisters',
'AccountingRegister': 'AccountingRegisters',
'CalculationRegister': 'CalculationRegisters',
}
ref_dir = dir_map.get(ref_type)
if ref_dir:
ref_path = os.path.join(config_dir, ref_dir, ref_name)
ref_xml = os.path.join(config_dir, ref_dir, ref_name + '.xml')
if not os.path.exists(ref_path) and not os.path.exists(ref_xml):
report_warn(f"10. Document.RegisterRecords references '{ref_val}' but object not found in config")
check10_issues += 1
# Register: must have at least one registrar document
register_types = ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'InformationRegister')
if md_type in register_types and config_dir and obj_name != '(unknown)':
needs_registrar = True
# InformationRegister with WriteMode=Independent does not need a registrar
if md_type == 'InformationRegister':
write_mode = find(props_node, 'md:WriteMode')
if write_mode is None or inner_text(write_mode) != 'RecorderSubordinate':
needs_registrar = False
if needs_registrar:
reg_ref = f'{md_type}.{obj_name}'
docs_dir = os.path.join(config_dir, 'Documents')
has_registrar = False
if os.path.isdir(docs_dir):
for fname in os.listdir(docs_dir):
if not fname.endswith('.xml'):
continue
fpath = os.path.join(docs_dir, fname)
if not os.path.isfile(fpath):
continue
with open(fpath, 'r', encoding='utf-8-sig') as f:
content = f.read()
if reg_ref in content:
has_registrar = True
break
if not has_registrar:
report_warn(f"10. {md_type}: no registrar document found (none references '{reg_ref}' in RegisterRecords)")
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)")
if stopped:
finalize()
sys.exit(1)
# ── Check 12: Forbidden properties per type ──────────────────
if props_node is not None and md_type in forbidden_properties:
forbidden = forbidden_properties[md_type]
check12_ok = True
for fp in forbidden:
fp_node = find(props_node, f"md:{fp}")
if fp_node is not None:
report_error(f"12. Forbidden property '{fp}' present in {md_type} (will fail on LoadConfigFromFiles)")
check12_ok = False
if check12_ok:
report_ok("12. Forbidden properties: none found")
if stopped:
finalize()
sys.exit(1)
# ── Check 13: Method reference validation ─────────────────────
if props_node is not None and md_type in ("EventSubscription", "ScheduledJob") and config_dir:
check13_ok = True
method_ref = None
prop_label = None
if md_type == "EventSubscription":
h_node = find(props_node, "md:Handler")
if h_node is not None:
method_ref = text_of(h_node)
prop_label = "Handler"
elif md_type == "ScheduledJob":
m_node = find(props_node, "md:MethodName")
if m_node is not None:
method_ref = text_of(m_node)
prop_label = "MethodName"
if method_ref:
parts = method_ref.split(".")
# Format: CommonModule.ModuleName.ProcedureName (3 parts) or ModuleName.ProcedureName (2 parts, legacy)
if len(parts) == 3 and parts[0] == "CommonModule":
cm_name = parts[1]
proc_name = parts[2]
elif len(parts) == 2:
cm_name = parts[0]
proc_name = parts[1]
else:
report_error(f"13. {md_type}.{prop_label} = '{method_ref}': expected format 'CommonModule.ModuleName.ProcedureName'")
check13_ok = False
cm_name = None
proc_name = None
if cm_name:
cm_xml = os.path.join(config_dir, "CommonModules", f"{cm_name}.xml")
if not os.path.exists(cm_xml):
report_error(f"13. {md_type}.{prop_label}: CommonModule '{cm_name}' not found (expected {cm_xml})")
check13_ok = False
else:
# Check BSL file for exported procedure
bsl_path = os.path.join(config_dir, "CommonModules", cm_name, "Ext", "Module.bsl")
if os.path.exists(bsl_path):
with open(bsl_path, "r", encoding="utf-8-sig") as f:
bsl_content = f.read()
export_pattern = rf"(?mi)^\s*(Procedure|Function|Процедура|Функция)\s+{re.escape(proc_name)}\s*\(.*\)\s+(Export|Экспорт)"
if not re.search(export_pattern, bsl_content):
report_warn(f"13. {md_type}.{prop_label}: procedure '{proc_name}' not found as exported in CommonModule '{cm_name}'")
check13_ok = False
else:
report_warn(f"13. {md_type}.{prop_label}: BSL file not found ({bsl_path}), cannot verify procedure")
if check13_ok:
report_ok(f"13. Method reference: {prop_label} = '{method_ref}'")
if stopped:
finalize()
sys.exit(1)
# ── Check 14: DocumentJournal Column content ──────────────────
if md_type == "DocumentJournal" and child_obj_node is not None:
columns = find_all(child_obj_node, "md:Column")
check14_ok = True
col_count = 0
empty_ref_count = 0
for col in columns:
col_count += 1
col_props = find(col, "md:Properties")
col_name_node = find(col_props, "md:Name") if col_props is not None else None
col_name = inner_text(col_name_node) if col_name_node is not None else "(unnamed)"
refs = find(col_props, "md:References") if col_props is not None else None
has_items = False
if refs is not None:
items = find_all(refs, "xr:Item")
if len(items) > 0:
has_items = True
if not has_items:
report_error(f"14. DocumentJournal Column '{col_name}': empty References (will fail on LoadConfigFromFiles)")
check14_ok = False
empty_ref_count += 1
if check14_ok and col_count > 0:
report_ok(f"14. DocumentJournal Columns: {col_count} column(s), all have References")
elif col_count == 0:
report_ok("14. DocumentJournal Columns: none")
# ── Final output ──────────────────────────────────────────────
finalize()
if errors > 0:
sys.exit(1)
sys.exit(0)