#!/usr/bin/env python3 # form-add v1.5 — Add managed form to 1C config object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os import re import sys import uuid from lxml import etree NSMAP = { "md": "http://v8.1c.ru/8.3/MDClasses", "v8": "http://v8.1c.ru/8.1/data/core", } def 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']+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 save_xml_with_bom(tree, path): """Save XML tree to file with UTF-8 BOM.""" xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") xml_bytes = xml_bytes.replace(b"", b'') 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 write_text_with_bom(path, text): """Write text to file with UTF-8 BOM.""" with open(path, "w", encoding="utf-8-sig") as f: f.write(text) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Add managed form to 1C config object", allow_abbrev=False) parser.add_argument("-ObjectPath", required=True) parser.add_argument("-FormName", required=True) parser.add_argument("-Synonym", default=None) parser.add_argument("-Purpose", default="Object") parser.add_argument("-SetDefault", action="store_true") args = parser.parse_args() object_path = args.ObjectPath form_name = args.FormName synonym = args.Synonym if args.Synonym is not None else form_name purpose = args.Purpose set_default = args.SetDefault # --- Phase 1: Determine object type --- # Resolve ObjectPath (directory → .xml) 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.rstrip("/\\")) candidate = os.path.join(object_path, dir_name + ".xml") sibling = os.path.join(os.path.dirname(object_path.rstrip("/\\")), dir_name + ".xml") if os.path.isfile(candidate): object_path = candidate elif os.path.isfile(sibling): object_path = sibling if not os.path.isfile(object_path): print(f"Файл объекта не найден: {object_path}", file=sys.stderr) sys.exit(1) object_xml_full = os.path.abspath(object_path) format_version = detect_format_version(os.path.dirname(object_xml_full)) parser_xml = etree.XMLParser(remove_blank_text=False) tree = etree.parse(object_xml_full, parser_xml) root = tree.getroot() supported_types = [ "Document", "Catalog", "DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task", ] object_type = None object_node = None for t in supported_types: node = root.find(f".//md:{t}", NSMAP) if node is not None: object_type = t object_node = node break if object_type is None: print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr) sys.exit(1) # Object name from Properties/Name name_node = root.find(f".//md:{object_type}/md:Properties/md:Name", NSMAP) if name_node is None or not name_node.text: print("Не удалось определить имя объекта из Properties/Name", file=sys.stderr) sys.exit(1) object_name = name_node.text print() print("=== form-add ===") print() print(f"Object: {object_type}.{object_name}") # --- Phase 2: Validate Purpose --- # Normalize: capitalize first letter, lowercase rest purpose = purpose[0].upper() + purpose[1:].lower() valid_purposes = ["Object", "List", "Choice", "Record"] if purpose not in valid_purposes: print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr) sys.exit(1) object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task"] processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"] if purpose == "List": if object_type == "DataProcessor": print("Purpose=List недопустим для DataProcessor", file=sys.stderr) sys.exit(1) elif purpose == "Choice": if object_type in processor_like_types or object_type == "InformationRegister": print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr) sys.exit(1) elif purpose == "Record": if object_type != "InformationRegister": print("Purpose=Record допустим только для InformationRegister", file=sys.stderr) sys.exit(1) # --- Phase 3: Create files --- object_dir = os.path.splitext(object_xml_full)[0] forms_dir = os.path.join(object_dir, "Forms") form_meta_path = os.path.join(forms_dir, f"{form_name}.xml") if os.path.exists(form_meta_path): print(f"Форма уже существует: {form_meta_path}", file=sys.stderr) sys.exit(1) form_dir = os.path.join(forms_dir, form_name) form_ext_dir = os.path.join(form_dir, "Ext") form_module_dir = os.path.join(form_ext_dir, "Form") os.makedirs(form_module_dir, exist_ok=True) # --- 3a. Form metadata --- form_uuid = str(uuid.uuid4()) form_meta_xml = ( '\n' '\n' f'\t
\n' '\t\t\n' f'\t\t\t{form_name}\n' '\t\t\t\n' '\t\t\t\t\n' '\t\t\t\t\tru\n' f'\t\t\t\t\t{synonym}\n' '\t\t\t\t\n' '\t\t\t\n' '\t\t\t\n' '\t\t\tManaged\n' '\t\t\tfalse\n' '\t\t\t\n' '\t\t\t\tPlatformApplication\n' '\t\t\t\tMobilePlatformApplication\n' '\t\t\t\n' + ('\t\t\t\n' if object_type in processor_like_types else '') + '\t\t\n' '\t
\n' '
' ) write_text_with_bom(form_meta_path, form_meta_xml) # --- 3b. Form.xml --- form_xml_path = os.path.join(form_ext_dir, "Form.xml") form_ns_decl = ( 'xmlns="http://v8.1c.ru/8.3/xcf/logform"' ' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"' ' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"' ' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"' ' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"' ' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"' ' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"' ' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"' ' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"' ' xmlns:v8="http://v8.1c.ru/8.1/data/core"' ' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"' ' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"' ' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"' ' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"' ' xmlns:xs="http://www.w3.org/2001/XMLSchema"' ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' ) if purpose in ("List", "Choice"): # Dynamic list main_table = f"{object_type}.{object_name}" form_xml = ( f'\n' f'
\n' '\t\n' '\t\ttrue\n' '\t\n' '\t\n' '\t\n' '\t\t\n' '\t\t\t\n' '\t\t\t\tcfg:DynamicList\n' '\t\t\t\n' '\t\t\ttrue\n' '\t\t\t\n' f'\t\t\t\t{main_table}\n' '\t\t\t\n' '\t\t\n' '\t\n' '' ) elif purpose == "Record": # Information register record main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c" main_attr_type = f"InformationRegisterRecordManager.{object_name}" form_xml = ( f'\n' f'
\n' '\t\n' '\t\ttrue\n' '\t\n' '\t\n' '\t\n' f'\t\t\n' '\t\t\t\n' f'\t\t\t\tcfg:{main_attr_type}\n' '\t\t\t\n' '\t\t\ttrue\n' '\t\t\ttrue\n' '\t\t\n' '\t\n' '' ) else: # Object — object form main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442" attr_type_map = { "Document": "DocumentObject", "Catalog": "CatalogObject", "DataProcessor": "DataProcessorObject", "Report": "ReportObject", "ExternalDataProcessor": "ExternalDataProcessorObject", "ExternalReport": "ExternalReportObject", "ChartOfAccounts": "ChartOfAccountsObject", "ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject", "ExchangePlan": "ExchangePlanObject", "BusinessProcess": "BusinessProcessObject", "Task": "TaskObject", "InformationRegister": "InformationRegisterRecordManager", "AccumulationRegister": "AccumulationRegisterRecordSet", } main_attr_type = f"{attr_type_map[object_type]}.{object_name}" # SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*) saved_data_line = '' if object_type not in processor_like_types: saved_data_line = '\t\t\ttrue\n' form_xml = ( f'\n' f'
\n' '\t\n' '\t\ttrue\n' '\t\n' '\t\n' '\t\n' f'\t\t\n' '\t\t\t\n' f'\t\t\t\tcfg:{main_attr_type}\n' '\t\t\t\n' '\t\t\ttrue\n' f'{saved_data_line}' '\t\t\n' '\t\n' '' ) if os.path.exists(form_xml_path): print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting") else: write_text_with_bom(form_xml_path, form_xml) # --- 3c. Module.bsl --- module_path = os.path.join(form_module_dir, "Module.bsl") module_bsl = ( '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n' '\n' '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' '\n' '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n' '\n' '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' '\n' '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n' '\n' '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' '\n' '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n' '\n' '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' '\n' '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n' '\n' '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438' ) if os.path.exists(module_path): print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting") else: write_text_with_bom(module_path, module_bsl) # --- Phase 4: Register in parent object --- ns = "http://v8.1c.ru/8.3/MDClasses" child_objects = root.find(f".//md:{object_type}/md:ChildObjects", NSMAP) if child_objects is None: print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr) sys.exit(1) # Add
$FormName
form_elem = etree.Element(f"{{{ns}}}Form") form_elem.text = form_name # Find first