mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-16 07:45:17 +03:00
d155086444
When working with existing configs dumped from newer platforms (8.3.27+), XML files use version="2.20" instead of "2.17". Skills now detect the version from the nearest Configuration.xml walking up the directory tree, falling back to "2.17" if not found. This prevents format version mismatch errors during LoadConfigFromFiles. Updated skills (11): meta-compile, form-compile, form-add, template-add, cfe-borrow, epf-add-form, help-add, role-compile, subsystem-compile, interface-edit. Also fixed form-validate to accept version 2.20. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1560 lines
72 KiB
Python
1560 lines
72 KiB
Python
#!/usr/bin/env python3
|
|
# cfe-borrow v1.3 — 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"},
|
|
],
|
|
"DefinedType": [
|
|
{"prefix": "DefinedType", "category": "DefinedType"},
|
|
],
|
|
}
|
|
|
|
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"]
|
|
|
|
# Standard system fields to skip when collecting DataPath references
|
|
STANDARD_FIELDS = [
|
|
"Code", "Description", "Ref", "Parent", "DeletionMark",
|
|
"Predefined", "IsFolder", "LineNumber", "RowsCount", "PredefinedDataName",
|
|
]
|
|
|
|
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 detect_format_version(d):
|
|
while d:
|
|
cfg_path = os.path.join(d, "Configuration.xml")
|
|
if os.path.isfile(cfg_path):
|
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
|
head = f.read(2000)
|
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
|
if m:
|
|
return m.group(1)
|
|
parent = os.path.dirname(d)
|
|
if parent == d:
|
|
break
|
|
d = parent
|
|
return "2.17"
|
|
|
|
|
|
def 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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
if not xml_bytes.endswith(b"\n"):
|
|
xml_bytes += b"\n"
|
|
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():
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
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)
|
|
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
|
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)
|
|
|
|
format_version = detect_format_version(ext_dir)
|
|
|
|
# --- 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}")
|
|
|
|
# Module-level list for borrowed files (used by both main loop and borrow_main_attribute)
|
|
borrowed_files = []
|
|
|
|
# --- 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="{format_version}">')
|
|
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}")
|
|
|
|
# --- 11b. Collect DataPath references from source Form.xml ---
|
|
def collect_form_data_paths(form_xml_path):
|
|
with open(form_xml_path, "r", encoding="utf-8-sig") as fh:
|
|
content = fh.read()
|
|
|
|
first_level = {}
|
|
deep_paths = []
|
|
|
|
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
|
path = m.group(1)
|
|
segments = path.split(".")
|
|
seg0 = segments[0]
|
|
if seg0 in STANDARD_FIELDS:
|
|
continue
|
|
first_level[seg0] = True
|
|
if len(segments) >= 2:
|
|
seg1 = segments[1]
|
|
if seg1 in STANDARD_FIELDS:
|
|
continue
|
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
|
|
|
# Also collect from TitleDataPath
|
|
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
|
|
path = m.group(1)
|
|
segments = path.split(".")
|
|
seg0 = segments[0]
|
|
if seg0 in STANDARD_FIELDS:
|
|
continue
|
|
first_level[seg0] = True
|
|
|
|
# Deduplicate deep paths
|
|
seen = set()
|
|
unique_deep = []
|
|
for dp in deep_paths:
|
|
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
|
if key not in seen:
|
|
seen.add(key)
|
|
unique_deep.append(dp)
|
|
|
|
return {"FirstLevel": first_level, "DeepPaths": unique_deep}
|
|
|
|
# --- 11c. Resolve source attributes and tabular sections ---
|
|
def resolve_source_attributes(type_name, obj_name, first_level_names):
|
|
# first_level_names: dict of names, or None for "all"
|
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
|
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()
|
|
|
|
ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
|
|
|
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 in source: {src_file}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
child_objs = src_el.find(f"{{{MD_NS}}}ChildObjects")
|
|
if child_objs is None:
|
|
return {"Attributes": [], "TabularSections": [], "ExtraProps": {}}
|
|
|
|
attrs = []
|
|
tab_sections = []
|
|
|
|
for child in child_objs:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
ln = localname(child)
|
|
|
|
if ln == "Attribute":
|
|
name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
|
|
if name_node is None:
|
|
continue
|
|
attr_name = (name_node.text or "").strip()
|
|
if first_level_names is not None and attr_name not in first_level_names:
|
|
continue
|
|
|
|
attr_uuid = child.get("uuid", "")
|
|
type_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Type")
|
|
type_xml = ""
|
|
if type_node is not None:
|
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
|
type_xml = ns_strip.sub("", type_xml)
|
|
|
|
attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml})
|
|
|
|
elif ln == "TabularSection":
|
|
name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
|
|
if name_node is None:
|
|
continue
|
|
ts_name = (name_node.text or "").strip()
|
|
if first_level_names is not None and ts_name not in first_level_names:
|
|
continue
|
|
|
|
ts_uuid = child.get("uuid", "")
|
|
|
|
# Extract GeneratedTypes from InternalInfo
|
|
ts_gen_types = []
|
|
ii_node = child.find(f"{{{MD_NS}}}InternalInfo")
|
|
if ii_node is not None:
|
|
for gt in ii_node:
|
|
if isinstance(gt.tag, str) and localname(gt) == "GeneratedType":
|
|
gt_name = gt.get("name", "")
|
|
gt_category = gt.get("category", "")
|
|
tid_el = gt.find(f"{{{XR_NS}}}TypeId")
|
|
vid_el = gt.find(f"{{{XR_NS}}}ValueId")
|
|
ts_gen_types.append({
|
|
"Name": gt_name,
|
|
"Category": gt_category,
|
|
"TypeId": (tid_el.text or "") if tid_el is not None else "",
|
|
"ValueId": (vid_el.text or "") if vid_el is not None else "",
|
|
})
|
|
|
|
# Extract ALL child attributes of TabularSection
|
|
ts_attrs = []
|
|
ts_child_objs = child.find(f"{{{MD_NS}}}ChildObjects")
|
|
if ts_child_objs is not None:
|
|
for ts_child in ts_child_objs:
|
|
if not isinstance(ts_child.tag, str) or localname(ts_child) != "Attribute":
|
|
continue
|
|
ts_attr_name_el = ts_child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
|
|
if ts_attr_name_el is None:
|
|
continue
|
|
ts_attr_uuid = ts_child.get("uuid", "")
|
|
ts_type_node = ts_child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Type")
|
|
ts_type_xml = ""
|
|
if ts_type_node is not None:
|
|
ts_type_xml = etree.tostring(ts_type_node, encoding="unicode")
|
|
ts_type_xml = ns_strip.sub("", ts_type_xml)
|
|
ts_attrs.append({
|
|
"Name": (ts_attr_name_el.text or "").strip(),
|
|
"Uuid": ts_attr_uuid,
|
|
"TypeXml": ts_type_xml,
|
|
})
|
|
|
|
tab_sections.append({
|
|
"Name": ts_name, "Uuid": ts_uuid,
|
|
"GeneratedTypes": ts_gen_types, "Attributes": ts_attrs,
|
|
})
|
|
|
|
# Extract extra Properties for main object enrichment
|
|
extra_props = {}
|
|
props_node = src_el.find(f"{{{MD_NS}}}Properties")
|
|
if props_node is not None:
|
|
props_to_extract = [
|
|
"Hierarchical", "FoldersOnTop", "CodeLength", "DescriptionLength",
|
|
"CodeType", "CodeAllowedLength", "NumberType", "NumberLength",
|
|
"NumberAllowedLength", "NumberPeriodicity",
|
|
]
|
|
for p_name in props_to_extract:
|
|
p_node = props_node.find(f"{{{MD_NS}}}{p_name}")
|
|
if p_node is not None:
|
|
extra_props[p_name] = (p_node.text or "").strip()
|
|
|
|
return {"Attributes": attrs, "TabularSections": tab_sections, "ExtraProps": extra_props}
|
|
|
|
# --- 11d. Build adopted attribute XML ---
|
|
def build_adopted_attribute_xml(name, source_uuid, type_xml, indent):
|
|
new_uuid_val = new_guid()
|
|
lines = [
|
|
f'{indent}<Attribute uuid="{new_uuid_val}">',
|
|
f'{indent}\t<InternalInfo/>',
|
|
f'{indent}\t<Properties>',
|
|
f'{indent}\t\t<ObjectBelonging>Adopted</ObjectBelonging>',
|
|
f'{indent}\t\t<Name>{name}</Name>',
|
|
f'{indent}\t\t<Comment/>',
|
|
f'{indent}\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>',
|
|
f'{indent}\t\t{type_xml}',
|
|
f'{indent}\t</Properties>',
|
|
f'{indent}</Attribute>',
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
# --- 11e. Build adopted tabular section XML ---
|
|
def build_adopted_tabular_section_xml(ts_name, source_uuid, generated_types, child_attrs, indent):
|
|
new_uuid_val = new_guid()
|
|
lines = [f'{indent}<TabularSection uuid="{new_uuid_val}">']
|
|
|
|
# InternalInfo with GeneratedTypes (new UUIDs, referencing source names)
|
|
if generated_types:
|
|
lines.append(f'{indent}\t<InternalInfo>')
|
|
for gt in generated_types:
|
|
new_tid = new_guid()
|
|
new_vid = new_guid()
|
|
lines.append(f'{indent}\t\t<xr:GeneratedType name="{gt["Name"]}" category="{gt["Category"]}">')
|
|
lines.append(f'{indent}\t\t\t<xr:TypeId>{new_tid}</xr:TypeId>')
|
|
lines.append(f'{indent}\t\t\t<xr:ValueId>{new_vid}</xr:ValueId>')
|
|
lines.append(f'{indent}\t\t</xr:GeneratedType>')
|
|
lines.append(f'{indent}\t</InternalInfo>')
|
|
else:
|
|
lines.append(f'{indent}\t<InternalInfo/>')
|
|
|
|
lines.append(f'{indent}\t<Properties>')
|
|
lines.append(f'{indent}\t\t<ObjectBelonging>Adopted</ObjectBelonging>')
|
|
lines.append(f'{indent}\t\t<Name>{ts_name}</Name>')
|
|
lines.append(f'{indent}\t\t<Comment/>')
|
|
lines.append(f'{indent}\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>')
|
|
lines.append(f'{indent}\t</Properties>')
|
|
|
|
# ChildObjects with all attributes
|
|
if child_attrs:
|
|
lines.append(f'{indent}\t<ChildObjects>')
|
|
for ca in child_attrs:
|
|
ca_xml = build_adopted_attribute_xml(ca["Name"], ca["Uuid"], ca["TypeXml"], f"{indent}\t\t")
|
|
lines.append(ca_xml)
|
|
lines.append(f'{indent}\t</ChildObjects>')
|
|
else:
|
|
lines.append(f'{indent}\t<ChildObjects/>')
|
|
|
|
lines.append(f'{indent}</TabularSection>')
|
|
return "\n".join(lines)
|
|
|
|
# --- 11f. Collect reference types from attribute Type XML strings ---
|
|
def collect_reference_types(type_xmls):
|
|
result = {}
|
|
for type_xml in type_xmls:
|
|
# cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc.
|
|
for m in re.finditer(r'cfg:(\w+)Ref\.(\w+)', type_xml):
|
|
ref_prefix = m.group(1)
|
|
obj_n = m.group(2)
|
|
key = f"{ref_prefix}.{obj_n}"
|
|
if key not in result:
|
|
result[key] = {"TypeName": ref_prefix, "ObjName": obj_n}
|
|
# cfg:DefinedType.XXX
|
|
for m in re.finditer(r'cfg:DefinedType\.(\w+)', type_xml):
|
|
dt_name = m.group(1)
|
|
key = f"DefinedType.{dt_name}"
|
|
if key not in result:
|
|
result[key] = {"TypeName": "DefinedType", "ObjName": dt_name}
|
|
return list(result.values())
|
|
|
|
# --- 11g. Merge adopted attributes into existing extension object XML ---
|
|
def merge_attributes_into_object(type_name, obj_name, attrs_to_add):
|
|
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"Cannot merge attributes: {obj_file} not found")
|
|
return
|
|
|
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
|
obj_content = fh.read()
|
|
|
|
# Collect existing attribute names for dedup (text-based)
|
|
existing_names = set()
|
|
for m in re.finditer(r'<Name>(\w+)</Name>', obj_content):
|
|
existing_names.add(m.group(1))
|
|
|
|
all_attr_xml = ""
|
|
added = 0
|
|
for attr in attrs_to_add:
|
|
if attr["Name"] in existing_names:
|
|
continue
|
|
all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
|
added += 1
|
|
|
|
if added > 0:
|
|
# Insert attributes — handle both <ChildObjects/> and <ChildObjects>...</ChildObjects>
|
|
if re.search(r'<ChildObjects\s*/>', obj_content):
|
|
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
|
|
else:
|
|
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
|
|
save_text_bom(obj_file, obj_content)
|
|
info(f" Merged {added} attribute(s) into: {obj_file}")
|
|
|
|
# --- 11h. Borrow main attribute orchestrator ---
|
|
def borrow_main_attribute(type_name, obj_name, form_name, mode):
|
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
|
info(f"Borrowing main attribute for {type_name}.{obj_name} (mode: {mode})...")
|
|
|
|
# Step 1: Collect DataPaths (Form mode) or take all (All mode)
|
|
first_level_names = None
|
|
deep_paths = []
|
|
if mode == "Form":
|
|
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)
|
|
dp = collect_form_data_paths(src_form_xml_path)
|
|
first_level_names = dp["FirstLevel"]
|
|
deep_paths = dp["DeepPaths"]
|
|
info(f" Collected {len(first_level_names)} first-level DataPath references, {len(deep_paths)} deep paths")
|
|
else:
|
|
info(" Mode All: borrowing all attributes and tabular sections")
|
|
|
|
# Step 2: Resolve source attributes
|
|
resolved = resolve_source_attributes(type_name, obj_name, first_level_names)
|
|
src_attrs = resolved["Attributes"]
|
|
src_ts = resolved["TabularSections"]
|
|
extra_props = resolved["ExtraProps"]
|
|
info(f" Resolved: {len(src_attrs)} attributes, {len(src_ts)} tabular section(s)")
|
|
|
|
# Identify which FirstLevel names are TabularSections (for deep path filtering)
|
|
ts_names = {ts["Name"]: True for ts in src_ts}
|
|
|
|
# Step 3: Build the adopted content and insert into main object XML
|
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
|
|
|
# Generate full object XML with attributes and TS
|
|
content_parts = []
|
|
for attr in src_attrs:
|
|
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
|
content_parts.append(attr_xml)
|
|
for ts in src_ts:
|
|
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
|
|
content_parts.append(ts_xml)
|
|
adopted_content = "\n".join(content_parts).rstrip()
|
|
|
|
# Read existing object XML and inject
|
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
|
obj_content = fh.read()
|
|
|
|
# Inject extra properties after ExtendedConfigurationObject
|
|
if extra_props:
|
|
props_xml = ""
|
|
for p_name, p_val in extra_props.items():
|
|
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
|
|
|
# Replace empty ChildObjects with adopted content
|
|
if adopted_content:
|
|
# Handle <ChildObjects/> (self-closing)
|
|
if re.search(r'<ChildObjects\s*/>', obj_content):
|
|
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>\r\n{adopted_content}\r\n\t\t</ChildObjects>", obj_content)
|
|
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
|
|
elif re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content):
|
|
m = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
|
|
existing_inner = m.group(1)
|
|
obj_content = obj_content.replace(
|
|
f"<ChildObjects>{existing_inner}</ChildObjects>",
|
|
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
|
|
)
|
|
|
|
save_text_bom(obj_file, obj_content)
|
|
info(f" Enriched object: {obj_file}")
|
|
|
|
# Step 4: Collect all reference types and borrow as shells
|
|
all_type_xmls = []
|
|
for a in src_attrs:
|
|
all_type_xmls.append(a["TypeXml"])
|
|
for ts in src_ts:
|
|
for tsa in ts["Attributes"]:
|
|
all_type_xmls.append(tsa["TypeXml"])
|
|
ref_types = collect_reference_types(all_type_xmls)
|
|
info(f" Reference types to borrow: {len(ref_types)}")
|
|
|
|
for rt in ref_types:
|
|
if rt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
|
warn(f" Unknown reference type: {rt['TypeName']}.{rt['ObjName']}")
|
|
continue
|
|
if test_object_borrowed(rt["TypeName"], rt["ObjName"]):
|
|
info(f" Already borrowed: {rt['TypeName']}.{rt['ObjName']}")
|
|
continue
|
|
rt_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]], f"{rt['ObjName']}.xml")
|
|
if not os.path.isfile(rt_src_file):
|
|
warn(f" Source not found: {rt['TypeName']}.{rt['ObjName']}")
|
|
continue
|
|
src = read_source_object(rt["TypeName"], rt["ObjName"])
|
|
borrowed_xml = build_borrowed_object_xml(rt["TypeName"], rt["ObjName"], src["Uuid"], src["Properties"])
|
|
target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]])
|
|
os.makedirs(target_dir, exist_ok=True)
|
|
target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml")
|
|
save_text_bom(target_file, borrowed_xml)
|
|
add_to_child_objects(rt["TypeName"], rt["ObjName"])
|
|
borrowed_files.append(target_file)
|
|
info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}")
|
|
|
|
# Step 5: Handle deep paths (Form mode only)
|
|
if mode == "Form" and deep_paths:
|
|
# Filter out deep paths where ObjectAttr is a TabularSection
|
|
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
|
|
|
if real_deep:
|
|
info(f" Processing {len(real_deep)} deep path(s)...")
|
|
|
|
# Group by ObjectAttr -> target catalog
|
|
deep_by_attr = {}
|
|
for dp in real_deep:
|
|
if dp["ObjectAttr"] not in deep_by_attr:
|
|
deep_by_attr[dp["ObjectAttr"]] = []
|
|
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
|
|
|
for attr_name, sub_attr_names in deep_by_attr.items():
|
|
# Find the attribute's type to determine target catalog
|
|
attr_info = None
|
|
for a in src_attrs:
|
|
if a["Name"] == attr_name:
|
|
attr_info = a
|
|
break
|
|
if not attr_info:
|
|
continue
|
|
|
|
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
|
if not cat_match:
|
|
continue
|
|
|
|
target_type_name = cat_match.group(1)
|
|
target_obj_name = cat_match.group(2)
|
|
|
|
# Ensure target is borrowed
|
|
if not test_object_borrowed(target_type_name, target_obj_name):
|
|
t_src = read_source_object(target_type_name, target_obj_name)
|
|
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
|
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
|
os.makedirs(t_target_dir, exist_ok=True)
|
|
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
|
save_text_bom(t_target_file, t_borrowed_xml)
|
|
add_to_child_objects(target_type_name, target_obj_name)
|
|
borrowed_files.append(t_target_file)
|
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
|
|
|
# Resolve sub-attributes in target catalog
|
|
sub_names = {sn: True for sn in sub_attr_names}
|
|
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
|
|
|
if sub_resolved["Attributes"]:
|
|
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
|
|
|
# Collect and borrow ref types from deep attributes
|
|
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
|
sub_ref_types = collect_reference_types(sub_type_xmls)
|
|
for srt in sub_ref_types:
|
|
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
|
continue
|
|
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
|
continue
|
|
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
|
if not os.path.isfile(s_src_file):
|
|
continue
|
|
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
|
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
|
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
|
os.makedirs(s_target_dir, exist_ok=True)
|
|
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
|
save_text_bom(s_target_file, s_borrowed_xml)
|
|
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
|
borrowed_files.append(s_target_file)
|
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
|
|
|
info(" Main attribute borrowing complete")
|
|
|
|
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
|
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="{format_version}">',
|
|
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", format_version)
|
|
|
|
src_auto_cmd = None
|
|
form_props = []
|
|
reached_visual = False
|
|
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:
|
|
reached_visual = True
|
|
src_auto_cmd = fc
|
|
continue
|
|
if ln in ("ChildItems", "Events", "Attributes", "Commands", "Parameters", "CommandSet"):
|
|
reached_visual = True
|
|
continue
|
|
if not reached_visual:
|
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
|
form_props.append(etree.tostring(fc, encoding="unicode"))
|
|
|
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
|
|
|
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
|
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>')
|
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
|
# Strip DataPath in AutoCommandBar buttons
|
|
if borrow_main_attr:
|
|
# Keep only Объект.* DataPaths
|
|
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
|
|
else:
|
|
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
|
|
|
|
# ChildItems: copy full tree, clean up base-config references
|
|
child_items_xml = ""
|
|
src_child_items = None
|
|
for fc in src_form_el:
|
|
if isinstance(fc.tag, str) and localname(fc) == "ChildItems":
|
|
src_child_items = fc
|
|
break
|
|
|
|
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)
|
|
# Replace all CommandName values with 0
|
|
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
|
# Strip DataPath / TitleDataPath / RowPictureDataPath
|
|
if borrow_main_attr:
|
|
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
|
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
|
|
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
|
|
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
else:
|
|
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
|
|
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
|
|
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
|
child_items_xml = re.sub(r'\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '', child_items_xml, flags=re.DOTALL)
|
|
# Strip element-level Events
|
|
child_items_xml = re.sub(r'\s*<Events>.*?</Events>', '', child_items_xml, flags=re.DOTALL)
|
|
|
|
# Collect CommonPicture references from ChildItems and AutoCommandBar
|
|
referenced_pictures = {}
|
|
for name in re.findall(r'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', child_items_xml):
|
|
referenced_pictures[name] = True
|
|
if auto_cmd_xml:
|
|
for name in re.findall(r'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', auto_cmd_xml):
|
|
referenced_pictures[name] = True
|
|
|
|
# Auto-borrow referenced CommonPictures
|
|
auto_borrowed_pics = []
|
|
for pic_name in referenced_pictures:
|
|
if not test_object_borrowed("CommonPicture", pic_name):
|
|
pic_src_file = os.path.join(cfg_dir, "CommonPictures", f"{pic_name}.xml")
|
|
if os.path.isfile(pic_src_file):
|
|
src = read_source_object("CommonPicture", pic_name)
|
|
borrowed_xml = build_borrowed_object_xml("CommonPicture", pic_name, src["Uuid"], src["Properties"])
|
|
target_dir = os.path.join(ext_dir, "CommonPictures")
|
|
os.makedirs(target_dir, exist_ok=True)
|
|
target_file = os.path.join(target_dir, f"{pic_name}.xml")
|
|
save_text_bom(target_file, borrowed_xml)
|
|
add_to_child_objects("CommonPicture", pic_name)
|
|
auto_borrowed_pics.append(pic_name)
|
|
borrowed_files.append(target_file)
|
|
info(f" Auto-borrowed: CommonPicture.{pic_name}")
|
|
else:
|
|
warn(f" CommonPicture.{pic_name} not found in source config — will strip from form")
|
|
|
|
# Collect all borrowed CommonPictures for Picture stripping
|
|
borrowed_pic_set = set()
|
|
for co_child in child_objs_el:
|
|
if isinstance(co_child.tag, str) and localname(co_child) == "CommonPicture":
|
|
borrowed_pic_set.add((co_child.text or "").strip())
|
|
|
|
# Strip <Picture> blocks referencing non-borrowed CommonPictures (reverse order)
|
|
pic_block_pattern = re.compile(r'\s*<Picture>\s*<xr:Ref>CommonPicture\.(\w+)</xr:Ref>.*?</Picture>', re.DOTALL)
|
|
pic_matches = list(pic_block_pattern.finditer(child_items_xml))
|
|
for pm in reversed(pic_matches):
|
|
cp_name = pm.group(1)
|
|
if cp_name not in borrowed_pic_set:
|
|
child_items_xml = child_items_xml[:pm.start()] + child_items_xml[pm.end():]
|
|
# Strip StdPicture blocks (except Print)
|
|
child_items_xml = re.sub(r'\s*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '', child_items_xml, flags=re.DOTALL)
|
|
|
|
# Same Picture strip for AutoCommandBar
|
|
if auto_cmd_xml:
|
|
ac_pic_matches = list(pic_block_pattern.finditer(auto_cmd_xml))
|
|
for pm in reversed(ac_pic_matches):
|
|
cp_name = pm.group(1)
|
|
if cp_name not in borrowed_pic_set:
|
|
auto_cmd_xml = auto_cmd_xml[:pm.start()] + auto_cmd_xml[pm.end():]
|
|
auto_cmd_xml = re.sub(r'\s*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '', auto_cmd_xml, flags=re.DOTALL)
|
|
|
|
# Auto-borrow StyleItems referenced in ChildItems
|
|
referenced_styles = set()
|
|
for m in re.finditer(r'ref="style:(\w+)"[^>]*kind="StyleItem"', child_items_xml):
|
|
referenced_styles.add(m.group(1))
|
|
for m in re.finditer(r'>style:(\w+)</\w+>', child_items_xml):
|
|
referenced_styles.add(m.group(1))
|
|
|
|
for style_name in referenced_styles:
|
|
if not test_object_borrowed("StyleItem", style_name):
|
|
style_src_file = os.path.join(cfg_dir, "StyleItems", f"{style_name}.xml")
|
|
if os.path.isfile(style_src_file):
|
|
src = read_source_object("StyleItem", style_name)
|
|
borrowed_xml = build_borrowed_object_xml("StyleItem", style_name, src["Uuid"], src["Properties"])
|
|
target_dir = os.path.join(ext_dir, "StyleItems")
|
|
os.makedirs(target_dir, exist_ok=True)
|
|
target_file = os.path.join(target_dir, f"{style_name}.xml")
|
|
save_text_bom(target_file, borrowed_xml)
|
|
add_to_child_objects("StyleItem", style_name)
|
|
borrowed_files.append(target_file)
|
|
info(f" Auto-borrowed: StyleItem.{style_name}")
|
|
else:
|
|
warn(f" StyleItem.{style_name} not found in source config")
|
|
|
|
# Auto-borrow Enums + EnumValues referenced via DesignTimeRef
|
|
referenced_enum_values = {} # enum_name -> set of value_names
|
|
for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', child_items_xml):
|
|
e_name, ev_name = m.group(1), m.group(2)
|
|
if e_name not in referenced_enum_values:
|
|
referenced_enum_values[e_name] = set()
|
|
referenced_enum_values[e_name].add(ev_name)
|
|
|
|
for enum_name, needed_values in referenced_enum_values.items():
|
|
if not test_object_borrowed("Enum", enum_name):
|
|
enum_src_file = os.path.join(cfg_dir, "Enums", f"{enum_name}.xml")
|
|
if os.path.isfile(enum_src_file):
|
|
# Read source Enum to find EnumValue UUIDs
|
|
src_enum_tree = etree.parse(enum_src_file, etree.XMLParser(remove_blank_text=False))
|
|
src_enum_root = src_enum_tree.getroot()
|
|
src_enum_el = None
|
|
for cn in src_enum_root:
|
|
if isinstance(cn.tag, str):
|
|
src_enum_el = cn
|
|
break
|
|
|
|
# Find needed EnumValues
|
|
ev_xmls = []
|
|
for ev_node in src_enum_el.iter():
|
|
if isinstance(ev_node.tag, str) and localname(ev_node) == "EnumValue":
|
|
ev_uuid = ev_node.get("uuid", "")
|
|
name_el = None
|
|
for props in ev_node:
|
|
if isinstance(props.tag, str) and localname(props) == "Properties":
|
|
for prop in props:
|
|
if isinstance(prop.tag, str) and localname(prop) == "Name":
|
|
name_el = prop
|
|
break
|
|
if name_el is not None and (name_el.text or "").strip() in needed_values:
|
|
new_ev_uuid = str(uuid.uuid4())
|
|
ev_xmls.append(
|
|
f'\t\t\t<EnumValue uuid="{new_ev_uuid}">\n'
|
|
f'\t\t\t\t<InternalInfo/>\n'
|
|
f'\t\t\t\t<Properties>\n'
|
|
f'\t\t\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>\n'
|
|
f'\t\t\t\t\t<Name>{name_el.text.strip()}</Name>\n'
|
|
f'\t\t\t\t\t<Comment/>\n'
|
|
f'\t\t\t\t\t<ExtendedConfigurationObject>{ev_uuid}</ExtendedConfigurationObject>\n'
|
|
f'\t\t\t\t</Properties>\n'
|
|
f'\t\t\t</EnumValue>'
|
|
)
|
|
|
|
# Build borrowed Enum with EnumValues
|
|
src_obj = read_source_object("Enum", enum_name)
|
|
borrowed_xml = build_borrowed_object_xml("Enum", enum_name, src_obj["Uuid"], src_obj["Properties"])
|
|
if ev_xmls:
|
|
ev_block = "\n".join(ev_xmls)
|
|
borrowed_xml = borrowed_xml.replace("<ChildObjects/>", f"<ChildObjects>\n{ev_block}\n\t\t</ChildObjects>")
|
|
|
|
target_dir = os.path.join(ext_dir, "Enums")
|
|
os.makedirs(target_dir, exist_ok=True)
|
|
target_file = os.path.join(target_dir, f"{enum_name}.xml")
|
|
save_text_bom(target_file, borrowed_xml)
|
|
add_to_child_objects("Enum", enum_name)
|
|
borrowed_files.append(target_file)
|
|
info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))")
|
|
else:
|
|
warn(f" Enum.{enum_name} not found in source config")
|
|
|
|
# Extract the <Form ...> opening tag from source text
|
|
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")
|
|
|
|
# Part 1: form properties + AutoCommandBar + ChildItems
|
|
for prop_xml in form_props:
|
|
prop_xml_clean = ns_strip_pattern.sub("", prop_xml)
|
|
parts.append(f"\t{prop_xml_clean}\r\n")
|
|
if auto_cmd_xml:
|
|
parts.append(f"\t{auto_cmd_xml}\r\n")
|
|
if child_items_xml:
|
|
parts.append(f"\t{child_items_xml}\r\n")
|
|
|
|
# Attributes: empty or with MainAttribute when borrow_main_attr
|
|
if borrow_main_attr:
|
|
obj_type_prefix = ""
|
|
gt_list = GENERATED_TYPES.get(type_name, [])
|
|
for g in gt_list:
|
|
if g["category"] == "Object":
|
|
obj_type_prefix = g["prefix"]
|
|
break
|
|
main_attr_type = f"cfg:{obj_type_prefix}.{obj_name}"
|
|
parts.append("\t<Attributes>\r\n")
|
|
parts.append('\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1000001">\r\n')
|
|
parts.append(f"\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
|
|
parts.append("\t\t\t<MainAttribute>true</MainAttribute>\r\n")
|
|
parts.append("\t\t\t<SavedData>true</SavedData>\r\n")
|
|
parts.append("\t\t</Attribute>\r\n")
|
|
parts.append("\t</Attributes>")
|
|
else:
|
|
parts.append("\t<Attributes/>")
|
|
parts.append("\r\n")
|
|
|
|
# BaseForm: same content, indented one more level
|
|
parts.append(f'\t<BaseForm version="{form_version}">\r\n')
|
|
|
|
for prop_xml in form_props:
|
|
prop_xml_clean = ns_strip_pattern.sub("", prop_xml)
|
|
parts.append(f"\t\t{prop_xml_clean}\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")
|
|
if child_items_xml:
|
|
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")
|
|
|
|
# BaseForm Attributes: same as main section
|
|
if borrow_main_attr:
|
|
parts.append("\t\t<Attributes>\r\n")
|
|
parts.append('\t\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1000001">\r\n')
|
|
parts.append(f"\t\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
|
|
parts.append("\t\t\t\t<MainAttribute>true</MainAttribute>\r\n")
|
|
parts.append("\t\t\t\t<SavedData>true</SavedData>\r\n")
|
|
parts.append("\t\t\t</Attribute>\r\n")
|
|
parts.append("\t\t</Attributes>")
|
|
else:
|
|
parts.append("\t\t<Attributes/>")
|
|
parts.append("\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)
|
|
|
|
# --- 9b. Validate -BorrowMainAttribute ---
|
|
borrow_main_attribute_mode = args.BorrowMainAttribute
|
|
if borrow_main_attribute_mode is not None:
|
|
if borrow_main_attribute_mode not in ("Form", "All"):
|
|
print("-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)", file=sys.stderr)
|
|
sys.exit(1)
|
|
# Validate: only with .Form. pattern
|
|
has_form = any(".Form." in item for item in items)
|
|
if not has_form:
|
|
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# --- 10. Process each item ---
|
|
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)
|
|
|
|
has_bma = borrow_main_attribute_mode is not None
|
|
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
|
|
borrowed_files.extend(form_files)
|
|
borrowed_count += 1
|
|
|
|
# Borrow main attribute if requested
|
|
if has_bma:
|
|
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
|
|
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()
|