mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-11 16:34:57 +03:00
1248 lines
51 KiB
Python
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)
|