fix(python): add stdout UTF-8 encoding for Windows compatibility

Python on Windows defaults to cp1251 for piped stdout, which cannot
handle Unicode box-drawing characters used in info/analysis output.
Added sys.stdout.reconfigure(encoding="utf-8") to all 59 Python scripts.

Tested on real config data: epf-init, epf-validate, cf-info, cf-validate,
meta-info, form-info, role-info, skd-info, subsystem-info — all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-25 16:29:26 +03:00
parent 86a959a354
commit d6abb2b651
59 changed files with 31076 additions and 31006 deletions
File diff suppressed because it is too large Load Diff
+401 -399
View File
@@ -1,399 +1,401 @@
#!/usr/bin/env python3
# cf-info v1.0 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from collections import OrderedDict
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve path ---
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
# Directory -> find Configuration.xml
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.isfile(candidate):
config_path = candidate
else:
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(config_path):
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(config_path, etree.XMLParser(remove_blank_text=False))
xml_root = tree.getroot()
NS = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xs": "http://www.w3.org/2001/XMLSchema",
"app": "http://v8.1c.ru/8.2/managed-application/core",
}
md_root = xml_root # root is MetaDataObject itself
if etree.QName(md_root.tag).localname != "MetaDataObject":
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
sys.exit(1)
cfg_node = md_root.find("md:Configuration", NS)
if cfg_node is None:
print("[ERROR] No <Configuration> element found", file=sys.stderr)
sys.exit(1)
version = md_root.get("version", "")
props_node = cfg_node.find("md:Properties", NS)
child_obj_node = cfg_node.find("md:ChildObjects", NS)
# --- Helpers ---
def get_ml_text(node):
if node is None:
return ""
item = node.find("v8:item/v8:content", NS)
if item is not None and item.text:
return item.text
return ""
def get_prop_text(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
if n is not None and n.text:
return n.text
return ""
def get_prop_ml(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
return get_ml_text(n)
# --- Type name maps (canonical order, 44 types) ---
type_order = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
]
type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
"SettingsStorage": "Хранилища настроек", "FunctionalOption": "Функциональные опции",
"FunctionalOptionsParameter": "Параметры ФО", "DefinedType": "Определяемые типы",
"CommonCommand": "Общие команды", "CommandGroup": "Группы команд", "Constant": "Константы",
"CommonForm": "Общие формы", "Catalog": "Справочники", "Document": "Документы",
"DocumentNumerator": "Нумераторы", "Sequence": "Последовательности", "DocumentJournal": "Журналы документов",
"Enum": "Перечисления", "Report": "Отчёты", "DataProcessor": "Обработки",
"InformationRegister": "Регистры сведений", "AccumulationRegister": "Регистры накопления",
"ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов",
"AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР",
"CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы",
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
}
# --- Count objects in ChildObjects ---
object_counts = OrderedDict()
total_objects = 0
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue # skip comments/PIs
type_name = etree.QName(child.tag).localname
if type_name not in object_counts:
object_counts[type_name] = 0
object_counts[type_name] += 1
total_objects += 1
# --- Read key properties ---
cfg_name = get_prop_text("Name")
cfg_synonym = get_prop_ml("Synonym")
cfg_version = get_prop_text("Version")
cfg_vendor = get_prop_text("Vendor")
cfg_compat = get_prop_text("CompatibilityMode")
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
cfg_default_run = get_prop_text("DefaultRunMode")
cfg_script = get_prop_text("ScriptVariant")
cfg_default_lang = get_prop_text("DefaultLanguage")
cfg_data_lock = get_prop_text("DataLockControlMode")
dash = "\u2014"
cfg_modality = get_prop_text("ModalityUseMode")
cfg_intf_compat = get_prop_text("InterfaceCompatibilityMode")
cfg_auto_num = get_prop_text("ObjectAutonumerationMode")
cfg_sync_calls = get_prop_text("SynchronousPlatformExtensionAndAddInCallUseMode")
cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode ---
if args.Mode == "brief":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
compat_part = f" | {cfg_compat}" if cfg_compat else ""
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
# --- OVERVIEW mode ---
if args.Mode == "overview":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
out()
# Key properties
out(f"Формат: {version}")
if cfg_vendor:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
out(f"Совместимость: {cfg_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
out(f"Язык: {cfg_default_lang}")
out(f"Блокировки: {cfg_data_lock}")
out(f"Модальность: {cfg_modality}")
out(f"Интерфейс: {cfg_intf_compat}")
out()
# Object counts table
out(f"--- Состав ({total_objects} объектов) ---")
out()
max_type_len = 0
for type_name in type_order:
if type_name in object_counts:
ru_name = type_ru_names.get(type_name, type_name)
if len(ru_name) > max_type_len:
max_type_len = len(ru_name)
if max_type_len < 10:
max_type_len = 10
for type_name in type_order:
if type_name in object_counts:
count = object_counts[type_name]
ru_name = type_ru_names.get(type_name, type_name)
padded = ru_name.ljust(max_type_len)
out(f" {padded} {count}")
# --- FULL mode ---
if args.Mode == "full":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
out()
# --- Section: Identification ---
out("--- Идентификация ---")
out(f"UUID: {cfg_node.get('uuid', '')}")
out(f"Имя: {cfg_name}")
if cfg_synonym:
out(f"Синоним: {cfg_synonym}")
cfg_comment = get_prop_text("Comment")
if cfg_comment:
out(f"Комментарий: {cfg_comment}")
cfg_prefix = get_prop_text("NamePrefix")
if cfg_prefix:
out(f"Префикс: {cfg_prefix}")
if cfg_vendor:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
if cfg_update_addr:
out(f"Каталог обн.: {cfg_update_addr}")
out()
# --- Section: Modes ---
out("--- Режимы работы ---")
out(f"Формат: {version}")
out(f"Совместимость: {cfg_compat}")
out(f"Совм. расширений: {cfg_ext_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
out(f"Блокировки: {cfg_data_lock}")
out(f"Автонумерация: {cfg_auto_num}")
out(f"Модальность: {cfg_modality}")
out(f"Синхр. вызовы: {cfg_sync_calls}")
out(f"Интерфейс: {cfg_intf_compat}")
out(f"Табл. пространства: {cfg_db_spaces}")
out(f"Режим окна: {cfg_window_mode}")
out()
# --- Section: Language, roles, purposes ---
out("--- Назначение ---")
out(f"Язык по умолч.: {cfg_default_lang}")
# UsePurposes
purpose_node = props_node.find("md:UsePurposes", NS)
if purpose_node is not None:
purposes = []
for val in purpose_node.findall("v8:Value", NS):
if val.text:
purposes.append(val.text)
if purposes:
out(f"Назначения: {', '.join(purposes)}")
# DefaultRoles
roles_node = props_node.find("md:DefaultRoles", NS)
if roles_node is not None:
roles = []
for item in roles_node.findall("xr:Item", NS):
if item.text:
roles.append(item.text)
if roles:
out(f"Роли по умолч.: {len(roles)}")
for r in roles:
out(f" - {r}")
# Booleans
use_mf = get_prop_text("UseManagedFormInOrdinaryApplication")
use_of = get_prop_text("UseOrdinaryFormInManagedApplication")
out(f"Управл.формы в обычн.: {use_mf}")
out(f"Обычн.формы в управл.: {use_of}")
out()
# --- Section: Storages & default forms ---
out("--- Хранилища и формы по умолчанию ---")
storage_props = [
"CommonSettingsStorage", "ReportsUserSettingsStorage", "ReportsVariantsStorage",
"FormDataSettingsStorage", "DynamicListsUserSettingsStorage", "URLExternalDataStorage",
]
for sp in storage_props:
val = get_prop_text(sp)
if val:
out(f" {sp}: {val}")
form_props = [
"DefaultReportForm", "DefaultReportVariantForm", "DefaultReportSettingsForm",
"DefaultReportAppearanceTemplate", "DefaultDynamicListSettingsForm", "DefaultSearchForm",
"DefaultDataHistoryChangeHistoryForm", "DefaultDataHistoryVersionDataForm",
"DefaultDataHistoryVersionDifferencesForm", "DefaultCollaborationSystemUsersChoiceForm",
"DefaultConstantsForm", "DefaultInterface", "DefaultStyle",
]
for fp in form_props:
val = get_prop_text(fp)
if val:
out(f" {fp}: {val}")
out()
# --- Section: Info ---
cfg_brief = get_prop_ml("BriefInformation")
cfg_detail = get_prop_ml("DetailedInformation")
cfg_copyright = get_prop_ml("Copyright")
cfg_vendor_addr = get_prop_ml("VendorInformationAddress")
cfg_info_addr = get_prop_ml("ConfigurationInformationAddress")
if cfg_brief or cfg_detail or cfg_copyright or cfg_vendor_addr or cfg_info_addr:
out("--- Информация ---")
if cfg_brief:
out(f"Краткая: {cfg_brief}")
if cfg_detail:
out(f"Подробная: {cfg_detail}")
if cfg_copyright:
out(f"Copyright: {cfg_copyright}")
if cfg_vendor_addr:
out(f"Сайт поставщика: {cfg_vendor_addr}")
if cfg_info_addr:
out(f"Адрес информ.: {cfg_info_addr}")
out()
# --- Section: Mobile functionalities ---
mobile_func = props_node.find("md:UsedMobileApplicationFunctionalities", NS)
if mobile_func is not None:
enabled_funcs = []
disabled_funcs = []
for func in mobile_func.findall("app:functionality", NS):
f_name = func.find("app:functionality", NS)
f_use = func.find("app:use", NS)
if f_name is not None and f_use is not None:
if f_use.text == "true":
enabled_funcs.append(f_name.text or "")
else:
disabled_funcs.append(f_name.text or "")
total_func = len(enabled_funcs) + len(disabled_funcs)
out(f"--- Мобильные функциональности ({total_func}, включено: {len(enabled_funcs)}) ---")
for f in enabled_funcs:
out(f" [+] {f}")
for f in disabled_funcs:
out(f" [-] {f}")
out()
# --- Section: InternalInfo ---
internal_info = cfg_node.find("md:InternalInfo", NS)
if internal_info is not None:
contained = internal_info.findall("xr:ContainedObject", NS)
out(f"--- InternalInfo ({len(contained)} ContainedObject) ---")
for co in contained:
class_id_node = co.find("xr:ClassId", NS)
object_id_node = co.find("xr:ObjectId", NS)
class_id = class_id_node.text if class_id_node is not None else ""
object_id = object_id_node.text if object_id_node is not None else ""
out(f" {class_id} -> {object_id}")
out()
# --- Section: ChildObjects (full list) ---
out(f"--- Состав ({total_objects} объектов) ---")
out()
for type_name in type_order:
if type_name not in object_counts:
continue
count = object_counts[type_name]
ru_name = type_ru_names.get(type_name, type_name)
out(f" {ru_name} ({type_name}): {count}")
# Collect names for this type
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == type_name:
out(f" {child.text or ''}")
# --- Pagination and output ---
total = len(lines_buf)
if args.Offset > 0 or args.Limit < total:
start = min(args.Offset, total)
end = min(start + args.Limit, total)
page = lines_buf[start:end]
result = "\n".join(page)
if end < total:
result += f"\n\n... ({end} of {total} lines, use -Offset {end} to continue)"
else:
result = "\n".join(lines_buf)
print(result)
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write(result)
print(f"\nWritten to: {out_file}")
#!/usr/bin/env python3
# cf-info v1.0 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from collections import OrderedDict
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve path ---
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
# Directory -> find Configuration.xml
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.isfile(candidate):
config_path = candidate
else:
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(config_path):
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(config_path, etree.XMLParser(remove_blank_text=False))
xml_root = tree.getroot()
NS = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
"xr": "http://v8.1c.ru/8.3/xcf/readable",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xs": "http://www.w3.org/2001/XMLSchema",
"app": "http://v8.1c.ru/8.2/managed-application/core",
}
md_root = xml_root # root is MetaDataObject itself
if etree.QName(md_root.tag).localname != "MetaDataObject":
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
sys.exit(1)
cfg_node = md_root.find("md:Configuration", NS)
if cfg_node is None:
print("[ERROR] No <Configuration> element found", file=sys.stderr)
sys.exit(1)
version = md_root.get("version", "")
props_node = cfg_node.find("md:Properties", NS)
child_obj_node = cfg_node.find("md:ChildObjects", NS)
# --- Helpers ---
def get_ml_text(node):
if node is None:
return ""
item = node.find("v8:item/v8:content", NS)
if item is not None and item.text:
return item.text
return ""
def get_prop_text(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
if n is not None and n.text:
return n.text
return ""
def get_prop_ml(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
return get_ml_text(n)
# --- Type name maps (canonical order, 44 types) ---
type_order = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
]
type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
"SettingsStorage": "Хранилища настроек", "FunctionalOption": "Функциональные опции",
"FunctionalOptionsParameter": "Параметры ФО", "DefinedType": "Определяемые типы",
"CommonCommand": "Общие команды", "CommandGroup": "Группы команд", "Constant": "Константы",
"CommonForm": "Общие формы", "Catalog": "Справочники", "Document": "Документы",
"DocumentNumerator": "Нумераторы", "Sequence": "Последовательности", "DocumentJournal": "Журналы документов",
"Enum": "Перечисления", "Report": "Отчёты", "DataProcessor": "Обработки",
"InformationRegister": "Регистры сведений", "AccumulationRegister": "Регистры накопления",
"ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов",
"AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР",
"CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы",
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
}
# --- Count objects in ChildObjects ---
object_counts = OrderedDict()
total_objects = 0
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue # skip comments/PIs
type_name = etree.QName(child.tag).localname
if type_name not in object_counts:
object_counts[type_name] = 0
object_counts[type_name] += 1
total_objects += 1
# --- Read key properties ---
cfg_name = get_prop_text("Name")
cfg_synonym = get_prop_ml("Synonym")
cfg_version = get_prop_text("Version")
cfg_vendor = get_prop_text("Vendor")
cfg_compat = get_prop_text("CompatibilityMode")
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
cfg_default_run = get_prop_text("DefaultRunMode")
cfg_script = get_prop_text("ScriptVariant")
cfg_default_lang = get_prop_text("DefaultLanguage")
cfg_data_lock = get_prop_text("DataLockControlMode")
dash = "\u2014"
cfg_modality = get_prop_text("ModalityUseMode")
cfg_intf_compat = get_prop_text("InterfaceCompatibilityMode")
cfg_auto_num = get_prop_text("ObjectAutonumerationMode")
cfg_sync_calls = get_prop_text("SynchronousPlatformExtensionAndAddInCallUseMode")
cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode ---
if args.Mode == "brief":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
compat_part = f" | {cfg_compat}" if cfg_compat else ""
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
# --- OVERVIEW mode ---
if args.Mode == "overview":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
out()
# Key properties
out(f"Формат: {version}")
if cfg_vendor:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
out(f"Совместимость: {cfg_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
out(f"Язык: {cfg_default_lang}")
out(f"Блокировки: {cfg_data_lock}")
out(f"Модальность: {cfg_modality}")
out(f"Интерфейс: {cfg_intf_compat}")
out()
# Object counts table
out(f"--- Состав ({total_objects} объектов) ---")
out()
max_type_len = 0
for type_name in type_order:
if type_name in object_counts:
ru_name = type_ru_names.get(type_name, type_name)
if len(ru_name) > max_type_len:
max_type_len = len(ru_name)
if max_type_len < 10:
max_type_len = 10
for type_name in type_order:
if type_name in object_counts:
count = object_counts[type_name]
ru_name = type_ru_names.get(type_name, type_name)
padded = ru_name.ljust(max_type_len)
out(f" {padded} {count}")
# --- FULL mode ---
if args.Mode == "full":
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
out()
# --- Section: Identification ---
out("--- Идентификация ---")
out(f"UUID: {cfg_node.get('uuid', '')}")
out(f"Имя: {cfg_name}")
if cfg_synonym:
out(f"Синоним: {cfg_synonym}")
cfg_comment = get_prop_text("Comment")
if cfg_comment:
out(f"Комментарий: {cfg_comment}")
cfg_prefix = get_prop_text("NamePrefix")
if cfg_prefix:
out(f"Префикс: {cfg_prefix}")
if cfg_vendor:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
if cfg_update_addr:
out(f"Каталог обн.: {cfg_update_addr}")
out()
# --- Section: Modes ---
out("--- Режимы работы ---")
out(f"Формат: {version}")
out(f"Совместимость: {cfg_compat}")
out(f"Совм. расширений: {cfg_ext_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
out(f"Блокировки: {cfg_data_lock}")
out(f"Автонумерация: {cfg_auto_num}")
out(f"Модальность: {cfg_modality}")
out(f"Синхр. вызовы: {cfg_sync_calls}")
out(f"Интерфейс: {cfg_intf_compat}")
out(f"Табл. пространства: {cfg_db_spaces}")
out(f"Режим окна: {cfg_window_mode}")
out()
# --- Section: Language, roles, purposes ---
out("--- Назначение ---")
out(f"Язык по умолч.: {cfg_default_lang}")
# UsePurposes
purpose_node = props_node.find("md:UsePurposes", NS)
if purpose_node is not None:
purposes = []
for val in purpose_node.findall("v8:Value", NS):
if val.text:
purposes.append(val.text)
if purposes:
out(f"Назначения: {', '.join(purposes)}")
# DefaultRoles
roles_node = props_node.find("md:DefaultRoles", NS)
if roles_node is not None:
roles = []
for item in roles_node.findall("xr:Item", NS):
if item.text:
roles.append(item.text)
if roles:
out(f"Роли по умолч.: {len(roles)}")
for r in roles:
out(f" - {r}")
# Booleans
use_mf = get_prop_text("UseManagedFormInOrdinaryApplication")
use_of = get_prop_text("UseOrdinaryFormInManagedApplication")
out(f"Управл.формы в обычн.: {use_mf}")
out(f"Обычн.формы в управл.: {use_of}")
out()
# --- Section: Storages & default forms ---
out("--- Хранилища и формы по умолчанию ---")
storage_props = [
"CommonSettingsStorage", "ReportsUserSettingsStorage", "ReportsVariantsStorage",
"FormDataSettingsStorage", "DynamicListsUserSettingsStorage", "URLExternalDataStorage",
]
for sp in storage_props:
val = get_prop_text(sp)
if val:
out(f" {sp}: {val}")
form_props = [
"DefaultReportForm", "DefaultReportVariantForm", "DefaultReportSettingsForm",
"DefaultReportAppearanceTemplate", "DefaultDynamicListSettingsForm", "DefaultSearchForm",
"DefaultDataHistoryChangeHistoryForm", "DefaultDataHistoryVersionDataForm",
"DefaultDataHistoryVersionDifferencesForm", "DefaultCollaborationSystemUsersChoiceForm",
"DefaultConstantsForm", "DefaultInterface", "DefaultStyle",
]
for fp in form_props:
val = get_prop_text(fp)
if val:
out(f" {fp}: {val}")
out()
# --- Section: Info ---
cfg_brief = get_prop_ml("BriefInformation")
cfg_detail = get_prop_ml("DetailedInformation")
cfg_copyright = get_prop_ml("Copyright")
cfg_vendor_addr = get_prop_ml("VendorInformationAddress")
cfg_info_addr = get_prop_ml("ConfigurationInformationAddress")
if cfg_brief or cfg_detail or cfg_copyright or cfg_vendor_addr or cfg_info_addr:
out("--- Информация ---")
if cfg_brief:
out(f"Краткая: {cfg_brief}")
if cfg_detail:
out(f"Подробная: {cfg_detail}")
if cfg_copyright:
out(f"Copyright: {cfg_copyright}")
if cfg_vendor_addr:
out(f"Сайт поставщика: {cfg_vendor_addr}")
if cfg_info_addr:
out(f"Адрес информ.: {cfg_info_addr}")
out()
# --- Section: Mobile functionalities ---
mobile_func = props_node.find("md:UsedMobileApplicationFunctionalities", NS)
if mobile_func is not None:
enabled_funcs = []
disabled_funcs = []
for func in mobile_func.findall("app:functionality", NS):
f_name = func.find("app:functionality", NS)
f_use = func.find("app:use", NS)
if f_name is not None and f_use is not None:
if f_use.text == "true":
enabled_funcs.append(f_name.text or "")
else:
disabled_funcs.append(f_name.text or "")
total_func = len(enabled_funcs) + len(disabled_funcs)
out(f"--- Мобильные функциональности ({total_func}, включено: {len(enabled_funcs)}) ---")
for f in enabled_funcs:
out(f" [+] {f}")
for f in disabled_funcs:
out(f" [-] {f}")
out()
# --- Section: InternalInfo ---
internal_info = cfg_node.find("md:InternalInfo", NS)
if internal_info is not None:
contained = internal_info.findall("xr:ContainedObject", NS)
out(f"--- InternalInfo ({len(contained)} ContainedObject) ---")
for co in contained:
class_id_node = co.find("xr:ClassId", NS)
object_id_node = co.find("xr:ObjectId", NS)
class_id = class_id_node.text if class_id_node is not None else ""
object_id = object_id_node.text if object_id_node is not None else ""
out(f" {class_id} -> {object_id}")
out()
# --- Section: ChildObjects (full list) ---
out(f"--- Состав ({total_objects} объектов) ---")
out()
for type_name in type_order:
if type_name not in object_counts:
continue
count = object_counts[type_name]
ru_name = type_ru_names.get(type_name, type_name)
out(f" {ru_name} ({type_name}): {count}")
# Collect names for this type
if child_obj_node is not None:
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
if etree.QName(child.tag).localname == type_name:
out(f" {child.text or ''}")
# --- Pagination and output ---
total = len(lines_buf)
if args.Offset > 0 or args.Limit < total:
start = min(args.Offset, total)
end = min(start + args.Limit, total)
page = lines_buf[start:end]
result = "\n".join(page)
if end < total:
result += f"\n\n... ({end} of {total} lines, use -Offset {end} to continue)"
else:
result = "\n".join(lines_buf)
print(result)
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write(result)
print(f"\nWritten to: {out_file}")
+202 -201
View File
@@ -1,201 +1,202 @@
#!/usr/bin/env python3
# cf-init v1.0 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
output_dir = args.OutputDir
version = args.Version
vendor = args.Vendor
compat = args.CompatibilityMode
# --- Resolve output dir ---
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- Check existing ---
cfg_file = os.path.join(output_dir, "Configuration.xml")
if os.path.exists(cfg_file):
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# --- Generate UUIDs ---
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities ---
mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
("Calendars","false"), ("PushNotifications","false"), ("LocalNotifications","false"),
("InAppPurchases","false"), ("PersonalComputerFileExchange","false"), ("Ads","false"),
("NumberDialing","false"), ("CallProcessing","false"), ("CallLog","false"),
("AutoSendSMS","false"), ("ReceiveSMS","false"), ("SMSLog","false"),
("Camera","false"), ("Microphone","false"), ("MusicLibrary","false"),
("PictureAndVideoLibraries","false"), ("AudioPlaybackAndVibration","false"),
("BackgroundAudioPlaybackAndVibration","false"), ("InstallPackages","false"),
("OSBackup","true"), ("ApplicationUsageStatistics","false"),
("BarcodeScanning","false"), ("BackgroundAudioRecording","false"),
("AllFilesAccess","false"), ("Videoconferences","false"), ("NFC","false"),
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
]
mobile_xml = ""
for func_name, func_use in mobile_funcs:
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<NamePrefix/>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles/>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t<UpdateCatalogAddress/>
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
\t\t\t<AdditionalFullTextSearchDictionaries/>
\t\t\t<CommonSettingsStorage/>
\t\t\t<ReportsUserSettingsStorage/>
\t\t\t<ReportsVariantsStorage/>
\t\t\t<FormDataSettingsStorage/>
\t\t\t<DynamicListsUserSettingsStorage/>
\t\t\t<URLExternalDataStorage/>
\t\t\t<Content/>
\t\t\t<DefaultReportForm/>
\t\t\t<DefaultReportVariantForm/>
\t\t\t<DefaultReportSettingsForm/>
\t\t\t<DefaultReportAppearanceTemplate/>
\t\t\t<DefaultDynamicListSettingsForm/>
\t\t\t<DefaultSearchForm/>
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
\t\t\t<DefaultDataHistoryVersionDataForm/>
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
\t\t\t<RequiredMobileApplicationPermissions/>
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
\t\t\t</UsedMobileApplicationFunctionalities>
\t\t\t<StandaloneConfigurationRestrictionRoles/>
\t\t\t<MobileApplicationURLs/>
\t\t\t<AllowedIncomingShareRequestTypes/>
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
\t\t\t<DefaultInterface/>
\t\t\t<DefaultStyle/>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/>
\t\t</Properties>
\t\t<ChildObjects>
\t\t\t<Language>Русский</Language>
\t\t</ChildObjects>
\t</Configuration>
</MetaDataObject>'''
# --- Languages/Русский.xml ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Language uuid="{uuid_lang}">
\t\t<Properties>
\t\t\t<Name>Русский</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>Русский</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<LanguageCode>ru</LanguageCode>
\t\t</Properties>
\t</Language>
</MetaDataObject>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# cf-init v1.0 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
output_dir = args.OutputDir
version = args.Version
vendor = args.Vendor
compat = args.CompatibilityMode
# --- Resolve output dir ---
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- Check existing ---
cfg_file = os.path.join(output_dir, "Configuration.xml")
if os.path.exists(cfg_file):
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# --- Generate UUIDs ---
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities ---
mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
("Calendars","false"), ("PushNotifications","false"), ("LocalNotifications","false"),
("InAppPurchases","false"), ("PersonalComputerFileExchange","false"), ("Ads","false"),
("NumberDialing","false"), ("CallProcessing","false"), ("CallLog","false"),
("AutoSendSMS","false"), ("ReceiveSMS","false"), ("SMSLog","false"),
("Camera","false"), ("Microphone","false"), ("MusicLibrary","false"),
("PictureAndVideoLibraries","false"), ("AudioPlaybackAndVibration","false"),
("BackgroundAudioPlaybackAndVibration","false"), ("InstallPackages","false"),
("OSBackup","true"), ("ApplicationUsageStatistics","false"),
("BarcodeScanning","false"), ("BackgroundAudioRecording","false"),
("AllFilesAccess","false"), ("Videoconferences","false"), ("NFC","false"),
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
]
mobile_xml = ""
for func_name, func_use in mobile_funcs:
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<NamePrefix/>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles/>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t<UpdateCatalogAddress/>
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
\t\t\t<AdditionalFullTextSearchDictionaries/>
\t\t\t<CommonSettingsStorage/>
\t\t\t<ReportsUserSettingsStorage/>
\t\t\t<ReportsVariantsStorage/>
\t\t\t<FormDataSettingsStorage/>
\t\t\t<DynamicListsUserSettingsStorage/>
\t\t\t<URLExternalDataStorage/>
\t\t\t<Content/>
\t\t\t<DefaultReportForm/>
\t\t\t<DefaultReportVariantForm/>
\t\t\t<DefaultReportSettingsForm/>
\t\t\t<DefaultReportAppearanceTemplate/>
\t\t\t<DefaultDynamicListSettingsForm/>
\t\t\t<DefaultSearchForm/>
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
\t\t\t<DefaultDataHistoryVersionDataForm/>
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
\t\t\t<RequiredMobileApplicationPermissions/>
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
\t\t\t</UsedMobileApplicationFunctionalities>
\t\t\t<StandaloneConfigurationRestrictionRoles/>
\t\t\t<MobileApplicationURLs/>
\t\t\t<AllowedIncomingShareRequestTypes/>
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
\t\t\t<DefaultInterface/>
\t\t\t<DefaultStyle/>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/>
\t\t</Properties>
\t\t<ChildObjects>
\t\t\t<Language>Русский</Language>
\t\t</ChildObjects>
\t</Configuration>
</MetaDataObject>'''
# --- Languages/Русский.xml ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Language uuid="{uuid_lang}">
\t\t<Properties>
\t\t\t<Name>Русский</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>Русский</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<LanguageCode>ru</LanguageCode>
\t\t</Properties>
\t</Language>
</MetaDataObject>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+238 -237
View File
@@ -1,237 +1,238 @@
#!/usr/bin/env python3
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid
from xml.etree import ElementTree as ET
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Create 1C configuration extension scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-NamePrefix', dest='NamePrefix', default=None)
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
parser.add_argument('-Purpose', dest='Purpose', default='Customization', choices=['Patch','Customization','AddOn'])
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
name_prefix = args.NamePrefix if args.NamePrefix else f"{name}_"
output_dir = args.OutputDir
purpose = args.Purpose
version = args.Version
vendor = args.Vendor
compat = args.CompatibilityMode
# --- Resolve output dir ---
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- Check existing ---
cfg_file = os.path.join(output_dir, "Configuration.xml")
if os.path.exists(cfg_file):
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath:
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.exists(candidate):
config_path = candidate
else:
print(f"No Configuration.xml in config directory: {config_path}", file=sys.stderr)
sys.exit(1)
if not os.path.exists(config_path):
print(f"Config file not found: {config_path}", file=sys.stderr)
sys.exit(1)
cfg_dir = os.path.dirname(os.path.abspath(config_path))
# Read Language UUID from base config
base_lang_file = os.path.join(cfg_dir, "Languages", "Русский.xml")
if os.path.exists(base_lang_file):
try:
base_tree = ET.parse(base_lang_file)
base_root = base_tree.getroot()
for child in base_root:
if child.tag.endswith('}Language') or child.tag == 'Language':
base_lang_uuid = child.get('uuid', base_lang_uuid)
print(f"[INFO] Base config Language UUID: {base_lang_uuid}")
break
except Exception:
print(f"[WARN] Could not parse {base_lang_file}")
else:
print(f"[WARN] Base config language not found: {base_lang_file}")
# Read CompatibilityMode from base config
try:
base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot()
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text:
compat = compat_node.text.strip()
print(f"[INFO] Base config CompatibilityMode: {compat}")
else:
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
except Exception:
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
else:
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
# --- Generate UUIDs ---
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
uuid_role = new_uuid()
co = [new_uuid() for _ in range(7)]
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
# --- Role name ---
role_name = f"{name_prefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
default_roles_xml = ""
if not args.NoRole:
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
# --- ChildObjects ---
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
if not args.NoRole:
child_objects_xml += f"\r\n\t\t\t<Role>{role_name}</Role>"
child_objects_xml += "\r\n\t\t"
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
\t\t</Properties>
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
\t</Configuration>
</MetaDataObject>'''
# --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>Русский</Name>
\t\t\t<Comment/>
\t\t\t<ExtendedConfigurationObject>{base_lang_uuid}</ExtendedConfigurationObject>
\t\t\t<LanguageCode>ru</LanguageCode>
\t\t</Properties>
\t</Language>
</MetaDataObject>'''
# --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Role uuid="{uuid_role}">
\t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name>
\t\t\t<Synonym/>
\t\t\t<Comment/>
\t\t</Properties>
\t</Role>
</MetaDataObject>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
# --- Role ---
role_file = None
if not args.NoRole:
role_dir = os.path.join(output_dir, "Roles")
os.makedirs(role_dir, exist_ok=True)
role_file = os.path.join(role_dir, f"{role_name}.xml")
write_utf8_bom(role_file, role_xml)
# --- Output ---
print(f"[OK] Создано расширение: {name}")
print(f" Каталог: {output_dir}")
print(f" Назначение: {purpose}")
print(f" Префикс: {name_prefix}")
print(f" Совместимость: {compat}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
if role_file:
print(f" Role: {role_file}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid
from xml.etree import ElementTree as ET
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Create 1C configuration extension scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-NamePrefix', dest='NamePrefix', default=None)
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
parser.add_argument('-Purpose', dest='Purpose', default='Customization', choices=['Patch','Customization','AddOn'])
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
name_prefix = args.NamePrefix if args.NamePrefix else f"{name}_"
output_dir = args.OutputDir
purpose = args.Purpose
version = args.Version
vendor = args.Vendor
compat = args.CompatibilityMode
# --- Resolve output dir ---
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- Check existing ---
cfg_file = os.path.join(output_dir, "Configuration.xml")
if os.path.exists(cfg_file):
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath:
config_path = args.ConfigPath
if not os.path.isabs(config_path):
config_path = os.path.join(os.getcwd(), config_path)
if os.path.isdir(config_path):
candidate = os.path.join(config_path, "Configuration.xml")
if os.path.exists(candidate):
config_path = candidate
else:
print(f"No Configuration.xml in config directory: {config_path}", file=sys.stderr)
sys.exit(1)
if not os.path.exists(config_path):
print(f"Config file not found: {config_path}", file=sys.stderr)
sys.exit(1)
cfg_dir = os.path.dirname(os.path.abspath(config_path))
# Read Language UUID from base config
base_lang_file = os.path.join(cfg_dir, "Languages", "Русский.xml")
if os.path.exists(base_lang_file):
try:
base_tree = ET.parse(base_lang_file)
base_root = base_tree.getroot()
for child in base_root:
if child.tag.endswith('}Language') or child.tag == 'Language':
base_lang_uuid = child.get('uuid', base_lang_uuid)
print(f"[INFO] Base config Language UUID: {base_lang_uuid}")
break
except Exception:
print(f"[WARN] Could not parse {base_lang_file}")
else:
print(f"[WARN] Base config language not found: {base_lang_file}")
# Read CompatibilityMode from base config
try:
base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot()
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text:
compat = compat_node.text.strip()
print(f"[INFO] Base config CompatibilityMode: {compat}")
else:
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
except Exception:
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
else:
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
# --- Generate UUIDs ---
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
uuid_role = new_uuid()
co = [new_uuid() for _ in range(7)]
# --- Synonym XML ---
synonym_xml = ""
if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else ""
version_xml = esc_xml(version) if version else ""
# --- Role name ---
role_name = f"{name_prefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
default_roles_xml = ""
if not args.NoRole:
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
# --- ChildObjects ---
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
if not args.NoRole:
child_objects_xml += f"\r\n\t\t\t<Role>{role_name}</Role>"
child_objects_xml += "\r\n\t\t"
class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7",
"9fcd25a0-4822-11d4-9414-008048da11f9",
"e3687481-0a87-462c-a166-9f34594f9bba",
"9de14907-ec23-4a07-96f0-85521cb6b53b",
"51f2d5d8-ea4d-4064-8892-82951750031e",
"e68182ea-4237-4383-967f-90c1e3370bc7",
"fb282519-d103-4dd3-bc12-cb271d631dfc",
]
contained_objects = ""
for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>{synonym_xml}</Synonym>
\t\t\t<Comment/>
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
\t\t\t<Vendor>{vendor_xml}</Vendor>
\t\t\t<Version>{version_xml}</Version>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/>
\t\t\t<Copyright/>
\t\t\t<VendorInformationAddress/>
\t\t\t<ConfigurationInformationAddress/>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
\t\t</Properties>
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
\t</Configuration>
</MetaDataObject>'''
# --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/>
\t\t<Properties>
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
\t\t\t<Name>Русский</Name>
\t\t\t<Comment/>
\t\t\t<ExtendedConfigurationObject>{base_lang_uuid}</ExtendedConfigurationObject>
\t\t\t<LanguageCode>ru</LanguageCode>
\t\t</Properties>
\t</Language>
</MetaDataObject>'''
# --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Role uuid="{uuid_role}">
\t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name>
\t\t\t<Synonym/>
\t\t\t<Comment/>
\t\t</Properties>
\t</Role>
</MetaDataObject>'''
# --- Create directories ---
os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True)
# --- Write files ---
write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml)
# --- Role ---
role_file = None
if not args.NoRole:
role_dir = os.path.join(output_dir, "Roles")
os.makedirs(role_dir, exist_ok=True)
role_file = os.path.join(role_dir, f"{role_name}.xml")
write_utf8_bom(role_file, role_xml)
# --- Output ---
print(f"[OK] Создано расширение: {name}")
print(f" Каталог: {output_dir}")
print(f" Назначение: {purpose}")
print(f" Префикс: {name_prefix}")
print(f" Совместимость: {compat}")
print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}")
if role_file:
print(f" Role: {role_file}")
if __name__ == '__main__':
main()
@@ -1,229 +1,230 @@
#!/usr/bin/env python3
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import xml.etree.ElementTree as ET
def main():
parser = argparse.ArgumentParser(
description="Generate method interceptor for 1C extension (CFE)",
allow_abbrev=False,
)
parser.add_argument("-ExtensionPath", required=True)
parser.add_argument("-ModulePath", required=True)
parser.add_argument("-MethodName", required=True)
parser.add_argument(
"-InterceptorType",
required=True,
choices=["Before", "After", "ModificationAndControl"],
)
parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере
parser.add_argument("-IsFunction", action="store_true")
args = parser.parse_args()
extension_path = args.ExtensionPath
module_path = args.ModulePath
method_name = args.MethodName
interceptor_type = args.InterceptorType
context = args.Context
is_function = args.IsFunction
# --- Resolve extension path ---
if not os.path.isabs(extension_path):
extension_path = os.path.join(os.getcwd(), extension_path)
if os.path.isfile(extension_path):
extension_path = os.path.dirname(extension_path)
cfg_file = os.path.join(extension_path, "Configuration.xml")
if not os.path.isfile(cfg_file):
print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr)
sys.exit(1)
# --- Read NamePrefix from Configuration.xml ---
tree = ET.parse(cfg_file)
root = tree.getroot()
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
props_node = root.find(".//md:Configuration/md:Properties", ns)
name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_
if props_node is not None:
prefix_node = props_node.find("md:NamePrefix", ns)
if prefix_node is not None and prefix_node.text:
name_prefix = prefix_node.text
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
type_dir_map = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"CommonModule": "CommonModules",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"ExchangePlan": "ExchangePlans",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
}
parts = module_path.split(".")
if len(parts) < 2:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in type_dir_map:
print(f"Unknown object type: {obj_type}", file=sys.stderr)
sys.exit(1)
dir_name = type_dir_map[obj_type]
bsl_file = None
if obj_type == "CommonModule":
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl")
elif len(parts) >= 4 and parts[2] == "Form":
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
form_name = parts[3]
bsl_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"
)
elif len(parts) >= 3:
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
module_name = parts[2]
module_file_map = {
"ObjectModule": "ObjectModule.bsl",
"ManagerModule": "ManagerModule.bsl",
"RecordSetModule": "RecordSetModule.bsl",
"CommandModule": "CommandModule.bsl",
}
module_file_name = module_file_map.get(module_name, f"{module_name}.bsl")
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name)
else:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
# --- Map InterceptorType to decorator ---
decorator_map = {
"Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед
"After": "&\u041f\u043e\u0441\u043b\u0435", # &После
"ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль
}
decorator = decorator_map[interceptor_type]
# --- Map Context to annotation ---
context_map = {
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере
"\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста
}
context_annotation = context_map.get(context, f"&{context}")
# --- Procedure name ---
proc_name = f"{name_prefix}{method_name}"
# --- Generate BSL code ---
keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура
end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры
body_lines = []
if interceptor_type == "Before":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода
elif interceptor_type == "After":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода
elif interceptor_type == "ModificationAndControl":
body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения,
body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки
if is_function:
body_lines.append("\t")
body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение
bsl_code = [
context_annotation,
f'{decorator}("{method_name}")',
f"{keyword} {proc_name}()",
]
bsl_code.extend(body_lines)
bsl_code.append(end_keyword)
bsl_text = "\r\n".join(bsl_code) + "\r\n"
# --- Check form borrowing for .Form. paths ---
if len(parts) >= 4 and parts[2] == "Form":
form_name = parts[3]
form_meta_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml"
)
form_xml_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml"
)
if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file):
print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.")
print(" Run /cfe-borrow first:")
print(
f" /cfe-borrow -ExtensionPath {extension_path} "
f'-ConfigPath <ConfigPath> -Object "{obj_type}.{obj_name}.Form.{form_name}"'
)
print()
# --- Check if file exists and append ---
bsl_dir = os.path.dirname(bsl_file)
if not os.path.isdir(bsl_dir):
os.makedirs(bsl_dir, exist_ok=True)
if os.path.isfile(bsl_file):
# Append to existing file
with open(bsl_file, "r", encoding="utf-8-sig") as f:
existing = f.read()
separator = "\r\n"
if existing and not existing.endswith("\n"):
separator = "\r\n\r\n"
new_content = existing + separator + bsl_text
with open(bsl_file, "w", encoding="utf-8-sig") as f:
f.write(new_content)
print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл
else:
with open(bsl_file, "w", encoding="utf-8-sig") as f:
f.write(bsl_text)
print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля
print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл:
print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор:
print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура:
print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст:
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import xml.etree.ElementTree as ET
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Generate method interceptor for 1C extension (CFE)",
allow_abbrev=False,
)
parser.add_argument("-ExtensionPath", required=True)
parser.add_argument("-ModulePath", required=True)
parser.add_argument("-MethodName", required=True)
parser.add_argument(
"-InterceptorType",
required=True,
choices=["Before", "After", "ModificationAndControl"],
)
parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере
parser.add_argument("-IsFunction", action="store_true")
args = parser.parse_args()
extension_path = args.ExtensionPath
module_path = args.ModulePath
method_name = args.MethodName
interceptor_type = args.InterceptorType
context = args.Context
is_function = args.IsFunction
# --- Resolve extension path ---
if not os.path.isabs(extension_path):
extension_path = os.path.join(os.getcwd(), extension_path)
if os.path.isfile(extension_path):
extension_path = os.path.dirname(extension_path)
cfg_file = os.path.join(extension_path, "Configuration.xml")
if not os.path.isfile(cfg_file):
print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr)
sys.exit(1)
# --- Read NamePrefix from Configuration.xml ---
tree = ET.parse(cfg_file)
root = tree.getroot()
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
props_node = root.find(".//md:Configuration/md:Properties", ns)
name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_
if props_node is not None:
prefix_node = props_node.find("md:NamePrefix", ns)
if prefix_node is not None and prefix_node.text:
name_prefix = prefix_node.text
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
type_dir_map = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"CommonModule": "CommonModules",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"ExchangePlan": "ExchangePlans",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
}
parts = module_path.split(".")
if len(parts) < 2:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in type_dir_map:
print(f"Unknown object type: {obj_type}", file=sys.stderr)
sys.exit(1)
dir_name = type_dir_map[obj_type]
bsl_file = None
if obj_type == "CommonModule":
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl")
elif len(parts) >= 4 and parts[2] == "Form":
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
form_name = parts[3]
bsl_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"
)
elif len(parts) >= 3:
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
module_name = parts[2]
module_file_map = {
"ObjectModule": "ObjectModule.bsl",
"ManagerModule": "ManagerModule.bsl",
"RecordSetModule": "RecordSetModule.bsl",
"CommandModule": "CommandModule.bsl",
}
module_file_name = module_file_map.get(module_name, f"{module_name}.bsl")
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name)
else:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
# --- Map InterceptorType to decorator ---
decorator_map = {
"Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед
"After": "&\u041f\u043e\u0441\u043b\u0435", # &После
"ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль
}
decorator = decorator_map[interceptor_type]
# --- Map Context to annotation ---
context_map = {
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере
"\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста
}
context_annotation = context_map.get(context, f"&{context}")
# --- Procedure name ---
proc_name = f"{name_prefix}{method_name}"
# --- Generate BSL code ---
keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура
end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры
body_lines = []
if interceptor_type == "Before":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода
elif interceptor_type == "After":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода
elif interceptor_type == "ModificationAndControl":
body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения,
body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки
if is_function:
body_lines.append("\t")
body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение
bsl_code = [
context_annotation,
f'{decorator}("{method_name}")',
f"{keyword} {proc_name}()",
]
bsl_code.extend(body_lines)
bsl_code.append(end_keyword)
bsl_text = "\r\n".join(bsl_code) + "\r\n"
# --- Check form borrowing for .Form. paths ---
if len(parts) >= 4 and parts[2] == "Form":
form_name = parts[3]
form_meta_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml"
)
form_xml_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml"
)
if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file):
print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.")
print(" Run /cfe-borrow first:")
print(
f" /cfe-borrow -ExtensionPath {extension_path} "
f'-ConfigPath <ConfigPath> -Object "{obj_type}.{obj_name}.Form.{form_name}"'
)
print()
# --- Check if file exists and append ---
bsl_dir = os.path.dirname(bsl_file)
if not os.path.isdir(bsl_dir):
os.makedirs(bsl_dir, exist_ok=True)
if os.path.isfile(bsl_file):
# Append to existing file
with open(bsl_file, "r", encoding="utf-8-sig") as f:
existing = f.read()
separator = "\r\n"
if existing and not existing.endswith("\n"):
separator = "\r\n\r\n"
new_content = existing + separator + bsl_text
with open(bsl_file, "w", encoding="utf-8-sig") as f:
f.write(new_content)
print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл
else:
with open(bsl_file, "w", encoding="utf-8-sig") as f:
f.write(bsl_text)
print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля
print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл:
print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор:
print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура:
print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст:
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+126 -125
View File
@@ -1,125 +1,126 @@
#!/usr/bin/env python3
# db-create v1.0 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Create 1C information base",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UseTemplate", default="")
parser.add_argument("-AddToList", action="store_true")
parser.add_argument("-ListName", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate template ---
if args.UseTemplate and not os.path.exists(args.UseTemplate):
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["CREATEINFOBASE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
else:
arguments.append(f'File="{args.InfoBasePath}"')
# --- Template ---
if args.UseTemplate:
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
# --- Add to list ---
if args.AddToList:
if args.ListName:
arguments.extend(["/AddToList", f'"{args.ListName}"'])
else:
arguments.append("/AddToList")
# --- Output ---
out_file = os.path.join(temp_dir, "create_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else:
print(f"Information base created successfully: {args.InfoBasePath}")
else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-create v1.0 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Create 1C information base",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UseTemplate", default="")
parser.add_argument("-AddToList", action="store_true")
parser.add_argument("-ListName", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate template ---
if args.UseTemplate and not os.path.exists(args.UseTemplate):
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["CREATEINFOBASE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
else:
arguments.append(f'File="{args.InfoBasePath}"')
# --- Template ---
if args.UseTemplate:
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
# --- Add to list ---
if args.AddToList:
if args.ListName:
arguments.extend(["/AddToList", f'"{args.ListName}"'])
else:
arguments.append("/AddToList")
# --- Output ---
out_file = os.path.join(temp_dir, "create_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else:
print(f"Information base created successfully: {args.InfoBasePath}")
else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+127 -126
View File
@@ -1,126 +1,127 @@
#!/usr/bin/env python3
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Dump 1C configuration to CF file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Dump 1C configuration to CF file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+172 -171
View File
@@ -1,171 +1,172 @@
#!/usr/bin/env python3
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Dump 1C configuration to XML files",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
parser.add_argument(
"-Mode",
default="Changes",
choices=["Full", "Changes", "Partial", "UpdateInfo"],
help="Dump mode (default: Changes)",
)
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
parser.add_argument("-Extension", default="", help="Extension name to dump")
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate Partial mode ---
if args.Mode == "Partial" and not args.Objects:
print("Error: -Objects required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- Create output dir if needed ---
if not os.path.exists(args.ConfigDir):
os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}")
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
arguments += ["-Format", args.Format]
if args.Mode == "Full":
print("Executing full configuration dump...")
elif args.Mode == "Changes":
print("Executing incremental configuration dump...")
arguments.append("-update")
arguments.append("-force")
elif args.Mode == "Partial":
print("Executing partial configuration dump...")
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
list_file = os.path.join(temp_dir, "dump_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(object_list))
arguments += ["-listFile", f'"{list_file}"']
print(f"Objects to dump: {len(object_list)}")
for obj in object_list:
print(f" {obj}")
elif args.Mode == "UpdateInfo":
print("Updating ConfigDumpInfo.xml...")
arguments.append("-configDumpInfoOnly")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Dump 1C configuration to XML files",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
parser.add_argument(
"-Mode",
default="Changes",
choices=["Full", "Changes", "Partial", "UpdateInfo"],
help="Dump mode (default: Changes)",
)
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
parser.add_argument("-Extension", default="", help="Extension name to dump")
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate Partial mode ---
if args.Mode == "Partial" and not args.Objects:
print("Error: -Objects required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- Create output dir if needed ---
if not os.path.exists(args.ConfigDir):
os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}")
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
arguments += ["-Format", args.Format]
if args.Mode == "Full":
print("Executing full configuration dump...")
elif args.Mode == "Changes":
print("Executing incremental configuration dump...")
arguments.append("-update")
arguments.append("-force")
elif args.Mode == "Partial":
print("Executing partial configuration dump...")
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
list_file = os.path.join(temp_dir, "dump_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(object_list))
arguments += ["-listFile", f'"{list_file}"']
print(f"Objects to dump: {len(object_list)}")
for obj in object_list:
print(f" {obj}")
elif args.Mode == "UpdateInfo":
print("Updating ConfigDumpInfo.xml...")
arguments.append("-configDumpInfoOnly")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+127 -126
View File
@@ -1,126 +1,127 @@
#!/usr/bin/env python3
# db-load-cf v1.0 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Load 1C configuration from CF file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_cf_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-load-cf v1.0 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Load 1C configuration from CF file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_cf_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+283 -282
View File
@@ -1,282 +1,283 @@
#!/usr/bin/env python3
# db-load-git v1.0 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def get_object_xml_from_bsl(relative_path):
"""Map BSL path to object XML path."""
parts = re.split(r"[\\/]", relative_path)
if len(parts) >= 2:
return f"{parts[0]}/{parts[1]}.xml"
return None
def run_git(config_dir, git_args):
"""Run a git command in config_dir and return output lines on success."""
result = subprocess.run(
["git"] + git_args,
capture_output=True,
text=True,
cwd=config_dir,
)
if result.returncode == 0:
return [line for line in result.stdout.splitlines() if line.strip()]
return []
def main():
parser = argparse.ArgumentParser(
description="Load Git changes into 1C database",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
parser.add_argument(
"-Source",
default="All",
choices=["All", "Staged", "Unstaged", "Commit"],
help="Change source (default: All)",
)
parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD")
parser.add_argument("-Extension", default="", help="Extension name to load")
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="File format (default: Hierarchical)",
)
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
args = parser.parse_args()
# --- Resolve V8Path (skip if DryRun) ---
v8path = None
if not args.DryRun:
v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) ---
if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
sys.exit(1)
# --- Validate Commit mode ---
if args.Source == "Commit" and not args.CommitRange:
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
sys.exit(1)
# --- Check git ---
try:
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: git not found in PATH", file=sys.stderr)
sys.exit(1)
# --- Get changed files from Git ---
changed_files = []
if args.Source == "Staged":
print("Getting staged changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"])
elif args.Source == "Unstaged":
print("Getting unstaged changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--name-only"])
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
elif args.Source == "Commit":
print(f"Getting changes from {args.CommitRange}...")
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", args.CommitRange])
elif args.Source == "All":
print("Getting all uncommitted changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"])
changed_files += run_git(args.ConfigDir, ["diff", "--name-only"])
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
# Deduplicate and filter blanks
changed_files = list(dict.fromkeys(f for f in changed_files if f.strip()))
if len(changed_files) == 0:
print("No changes found")
sys.exit(0)
print(f"Git changes detected: {len(changed_files)} files")
# --- Filter and map to config files ---
config_files = []
for file in changed_files:
file = file.strip().replace("\\", "/")
if not file:
continue
# Skip service files
if file == "ConfigDumpInfo.xml":
continue
# Only process .xml and .bsl files
if not re.search(r"\.(xml|bsl)$", file):
continue
# Check file exists in config dir
full_path = os.path.join(args.ConfigDir, file)
if file.endswith(".xml"):
if os.path.exists(full_path):
if file not in config_files:
config_files.append(file)
elif file.endswith(".bsl"):
# For BSL: add the BSL itself + parent object XML + all Ext/ files
object_xml = get_object_xml_from_bsl(file)
if object_xml:
full_xml_path = os.path.join(args.ConfigDir, object_xml)
if os.path.exists(full_xml_path):
if object_xml not in config_files:
config_files.append(object_xml)
if file not in config_files:
config_files.append(file)
# Add all files from Ext/ directory of the object
parts = re.split(r"[\\/]", file)
if len(parts) >= 2:
ext_dir = os.path.join(args.ConfigDir, parts[0], parts[1], "Ext")
if os.path.isdir(ext_dir):
for root, dirs, files in os.walk(ext_dir):
for fname in files:
abs_path = os.path.join(root, fname)
# Build relative path from ConfigDir
rel_path = os.path.relpath(abs_path, args.ConfigDir).replace("\\", "/")
if rel_path not in config_files:
config_files.append(rel_path)
if len(config_files) == 0:
print("No configuration files found in changes")
sys.exit(0)
print(f"Files for loading: {len(config_files)}")
for f in config_files:
print(f" {f}")
# --- DryRun: stop here ---
if args.DryRun:
print("")
print("DryRun mode - no changes applied")
sys.exit(0)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_git_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(config_files))
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", f'"{list_file}"']
arguments += ["-Format", args.Format]
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print("")
print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
print("")
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-load-git v1.0 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def get_object_xml_from_bsl(relative_path):
"""Map BSL path to object XML path."""
parts = re.split(r"[\\/]", relative_path)
if len(parts) >= 2:
return f"{parts[0]}/{parts[1]}.xml"
return None
def run_git(config_dir, git_args):
"""Run a git command in config_dir and return output lines on success."""
result = subprocess.run(
["git"] + git_args,
capture_output=True,
text=True,
cwd=config_dir,
)
if result.returncode == 0:
return [line for line in result.stdout.splitlines() if line.strip()]
return []
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Load Git changes into 1C database",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
parser.add_argument(
"-Source",
default="All",
choices=["All", "Staged", "Unstaged", "Commit"],
help="Change source (default: All)",
)
parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD")
parser.add_argument("-Extension", default="", help="Extension name to load")
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="File format (default: Hierarchical)",
)
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
args = parser.parse_args()
# --- Resolve V8Path (skip if DryRun) ---
v8path = None
if not args.DryRun:
v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) ---
if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
sys.exit(1)
# --- Validate Commit mode ---
if args.Source == "Commit" and not args.CommitRange:
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
sys.exit(1)
# --- Check git ---
try:
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: git not found in PATH", file=sys.stderr)
sys.exit(1)
# --- Get changed files from Git ---
changed_files = []
if args.Source == "Staged":
print("Getting staged changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"])
elif args.Source == "Unstaged":
print("Getting unstaged changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--name-only"])
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
elif args.Source == "Commit":
print(f"Getting changes from {args.CommitRange}...")
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", args.CommitRange])
elif args.Source == "All":
print("Getting all uncommitted changes...")
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"])
changed_files += run_git(args.ConfigDir, ["diff", "--name-only"])
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
# Deduplicate and filter blanks
changed_files = list(dict.fromkeys(f for f in changed_files if f.strip()))
if len(changed_files) == 0:
print("No changes found")
sys.exit(0)
print(f"Git changes detected: {len(changed_files)} files")
# --- Filter and map to config files ---
config_files = []
for file in changed_files:
file = file.strip().replace("\\", "/")
if not file:
continue
# Skip service files
if file == "ConfigDumpInfo.xml":
continue
# Only process .xml and .bsl files
if not re.search(r"\.(xml|bsl)$", file):
continue
# Check file exists in config dir
full_path = os.path.join(args.ConfigDir, file)
if file.endswith(".xml"):
if os.path.exists(full_path):
if file not in config_files:
config_files.append(file)
elif file.endswith(".bsl"):
# For BSL: add the BSL itself + parent object XML + all Ext/ files
object_xml = get_object_xml_from_bsl(file)
if object_xml:
full_xml_path = os.path.join(args.ConfigDir, object_xml)
if os.path.exists(full_xml_path):
if object_xml not in config_files:
config_files.append(object_xml)
if file not in config_files:
config_files.append(file)
# Add all files from Ext/ directory of the object
parts = re.split(r"[\\/]", file)
if len(parts) >= 2:
ext_dir = os.path.join(args.ConfigDir, parts[0], parts[1], "Ext")
if os.path.isdir(ext_dir):
for root, dirs, files in os.walk(ext_dir):
for fname in files:
abs_path = os.path.join(root, fname)
# Build relative path from ConfigDir
rel_path = os.path.relpath(abs_path, args.ConfigDir).replace("\\", "/")
if rel_path not in config_files:
config_files.append(rel_path)
if len(config_files) == 0:
print("No configuration files found in changes")
sys.exit(0)
print(f"Files for loading: {len(config_files)}")
for f in config_files:
print(f" {f}")
# --- DryRun: stop here ---
if args.DryRun:
print("")
print("DryRun mode - no changes applied")
sys.exit(0)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_git_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(config_files))
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", f'"{list_file}"']
arguments += ["-Format", args.Format]
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print("")
print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
print("")
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+179 -178
View File
@@ -1,178 +1,179 @@
#!/usr/bin/env python3
# db-load-xml v1.0 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Load 1C configuration from XML files",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
parser.add_argument(
"-Mode",
default="Full",
choices=["Full", "Partial"],
help="Load mode (default: Full)",
)
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)")
parser.add_argument("-Extension", default="", help="Extension name to load")
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="File format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
sys.exit(1)
# --- Validate Partial mode ---
if args.Mode == "Partial" and not args.Files and not args.ListFile:
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
if args.Mode == "Full":
print("Executing full configuration load...")
else:
print("Executing partial configuration load...")
# Build list file
generated_list_file = None
if args.ListFile:
# Use provided list file
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
generated_list_file = args.ListFile
else:
# Generate from -Files parameter
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", f'"{generated_list_file}"']
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
arguments += ["-Format", args.Format]
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-load-xml v1.0 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Load 1C configuration from XML files",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
parser.add_argument(
"-Mode",
default="Full",
choices=["Full", "Partial"],
help="Load mode (default: Full)",
)
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)")
parser.add_argument("-Extension", default="", help="Extension name to load")
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="File format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
sys.exit(1)
# --- Validate Partial mode ---
if args.Mode == "Partial" and not args.Files and not args.ListFile:
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
if args.Mode == "Full":
print("Executing full configuration load...")
else:
print("Executing partial configuration load...")
# Build list file
generated_list_file = None
if args.ListFile:
# Use provided list file
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
generated_list_file = args.ListFile
else:
# Generate from -Files parameter
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", f'"{generated_list_file}"']
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
arguments += ["-Format", args.Format]
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+93 -92
View File
@@ -1,92 +1,93 @@
#!/usr/bin/env python3
# db-run v1.0 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import subprocess
import sys
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Launch 1C:Enterprise",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-Execute", default="")
parser.add_argument("-CParam", default="")
parser.add_argument("-URL", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Build arguments ---
arguments = ["ENTERPRISE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
# --- Optional params ---
execute = args.Execute
if execute:
ext = os.path.splitext(execute)[1].lower()
if ext == ".erf":
print("[WARN] /Execute does not support ERF files (external reports).")
print(f" Open the report via File -> Open: {execute}")
print(" Launching database without /Execute.")
execute = ""
if execute:
arguments.extend(["/Execute", f'"{execute}"'])
if args.CParam:
arguments.extend(["/C", f'"{args.CParam}"'])
if args.URL:
arguments.extend(["/URL", f'"{args.URL}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute (background, no wait) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
subprocess.Popen([v8path] + arguments)
print("1C:Enterprise launched")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-run v1.0 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import subprocess
import sys
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Launch 1C:Enterprise",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-Execute", default="")
parser.add_argument("-CParam", default="")
parser.add_argument("-URL", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Build arguments ---
arguments = ["ENTERPRISE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
# --- Optional params ---
execute = args.Execute
if execute:
ext = os.path.splitext(execute)[1].lower()
if ext == ".erf":
print("[WARN] /Execute does not support ERF files (external reports).")
print(f" Open the report via File -> Open: {execute}")
print(" Launching database without /Execute.")
execute = ""
if execute:
arguments.extend(["/Execute", f'"{execute}"'])
if args.CParam:
arguments.extend(["/C", f'"{args.CParam}"'])
if args.URL:
arguments.extend(["/URL", f'"{args.URL}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute (background, no wait) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
subprocess.Popen([v8path] + arguments)
print("1C:Enterprise launched")
if __name__ == "__main__":
main()
+132 -131
View File
@@ -1,131 +1,132 @@
#!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Update 1C database configuration",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.append("/UpdateDBCfg")
# --- Options ---
if args.Dynamic:
arguments.append(f"-Dynamic{args.Dynamic}")
if args.Server:
arguments.append("-Server")
if args.WarningsAsErrors:
arguments.append("-WarningsAsErrors")
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "update_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Update 1C database configuration",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.append("/UpdateDBCfg")
# --- Options ---
if args.Dynamic:
arguments.append(f"-Dynamic{args.Dynamic}")
if args.Server:
arguments.append("-Server")
if args.WarningsAsErrors:
arguments.append("-WarningsAsErrors")
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "update_log.txt")
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+250 -249
View File
@@ -1,249 +1,250 @@
#!/usr/bin/env python3
# add-form v1.0 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
parser.add_argument("-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Main", action="store_true")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
processor_name = args.ProcessorName
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
is_main = args.Main
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# add-form v1.0 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
parser.add_argument("-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Main", action="store_true")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
processor_name = args.ProcessorName
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
is_main = args.Main
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
+128 -127
View File
@@ -1,127 +1,128 @@
#!/usr/bin/env python3
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Build external data processor or report (EPF/ERF) from XML sources",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate source file ---
if not os.path.isfile(args.SourceFile):
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_build_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
# --- Output ---
out_file = os.path.join(temp_dir, "build_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}")
else:
print(f"Error building (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Build external data processor or report (EPF/ERF) from XML sources",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate source file ---
if not os.path.isfile(args.SourceFile):
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_build_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
# --- Output ---
out_file = os.path.join(temp_dir, "build_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}")
else:
print(f"Error building (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+134 -133
View File
@@ -1,133 +1,134 @@
#!/usr/bin/env python3
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
parser = argparse.ArgumentParser(
description="Dump external data processor or report (EPF/ERF) to XML sources",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
if not os.path.exists(args.OutputDir):
os.makedirs(args.OutputDir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
arguments += ["-Format", args.Format]
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}")
else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import os
import random
import shutil
import subprocess
import sys
import tempfile
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
if candidates:
candidates.sort()
return candidates[-1]
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Dump external data processor or report (EPF/ERF) to XML sources",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
parser.add_argument(
"-Format",
default="Hierarchical",
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
if not os.path.exists(args.OutputDir):
os.makedirs(args.OutputDir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
arguments += ["-Format", args.Format]
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}")
else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+98 -97
View File
@@ -1,97 +1,98 @@
#!/usr/bin/env python3
# epf-init v1.0 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Init 1C external data processor scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir
uuid1 = new_uuid()
uuid2 = new_uuid()
uuid3 = new_uuid()
uuid4 = new_uuid()
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<ExternalDataProcessor uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
\t\t\t</xr:ContainedObject>
\t\t\t<xr:GeneratedType name="ExternalDataProcessorObject.{name}" category="Object">
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<DefaultForm/>
\t\t\t<AuxiliaryForm/>
\t\t</Properties>
\t\t<ChildObjects/>
\t</ExternalDataProcessor>
</MetaDataObject>'''
root_file = os.path.join(src_dir, f"{name}.xml")
processor_dir = os.path.join(src_dir, name)
if os.path.exists(root_file):
print(f"Файл уже существует: {root_file}", file=sys.stderr)
sys.exit(1)
os.makedirs(src_dir, exist_ok=True)
ext_dir = os.path.join(processor_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
print(f"[OK] Создана обработка: {root_file}")
print(f" Каталог: {processor_dir}")
print(f" Модуль: {module_path}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# epf-init v1.0 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Init 1C external data processor scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir
uuid1 = new_uuid()
uuid2 = new_uuid()
uuid3 = new_uuid()
uuid4 = new_uuid()
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<ExternalDataProcessor uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
\t\t\t</xr:ContainedObject>
\t\t\t<xr:GeneratedType name="ExternalDataProcessorObject.{name}" category="Object">
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<DefaultForm/>
\t\t\t<AuxiliaryForm/>
\t\t</Properties>
\t\t<ChildObjects/>
\t</ExternalDataProcessor>
</MetaDataObject>'''
root_file = os.path.join(src_dir, f"{name}.xml")
processor_dir = os.path.join(src_dir, name)
if os.path.exists(root_file):
print(f"Файл уже существует: {root_file}", file=sys.stderr)
sys.exit(1)
os.makedirs(src_dir, exist_ok=True)
ext_dir = os.path.join(processor_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
print(f"[OK] Создана обработка: {root_file}")
print(f" Каталог: {processor_dir}")
print(f" Модуль: {module_path}")
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
+166 -165
View File
@@ -1,165 +1,166 @@
#!/usr/bin/env python3
# erf-init v1.0 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
parser = argparse.ArgumentParser(description='Init 1C external report scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir
uuid1 = new_uuid()
uuid2 = new_uuid()
uuid3 = new_uuid()
uuid4 = new_uuid()
# --- Properties ---
main_dcs_value = ""
child_objects_content = ""
if args.WithSKD:
main_dcs_value = f"ExternalReport.{name}.Template.ОсновнаяСхемаКомпоновкиДанных"
child_objects_content = f"\n\t\t\t<Template>ОсновнаяСхемаКомпоновкиДанных</Template>\n"
main_dcs_element = f"<MainDataCompositionSchema>{main_dcs_value}</MainDataCompositionSchema>" if main_dcs_value else "<MainDataCompositionSchema/>"
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<ExternalReport uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
\t\t\t</xr:ContainedObject>
\t\t\t<xr:GeneratedType name="ExternalReportObject.{name}" category="Object">
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<DefaultForm/>
\t\t\t<AuxiliaryForm/>
\t\t\t{main_dcs_element}
\t\t\t<DefaultSettingsForm/>
\t\t\t<AuxiliarySettingsForm/>
\t\t\t<DefaultVariantForm/>
\t\t\t<VariantsStorage/>
\t\t\t<SettingsStorage/>
\t\t</Properties>
\t\t{child_objects_xml}
\t</ExternalReport>
</MetaDataObject>'''
root_file = os.path.join(src_dir, f"{name}.xml")
report_dir = os.path.join(src_dir, name)
if os.path.exists(root_file):
print(f"Файл уже существует: {root_file}", file=sys.stderr)
sys.exit(1)
os.makedirs(src_dir, exist_ok=True)
ext_dir = os.path.join(report_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
print(f"[OK] Создан отчёт: {root_file}")
print(f" Каталог: {report_dir}")
print(f" Модуль: {module_path}")
# --- СКД-макет ---
if args.WithSKD:
templates_dir = os.path.join(report_dir, "Templates")
skd_name = "ОсновнаяСхемаКомпоновкиДанных"
skd_meta_path = os.path.join(templates_dir, f"{skd_name}.xml")
skd_ext_dir = os.path.join(templates_dir, skd_name, "Ext")
os.makedirs(skd_ext_dir, exist_ok=True)
skd_uuid = new_uuid()
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Template uuid="{skd_uuid}">
\t\t<Properties>
\t\t\t<Name>{skd_name}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>Основная схема компоновки данных</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<TemplateType>DataCompositionSchema</TemplateType>
\t\t</Properties>
\t</Template>
</MetaDataObject>'''
write_utf8_bom(skd_meta_path, skd_meta_xml)
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"
\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"
\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
\t<dataSource>
\t\t<name>ИсточникДанных1</name>
\t\t<dataSourceType>Local</dataSourceType>
\t</dataSource>
</DataCompositionSchema>'''
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
write_utf8_bom(skd_file_path, skd_content)
print(f" СКД: {skd_meta_path}")
print(f" Тело: {skd_file_path}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# erf-init v1.0 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid
def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Init 1C external report scaffold', allow_abbrev=False)
parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = parser.parse_args()
name = args.Name
synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir
uuid1 = new_uuid()
uuid2 = new_uuid()
uuid3 = new_uuid()
uuid4 = new_uuid()
# --- Properties ---
main_dcs_value = ""
child_objects_content = ""
if args.WithSKD:
main_dcs_value = f"ExternalReport.{name}.Template.ОсновнаяСхемаКомпоновкиДанных"
child_objects_content = f"\n\t\t\t<Template>ОсновнаяСхемаКомпоновкиДанных</Template>\n"
main_dcs_element = f"<MainDataCompositionSchema>{main_dcs_value}</MainDataCompositionSchema>" if main_dcs_value else "<MainDataCompositionSchema/>"
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<ExternalReport uuid="{uuid1}">
\t\t<InternalInfo>
\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
\t\t\t</xr:ContainedObject>
\t\t\t<xr:GeneratedType name="ExternalReportObject.{name}" category="Object">
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
\t\t\t</xr:GeneratedType>
\t\t</InternalInfo>
\t\t<Properties>
\t\t\t<Name>{esc_xml(name)}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<DefaultForm/>
\t\t\t<AuxiliaryForm/>
\t\t\t{main_dcs_element}
\t\t\t<DefaultSettingsForm/>
\t\t\t<AuxiliarySettingsForm/>
\t\t\t<DefaultVariantForm/>
\t\t\t<VariantsStorage/>
\t\t\t<SettingsStorage/>
\t\t</Properties>
\t\t{child_objects_xml}
\t</ExternalReport>
</MetaDataObject>'''
root_file = os.path.join(src_dir, f"{name}.xml")
report_dir = os.path.join(src_dir, name)
if os.path.exists(root_file):
print(f"Файл уже существует: {root_file}", file=sys.stderr)
sys.exit(1)
os.makedirs(src_dir, exist_ok=True)
ext_dir = os.path.join(report_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта ---
module_bsl = """\
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl)
print(f"[OK] Создан отчёт: {root_file}")
print(f" Каталог: {report_dir}")
print(f" Модуль: {module_path}")
# --- СКД-макет ---
if args.WithSKD:
templates_dir = os.path.join(report_dir, "Templates")
skd_name = "ОсновнаяСхемаКомпоновкиДанных"
skd_meta_path = os.path.join(templates_dir, f"{skd_name}.xml")
skd_ext_dir = os.path.join(templates_dir, skd_name, "Ext")
os.makedirs(skd_ext_dir, exist_ok=True)
skd_uuid = new_uuid()
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
\t<Template uuid="{skd_uuid}">
\t\t<Properties>
\t\t\t<Name>{skd_name}</Name>
\t\t\t<Synonym>
\t\t\t\t<v8:item>
\t\t\t\t\t<v8:lang>ru</v8:lang>
\t\t\t\t\t<v8:content>Основная схема компоновки данных</v8:content>
\t\t\t\t</v8:item>
\t\t\t</Synonym>
\t\t\t<Comment/>
\t\t\t<TemplateType>DataCompositionSchema</TemplateType>
\t\t</Properties>
\t</Template>
</MetaDataObject>'''
write_utf8_bom(skd_meta_path, skd_meta_xml)
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"
\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"
\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
\t<dataSource>
\t\t<name>ИсточникДанных1</name>
\t\t<dataSourceType>Local</dataSourceType>
\t</dataSource>
</DataCompositionSchema>'''
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
write_utf8_bom(skd_file_path, skd_content)
print(f" СКД: {skd_meta_path}")
print(f" Тело: {skd_file_path}")
if __name__ == '__main__':
main()
+447 -446
View File
@@ -1,446 +1,447 @@
#!/usr/bin/env python3
# form-add v1.0 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add managed form to 1C config object", allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Purpose", default="Object")
parser.add_argument("-SetDefault", action="store_true")
args = parser.parse_args()
object_path = args.ObjectPath
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
purpose = args.Purpose
set_default = args.SetDefault
# --- Phase 1: Determine object type ---
if not os.path.exists(object_path):
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
sys.exit(1)
object_xml_full = os.path.abspath(object_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml)
root = tree.getroot()
supported_types = [
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
]
object_type = None
object_node = None
for t in supported_types:
node = root.find(f".//md:{t}", NSMAP)
if node is not None:
object_type = t
object_node = node
break
if object_type is None:
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
sys.exit(1)
# Object name from Properties/Name
name_node = root.find(f".//md:{object_type}/md:Properties/md:Name", NSMAP)
if name_node is None or not name_node.text:
print("Не удалось определить имя объекта из Properties/Name", file=sys.stderr)
sys.exit(1)
object_name = name_node.text
print()
print("=== form-add ===")
print()
print(f"Object: {object_type}.{object_name}")
# --- Phase 2: Validate Purpose ---
# Normalize: capitalize first letter, lowercase rest
purpose = purpose[0].upper() + purpose[1:].lower()
valid_purposes = ["Object", "List", "Choice", "Record"]
if purpose not in valid_purposes:
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
sys.exit(1)
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"]
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
if purpose == "List":
if object_type == "DataProcessor":
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
sys.exit(1)
elif purpose == "Choice":
if object_type in processor_like_types or object_type == "InformationRegister":
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
sys.exit(1)
elif purpose == "Record":
if object_type != "InformationRegister":
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
sys.exit(1)
# --- Phase 3: Create files ---
object_dir = os.path.splitext(object_xml_full)[0]
forms_dir = os.path.join(object_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 3a. Form metadata ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 3b. Form.xml ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
if purpose in ("List", "Choice"):
# Dynamic list
main_table = f"{object_type}.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
'\t\t\t<Type>\n'
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<Settings xsi:type="DynamicList">\n'
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
'\t\t\t</Settings>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
elif purpose == "Record":
# Information register record
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
else:
# Object — object form
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
attr_type_map = {
"Document": "DocumentObject",
"Catalog": "CatalogObject",
"DataProcessor": "DataProcessorObject",
"Report": "ReportObject",
"ExternalDataProcessor": "ExternalDataProcessorObject",
"ExternalReport": "ExternalReportObject",
"ChartOfAccounts": "ChartOfAccountsObject",
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
"ExchangePlan": "ExchangePlanObject",
"BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager",
}
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
else:
write_text_with_bom(form_xml_path, form_xml)
# --- 3c. Module.bsl ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
'\n'
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
if os.path.exists(module_path):
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
else:
write_text_with_bom(module_path, module_bsl)
# --- Phase 4: Register in parent object ---
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(f".//md:{object_type}/md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1)
# Add <Form>$FormName</Form>
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault ---
is_first_form_for_purpose = False
default_prop_name = None
default_value = f"{object_type}.{object_name}.Form.{form_name}"
# Determine property name for DefaultForm
if purpose == "Object":
if object_type in processor_like_types:
default_prop_name = "DefaultForm"
else:
default_prop_name = "DefaultObjectForm"
elif purpose == "List":
default_prop_name = "DefaultListForm"
elif purpose == "Choice":
default_prop_name = "DefaultChoiceForm"
elif purpose == "Record":
default_prop_name = "DefaultRecordForm"
# Check if value is already set
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
if default_node is not None:
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
default_updated = False
if set_default or is_first_form_for_purpose:
if default_node is not None:
default_node.text = default_value
default_updated = True
# Save with BOM
save_xml_with_bom(tree, object_xml_full)
# --- Phase 5: Output ---
obj_dir_name = os.path.dirname(object_path)
obj_base_name = os.path.splitext(os.path.basename(object_path))[0]
print("Created:")
print(f" Metadata: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}.xml")
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated:
print(f"{default_prop_name}: {default_value}")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# form-add v1.0 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.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 ---
if not os.path.exists(object_path):
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
sys.exit(1)
object_xml_full = os.path.abspath(object_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml)
root = tree.getroot()
supported_types = [
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
]
object_type = None
object_node = None
for t in supported_types:
node = root.find(f".//md:{t}", NSMAP)
if node is not None:
object_type = t
object_node = node
break
if object_type is None:
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
sys.exit(1)
# Object name from Properties/Name
name_node = root.find(f".//md:{object_type}/md:Properties/md:Name", NSMAP)
if name_node is None or not name_node.text:
print("Не удалось определить имя объекта из Properties/Name", file=sys.stderr)
sys.exit(1)
object_name = name_node.text
print()
print("=== form-add ===")
print()
print(f"Object: {object_type}.{object_name}")
# --- Phase 2: Validate Purpose ---
# Normalize: capitalize first letter, lowercase rest
purpose = purpose[0].upper() + purpose[1:].lower()
valid_purposes = ["Object", "List", "Choice", "Record"]
if purpose not in valid_purposes:
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
sys.exit(1)
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"]
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
if purpose == "List":
if object_type == "DataProcessor":
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
sys.exit(1)
elif purpose == "Choice":
if object_type in processor_like_types or object_type == "InformationRegister":
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
sys.exit(1)
elif purpose == "Record":
if object_type != "InformationRegister":
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
sys.exit(1)
# --- Phase 3: Create files ---
object_dir = os.path.splitext(object_xml_full)[0]
forms_dir = os.path.join(object_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 3a. Form metadata ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 3b. Form.xml ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
)
if purpose in ("List", "Choice"):
# Dynamic list
main_table = f"{object_type}.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
'\t\t\t<Type>\n'
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<Settings xsi:type="DynamicList">\n'
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
'\t\t\t</Settings>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
elif purpose == "Record":
# Information register record
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
else:
# Object — object form
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
attr_type_map = {
"Document": "DocumentObject",
"Catalog": "CatalogObject",
"DataProcessor": "DataProcessorObject",
"Report": "ReportObject",
"ExternalDataProcessor": "ExternalDataProcessorObject",
"ExternalReport": "ExternalReportObject",
"ChartOfAccounts": "ChartOfAccountsObject",
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
"ExchangePlan": "ExchangePlanObject",
"BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager",
}
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
else:
write_text_with_bom(form_xml_path, form_xml)
# --- 3c. Module.bsl ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
'\n'
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
if os.path.exists(module_path):
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
else:
write_text_with_bom(module_path, module_bsl)
# --- Phase 4: Register in parent object ---
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(f".//md:{object_type}/md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1)
# Add <Form>$FormName</Form>
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault ---
is_first_form_for_purpose = False
default_prop_name = None
default_value = f"{object_type}.{object_name}.Form.{form_name}"
# Determine property name for DefaultForm
if purpose == "Object":
if object_type in processor_like_types:
default_prop_name = "DefaultForm"
else:
default_prop_name = "DefaultObjectForm"
elif purpose == "List":
default_prop_name = "DefaultListForm"
elif purpose == "Choice":
default_prop_name = "DefaultChoiceForm"
elif purpose == "Record":
default_prop_name = "DefaultRecordForm"
# Check if value is already set
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
if default_node is not None:
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
default_updated = False
if set_default or is_first_form_for_purpose:
if default_node is not None:
default_node.text = default_value
default_updated = True
# Save with BOM
save_xml_with_bom(tree, object_xml_full)
# --- Phase 5: Output ---
obj_dir_name = os.path.dirname(object_path)
obj_base_name = os.path.splitext(os.path.basename(object_path))[0]
print("Created:")
print(f" Metadata: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}.xml")
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated:
print(f"{default_prop_name}: {default_value}")
print()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,97 +1,98 @@
#!/usr/bin/env python3
# remove-form v1.0 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
form_name = args.FormName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
form_dir = os.path.join(forms_dir, form_name)
if not os.path.exists(form_meta_path):
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(form_dir):
shutil.rmtree(form_dir)
print(f"[OK] Удалён каталог: {form_dir}")
os.remove(form_meta_path)
print(f"[OK] Удалён файл: {form_meta_path}")
# --- Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
# Remove <Form>FormName</Form> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
if node.text and node.text.strip() == form_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
break
# Clear DefaultForm if it pointed to removed form
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None and default_form.text:
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
default_form.text = ""
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# remove-form v1.0 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
form_name = args.FormName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
form_dir = os.path.join(forms_dir, form_name)
if not os.path.exists(form_meta_path):
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(form_dir):
shutil.rmtree(form_dir)
print(f"[OK] Удалён каталог: {form_dir}")
os.remove(form_meta_path)
print(f"[OK] Удалён файл: {form_meta_path}")
# --- Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
# Remove <Form>FormName</Form> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
if node.text and node.text.strip() == form_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
break
# Clear DefaultForm if it pointed to removed form
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None and default_form.text:
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
default_form.text = ""
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+144 -143
View File
@@ -1,143 +1,144 @@
#!/usr/bin/env python3
# add-help v1.0 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-Lang", default="ru")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
lang = args.Lang
src_dir = args.SrcDir
# --- Checks ---
processor_dir = os.path.join(src_dir, object_name)
ext_dir = os.path.join(processor_dir, "Ext")
if not os.path.isdir(ext_dir):
print(f"Каталог обработки не найден: {ext_dir}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
help_xml_path = os.path.join(ext_dir, "Help.xml")
if os.path.exists(help_xml_path):
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
sys.exit(1)
# --- 1. Help.xml ---
help_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Page>{lang}</Page>\n'
'</Help>'
)
write_text_with_bom(help_xml_path, help_xml)
# --- 2. Help/<lang>.html ---
help_dir = os.path.join(ext_dir, "Help")
os.makedirs(help_dir, exist_ok=True)
help_html_path = os.path.join(help_dir, f"{lang}.html")
help_html = (
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
'<html>\n'
'<head>\n'
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
'</head>\n'
'<body>\n'
f' <h1>{object_name}</h1>\n'
' <p>Описание обработки.</p>\n'
'</body>\n'
'</html>'
)
write_text_with_bom(help_html_path, help_html)
# --- 3. Check IncludeHelpInContents in form metadata ---
forms_dir = os.path.join(processor_dir, "Forms")
if os.path.isdir(forms_dir):
for entry in os.listdir(forms_dir):
if not entry.endswith(".xml"):
continue
form_meta_full = os.path.join(forms_dir, entry)
if not os.path.isfile(form_meta_full):
continue
parser_xml = etree.XMLParser(remove_blank_text=False)
form_tree = etree.parse(form_meta_full, parser_xml)
form_root = form_tree.getroot()
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
if include_help is not None:
continue
# Add after <FormType>
form_type = form_root.find(".//md:FormType", NSMAP)
if form_type is None:
continue
parent = form_type.getparent()
ns = "http://v8.1c.ru/8.3/MDClasses"
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
new_elem.text = "false"
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
parent.remove(new_elem)
# Find index of FormType in parent
form_type_idx = list(parent).index(form_type)
# Insert after FormType
parent.insert(form_type_idx + 1, new_elem)
# Whitespace handling: copy FormType's tail as new_elem's tail,
# and set FormType's tail to include newline + indent
new_elem.tail = form_type.tail
form_type.tail = "\n\t\t\t"
save_xml_with_bom(form_tree, form_meta_full)
print(f" IncludeHelpInContents добавлен: {entry}")
print(f"[OK] Создана справка: {object_name}")
print(f" Метаданные: {help_xml_path}")
print(f" Страница: {help_html_path}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# add-help v1.0 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-Lang", default="ru")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
lang = args.Lang
src_dir = args.SrcDir
# --- Checks ---
processor_dir = os.path.join(src_dir, object_name)
ext_dir = os.path.join(processor_dir, "Ext")
if not os.path.isdir(ext_dir):
print(f"Каталог обработки не найден: {ext_dir}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
help_xml_path = os.path.join(ext_dir, "Help.xml")
if os.path.exists(help_xml_path):
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
sys.exit(1)
# --- 1. Help.xml ---
help_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Page>{lang}</Page>\n'
'</Help>'
)
write_text_with_bom(help_xml_path, help_xml)
# --- 2. Help/<lang>.html ---
help_dir = os.path.join(ext_dir, "Help")
os.makedirs(help_dir, exist_ok=True)
help_html_path = os.path.join(help_dir, f"{lang}.html")
help_html = (
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
'<html>\n'
'<head>\n'
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
'</head>\n'
'<body>\n'
f' <h1>{object_name}</h1>\n'
' <p>Описание обработки.</p>\n'
'</body>\n'
'</html>'
)
write_text_with_bom(help_html_path, help_html)
# --- 3. Check IncludeHelpInContents in form metadata ---
forms_dir = os.path.join(processor_dir, "Forms")
if os.path.isdir(forms_dir):
for entry in os.listdir(forms_dir):
if not entry.endswith(".xml"):
continue
form_meta_full = os.path.join(forms_dir, entry)
if not os.path.isfile(form_meta_full):
continue
parser_xml = etree.XMLParser(remove_blank_text=False)
form_tree = etree.parse(form_meta_full, parser_xml)
form_root = form_tree.getroot()
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
if include_help is not None:
continue
# Add after <FormType>
form_type = form_root.find(".//md:FormType", NSMAP)
if form_type is None:
continue
parent = form_type.getparent()
ns = "http://v8.1c.ru/8.3/MDClasses"
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
new_elem.text = "false"
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
parent.remove(new_elem)
# Find index of FormType in parent
form_type_idx = list(parent).index(form_type)
# Insert after FormType
parent.insert(form_type_idx + 1, new_elem)
# Whitespace handling: copy FormType's tail as new_elem's tail,
# and set FormType's tail to include newline + indent
new_elem.tail = form_type.tail
form_type.tail = "\n\t\t\t"
save_xml_with_bom(form_tree, form_meta_full)
print(f" IncludeHelpInContents добавлен: {entry}")
print(f"[OK] Создана справка: {object_name}")
print(f" Метаданные: {help_xml_path}")
print(f" Страница: {help_html_path}")
if __name__ == "__main__":
main()
+114 -112
View File
@@ -1,112 +1,114 @@
"""Overlay a numbered grid on an image to help determine column/row proportions.
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
The grid helps an LLM count "squares" to determine exact column widths
and positions when analyzing printed forms for MXL template generation.
Numbers are rendered in a dedicated margin band outside the image content,
so they never overlap with the form and remain readable at any grid density.
"""
import argparse
import os
from PIL import Image, ImageDraw, ImageFont
MARGIN_TOP = 20
MARGIN_LEFT = 24
def main():
parser = argparse.ArgumentParser(description="Overlay numbered grid on image")
parser.add_argument("image", help="Input image path")
parser.add_argument("-c", "--cols", type=int, default=50,
help="Number of vertical divisions (default: 50)")
parser.add_argument("-r", "--rows", type=int, default=0,
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args()
src = Image.open(args.image).convert("RGBA")
sw, sh = src.size
cols = args.cols
step_x = sw / cols
rows = args.rows
if rows == 0:
rows = round(sh / step_x)
step_y = sh / rows
# Canvas with margins for labels
cw = MARGIN_LEFT + sw
ch = MARGIN_TOP + sh
canvas = Image.new("RGBA", (cw, ch), (255, 255, 255, 255))
canvas.paste(src, (MARGIN_LEFT, MARGIN_TOP))
overlay = Image.new("RGBA", (cw, ch), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# Font for labels in margin
label_font_size = 12
try:
label_font = ImageFont.truetype("arial.ttf", label_font_size)
except Exception:
label_font = ImageFont.load_default()
# --- Vertical lines + numbers in top margin ---
for i in range(cols + 1):
x = MARGIN_LEFT + round(i * step_x)
major = i % 10 == 0
mid = i % 5 == 0
alpha = 160 if major else (110 if mid else 40)
lw = 2 if major else 1
draw.line([(x, MARGIN_TOP), (x, ch)], fill=(255, 0, 0, alpha), width=lw)
# Labels: always show multiples of 5; show all if spacing allows
show_label = major or mid or step_x >= 20
if show_label:
label = str(i)
bbox = label_font.getbbox(label)
tw = bbox[2] - bbox[0]
tx = x - tw // 2
ty = 2
color = (200, 0, 0, 255) if (major or mid) else (200, 0, 0, 180)
draw.text((tx, ty), label, fill=color, font=label_font)
# --- Horizontal lines + numbers in left margin ---
for j in range(rows + 1):
y = MARGIN_TOP + round(j * step_y)
major = j % 10 == 0
mid = j % 5 == 0
alpha = 160 if major else (110 if mid else 20)
lw = 2 if major else 1
draw.line([(MARGIN_LEFT, y), (cw, y)], fill=(0, 0, 200, alpha), width=lw)
show_label = major or mid or step_y >= 20
if show_label:
label = str(j)
bbox = label_font.getbbox(label)
tw = bbox[2] - bbox[0]
tx = MARGIN_LEFT - tw - 3
ty = y - label_font_size // 2
color = (0, 0, 200, 255) if (major or mid) else (0, 0, 200, 180)
draw.text((tx, ty), label, fill=color, font=label_font)
result = Image.alpha_composite(canvas, overlay).convert("RGB")
if args.output:
out = args.output
else:
name, ext = os.path.splitext(args.image)
out = f"{name}-grid{ext}"
result.save(out)
print(f"Grid: {cols} x {rows} cells")
print(f"Cell size: {step_x:.1f} x {step_y:.1f} px")
print(f"Image: {sw} x {sh} px")
print(f"Saved: {out}")
if __name__ == "__main__":
main()
"""Overlay a numbered grid on an image to help determine column/row proportions.
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
The grid helps an LLM count "squares" to determine exact column widths
and positions when analyzing printed forms for MXL template generation.
Numbers are rendered in a dedicated margin band outside the image content,
so they never overlap with the form and remain readable at any grid density.
"""
import argparse
import os
import sys
from PIL import Image, ImageDraw, ImageFont
MARGIN_TOP = 20
MARGIN_LEFT = 24
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Overlay numbered grid on image")
parser.add_argument("image", help="Input image path")
parser.add_argument("-c", "--cols", type=int, default=50,
help="Number of vertical divisions (default: 50)")
parser.add_argument("-r", "--rows", type=int, default=0,
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args()
src = Image.open(args.image).convert("RGBA")
sw, sh = src.size
cols = args.cols
step_x = sw / cols
rows = args.rows
if rows == 0:
rows = round(sh / step_x)
step_y = sh / rows
# Canvas with margins for labels
cw = MARGIN_LEFT + sw
ch = MARGIN_TOP + sh
canvas = Image.new("RGBA", (cw, ch), (255, 255, 255, 255))
canvas.paste(src, (MARGIN_LEFT, MARGIN_TOP))
overlay = Image.new("RGBA", (cw, ch), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# Font for labels in margin
label_font_size = 12
try:
label_font = ImageFont.truetype("arial.ttf", label_font_size)
except Exception:
label_font = ImageFont.load_default()
# --- Vertical lines + numbers in top margin ---
for i in range(cols + 1):
x = MARGIN_LEFT + round(i * step_x)
major = i % 10 == 0
mid = i % 5 == 0
alpha = 160 if major else (110 if mid else 40)
lw = 2 if major else 1
draw.line([(x, MARGIN_TOP), (x, ch)], fill=(255, 0, 0, alpha), width=lw)
# Labels: always show multiples of 5; show all if spacing allows
show_label = major or mid or step_x >= 20
if show_label:
label = str(i)
bbox = label_font.getbbox(label)
tw = bbox[2] - bbox[0]
tx = x - tw // 2
ty = 2
color = (200, 0, 0, 255) if (major or mid) else (200, 0, 0, 180)
draw.text((tx, ty), label, fill=color, font=label_font)
# --- Horizontal lines + numbers in left margin ---
for j in range(rows + 1):
y = MARGIN_TOP + round(j * step_y)
major = j % 10 == 0
mid = j % 5 == 0
alpha = 160 if major else (110 if mid else 20)
lw = 2 if major else 1
draw.line([(MARGIN_LEFT, y), (cw, y)], fill=(0, 0, 200, alpha), width=lw)
show_label = major or mid or step_y >= 20
if show_label:
label = str(j)
bbox = label_font.getbbox(label)
tw = bbox[2] - bbox[0]
tx = MARGIN_LEFT - tw - 3
ty = y - label_font_size // 2
color = (0, 0, 200, 255) if (major or mid) else (0, 0, 200, 180)
draw.text((tx, ty), label, fill=color, font=label_font)
result = Image.alpha_composite(canvas, overlay).convert("RGB")
if args.output:
out = args.output
else:
name, ext = os.path.splitext(args.image)
out = f"{name}-grid{ext}"
result.save(out)
print(f"Grid: {cols} x {rows} cells")
print(f"Cell size: {step_x:.1f} x {step_y:.1f} px")
print(f"Image: {sw} x {sh} px")
print(f"Saved: {out}")
if __name__ == "__main__":
main()
@@ -1,442 +1,443 @@
#!/usr/bin/env python3
# interface-edit v1.0 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from lxml import etree
CI_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
XS_NS = "http://www.w3.org/2001/XMLSchema"
SECTION_ORDER = ["CommandsVisibility", "CommandsPlacement", "CommandsOrder", "SubsystemsOrder", "GroupsOrder"]
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
for child in container:
if child.tail and "\n" in child.tail:
after_nl = child.tail.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
children = list(container)
if len(children) == 0:
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def remove_with_indent(el):
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
if el.tail:
prev.tail = el.tail
else:
if el.tail:
parent.text = el.tail
parent.remove(el)
def import_ci_fragment(xml_string):
wrapper = (
f'<_W xmlns="{CI_NS}" xmlns:xr="{XR_NS}" '
f'xmlns:xsi="{XSI_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
nodes = []
for child in frag:
nodes.append(child)
return nodes
def parse_value_list(val):
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def find_command_by_name(section, cmd_name):
for child in section:
if isinstance(child.tag, str) and localname(child) == "Command":
if child.get("name") == cmd_name:
return child
return None
def main():
parser = argparse.ArgumentParser(description="Edit 1C CommandInterface.xml", allow_abbrev=False)
parser.add_argument("-CIPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["hide", "show", "place", "order", "subsystem-order", "group-order"])
parser.add_argument("-Value", default=None)
parser.add_argument("-CreateIfMissing", action="store_true")
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Resolve path ---
ci_path = args.CIPath
if not os.path.isabs(ci_path):
ci_path = os.path.join(os.getcwd(), ci_path)
resolved_path = ci_path
# --- Create if missing ---
if not os.path.isfile(ci_path):
if args.CreateIfMissing:
parent_dir = os.path.dirname(ci_path)
if parent_dir and not os.path.isdir(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
empty_ci = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<CommandInterface xmlns="{CI_NS}"\n'
f'\txmlns:xr="{XR_NS}"\n'
f'\txmlns:xs="{XS_NS}"\n'
f'\txmlns:xsi="{XSI_NS}"\n'
f'\tversion="2.17">\n'
f'</CommandInterface>'
)
with open(ci_path, "w", encoding="utf-8-sig") as fh:
fh.write(empty_ci)
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
else:
print(f"File not found: {ci_path} (use -CreateIfMissing to create)", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(ci_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
if localname(root) != "CommandInterface":
print(f"Expected <CommandInterface> root element, got <{localname(root)}>", file=sys.stderr)
sys.exit(1)
def ensure_section(section_name):
# Find existing
for child in root:
if isinstance(child.tag, str) and localname(child) == section_name:
return child
# Create new section
new_section = etree.Element(f"{{{CI_NS}}}{section_name}")
my_idx = SECTION_ORDER.index(section_name) if section_name in SECTION_ORDER else -1
ref_node = None
for child in root:
if not isinstance(child.tag, str):
continue
child_idx = SECTION_ORDER.index(localname(child)) if localname(child) in SECTION_ORDER else -1
if child_idx > my_idx:
ref_node = child
break
root_indent = get_child_indent(root)
new_section.text = "\r\n" + root_indent
if ref_node is not None:
# Insert before ref_node
idx = list(root).index(ref_node)
new_section.tail = "\r\n" + root_indent
root.insert(idx, new_section)
else:
insert_before_closing(root, new_section, root_indent)
return new_section
def do_hide(commands):
nonlocal add_count, modify_count
section = ensure_section("CommandsVisibility")
section_indent = get_child_indent(section)
for cmd in commands:
existing = find_command_by_name(section, cmd)
if existing is not None:
common_el = None
for vis in existing:
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
for c in vis:
if isinstance(c.tag, str) and localname(c) == "Common":
common_el = c
break
if common_el is not None and (common_el.text or "").strip() == "false":
warn(f"Already hidden: {cmd}")
continue
if common_el is not None:
common_el.text = "false"
modify_count += 1
info(f"Changed to hidden: {cmd}")
continue
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>false</xr:Common></Visibility></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Hidden: {cmd}")
def do_show(commands):
nonlocal add_count, modify_count
section = None
for child in root:
if isinstance(child.tag, str) and localname(child) == "CommandsVisibility":
section = child
break
for cmd in commands:
if section is None:
section = ensure_section("CommandsVisibility")
existing = find_command_by_name(section, cmd)
if existing is not None:
common_el = None
for vis in existing:
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
for c in vis:
if isinstance(c.tag, str) and localname(c) == "Common":
common_el = c
break
if common_el is not None and (common_el.text or "").strip() == "true":
warn(f"Already shown: {cmd}")
continue
if common_el is not None and (common_el.text or "").strip() == "false":
common_el.text = "true"
modify_count += 1
info(f"Changed to shown: {cmd}")
continue
section_indent = get_child_indent(section)
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>true</xr:Common></Visibility></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Shown: {cmd}")
def do_place(json_val):
nonlocal add_count, modify_count
defn = json.loads(json_val)
cmd_name = str(defn["command"])
group_name = str(defn["group"])
if not cmd_name or not group_name:
print("place requires {command, group}", file=sys.stderr)
sys.exit(1)
section = ensure_section("CommandsPlacement")
section_indent = get_child_indent(section)
existing = find_command_by_name(section, cmd_name)
if existing is not None:
for child in existing:
if isinstance(child.tag, str) and localname(child) == "CommandGroup":
child.text = group_name
modify_count += 1
info(f"Updated placement: {cmd_name} -> {group_name}")
return
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup><Placement>Auto</Placement></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Placed: {cmd_name} -> {group_name}")
def do_order(json_val):
nonlocal add_count, remove_count
defn = json.loads(json_val)
group_name = str(defn["group"])
commands = [str(c) for c in defn["commands"]]
if not group_name or not commands:
print("order requires {group, commands:[...]}", file=sys.stderr)
sys.exit(1)
section = ensure_section("CommandsOrder")
section_indent = get_child_indent(section)
# Remove existing entries for this group
to_remove = []
for child in section:
if not isinstance(child.tag, str) or localname(child) != "Command":
continue
for gc in child:
if isinstance(gc.tag, str) and localname(gc) == "CommandGroup" and (gc.text or "").strip() == group_name:
to_remove.append(child)
break
for node in to_remove:
remove_with_indent(node)
remove_count += 1
# Add new entries
for cmd_name in commands:
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Set order for {group_name} : {len(commands)} commands")
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
sys.exit(1)
section = ensure_section("SubsystemsOrder")
section_indent = get_child_indent(section)
# Clear existing
for child in list(section):
if isinstance(child.tag, str):
remove_with_indent(child)
remove_count += 1
# Add new entries
for sub in subsystems:
new_el = etree.Element(f"{{{CI_NS}}}Subsystem")
new_el.text = sub
insert_before_closing(section, new_el, section_indent)
add_count += 1
info(f"Set subsystem order: {len(subsystems)} entries")
def do_group_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
sys.exit(1)
section = ensure_section("GroupsOrder")
section_indent = get_child_indent(section)
# Clear existing
for child in list(section):
if isinstance(child.tag, str):
remove_with_indent(child)
remove_count += 1
# Add new entries
for grp in groups:
new_el = etree.Element(f"{{{CI_NS}}}Group")
new_el.text = grp
insert_before_closing(section, new_el, section_indent)
add_count += 1
info(f"Set group order: {len(groups)} entries")
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "hide":
do_hide(parse_value_list(op_value))
elif op_name == "show":
do_show(parse_value_list(op_value))
elif op_name == "place":
do_place(op_value)
elif op_name == "order":
do_order(op_value)
elif op_name == "subsystem-order":
do_subsystem_order(op_value)
elif op_name == "group-order":
do_group_order(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "interface-validate", "scripts", "interface-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
# --- Summary ---
print()
print("=== interface-edit summary ===")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# interface-edit v1.0 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from lxml import etree
CI_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
XS_NS = "http://www.w3.org/2001/XMLSchema"
SECTION_ORDER = ["CommandsVisibility", "CommandsPlacement", "CommandsOrder", "SubsystemsOrder", "GroupsOrder"]
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
for child in container:
if child.tail and "\n" in child.tail:
after_nl = child.tail.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
children = list(container)
if len(children) == 0:
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def remove_with_indent(el):
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
if el.tail:
prev.tail = el.tail
else:
if el.tail:
parent.text = el.tail
parent.remove(el)
def import_ci_fragment(xml_string):
wrapper = (
f'<_W xmlns="{CI_NS}" xmlns:xr="{XR_NS}" '
f'xmlns:xsi="{XSI_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
nodes = []
for child in frag:
nodes.append(child)
return nodes
def parse_value_list(val):
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def find_command_by_name(section, cmd_name):
for child in section:
if isinstance(child.tag, str) and localname(child) == "Command":
if child.get("name") == cmd_name:
return child
return None
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Edit 1C CommandInterface.xml", allow_abbrev=False)
parser.add_argument("-CIPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["hide", "show", "place", "order", "subsystem-order", "group-order"])
parser.add_argument("-Value", default=None)
parser.add_argument("-CreateIfMissing", action="store_true")
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Resolve path ---
ci_path = args.CIPath
if not os.path.isabs(ci_path):
ci_path = os.path.join(os.getcwd(), ci_path)
resolved_path = ci_path
# --- Create if missing ---
if not os.path.isfile(ci_path):
if args.CreateIfMissing:
parent_dir = os.path.dirname(ci_path)
if parent_dir and not os.path.isdir(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
empty_ci = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<CommandInterface xmlns="{CI_NS}"\n'
f'\txmlns:xr="{XR_NS}"\n'
f'\txmlns:xs="{XS_NS}"\n'
f'\txmlns:xsi="{XSI_NS}"\n'
f'\tversion="2.17">\n'
f'</CommandInterface>'
)
with open(ci_path, "w", encoding="utf-8-sig") as fh:
fh.write(empty_ci)
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
else:
print(f"File not found: {ci_path} (use -CreateIfMissing to create)", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(ci_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
if localname(root) != "CommandInterface":
print(f"Expected <CommandInterface> root element, got <{localname(root)}>", file=sys.stderr)
sys.exit(1)
def ensure_section(section_name):
# Find existing
for child in root:
if isinstance(child.tag, str) and localname(child) == section_name:
return child
# Create new section
new_section = etree.Element(f"{{{CI_NS}}}{section_name}")
my_idx = SECTION_ORDER.index(section_name) if section_name in SECTION_ORDER else -1
ref_node = None
for child in root:
if not isinstance(child.tag, str):
continue
child_idx = SECTION_ORDER.index(localname(child)) if localname(child) in SECTION_ORDER else -1
if child_idx > my_idx:
ref_node = child
break
root_indent = get_child_indent(root)
new_section.text = "\r\n" + root_indent
if ref_node is not None:
# Insert before ref_node
idx = list(root).index(ref_node)
new_section.tail = "\r\n" + root_indent
root.insert(idx, new_section)
else:
insert_before_closing(root, new_section, root_indent)
return new_section
def do_hide(commands):
nonlocal add_count, modify_count
section = ensure_section("CommandsVisibility")
section_indent = get_child_indent(section)
for cmd in commands:
existing = find_command_by_name(section, cmd)
if existing is not None:
common_el = None
for vis in existing:
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
for c in vis:
if isinstance(c.tag, str) and localname(c) == "Common":
common_el = c
break
if common_el is not None and (common_el.text or "").strip() == "false":
warn(f"Already hidden: {cmd}")
continue
if common_el is not None:
common_el.text = "false"
modify_count += 1
info(f"Changed to hidden: {cmd}")
continue
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>false</xr:Common></Visibility></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Hidden: {cmd}")
def do_show(commands):
nonlocal add_count, modify_count
section = None
for child in root:
if isinstance(child.tag, str) and localname(child) == "CommandsVisibility":
section = child
break
for cmd in commands:
if section is None:
section = ensure_section("CommandsVisibility")
existing = find_command_by_name(section, cmd)
if existing is not None:
common_el = None
for vis in existing:
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
for c in vis:
if isinstance(c.tag, str) and localname(c) == "Common":
common_el = c
break
if common_el is not None and (common_el.text or "").strip() == "true":
warn(f"Already shown: {cmd}")
continue
if common_el is not None and (common_el.text or "").strip() == "false":
common_el.text = "true"
modify_count += 1
info(f"Changed to shown: {cmd}")
continue
section_indent = get_child_indent(section)
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>true</xr:Common></Visibility></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Shown: {cmd}")
def do_place(json_val):
nonlocal add_count, modify_count
defn = json.loads(json_val)
cmd_name = str(defn["command"])
group_name = str(defn["group"])
if not cmd_name or not group_name:
print("place requires {command, group}", file=sys.stderr)
sys.exit(1)
section = ensure_section("CommandsPlacement")
section_indent = get_child_indent(section)
existing = find_command_by_name(section, cmd_name)
if existing is not None:
for child in existing:
if isinstance(child.tag, str) and localname(child) == "CommandGroup":
child.text = group_name
modify_count += 1
info(f"Updated placement: {cmd_name} -> {group_name}")
return
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup><Placement>Auto</Placement></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Placed: {cmd_name} -> {group_name}")
def do_order(json_val):
nonlocal add_count, remove_count
defn = json.loads(json_val)
group_name = str(defn["group"])
commands = [str(c) for c in defn["commands"]]
if not group_name or not commands:
print("order requires {group, commands:[...]}", file=sys.stderr)
sys.exit(1)
section = ensure_section("CommandsOrder")
section_indent = get_child_indent(section)
# Remove existing entries for this group
to_remove = []
for child in section:
if not isinstance(child.tag, str) or localname(child) != "Command":
continue
for gc in child:
if isinstance(gc.tag, str) and localname(gc) == "CommandGroup" and (gc.text or "").strip() == group_name:
to_remove.append(child)
break
for node in to_remove:
remove_with_indent(node)
remove_count += 1
# Add new entries
for cmd_name in commands:
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup></Command>'
nodes = import_ci_fragment(frag_xml)
if nodes:
insert_before_closing(section, nodes[0], section_indent)
add_count += 1
info(f"Set order for {group_name} : {len(commands)} commands")
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
sys.exit(1)
section = ensure_section("SubsystemsOrder")
section_indent = get_child_indent(section)
# Clear existing
for child in list(section):
if isinstance(child.tag, str):
remove_with_indent(child)
remove_count += 1
# Add new entries
for sub in subsystems:
new_el = etree.Element(f"{{{CI_NS}}}Subsystem")
new_el.text = sub
insert_before_closing(section, new_el, section_indent)
add_count += 1
info(f"Set subsystem order: {len(subsystems)} entries")
def do_group_order(json_val):
nonlocal add_count, remove_count
parsed = json.loads(json_val)
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
sys.exit(1)
section = ensure_section("GroupsOrder")
section_indent = get_child_indent(section)
# Clear existing
for child in list(section):
if isinstance(child.tag, str):
remove_with_indent(child)
remove_count += 1
# Add new entries
for grp in groups:
new_el = etree.Element(f"{{{CI_NS}}}Group")
new_el.text = grp
insert_before_closing(section, new_el, section_indent)
add_count += 1
info(f"Set group order: {len(groups)} entries")
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "hide":
do_hide(parse_value_list(op_value))
elif op_name == "show":
do_show(parse_value_list(op_value))
elif op_name == "place":
do_place(op_value)
elif op_name == "order":
do_order(op_value)
elif op_name == "subsystem-order":
do_subsystem_order(op_value)
elif op_name == "group-order":
do_group_order(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "interface-validate", "scripts", "interface-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
# --- Summary ---
print()
print("=== interface-edit summary ===")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
@@ -1,390 +1,391 @@
#!/usr/bin/env python3
# interface-validate v1.0 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
import sys, os, argparse, re
from lxml import etree
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS_XS = 'http://www.w3.org/2001/XMLSchema'
NS = {
'ci': NS_CI,
'xr': NS_XR,
'xsi': NS_XSI,
'xs': NS_XS,
}
VALID_SECTIONS = [
'CommandsVisibility', 'CommandsPlacement', 'CommandsOrder',
'SubsystemsOrder', 'GroupsOrder'
]
STD_CMD_PATTERN = re.compile(r'^[A-Za-z]+\.[^\s\.]+\.StandardCommand\.\w+$')
CUSTOM_CMD_PATTERN = re.compile(r'^[A-Za-z]+\.[^\s\.]+\.Command\.\w+$')
COMMON_CMD_PATTERN = re.compile(r'^CommonCommand\.\w+$')
UUID_CMD_PATTERN = re.compile(
r'^0:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def find_duplicates(items):
seen = {}
dupes = []
for item in items:
seen[item] = seen.get(item, 0) + 1
for item, count in seen.items():
if count > 1 and item not in dupes:
dupes.append(item)
return dupes
def main():
parser = argparse.ArgumentParser(
description='Validate 1C CommandInterface.xml structure', allow_abbrev=False
)
parser.add_argument('-CIPath', dest='CIPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
ci_path = args.CIPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(ci_path):
ci_path = os.path.join(os.getcwd(), ci_path)
if not os.path.exists(ci_path):
print(f'[ERROR] File not found: {ci_path}')
sys.exit(1)
resolved_path = os.path.abspath(ci_path)
# --- Derive context name from path ---
context_name = ''
parts = re.split(r'[/\\]', resolved_path)
for i in range(len(parts)):
if parts[i] == 'Subsystems' and (i + 1) < len(parts):
context_name = parts[i + 1]
if not context_name:
context_name = 'Root'
r = Reporter(max_errors)
all_command_names = []
r.out(f'=== Validation: CommandInterface ({context_name}) ===')
r.out('')
# --- 1. XML well-formedness + root structure ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.error(f'1. XML parse error: {e}')
r.stopped = True
root = None
if not r.stopped:
root = xml_doc.getroot()
root_local = etree.QName(root.tag).localname
if root_local != 'CommandInterface':
r.error(f'1. Root element: expected <CommandInterface>, got <{root_local}>')
r.stopped = True
else:
ns_uri = etree.QName(root.tag).namespace or ''
version = root.get('version', '')
expected_ns = NS_CI
if ns_uri != expected_ns:
r.error(f'1. Root namespace: expected {expected_ns}, got {ns_uri}')
elif not version:
r.warn('1. Root structure: CommandInterface, namespace valid, but no version attribute')
else:
r.ok(f'1. Root structure: CommandInterface, version {version}, namespace valid')
# --- 2. Valid child elements ---
found_sections = []
if not r.stopped:
invalid_elements = []
for child in root:
if not isinstance(child.tag, str):
continue
local_name = etree.QName(child.tag).localname
if local_name in VALID_SECTIONS:
found_sections.append(local_name)
else:
invalid_elements.append(local_name)
if len(invalid_elements) > 0:
r.error(f'2. Invalid child elements: {", ".join(invalid_elements)}')
else:
r.ok(f'2. Child elements: {len(found_sections)} valid sections')
# --- 3. Section order ---
if not r.stopped:
order_ok = True
last_idx = -1
for sec in found_sections:
idx = VALID_SECTIONS.index(sec) if sec in VALID_SECTIONS else -1
if idx < last_idx:
r.error(f"3. Section order: '{sec}' appears after a later section (expected: CommandsVisibility -> CommandsPlacement -> CommandsOrder -> SubsystemsOrder -> GroupsOrder)")
order_ok = False
break
last_idx = idx
if order_ok:
r.ok('3. Section order: correct')
# --- 4. No duplicate sections ---
if not r.stopped:
dupes = find_duplicates(found_sections)
if dupes:
r.error(f'4. Duplicate sections: {", ".join(dupes)}')
else:
r.ok('4. No duplicate sections')
# --- 5. CommandsVisibility ---
vis_names = []
if not r.stopped:
vis_section = root.find(f'{{{NS_CI}}}CommandsVisibility')
if vis_section is not None:
vis_ok = True
vis_count = 0
for cmd in vis_section:
if not isinstance(cmd.tag, str):
continue
vis_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("5. CommandsVisibility: Command element without 'name' attribute")
vis_ok = False
continue
vis_names.append(cmd_name)
all_command_names.append(cmd_name)
visibility = cmd.find(f'{{{NS_CI}}}Visibility')
if visibility is None:
r.error(f'5. CommandsVisibility[{cmd_name}]: missing <Visibility>')
vis_ok = False
continue
common = visibility.find(f'{{{NS_XR}}}Common')
if common is None:
r.error(f'5. CommandsVisibility[{cmd_name}]: missing <xr:Common>')
vis_ok = False
continue
val = (common.text or '').strip()
if val not in ('true', 'false'):
r.error(f"5. CommandsVisibility[{cmd_name}]: xr:Common='{val}' (expected true/false)")
vis_ok = False
if vis_ok:
r.ok(f'5. CommandsVisibility: {vis_count} entries, all valid')
else:
r.ok('5. CommandsVisibility: not present')
# --- 6. CommandsVisibility duplicates ---
if not r.stopped:
if len(vis_names) > 0:
dupes = find_duplicates(vis_names)
if dupes:
r.warn(f'6. CommandsVisibility: duplicates: {", ".join(dupes)}')
else:
r.ok('6. CommandsVisibility: no duplicates')
else:
r.ok('6. CommandsVisibility: no duplicates (empty)')
# --- 7. CommandsPlacement ---
if not r.stopped:
plc_section = root.find(f'{{{NS_CI}}}CommandsPlacement')
if plc_section is not None:
plc_ok = True
plc_count = 0
for cmd in plc_section:
if not isinstance(cmd.tag, str):
continue
plc_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("7. CommandsPlacement: Command without 'name' attribute")
plc_ok = False
continue
all_command_names.append(cmd_name)
grp_el = cmd.find(f'{{{NS_CI}}}CommandGroup')
if grp_el is None or not (grp_el.text or '').strip():
r.error(f'7. CommandsPlacement[{cmd_name}]: missing or empty <CommandGroup>')
plc_ok = False
continue
placement_el = cmd.find(f'{{{NS_CI}}}Placement')
if placement_el is None:
r.error(f'7. CommandsPlacement[{cmd_name}]: missing <Placement>')
plc_ok = False
elif (placement_el.text or '').strip() != 'Auto':
r.warn(f"7. CommandsPlacement[{cmd_name}]: Placement='{(placement_el.text or '').strip()}' (expected Auto)")
if plc_ok:
r.ok(f'7. CommandsPlacement: {plc_count} entries, all valid')
else:
r.ok('7. CommandsPlacement: not present')
# --- 8. CommandsOrder ---
if not r.stopped:
ord_section = root.find(f'{{{NS_CI}}}CommandsOrder')
if ord_section is not None:
ord_ok = True
ord_count = 0
for cmd in ord_section:
if not isinstance(cmd.tag, str):
continue
ord_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("8. CommandsOrder: Command without 'name' attribute")
ord_ok = False
continue
all_command_names.append(cmd_name)
grp_el = cmd.find(f'{{{NS_CI}}}CommandGroup')
if grp_el is None or not (grp_el.text or '').strip():
r.error(f'8. CommandsOrder[{cmd_name}]: missing or empty <CommandGroup>')
ord_ok = False
if ord_ok:
r.ok(f'8. CommandsOrder: {ord_count} entries, all valid')
else:
r.ok('8. CommandsOrder: not present')
# --- 9. SubsystemsOrder format ---
sub_names = []
if not r.stopped:
sub_section = root.find(f'{{{NS_CI}}}SubsystemsOrder')
if sub_section is not None:
sub_ok = True
sub_count = 0
for sub_el in sub_section:
if not isinstance(sub_el.tag, str):
continue
sub_count += 1
text = (sub_el.text or '').strip()
sub_names.append(text)
if not text:
r.error('9. SubsystemsOrder: empty <Subsystem> element')
sub_ok = False
elif not text.startswith('Subsystem.'):
r.error(f"9. SubsystemsOrder: '{text}' - expected format Subsystem.X...")
sub_ok = False
if sub_ok:
r.ok(f'9. SubsystemsOrder: {sub_count} entries, all valid format')
else:
r.ok('9. SubsystemsOrder: not present')
# --- 10. SubsystemsOrder duplicates ---
if not r.stopped:
if len(sub_names) > 0:
dupes = find_duplicates(sub_names)
if dupes:
r.warn(f'10. SubsystemsOrder: duplicates: {", ".join(dupes)}')
else:
r.ok('10. SubsystemsOrder: no duplicates')
else:
r.ok('10. SubsystemsOrder: no duplicates (empty)')
# --- 11. GroupsOrder entries ---
grp_names = []
if not r.stopped:
grp_section = root.find(f'{{{NS_CI}}}GroupsOrder')
if grp_section is not None:
grp_ok = True
grp_count = 0
for grp in grp_section:
if not isinstance(grp.tag, str):
continue
grp_count += 1
text = (grp.text or '').strip()
grp_names.append(text)
if not text:
r.error('11. GroupsOrder: empty <Group> element')
grp_ok = False
if grp_ok:
r.ok(f'11. GroupsOrder: {grp_count} entries, all valid')
else:
r.ok('11. GroupsOrder: not present')
# --- 12. GroupsOrder duplicates ---
if not r.stopped:
if len(grp_names) > 0:
dupes = find_duplicates(grp_names)
if dupes:
r.warn(f'12. GroupsOrder: duplicates: {", ".join(dupes)}')
else:
r.ok('12. GroupsOrder: no duplicates')
else:
r.ok('12. GroupsOrder: no duplicates (empty)')
# --- 13. Command reference format ---
if not r.stopped:
if len(all_command_names) > 0:
bad_refs = []
for ref in all_command_names:
if STD_CMD_PATTERN.match(ref):
continue
if CUSTOM_CMD_PATTERN.match(ref):
continue
if COMMON_CMD_PATTERN.match(ref):
continue
if UUID_CMD_PATTERN.match(ref):
continue
bad_refs.append(ref)
if len(bad_refs) == 0:
r.ok(f'13. Command reference format: all {len(all_command_names)} valid')
else:
shown = bad_refs[:5]
suffix = ' ...' if len(bad_refs) > 5 else ''
r.warn(f'13. Command reference format: {len(bad_refs)} unrecognized: {", ".join(shown)}{suffix}')
else:
r.ok('13. Command reference format: n/a (no commands)')
# --- Finalize ---
r.out('---')
r.out(f'Errors: {r.errors}, Warnings: {r.warnings}')
result = r.text()
print(result, end='')
if out_file:
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# interface-validate v1.0 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
import sys, os, argparse, re
from lxml import etree
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS_XS = 'http://www.w3.org/2001/XMLSchema'
NS = {
'ci': NS_CI,
'xr': NS_XR,
'xsi': NS_XSI,
'xs': NS_XS,
}
VALID_SECTIONS = [
'CommandsVisibility', 'CommandsPlacement', 'CommandsOrder',
'SubsystemsOrder', 'GroupsOrder'
]
STD_CMD_PATTERN = re.compile(r'^[A-Za-z]+\.[^\s\.]+\.StandardCommand\.\w+$')
CUSTOM_CMD_PATTERN = re.compile(r'^[A-Za-z]+\.[^\s\.]+\.Command\.\w+$')
COMMON_CMD_PATTERN = re.compile(r'^CommonCommand\.\w+$')
UUID_CMD_PATTERN = re.compile(
r'^0:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def find_duplicates(items):
seen = {}
dupes = []
for item in items:
seen[item] = seen.get(item, 0) + 1
for item, count in seen.items():
if count > 1 and item not in dupes:
dupes.append(item)
return dupes
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description='Validate 1C CommandInterface.xml structure', allow_abbrev=False
)
parser.add_argument('-CIPath', dest='CIPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
ci_path = args.CIPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(ci_path):
ci_path = os.path.join(os.getcwd(), ci_path)
if not os.path.exists(ci_path):
print(f'[ERROR] File not found: {ci_path}')
sys.exit(1)
resolved_path = os.path.abspath(ci_path)
# --- Derive context name from path ---
context_name = ''
parts = re.split(r'[/\\]', resolved_path)
for i in range(len(parts)):
if parts[i] == 'Subsystems' and (i + 1) < len(parts):
context_name = parts[i + 1]
if not context_name:
context_name = 'Root'
r = Reporter(max_errors)
all_command_names = []
r.out(f'=== Validation: CommandInterface ({context_name}) ===')
r.out('')
# --- 1. XML well-formedness + root structure ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.error(f'1. XML parse error: {e}')
r.stopped = True
root = None
if not r.stopped:
root = xml_doc.getroot()
root_local = etree.QName(root.tag).localname
if root_local != 'CommandInterface':
r.error(f'1. Root element: expected <CommandInterface>, got <{root_local}>')
r.stopped = True
else:
ns_uri = etree.QName(root.tag).namespace or ''
version = root.get('version', '')
expected_ns = NS_CI
if ns_uri != expected_ns:
r.error(f'1. Root namespace: expected {expected_ns}, got {ns_uri}')
elif not version:
r.warn('1. Root structure: CommandInterface, namespace valid, but no version attribute')
else:
r.ok(f'1. Root structure: CommandInterface, version {version}, namespace valid')
# --- 2. Valid child elements ---
found_sections = []
if not r.stopped:
invalid_elements = []
for child in root:
if not isinstance(child.tag, str):
continue
local_name = etree.QName(child.tag).localname
if local_name in VALID_SECTIONS:
found_sections.append(local_name)
else:
invalid_elements.append(local_name)
if len(invalid_elements) > 0:
r.error(f'2. Invalid child elements: {", ".join(invalid_elements)}')
else:
r.ok(f'2. Child elements: {len(found_sections)} valid sections')
# --- 3. Section order ---
if not r.stopped:
order_ok = True
last_idx = -1
for sec in found_sections:
idx = VALID_SECTIONS.index(sec) if sec in VALID_SECTIONS else -1
if idx < last_idx:
r.error(f"3. Section order: '{sec}' appears after a later section (expected: CommandsVisibility -> CommandsPlacement -> CommandsOrder -> SubsystemsOrder -> GroupsOrder)")
order_ok = False
break
last_idx = idx
if order_ok:
r.ok('3. Section order: correct')
# --- 4. No duplicate sections ---
if not r.stopped:
dupes = find_duplicates(found_sections)
if dupes:
r.error(f'4. Duplicate sections: {", ".join(dupes)}')
else:
r.ok('4. No duplicate sections')
# --- 5. CommandsVisibility ---
vis_names = []
if not r.stopped:
vis_section = root.find(f'{{{NS_CI}}}CommandsVisibility')
if vis_section is not None:
vis_ok = True
vis_count = 0
for cmd in vis_section:
if not isinstance(cmd.tag, str):
continue
vis_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("5. CommandsVisibility: Command element without 'name' attribute")
vis_ok = False
continue
vis_names.append(cmd_name)
all_command_names.append(cmd_name)
visibility = cmd.find(f'{{{NS_CI}}}Visibility')
if visibility is None:
r.error(f'5. CommandsVisibility[{cmd_name}]: missing <Visibility>')
vis_ok = False
continue
common = visibility.find(f'{{{NS_XR}}}Common')
if common is None:
r.error(f'5. CommandsVisibility[{cmd_name}]: missing <xr:Common>')
vis_ok = False
continue
val = (common.text or '').strip()
if val not in ('true', 'false'):
r.error(f"5. CommandsVisibility[{cmd_name}]: xr:Common='{val}' (expected true/false)")
vis_ok = False
if vis_ok:
r.ok(f'5. CommandsVisibility: {vis_count} entries, all valid')
else:
r.ok('5. CommandsVisibility: not present')
# --- 6. CommandsVisibility duplicates ---
if not r.stopped:
if len(vis_names) > 0:
dupes = find_duplicates(vis_names)
if dupes:
r.warn(f'6. CommandsVisibility: duplicates: {", ".join(dupes)}')
else:
r.ok('6. CommandsVisibility: no duplicates')
else:
r.ok('6. CommandsVisibility: no duplicates (empty)')
# --- 7. CommandsPlacement ---
if not r.stopped:
plc_section = root.find(f'{{{NS_CI}}}CommandsPlacement')
if plc_section is not None:
plc_ok = True
plc_count = 0
for cmd in plc_section:
if not isinstance(cmd.tag, str):
continue
plc_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("7. CommandsPlacement: Command without 'name' attribute")
plc_ok = False
continue
all_command_names.append(cmd_name)
grp_el = cmd.find(f'{{{NS_CI}}}CommandGroup')
if grp_el is None or not (grp_el.text or '').strip():
r.error(f'7. CommandsPlacement[{cmd_name}]: missing or empty <CommandGroup>')
plc_ok = False
continue
placement_el = cmd.find(f'{{{NS_CI}}}Placement')
if placement_el is None:
r.error(f'7. CommandsPlacement[{cmd_name}]: missing <Placement>')
plc_ok = False
elif (placement_el.text or '').strip() != 'Auto':
r.warn(f"7. CommandsPlacement[{cmd_name}]: Placement='{(placement_el.text or '').strip()}' (expected Auto)")
if plc_ok:
r.ok(f'7. CommandsPlacement: {plc_count} entries, all valid')
else:
r.ok('7. CommandsPlacement: not present')
# --- 8. CommandsOrder ---
if not r.stopped:
ord_section = root.find(f'{{{NS_CI}}}CommandsOrder')
if ord_section is not None:
ord_ok = True
ord_count = 0
for cmd in ord_section:
if not isinstance(cmd.tag, str):
continue
ord_count += 1
cmd_name = cmd.get('name', '')
if not cmd_name:
r.error("8. CommandsOrder: Command without 'name' attribute")
ord_ok = False
continue
all_command_names.append(cmd_name)
grp_el = cmd.find(f'{{{NS_CI}}}CommandGroup')
if grp_el is None or not (grp_el.text or '').strip():
r.error(f'8. CommandsOrder[{cmd_name}]: missing or empty <CommandGroup>')
ord_ok = False
if ord_ok:
r.ok(f'8. CommandsOrder: {ord_count} entries, all valid')
else:
r.ok('8. CommandsOrder: not present')
# --- 9. SubsystemsOrder format ---
sub_names = []
if not r.stopped:
sub_section = root.find(f'{{{NS_CI}}}SubsystemsOrder')
if sub_section is not None:
sub_ok = True
sub_count = 0
for sub_el in sub_section:
if not isinstance(sub_el.tag, str):
continue
sub_count += 1
text = (sub_el.text or '').strip()
sub_names.append(text)
if not text:
r.error('9. SubsystemsOrder: empty <Subsystem> element')
sub_ok = False
elif not text.startswith('Subsystem.'):
r.error(f"9. SubsystemsOrder: '{text}' - expected format Subsystem.X...")
sub_ok = False
if sub_ok:
r.ok(f'9. SubsystemsOrder: {sub_count} entries, all valid format')
else:
r.ok('9. SubsystemsOrder: not present')
# --- 10. SubsystemsOrder duplicates ---
if not r.stopped:
if len(sub_names) > 0:
dupes = find_duplicates(sub_names)
if dupes:
r.warn(f'10. SubsystemsOrder: duplicates: {", ".join(dupes)}')
else:
r.ok('10. SubsystemsOrder: no duplicates')
else:
r.ok('10. SubsystemsOrder: no duplicates (empty)')
# --- 11. GroupsOrder entries ---
grp_names = []
if not r.stopped:
grp_section = root.find(f'{{{NS_CI}}}GroupsOrder')
if grp_section is not None:
grp_ok = True
grp_count = 0
for grp in grp_section:
if not isinstance(grp.tag, str):
continue
grp_count += 1
text = (grp.text or '').strip()
grp_names.append(text)
if not text:
r.error('11. GroupsOrder: empty <Group> element')
grp_ok = False
if grp_ok:
r.ok(f'11. GroupsOrder: {grp_count} entries, all valid')
else:
r.ok('11. GroupsOrder: not present')
# --- 12. GroupsOrder duplicates ---
if not r.stopped:
if len(grp_names) > 0:
dupes = find_duplicates(grp_names)
if dupes:
r.warn(f'12. GroupsOrder: duplicates: {", ".join(dupes)}')
else:
r.ok('12. GroupsOrder: no duplicates')
else:
r.ok('12. GroupsOrder: no duplicates (empty)')
# --- 13. Command reference format ---
if not r.stopped:
if len(all_command_names) > 0:
bad_refs = []
for ref in all_command_names:
if STD_CMD_PATTERN.match(ref):
continue
if CUSTOM_CMD_PATTERN.match(ref):
continue
if COMMON_CMD_PATTERN.match(ref):
continue
if UUID_CMD_PATTERN.match(ref):
continue
bad_refs.append(ref)
if len(bad_refs) == 0:
r.ok(f'13. Command reference format: all {len(all_command_names)} valid')
else:
shown = bad_refs[:5]
suffix = ' ...' if len(bad_refs) > 5 else ''
r.warn(f'13. Command reference format: {len(bad_refs)} unrecognized: {", ".join(shown)}{suffix}')
else:
r.ok('13. Command reference format: n/a (no commands)')
# --- Finalize ---
r.out('---')
r.out(f'Errors: {r.errors}, Warnings: {r.warnings}')
result = r.text()
print(result, end='')
if out_file:
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+469 -468
View File
@@ -1,468 +1,469 @@
#!/usr/bin/env python3
# meta-remove v1.0 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import shutil
from lxml import etree
# --- Type -> plural directory mapping ---
TYPE_PLURAL_MAP = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"Constant": "Constants",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"ExchangePlan": "ExchangePlans",
"DocumentJournal": "DocumentJournals",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"CommonModule": "CommonModules",
"ScheduledJob": "ScheduledJobs",
"EventSubscription": "EventSubscriptions",
"HTTPService": "HTTPServices",
"WebService": "WebServices",
"DefinedType": "DefinedTypes",
"Role": "Roles",
"Subsystem": "Subsystems",
"CommonForm": "CommonForms",
"CommonTemplate": "CommonTemplates",
"CommonPicture": "CommonPictures",
"CommonAttribute": "CommonAttributes",
"SessionParameter": "SessionParameters",
"FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
"Sequence": "Sequences",
"FilterCriterion": "FilterCriteria",
"SettingsStorage": "SettingsStorages",
"XDTOPackage": "XDTOPackages",
"WSReference": "WSReferences",
"StyleItem": "StyleItems",
"Language": "Languages",
}
# Type -> reference type names (used in XML <v8:Type> elements)
TYPE_REF_NAMES = {
"Catalog": ["CatalogRef", "CatalogObject"],
"Document": ["DocumentRef", "DocumentObject"],
"Enum": ["EnumRef"],
"ExchangePlan": ["ExchangePlanRef", "ExchangePlanObject"],
"ChartOfAccounts": ["ChartOfAccountsRef", "ChartOfAccountsObject"],
"ChartOfCharacteristicTypes": ["ChartOfCharacteristicTypesRef", "ChartOfCharacteristicTypesObject"],
"ChartOfCalculationTypes": ["ChartOfCalculationTypesRef", "ChartOfCalculationTypesObject"],
"BusinessProcess": ["BusinessProcessRef", "BusinessProcessObject"],
"Task": ["TaskRef", "TaskObject"],
}
# Type -> Russian manager name (used in BSL code)
TYPE_RU_MANAGER = {
"Catalog": "\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438",
"Document": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
"Enum": "\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f",
"Constant": "\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b",
"InformationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439",
"AccumulationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f",
"AccountingRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438",
"CalculationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
"ChartOfAccounts": "\u041f\u043b\u0430\u043d\u044b\u0421\u0447\u0435\u0442\u043e\u0432",
"ChartOfCharacteristicTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a",
"ChartOfCalculationTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
"BusinessProcess": "\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u044b",
"Task": "\u0417\u0430\u0434\u0430\u0447\u0438",
"ExchangePlan": "\u041f\u043b\u0430\u043d\u044b\u041e\u0431\u043c\u0435\u043d\u0430",
"Report": "\u041e\u0442\u0447\u0435\u0442\u044b",
"DataProcessor": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438",
"DocumentJournal": "\u0416\u0443\u0440\u043d\u0430\u043b\u044b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
"CommonModule": None,
}
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core"
NSMAP = {"md": MD_NS, "v8": V8_NS}
def localname(el):
return etree.QName(el.tag).localname
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Remove metadata object from 1C configuration dump", allow_abbrev=False)
parser.add_argument("-ConfigDir", required=True)
parser.add_argument("-Object", required=True)
parser.add_argument("-DryRun", action="store_true")
parser.add_argument("-KeepFiles", action="store_true")
parser.add_argument("-Force", action="store_true")
args = parser.parse_args()
config_dir = args.ConfigDir
if not os.path.isabs(config_dir):
config_dir = os.path.join(os.getcwd(), config_dir)
if not os.path.isdir(config_dir):
print(f"[ERROR] Config directory not found: {config_dir}")
sys.exit(1)
config_xml = os.path.join(config_dir, "Configuration.xml")
if not os.path.isfile(config_xml):
print(f"[ERROR] Configuration.xml not found in: {config_dir}")
sys.exit(1)
# --- Parse object spec ---
parts = args.Object.split(".", 1)
if len(parts) != 2 or not parts[0] or not parts[1]:
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b)")
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in TYPE_PLURAL_MAP:
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
sys.exit(1)
type_plural = TYPE_PLURAL_MAP[obj_type]
print(f"=== meta-remove: {obj_type}.{obj_name} ===")
print()
if args.DryRun:
print("[DRY-RUN] No changes will be made")
print()
actions = 0
errors = 0
# --- 1. Find object files ---
type_dir = os.path.join(config_dir, type_plural)
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
obj_dir = os.path.join(type_dir, obj_name)
has_xml = os.path.isfile(obj_xml)
has_dir = os.path.isdir(obj_dir)
if not has_xml and not has_dir:
print(f"[WARN] Object files not found: {type_plural}/{obj_name}.xml")
print(" Proceeding with deregistration only...")
else:
if has_xml:
print(f"[FOUND] {type_plural}/{obj_name}.xml")
if has_dir:
file_count = sum(len(files) for _, _, files in os.walk(obj_dir))
print(f"[FOUND] {type_plural}/{obj_name}/ ({file_count} files)")
# --- 2. Reference check ---
print()
print("--- Reference check ---")
search_patterns = []
# 1) XML type references
if obj_type in TYPE_REF_NAMES:
for ref_name in TYPE_REF_NAMES[obj_type]:
search_patterns.append(f"{ref_name}.{obj_name}")
# 2) BSL code references
ru_mgr = TYPE_RU_MANAGER.get(obj_type)
if ru_mgr:
search_patterns.append(f"{ru_mgr}.{obj_name}")
search_patterns.append(f"{type_plural}.{obj_name}")
# 3) CommonModule: method calls
if obj_type == "CommonModule":
search_patterns.append(f"{obj_name}.")
# 4) ScheduledJob/EventSubscription handler references
if obj_type == "CommonModule":
search_patterns.append(f"<Handler>{obj_name}.")
search_patterns.append(f"<MethodName>{obj_name}.")
# Exclude object's own files
exclude_dirs = []
if has_dir:
exclude_dirs.append(obj_dir)
exclude_file = obj_xml if has_xml else ""
# Search all XML and BSL files
references = []
search_extensions = (".xml", ".bsl")
for root_path, dirs, files in os.walk(config_dir):
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in search_extensions:
continue
full_path = os.path.join(root_path, fname)
# Skip own files
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
continue
skip = False
for ed in exclude_dirs:
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
skip = True
break
if skip:
continue
# Get relative path
rel_path = os.path.relpath(full_path, config_dir)
rel_path_fwd = rel_path.replace("\\", "/")
# Skip auto-cleaned files
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
continue
try:
with open(full_path, "r", encoding="utf-8-sig") as fh:
content = fh.read()
except Exception:
continue
for pat in search_patterns:
if pat in content:
references.append({"File": rel_path, "Pattern": pat})
break
# Also check Type.Name references
type_name_ref = f"{obj_type}.{obj_name}"
already_found_files = {r["File"] for r in references}
for root_path, dirs, files in os.walk(config_dir):
for fname in files:
if not fname.lower().endswith(".xml"):
continue
full_path = os.path.join(root_path, fname)
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
continue
skip = False
for ed in exclude_dirs:
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
skip = True
break
if skip:
continue
rel_path = os.path.relpath(full_path, config_dir)
rel_path_fwd = rel_path.replace("\\", "/")
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
continue
if rel_path in already_found_files:
continue
try:
with open(full_path, "r", encoding="utf-8-sig") as fh:
content = fh.read()
except Exception:
continue
if type_name_ref in content:
references.append({"File": rel_path, "Pattern": type_name_ref})
if references:
print(f"[WARN] Found {len(references)} reference(s) to {obj_type}.{obj_name}:")
print()
shown = 0
for ref in references:
print(f" {ref['File']}")
print(f" pattern: {ref['Pattern']}")
shown += 1
if shown >= 20:
remaining = len(references) - shown
if remaining > 0:
print(f" ... and {remaining} more")
break
print()
if not args.Force:
print(f"[ERROR] Cannot remove: object has {len(references)} reference(s).")
print(" Use -Force to remove anyway, or fix references first.")
sys.exit(1)
else:
print("[WARN] -Force specified, proceeding despite references")
else:
print("[OK] No references found")
# --- 3. Remove from Configuration.xml ChildObjects ---
print()
print("--- Configuration.xml ---")
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(config_xml, xml_parser)
xml_root = tree.getroot()
cfg_node = xml_root.find(f"{{{MD_NS}}}Configuration")
if cfg_node is None:
print("[ERROR] Configuration element not found in Configuration.xml")
errors += 1
else:
child_objects = cfg_node.find(f"{{{MD_NS}}}ChildObjects")
if child_objects is not None:
found = False
for child in list(child_objects):
if not isinstance(child.tag, str):
continue
if localname(child) == obj_type and (child.text or "").strip() == obj_name:
found = True
if not args.DryRun:
# Remove preceding whitespace (tail of previous sibling or text of parent)
prev = child.getprevious()
if prev is not None:
if prev.tail and prev.tail.strip() == "":
prev.tail = prev.tail.rsplit("\n", 1)[0] + "\n" if "\n" in prev.tail else ""
if not prev.tail.strip():
# Keep just the last newline+indent before the next element
pass
child_objects.remove(child)
print(f"[OK] Removed <{obj_type}>{obj_name}</{obj_type}> from ChildObjects")
actions += 1
break
if not found:
print(f"[WARN] <{obj_type}>{obj_name}</{obj_type}> not found in ChildObjects")
# Save Configuration.xml
if actions > 0 and not args.DryRun:
save_xml_bom(tree, config_xml)
print("[OK] Configuration.xml saved")
# --- 4. Remove from subsystem Content ---
print()
print("--- Subsystems ---")
subsystems_dir = os.path.join(config_dir, "Subsystems")
subsystems_found = 0
subsystems_cleaned = 0
def remove_from_subsystems(dir_path):
nonlocal subsystems_found, subsystems_cleaned
if not os.path.isdir(dir_path):
return
for fname in os.listdir(dir_path):
if not fname.lower().endswith(".xml"):
continue
xml_file = os.path.join(dir_path, fname)
if not os.path.isfile(xml_file):
continue
ss_parser = etree.XMLParser(remove_blank_text=False)
try:
ss_tree = etree.parse(xml_file, ss_parser)
except Exception:
continue
ss_root = ss_tree.getroot()
ss_node = None
for child in ss_root:
if isinstance(child.tag, str) and localname(child) == "Subsystem":
ss_node = child
break
if ss_node is None:
continue
props_node = ss_node.find(f"{{{MD_NS}}}Properties")
if props_node is None:
continue
content_node = props_node.find(f"{{{MD_NS}}}Content")
if content_node is None:
continue
ss_name_node = props_node.find(f"{{{MD_NS}}}Name")
ss_name = ss_name_node.text if ss_name_node is not None and ss_name_node.text else os.path.splitext(fname)[0]
target_ref = f"{obj_type}.{obj_name}"
modified = False
for item in list(content_node):
if not isinstance(item.tag, str):
continue
val = (item.text or "").strip()
if val == target_ref:
subsystems_found += 1
if not args.DryRun:
content_node.remove(item)
modified = True
print(f"[OK] Removed from subsystem '{ss_name}'")
subsystems_cleaned += 1
if modified and not args.DryRun:
save_xml_bom(ss_tree, xml_file)
# Recurse into child subsystems
base_name = os.path.splitext(fname)[0]
child_dir = os.path.join(dir_path, base_name, "Subsystems")
if os.path.isdir(child_dir):
remove_from_subsystems(child_dir)
if os.path.isdir(subsystems_dir):
remove_from_subsystems(subsystems_dir)
if subsystems_cleaned == 0:
print("[OK] Not referenced in any subsystem")
else:
print("[OK] No Subsystems directory")
# --- 5. Delete object files ---
print()
print("--- Files ---")
if not args.KeepFiles:
if has_dir and not args.DryRun:
shutil.rmtree(obj_dir)
print(f"[OK] Deleted directory: {type_plural}/{obj_name}/")
actions += 1
elif has_dir:
print(f"[DRY] Would delete directory: {type_plural}/{obj_name}/")
actions += 1
if has_xml and not args.DryRun:
os.remove(obj_xml)
print(f"[OK] Deleted file: {type_plural}/{obj_name}.xml")
actions += 1
elif has_xml:
print(f"[DRY] Would delete file: {type_plural}/{obj_name}.xml")
actions += 1
if not has_xml and not has_dir:
print("[OK] No files to delete")
else:
print("[SKIP] File deletion skipped (-KeepFiles)")
# --- Summary ---
print()
total_actions = actions + subsystems_cleaned
if args.DryRun:
print(f"=== Dry run complete: {total_actions} actions would be performed ===")
else:
print(f"=== Done: {total_actions} actions performed ({subsystems_cleaned} subsystem references removed) ===")
if errors > 0:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# meta-remove v1.0 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import shutil
from lxml import etree
# --- Type -> plural directory mapping ---
TYPE_PLURAL_MAP = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"Constant": "Constants",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"ExchangePlan": "ExchangePlans",
"DocumentJournal": "DocumentJournals",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"CommonModule": "CommonModules",
"ScheduledJob": "ScheduledJobs",
"EventSubscription": "EventSubscriptions",
"HTTPService": "HTTPServices",
"WebService": "WebServices",
"DefinedType": "DefinedTypes",
"Role": "Roles",
"Subsystem": "Subsystems",
"CommonForm": "CommonForms",
"CommonTemplate": "CommonTemplates",
"CommonPicture": "CommonPictures",
"CommonAttribute": "CommonAttributes",
"SessionParameter": "SessionParameters",
"FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
"Sequence": "Sequences",
"FilterCriterion": "FilterCriteria",
"SettingsStorage": "SettingsStorages",
"XDTOPackage": "XDTOPackages",
"WSReference": "WSReferences",
"StyleItem": "StyleItems",
"Language": "Languages",
}
# Type -> reference type names (used in XML <v8:Type> elements)
TYPE_REF_NAMES = {
"Catalog": ["CatalogRef", "CatalogObject"],
"Document": ["DocumentRef", "DocumentObject"],
"Enum": ["EnumRef"],
"ExchangePlan": ["ExchangePlanRef", "ExchangePlanObject"],
"ChartOfAccounts": ["ChartOfAccountsRef", "ChartOfAccountsObject"],
"ChartOfCharacteristicTypes": ["ChartOfCharacteristicTypesRef", "ChartOfCharacteristicTypesObject"],
"ChartOfCalculationTypes": ["ChartOfCalculationTypesRef", "ChartOfCalculationTypesObject"],
"BusinessProcess": ["BusinessProcessRef", "BusinessProcessObject"],
"Task": ["TaskRef", "TaskObject"],
}
# Type -> Russian manager name (used in BSL code)
TYPE_RU_MANAGER = {
"Catalog": "\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438",
"Document": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
"Enum": "\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f",
"Constant": "\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b",
"InformationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439",
"AccumulationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f",
"AccountingRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438",
"CalculationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
"ChartOfAccounts": "\u041f\u043b\u0430\u043d\u044b\u0421\u0447\u0435\u0442\u043e\u0432",
"ChartOfCharacteristicTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a",
"ChartOfCalculationTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
"BusinessProcess": "\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u044b",
"Task": "\u0417\u0430\u0434\u0430\u0447\u0438",
"ExchangePlan": "\u041f\u043b\u0430\u043d\u044b\u041e\u0431\u043c\u0435\u043d\u0430",
"Report": "\u041e\u0442\u0447\u0435\u0442\u044b",
"DataProcessor": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438",
"DocumentJournal": "\u0416\u0443\u0440\u043d\u0430\u043b\u044b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
"CommonModule": None,
}
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core"
NSMAP = {"md": MD_NS, "v8": V8_NS}
def localname(el):
return etree.QName(el.tag).localname
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Remove metadata object from 1C configuration dump", allow_abbrev=False)
parser.add_argument("-ConfigDir", required=True)
parser.add_argument("-Object", required=True)
parser.add_argument("-DryRun", action="store_true")
parser.add_argument("-KeepFiles", action="store_true")
parser.add_argument("-Force", action="store_true")
args = parser.parse_args()
config_dir = args.ConfigDir
if not os.path.isabs(config_dir):
config_dir = os.path.join(os.getcwd(), config_dir)
if not os.path.isdir(config_dir):
print(f"[ERROR] Config directory not found: {config_dir}")
sys.exit(1)
config_xml = os.path.join(config_dir, "Configuration.xml")
if not os.path.isfile(config_xml):
print(f"[ERROR] Configuration.xml not found in: {config_dir}")
sys.exit(1)
# --- Parse object spec ---
parts = args.Object.split(".", 1)
if len(parts) != 2 or not parts[0] or not parts[1]:
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b)")
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in TYPE_PLURAL_MAP:
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
sys.exit(1)
type_plural = TYPE_PLURAL_MAP[obj_type]
print(f"=== meta-remove: {obj_type}.{obj_name} ===")
print()
if args.DryRun:
print("[DRY-RUN] No changes will be made")
print()
actions = 0
errors = 0
# --- 1. Find object files ---
type_dir = os.path.join(config_dir, type_plural)
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
obj_dir = os.path.join(type_dir, obj_name)
has_xml = os.path.isfile(obj_xml)
has_dir = os.path.isdir(obj_dir)
if not has_xml and not has_dir:
print(f"[WARN] Object files not found: {type_plural}/{obj_name}.xml")
print(" Proceeding with deregistration only...")
else:
if has_xml:
print(f"[FOUND] {type_plural}/{obj_name}.xml")
if has_dir:
file_count = sum(len(files) for _, _, files in os.walk(obj_dir))
print(f"[FOUND] {type_plural}/{obj_name}/ ({file_count} files)")
# --- 2. Reference check ---
print()
print("--- Reference check ---")
search_patterns = []
# 1) XML type references
if obj_type in TYPE_REF_NAMES:
for ref_name in TYPE_REF_NAMES[obj_type]:
search_patterns.append(f"{ref_name}.{obj_name}")
# 2) BSL code references
ru_mgr = TYPE_RU_MANAGER.get(obj_type)
if ru_mgr:
search_patterns.append(f"{ru_mgr}.{obj_name}")
search_patterns.append(f"{type_plural}.{obj_name}")
# 3) CommonModule: method calls
if obj_type == "CommonModule":
search_patterns.append(f"{obj_name}.")
# 4) ScheduledJob/EventSubscription handler references
if obj_type == "CommonModule":
search_patterns.append(f"<Handler>{obj_name}.")
search_patterns.append(f"<MethodName>{obj_name}.")
# Exclude object's own files
exclude_dirs = []
if has_dir:
exclude_dirs.append(obj_dir)
exclude_file = obj_xml if has_xml else ""
# Search all XML and BSL files
references = []
search_extensions = (".xml", ".bsl")
for root_path, dirs, files in os.walk(config_dir):
for fname in files:
ext = os.path.splitext(fname)[1].lower()
if ext not in search_extensions:
continue
full_path = os.path.join(root_path, fname)
# Skip own files
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
continue
skip = False
for ed in exclude_dirs:
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
skip = True
break
if skip:
continue
# Get relative path
rel_path = os.path.relpath(full_path, config_dir)
rel_path_fwd = rel_path.replace("\\", "/")
# Skip auto-cleaned files
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
continue
try:
with open(full_path, "r", encoding="utf-8-sig") as fh:
content = fh.read()
except Exception:
continue
for pat in search_patterns:
if pat in content:
references.append({"File": rel_path, "Pattern": pat})
break
# Also check Type.Name references
type_name_ref = f"{obj_type}.{obj_name}"
already_found_files = {r["File"] for r in references}
for root_path, dirs, files in os.walk(config_dir):
for fname in files:
if not fname.lower().endswith(".xml"):
continue
full_path = os.path.join(root_path, fname)
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
continue
skip = False
for ed in exclude_dirs:
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
skip = True
break
if skip:
continue
rel_path = os.path.relpath(full_path, config_dir)
rel_path_fwd = rel_path.replace("\\", "/")
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
continue
if rel_path in already_found_files:
continue
try:
with open(full_path, "r", encoding="utf-8-sig") as fh:
content = fh.read()
except Exception:
continue
if type_name_ref in content:
references.append({"File": rel_path, "Pattern": type_name_ref})
if references:
print(f"[WARN] Found {len(references)} reference(s) to {obj_type}.{obj_name}:")
print()
shown = 0
for ref in references:
print(f" {ref['File']}")
print(f" pattern: {ref['Pattern']}")
shown += 1
if shown >= 20:
remaining = len(references) - shown
if remaining > 0:
print(f" ... and {remaining} more")
break
print()
if not args.Force:
print(f"[ERROR] Cannot remove: object has {len(references)} reference(s).")
print(" Use -Force to remove anyway, or fix references first.")
sys.exit(1)
else:
print("[WARN] -Force specified, proceeding despite references")
else:
print("[OK] No references found")
# --- 3. Remove from Configuration.xml ChildObjects ---
print()
print("--- Configuration.xml ---")
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(config_xml, xml_parser)
xml_root = tree.getroot()
cfg_node = xml_root.find(f"{{{MD_NS}}}Configuration")
if cfg_node is None:
print("[ERROR] Configuration element not found in Configuration.xml")
errors += 1
else:
child_objects = cfg_node.find(f"{{{MD_NS}}}ChildObjects")
if child_objects is not None:
found = False
for child in list(child_objects):
if not isinstance(child.tag, str):
continue
if localname(child) == obj_type and (child.text or "").strip() == obj_name:
found = True
if not args.DryRun:
# Remove preceding whitespace (tail of previous sibling or text of parent)
prev = child.getprevious()
if prev is not None:
if prev.tail and prev.tail.strip() == "":
prev.tail = prev.tail.rsplit("\n", 1)[0] + "\n" if "\n" in prev.tail else ""
if not prev.tail.strip():
# Keep just the last newline+indent before the next element
pass
child_objects.remove(child)
print(f"[OK] Removed <{obj_type}>{obj_name}</{obj_type}> from ChildObjects")
actions += 1
break
if not found:
print(f"[WARN] <{obj_type}>{obj_name}</{obj_type}> not found in ChildObjects")
# Save Configuration.xml
if actions > 0 and not args.DryRun:
save_xml_bom(tree, config_xml)
print("[OK] Configuration.xml saved")
# --- 4. Remove from subsystem Content ---
print()
print("--- Subsystems ---")
subsystems_dir = os.path.join(config_dir, "Subsystems")
subsystems_found = 0
subsystems_cleaned = 0
def remove_from_subsystems(dir_path):
nonlocal subsystems_found, subsystems_cleaned
if not os.path.isdir(dir_path):
return
for fname in os.listdir(dir_path):
if not fname.lower().endswith(".xml"):
continue
xml_file = os.path.join(dir_path, fname)
if not os.path.isfile(xml_file):
continue
ss_parser = etree.XMLParser(remove_blank_text=False)
try:
ss_tree = etree.parse(xml_file, ss_parser)
except Exception:
continue
ss_root = ss_tree.getroot()
ss_node = None
for child in ss_root:
if isinstance(child.tag, str) and localname(child) == "Subsystem":
ss_node = child
break
if ss_node is None:
continue
props_node = ss_node.find(f"{{{MD_NS}}}Properties")
if props_node is None:
continue
content_node = props_node.find(f"{{{MD_NS}}}Content")
if content_node is None:
continue
ss_name_node = props_node.find(f"{{{MD_NS}}}Name")
ss_name = ss_name_node.text if ss_name_node is not None and ss_name_node.text else os.path.splitext(fname)[0]
target_ref = f"{obj_type}.{obj_name}"
modified = False
for item in list(content_node):
if not isinstance(item.tag, str):
continue
val = (item.text or "").strip()
if val == target_ref:
subsystems_found += 1
if not args.DryRun:
content_node.remove(item)
modified = True
print(f"[OK] Removed from subsystem '{ss_name}'")
subsystems_cleaned += 1
if modified and not args.DryRun:
save_xml_bom(ss_tree, xml_file)
# Recurse into child subsystems
base_name = os.path.splitext(fname)[0]
child_dir = os.path.join(dir_path, base_name, "Subsystems")
if os.path.isdir(child_dir):
remove_from_subsystems(child_dir)
if os.path.isdir(subsystems_dir):
remove_from_subsystems(subsystems_dir)
if subsystems_cleaned == 0:
print("[OK] Not referenced in any subsystem")
else:
print("[OK] No Subsystems directory")
# --- 5. Delete object files ---
print()
print("--- Files ---")
if not args.KeepFiles:
if has_dir and not args.DryRun:
shutil.rmtree(obj_dir)
print(f"[OK] Deleted directory: {type_plural}/{obj_name}/")
actions += 1
elif has_dir:
print(f"[DRY] Would delete directory: {type_plural}/{obj_name}/")
actions += 1
if has_xml and not args.DryRun:
os.remove(obj_xml)
print(f"[OK] Deleted file: {type_plural}/{obj_name}.xml")
actions += 1
elif has_xml:
print(f"[DRY] Would delete file: {type_plural}/{obj_name}.xml")
actions += 1
if not has_xml and not has_dir:
print("[OK] No files to delete")
else:
print("[SKIP] File deletion skipped (-KeepFiles)")
# --- Summary ---
print()
total_actions = actions + subsystems_cleaned
if args.DryRun:
print(f"=== Dry run complete: {total_actions} actions would be performed ===")
else:
print(f"=== Done: {total_actions} actions performed ({subsystems_cleaned} subsystem references removed) ===")
if errors > 0:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+444 -442
View File
@@ -1,442 +1,444 @@
#!/usr/bin/env python3
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C spreadsheet (MXL) structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", default="", help="Path to Template.xml")
parser.add_argument("-ProcessorName", default="", help="Processor name (used with -TemplateName)")
parser.add_argument("-TemplateName", default="", help="Template name (used with -ProcessorName)")
parser.add_argument("-SrcDir", default="src", help="Source directory (default: src)")
parser.add_argument("-Format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("-WithText", action="store_true", default=False, help="Include text content")
parser.add_argument("-MaxParams", type=int, default=10, help="Max parameters to show per area")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
args = parser.parse_args()
# --- Resolve template path ---
template_path = args.TemplatePath
if not template_path:
if not args.ProcessorName or not args.TemplateName:
print("Specify -TemplatePath or both -ProcessorName and -TemplateName", file=sys.stderr)
sys.exit(1)
template_path = os.path.join(args.SrcDir, args.ProcessorName, "Templates", args.TemplateName, "Ext", "Template.xml")
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.isfile(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(template_path, etree.XMLParser(remove_blank_text=True))
root = tree.getroot()
NS = {
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
"v8": "http://v8.1c.ru/8.1/data/core",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
# --- Column sets ---
column_sets = []
default_col_count = 0
for cols in root.findall("d:columns", NS):
size_node = cols.find("d:size", NS)
id_node = cols.find("d:id", NS)
size = int(size_node.text) if size_node is not None and size_node.text else 0
if id_node is not None:
column_sets.append({"Id": id_node.text or "", "Size": size})
else:
default_col_count = size
# --- Rows: collect row data ---
row_nodes = root.findall("d:rowsItem", NS)
total_rows = len(row_nodes)
height_node = root.find("d:height", NS)
doc_height = int(height_node.text) if height_node is not None and height_node.text else total_rows
# --- Named items ---
named_areas = []
named_drawings = []
for ni in root.findall("d:namedItem", NS):
ni_type = ni.get(f"{{{XSI_NS}}}type", "")
name_node = ni.find("d:name", NS)
name = name_node.text if name_node is not None else ""
if "NamedItemCells" in ni_type:
area = ni.find("d:area", NS)
area_type_node = area.find("d:type", NS)
area_type = area_type_node.text if area_type_node is not None else ""
begin_row = int(area.find("d:beginRow", NS).text)
end_row = int(area.find("d:endRow", NS).text)
begin_col = int(area.find("d:beginColumn", NS).text)
end_col = int(area.find("d:endColumn", NS).text)
cols_id = None
cols_id_node = area.find("d:columnsID", NS)
if cols_id_node is not None:
cols_id = cols_id_node.text
named_areas.append({
"Name": name,
"AreaType": area_type,
"BeginRow": begin_row,
"EndRow": end_row,
"BeginCol": begin_col,
"EndCol": end_col,
"ColumnsID": cols_id,
})
elif "NamedItemDrawing" in ni_type:
draw_id_node = ni.find("d:drawingID", NS)
draw_id = draw_id_node.text if draw_id_node is not None else ""
named_drawings.append({"Name": name, "DrawingID": draw_id})
# --- Scan rows for parameters and text ---
# Build row index map: rowIndex -> XmlNode
row_map = {}
for ri in row_nodes:
idx_node = ri.find("d:index", NS)
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
row_map[idx] = ri
def get_cell_data(row_node, include_text):
row = row_node.find("d:row", NS)
if row is None:
return []
results = []
for c_group in row.findall("d:c", NS):
cell = c_group.find("d:c", NS)
if cell is None:
continue
param = cell.find("d:parameter", NS)
detail = cell.find("d:detailParameter", NS)
tl = cell.find("d:tl", NS)
if param is not None:
entry = {"Kind": "Parameter", "Value": param.text or ""}
if detail is not None:
entry["Detail"] = detail.text or ""
results.append(entry)
if tl is not None:
content = tl.find("v8:item/v8:content", NS)
if content is not None and content.text:
text = content.text
is_template = bool(re.search(r'\[.+\]', text))
if is_template:
# Extract parameter names from [Param] placeholders
# Skip numeric-only like [5]
for m in re.finditer(r'\[([^\]]+)\]', text):
val = m.group(1)
if not re.match(r'^\d+$', val):
results.append({"Kind": "TemplateParam", "Value": val})
# Full template text only with -WithText
if include_text:
results.append({"Kind": "Template", "Value": text})
elif include_text:
results.append({"Kind": "Text", "Value": text})
return results
def get_area_cell_data(area, row_map_ref, include_text):
params = []
details = []
texts = []
templates = []
start_row = area["BeginRow"]
end_row = area["EndRow"]
if start_row == -1:
start_row = 0
if end_row == -1:
end_row = doc_height - 1
for r in range(start_row, end_row + 1):
if r in row_map_ref:
cells = get_cell_data(row_map_ref[r], include_text)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
params.append(c["Value"])
if "Detail" in c:
details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
texts.append(c["Value"])
elif kind == "Template":
templates.append(c["Value"])
return {"Params": params, "Details": details, "Texts": texts, "Templates": templates}
# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow
def area_sort_key(a):
if a["AreaType"] == "Columns":
return (a["BeginCol"], a["Name"])
return (a["BeginRow"], a["Name"])
named_areas.sort(key=area_sort_key)
# Collect data for each area
area_data = []
covered_rows = set()
for area in named_areas:
data = get_area_cell_data(area, row_map, args.WithText)
area_data.append({
"Area": area,
"Params": data["Params"],
"Details": data["Details"],
"Texts": data["Texts"],
"Templates": data["Templates"],
})
# Track covered rows
sr = area["BeginRow"]
er = area["EndRow"]
if sr != -1 and er != -1:
for r in range(sr, er + 1):
covered_rows.add(r)
# Find parameters outside named areas
outside_params = []
outside_details = []
outside_texts = []
outside_templates = []
for r in sorted(row_map.keys()):
if r not in covered_rows:
cells = get_cell_data(row_map[r], args.WithText)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
outside_params.append(c["Value"])
if "Detail" in c:
outside_details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
outside_params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
outside_texts.append(c["Value"])
elif kind == "Template":
outside_templates.append(c["Value"])
# --- Counts ---
merge_count = len(root.findall("d:merge", NS))
drawing_nodes = root.findall("d:drawing", NS)
drawing_count = len(drawing_nodes)
# --- Output ---
def truncate_list(items, max_count):
if len(items) <= max_count:
return ", ".join(items)
shown = ", ".join(items[:max_count])
remaining = len(items) - max_count
return f"{shown}, ... (+{remaining})"
# Determine template name from path
template_name = os.path.basename(os.path.dirname(os.path.dirname(template_path)))
if args.Format == "json":
result = {
"name": template_name,
"rows": doc_height,
"columns": default_col_count,
"columnSets": [{"id": cs["Id"], "size": cs["Size"]} for cs in column_sets],
"areas": [],
"outsideParams": list(outside_params),
"mergeCount": merge_count,
"drawingCount": drawing_count,
}
for ad in area_data:
area_obj = {
"name": ad["Area"]["Name"],
"type": ad["Area"]["AreaType"],
"beginRow": ad["Area"]["BeginRow"],
"endRow": ad["Area"]["EndRow"],
"beginCol": ad["Area"]["BeginCol"],
"endCol": ad["Area"]["EndCol"],
"params": list(ad["Params"]),
}
if ad["Area"]["ColumnsID"]:
area_obj["columnsID"] = ad["Area"]["ColumnsID"]
if args.WithText:
area_obj["texts"] = list(ad["Texts"])
area_obj["templates"] = list(ad["Templates"])
result["areas"].append(area_obj)
if args.WithText:
result["outsideTexts"] = list(outside_texts)
result["outsideTemplates"] = list(outside_templates)
for nd in named_drawings:
result["areas"].append({
"name": nd["Name"],
"type": "Drawing",
"drawingID": nd["DrawingID"],
})
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
# --- Text format output ---
lines = []
lines.append(f"=== {template_name} ===")
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
if len(column_sets) == 0:
lines.append(" Column sets: 1 (default only)")
else:
lines.append(f" Column sets: {len(column_sets) + 1} (default={default_col_count} cols + {len(column_sets)} additional)")
for cs in column_sets:
lines.append(f" {cs['Id'][:8]}...: {cs['Size']} cols")
lines.append("")
lines.append("--- Named areas ---")
for ad in area_data:
a = ad["Area"]
param_count = len(ad["Params"])
row_range = ""
if a["AreaType"] == "Rows":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}"
elif a["AreaType"] == "Columns":
row_range = f"cols {a['BeginCol']}-{a['EndCol']}"
elif a["AreaType"] == "Rectangle":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}, cols {a['BeginCol']}-{a['EndCol']}"
cols_info = ""
if a["ColumnsID"]:
cs_size = ""
for cs in column_sets:
if cs["Id"] == a["ColumnsID"]:
cs_size = f" {cs['Size']}cols"
break
cols_info = f" [colset{cs_size}]"
param_info = f"({param_count} params)"
name_str = a["Name"].ljust(25)
type_str = a["AreaType"].ljust(12)
lines.append(f" {name_str} {type_str} {row_range} {param_info}{cols_info}")
for nd in named_drawings:
name_str = nd["Name"].ljust(25)
lines.append(f" {name_str} Drawing drawingID={nd['DrawingID']}")
# Detect intersection pairs (Rows + Columns areas that overlap)
rows_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Rows"]
cols_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Columns"]
intersections = []
if rows_areas and cols_areas:
for ra in rows_areas:
for ca in cols_areas:
intersections.append(f"{ra['Area']['Name']}|{ca['Area']['Name']}")
if intersections:
lines.append("")
lines.append("--- Intersections (use with GetArea) ---")
for pair in intersections:
lines.append(f" {pair}")
# Parameters by area
has_params = any(len(ad["Params"]) > 0 for ad in area_data) or len(outside_params) > 0
if has_params:
lines.append("")
lines.append("--- Parameters by area ---")
for ad in area_data:
if len(ad["Params"]) > 0:
param_str = truncate_list(ad["Params"], args.MaxParams)
lines.append(f" {ad['Area']['Name']}: {param_str}")
# Show detailParameters if any
if len(ad["Details"]) > 0:
detail_str = truncate_list(ad["Details"], args.MaxParams)
lines.append(f" detail: {detail_str}")
if len(outside_params) > 0:
param_str = truncate_list(outside_params, args.MaxParams)
lines.append(f" (outside areas): {param_str}")
if len(outside_details) > 0:
detail_str = truncate_list(outside_details, args.MaxParams)
lines.append(f" detail: {detail_str}")
# WithText sections
if args.WithText:
has_text = any(len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0 for ad in area_data) or len(outside_texts) > 0 or len(outside_templates) > 0
if has_text:
lines.append("")
lines.append("--- Text content ---")
for ad in area_data:
if len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0:
lines.append(f" {ad['Area']['Name']}:")
if len(ad["Texts"]) > 0:
text_items = [f'"{t}"' for t in ad["Texts"]]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(ad["Templates"]) > 0:
tpl_items = [f'"{t}"' for t in ad["Templates"]]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
if len(outside_texts) > 0 or len(outside_templates) > 0:
lines.append(" (outside areas):")
if len(outside_texts) > 0:
text_items = [f'"{t}"' for t in outside_texts]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(outside_templates) > 0:
tpl_items = [f'"{t}"' for t in outside_templates]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
lines.append("")
lines.append("--- Stats ---")
lines.append(f" Merges: {merge_count}")
lines.append(f" Drawings: {drawing_count}")
# --- Truncation protection ---
total_lines = len(lines)
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
lines = lines[args.Offset:]
if len(lines) > args.Limit:
shown = lines[:args.Limit]
for l in shown:
print(l)
remaining = total_lines - args.Offset - args.Limit
print("")
print(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
else:
for l in lines:
print(l)
#!/usr/bin/env python3
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C spreadsheet (MXL) structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", default="", help="Path to Template.xml")
parser.add_argument("-ProcessorName", default="", help="Processor name (used with -TemplateName)")
parser.add_argument("-TemplateName", default="", help="Template name (used with -ProcessorName)")
parser.add_argument("-SrcDir", default="src", help="Source directory (default: src)")
parser.add_argument("-Format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("-WithText", action="store_true", default=False, help="Include text content")
parser.add_argument("-MaxParams", type=int, default=10, help="Max parameters to show per area")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
args = parser.parse_args()
# --- Resolve template path ---
template_path = args.TemplatePath
if not template_path:
if not args.ProcessorName or not args.TemplateName:
print("Specify -TemplatePath or both -ProcessorName and -TemplateName", file=sys.stderr)
sys.exit(1)
template_path = os.path.join(args.SrcDir, args.ProcessorName, "Templates", args.TemplateName, "Ext", "Template.xml")
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.isfile(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
# --- Load XML ---
tree = etree.parse(template_path, etree.XMLParser(remove_blank_text=True))
root = tree.getroot()
NS = {
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
"v8": "http://v8.1c.ru/8.1/data/core",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
# --- Column sets ---
column_sets = []
default_col_count = 0
for cols in root.findall("d:columns", NS):
size_node = cols.find("d:size", NS)
id_node = cols.find("d:id", NS)
size = int(size_node.text) if size_node is not None and size_node.text else 0
if id_node is not None:
column_sets.append({"Id": id_node.text or "", "Size": size})
else:
default_col_count = size
# --- Rows: collect row data ---
row_nodes = root.findall("d:rowsItem", NS)
total_rows = len(row_nodes)
height_node = root.find("d:height", NS)
doc_height = int(height_node.text) if height_node is not None and height_node.text else total_rows
# --- Named items ---
named_areas = []
named_drawings = []
for ni in root.findall("d:namedItem", NS):
ni_type = ni.get(f"{{{XSI_NS}}}type", "")
name_node = ni.find("d:name", NS)
name = name_node.text if name_node is not None else ""
if "NamedItemCells" in ni_type:
area = ni.find("d:area", NS)
area_type_node = area.find("d:type", NS)
area_type = area_type_node.text if area_type_node is not None else ""
begin_row = int(area.find("d:beginRow", NS).text)
end_row = int(area.find("d:endRow", NS).text)
begin_col = int(area.find("d:beginColumn", NS).text)
end_col = int(area.find("d:endColumn", NS).text)
cols_id = None
cols_id_node = area.find("d:columnsID", NS)
if cols_id_node is not None:
cols_id = cols_id_node.text
named_areas.append({
"Name": name,
"AreaType": area_type,
"BeginRow": begin_row,
"EndRow": end_row,
"BeginCol": begin_col,
"EndCol": end_col,
"ColumnsID": cols_id,
})
elif "NamedItemDrawing" in ni_type:
draw_id_node = ni.find("d:drawingID", NS)
draw_id = draw_id_node.text if draw_id_node is not None else ""
named_drawings.append({"Name": name, "DrawingID": draw_id})
# --- Scan rows for parameters and text ---
# Build row index map: rowIndex -> XmlNode
row_map = {}
for ri in row_nodes:
idx_node = ri.find("d:index", NS)
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
row_map[idx] = ri
def get_cell_data(row_node, include_text):
row = row_node.find("d:row", NS)
if row is None:
return []
results = []
for c_group in row.findall("d:c", NS):
cell = c_group.find("d:c", NS)
if cell is None:
continue
param = cell.find("d:parameter", NS)
detail = cell.find("d:detailParameter", NS)
tl = cell.find("d:tl", NS)
if param is not None:
entry = {"Kind": "Parameter", "Value": param.text or ""}
if detail is not None:
entry["Detail"] = detail.text or ""
results.append(entry)
if tl is not None:
content = tl.find("v8:item/v8:content", NS)
if content is not None and content.text:
text = content.text
is_template = bool(re.search(r'\[.+\]', text))
if is_template:
# Extract parameter names from [Param] placeholders
# Skip numeric-only like [5]
for m in re.finditer(r'\[([^\]]+)\]', text):
val = m.group(1)
if not re.match(r'^\d+$', val):
results.append({"Kind": "TemplateParam", "Value": val})
# Full template text only with -WithText
if include_text:
results.append({"Kind": "Template", "Value": text})
elif include_text:
results.append({"Kind": "Text", "Value": text})
return results
def get_area_cell_data(area, row_map_ref, include_text):
params = []
details = []
texts = []
templates = []
start_row = area["BeginRow"]
end_row = area["EndRow"]
if start_row == -1:
start_row = 0
if end_row == -1:
end_row = doc_height - 1
for r in range(start_row, end_row + 1):
if r in row_map_ref:
cells = get_cell_data(row_map_ref[r], include_text)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
params.append(c["Value"])
if "Detail" in c:
details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
texts.append(c["Value"])
elif kind == "Template":
templates.append(c["Value"])
return {"Params": params, "Details": details, "Texts": texts, "Templates": templates}
# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow
def area_sort_key(a):
if a["AreaType"] == "Columns":
return (a["BeginCol"], a["Name"])
return (a["BeginRow"], a["Name"])
named_areas.sort(key=area_sort_key)
# Collect data for each area
area_data = []
covered_rows = set()
for area in named_areas:
data = get_area_cell_data(area, row_map, args.WithText)
area_data.append({
"Area": area,
"Params": data["Params"],
"Details": data["Details"],
"Texts": data["Texts"],
"Templates": data["Templates"],
})
# Track covered rows
sr = area["BeginRow"]
er = area["EndRow"]
if sr != -1 and er != -1:
for r in range(sr, er + 1):
covered_rows.add(r)
# Find parameters outside named areas
outside_params = []
outside_details = []
outside_texts = []
outside_templates = []
for r in sorted(row_map.keys()):
if r not in covered_rows:
cells = get_cell_data(row_map[r], args.WithText)
for c in cells:
kind = c["Kind"]
if kind == "Parameter":
outside_params.append(c["Value"])
if "Detail" in c:
outside_details.append(f"{c['Value']}->{c['Detail']}")
elif kind == "TemplateParam":
outside_params.append(f"{c['Value']} [tpl]")
elif kind == "Text":
outside_texts.append(c["Value"])
elif kind == "Template":
outside_templates.append(c["Value"])
# --- Counts ---
merge_count = len(root.findall("d:merge", NS))
drawing_nodes = root.findall("d:drawing", NS)
drawing_count = len(drawing_nodes)
# --- Output ---
def truncate_list(items, max_count):
if len(items) <= max_count:
return ", ".join(items)
shown = ", ".join(items[:max_count])
remaining = len(items) - max_count
return f"{shown}, ... (+{remaining})"
# Determine template name from path
template_name = os.path.basename(os.path.dirname(os.path.dirname(template_path)))
if args.Format == "json":
result = {
"name": template_name,
"rows": doc_height,
"columns": default_col_count,
"columnSets": [{"id": cs["Id"], "size": cs["Size"]} for cs in column_sets],
"areas": [],
"outsideParams": list(outside_params),
"mergeCount": merge_count,
"drawingCount": drawing_count,
}
for ad in area_data:
area_obj = {
"name": ad["Area"]["Name"],
"type": ad["Area"]["AreaType"],
"beginRow": ad["Area"]["BeginRow"],
"endRow": ad["Area"]["EndRow"],
"beginCol": ad["Area"]["BeginCol"],
"endCol": ad["Area"]["EndCol"],
"params": list(ad["Params"]),
}
if ad["Area"]["ColumnsID"]:
area_obj["columnsID"] = ad["Area"]["ColumnsID"]
if args.WithText:
area_obj["texts"] = list(ad["Texts"])
area_obj["templates"] = list(ad["Templates"])
result["areas"].append(area_obj)
if args.WithText:
result["outsideTexts"] = list(outside_texts)
result["outsideTemplates"] = list(outside_templates)
for nd in named_drawings:
result["areas"].append({
"name": nd["Name"],
"type": "Drawing",
"drawingID": nd["DrawingID"],
})
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
# --- Text format output ---
lines = []
lines.append(f"=== {template_name} ===")
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
if len(column_sets) == 0:
lines.append(" Column sets: 1 (default only)")
else:
lines.append(f" Column sets: {len(column_sets) + 1} (default={default_col_count} cols + {len(column_sets)} additional)")
for cs in column_sets:
lines.append(f" {cs['Id'][:8]}...: {cs['Size']} cols")
lines.append("")
lines.append("--- Named areas ---")
for ad in area_data:
a = ad["Area"]
param_count = len(ad["Params"])
row_range = ""
if a["AreaType"] == "Rows":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}"
elif a["AreaType"] == "Columns":
row_range = f"cols {a['BeginCol']}-{a['EndCol']}"
elif a["AreaType"] == "Rectangle":
row_range = f"rows {a['BeginRow']}-{a['EndRow']}, cols {a['BeginCol']}-{a['EndCol']}"
cols_info = ""
if a["ColumnsID"]:
cs_size = ""
for cs in column_sets:
if cs["Id"] == a["ColumnsID"]:
cs_size = f" {cs['Size']}cols"
break
cols_info = f" [colset{cs_size}]"
param_info = f"({param_count} params)"
name_str = a["Name"].ljust(25)
type_str = a["AreaType"].ljust(12)
lines.append(f" {name_str} {type_str} {row_range} {param_info}{cols_info}")
for nd in named_drawings:
name_str = nd["Name"].ljust(25)
lines.append(f" {name_str} Drawing drawingID={nd['DrawingID']}")
# Detect intersection pairs (Rows + Columns areas that overlap)
rows_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Rows"]
cols_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Columns"]
intersections = []
if rows_areas and cols_areas:
for ra in rows_areas:
for ca in cols_areas:
intersections.append(f"{ra['Area']['Name']}|{ca['Area']['Name']}")
if intersections:
lines.append("")
lines.append("--- Intersections (use with GetArea) ---")
for pair in intersections:
lines.append(f" {pair}")
# Parameters by area
has_params = any(len(ad["Params"]) > 0 for ad in area_data) or len(outside_params) > 0
if has_params:
lines.append("")
lines.append("--- Parameters by area ---")
for ad in area_data:
if len(ad["Params"]) > 0:
param_str = truncate_list(ad["Params"], args.MaxParams)
lines.append(f" {ad['Area']['Name']}: {param_str}")
# Show detailParameters if any
if len(ad["Details"]) > 0:
detail_str = truncate_list(ad["Details"], args.MaxParams)
lines.append(f" detail: {detail_str}")
if len(outside_params) > 0:
param_str = truncate_list(outside_params, args.MaxParams)
lines.append(f" (outside areas): {param_str}")
if len(outside_details) > 0:
detail_str = truncate_list(outside_details, args.MaxParams)
lines.append(f" detail: {detail_str}")
# WithText sections
if args.WithText:
has_text = any(len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0 for ad in area_data) or len(outside_texts) > 0 or len(outside_templates) > 0
if has_text:
lines.append("")
lines.append("--- Text content ---")
for ad in area_data:
if len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0:
lines.append(f" {ad['Area']['Name']}:")
if len(ad["Texts"]) > 0:
text_items = [f'"{t}"' for t in ad["Texts"]]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(ad["Templates"]) > 0:
tpl_items = [f'"{t}"' for t in ad["Templates"]]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
if len(outside_texts) > 0 or len(outside_templates) > 0:
lines.append(" (outside areas):")
if len(outside_texts) > 0:
text_items = [f'"{t}"' for t in outside_texts]
text_str = truncate_list(text_items, args.MaxParams)
lines.append(f" Text: {text_str}")
if len(outside_templates) > 0:
tpl_items = [f'"{t}"' for t in outside_templates]
tpl_str = truncate_list(tpl_items, args.MaxParams)
lines.append(f" Templates: {tpl_str}")
lines.append("")
lines.append("--- Stats ---")
lines.append(f" Merges: {merge_count}")
lines.append(f" Drawings: {drawing_count}")
# --- Truncation protection ---
total_lines = len(lines)
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
lines = lines[args.Offset:]
if len(lines) > args.Limit:
shown = lines[:args.Limit]
for l in shown:
print(l)
remaining = total_lines - args.Offset - args.Limit
print("")
print(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
else:
for l in lines:
print(l)
@@ -1,365 +1,366 @@
#!/usr/bin/env python3
# mxl-validate v1.0 — Validate 1C spreadsheet document Template.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges."""
import sys, os, argparse
from lxml import etree
NS_D = 'http://v8.1c.ru/8.2/data/spreadsheet'
NS_V8 = 'http://v8.1c.ru/8.1/data/core'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS = {
'd': NS_D,
'v8': NS_V8,
'xsi': NS_XSI,
}
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
def ok(self, msg):
print(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
print(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
print(f'[WARN] {msg}')
def int_text(node):
"""Return int from node text, or 0 if None."""
if node is not None and node.text:
return int(node.text)
return 0
def main():
parser = argparse.ArgumentParser(
description='Validate 1C spreadsheet document Template.xml', allow_abbrev=False
)
parser.add_argument('-TemplatePath', dest='TemplatePath', default='')
parser.add_argument('-ProcessorName', dest='ProcessorName', default='')
parser.add_argument('-TemplateName', dest='TemplateName', default='')
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=20)
args = parser.parse_args()
template_path = args.TemplatePath
processor_name = args.ProcessorName
template_name_arg = args.TemplateName
src_dir = args.SrcDir
max_errors = args.MaxErrors
# --- Resolve template path ---
if not template_path:
if not processor_name or not template_name_arg:
print('Specify -TemplatePath or both -ProcessorName and -TemplateName', file=sys.stderr)
sys.exit(1)
template_path = os.path.join(src_dir, processor_name, 'Templates',
template_name_arg, 'Ext', 'Template.xml')
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.exists(template_path):
print(f'File not found: {template_path}', file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(template_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
root = xml_doc.getroot()
r = Reporter(max_errors)
# Derive template name from path: .../Templates/<Name>/Ext/Template.xml
# Go up 2 levels from Template.xml -> Ext -> <Name>
template_display_name = os.path.basename(os.path.dirname(os.path.dirname(resolved_path)))
print(f'=== Validation: {template_display_name} ===')
print()
# --- Collect palettes ---
line_nodes = root.findall(f'{{{NS_D}}}line')
line_count = len(line_nodes)
font_nodes = [node for node in root if isinstance(node.tag, str) and etree.QName(node.tag).localname == 'font']
font_count = len(font_nodes)
format_nodes = [node for node in root if isinstance(node.tag, str) and etree.QName(node.tag).localname == 'format']
format_count = len(format_nodes)
picture_nodes = root.findall(f'{{{NS_D}}}picture')
picture_count = len(picture_nodes)
# --- Collect column sets ---
column_sets = {} # id -> size
default_col_count = 0
for cols in root.findall(f'{{{NS_D}}}columns'):
size_node = cols.find(f'{{{NS_D}}}size')
id_node = cols.find(f'{{{NS_D}}}id')
size = int_text(size_node)
if id_node is not None and id_node.text:
column_sets[id_node.text] = size
else:
default_col_count = size
# --- Check 1: height vs actual rows ---
row_nodes = root.findall(f'{{{NS_D}}}rowsItem')
height_node = root.find(f'{{{NS_D}}}height')
doc_height = int_text(height_node)
max_row_index = -1
for ri in row_nodes:
idx_node = ri.find(f'{{{NS_D}}}index')
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
if idx > max_row_index:
max_row_index = idx
expected_min_height = max_row_index + 1
if doc_height >= expected_min_height:
r.ok(f'height ({doc_height}) >= max row index + 1 ({expected_min_height}), rowsItem count={len(row_nodes)}')
else:
r.error(f'height={doc_height} but max row index={max_row_index} (need at least {expected_min_height})')
# --- Check 2: vgRows <= height ---
vg_rows_node = root.find(f'{{{NS_D}}}vgRows')
if vg_rows_node is not None:
vg_rows = int_text(vg_rows_node)
if vg_rows <= doc_height:
r.ok(f'vgRows ({vg_rows}) <= height ({doc_height})')
else:
r.warn(f'vgRows ({vg_rows}) > height ({doc_height})')
# --- Build row data for checks ---
max_format_ref = 0
max_font_ref = 0
max_line_ref = 0
# Check format palette references in formats (font, border indices)
for fmt in format_nodes:
font_idx_node = fmt.find(f'{{{NS_D}}}font')
if font_idx_node is not None and font_idx_node.text:
val = int(font_idx_node.text)
if val > max_font_ref:
max_font_ref = val
for border_name in ('leftBorder', 'topBorder', 'rightBorder', 'bottomBorder', 'drawingBorder'):
border_node = fmt.find(f'{{{NS_D}}}{border_name}')
if border_node is not None and border_node.text:
val = int(border_node.text)
if val > max_line_ref:
max_line_ref = val
# --- Check 10: font indices in formats ---
if font_count > 0:
if max_font_ref < font_count:
r.ok(f'Font refs: max={max_font_ref}, palette size={font_count}')
else:
r.error(f'Font index {max_font_ref} exceeds palette size ({font_count})')
elif max_font_ref > 0:
r.error(f'Font index {max_font_ref} referenced but no fonts defined')
else:
r.ok('No font references')
# --- Check 11: line/border indices in formats ---
if line_count > 0:
if max_line_ref < line_count:
r.ok(f'Line/border refs: max={max_line_ref}, palette size={line_count}')
else:
r.error(f'Line index {max_line_ref} exceeds palette size ({line_count})')
elif max_line_ref > 0:
r.error(f'Line index {max_line_ref} referenced but no lines defined')
else:
r.ok('No line/border references')
# --- Check 3, 4, 5, 6: row/cell checks ---
max_cell_format_ref = 0
max_row_format_ref = 0
max_default_col_idx = 0
row_index = 0
for ri in row_nodes:
if r.stopped:
break
idx_node = ri.find(f'{{{NS_D}}}index')
if idx_node is not None and idx_node.text:
row_index = int(idx_node.text)
row = ri.find(f'{{{NS_D}}}row')
if row is None:
row_index += 1
continue
# Row formatIndex
row_fmt_node = row.find(f'{{{NS_D}}}formatIndex')
if row_fmt_node is not None and row_fmt_node.text:
val = int(row_fmt_node.text)
if val > max_row_format_ref:
max_row_format_ref = val
if val > format_count:
r.error(f'Row {row_index}: formatIndex={val} > format palette size ({format_count})')
# Check columnsID
row_cols_id = None
cols_id_node = row.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
row_cols_id = cols_id_node.text
if row_cols_id not in column_sets:
r.error(f"Row {row_index}: columnsID '{row_cols_id[:8]}...' not found in column sets")
# Determine column count for this row
row_col_count = default_col_count
if row_cols_id and row_cols_id in column_sets:
row_col_count = column_sets[row_cols_id]
# Cell checks
for c_group in row.findall(f'{{{NS_D}}}c'):
i_node = c_group.find(f'{{{NS_D}}}i')
col_idx = None
if i_node is not None and i_node.text:
col_idx = int(i_node.text)
# Track max index for default column set only
if row_cols_id is None and col_idx > max_default_col_idx:
max_default_col_idx = col_idx
# Check against row's column count
if row_col_count > 0 and col_idx >= row_col_count:
r.error(f'Row {row_index}: column index {col_idx} >= column count ({row_col_count})')
cell = c_group.find(f'{{{NS_D}}}c')
if cell is not None:
f_node = cell.find(f'{{{NS_D}}}f')
if f_node is not None and f_node.text:
val = int(f_node.text)
if val > max_cell_format_ref:
max_cell_format_ref = val
if val > format_count:
r.error(f'Row {row_index}: cell format index {val} > format palette size ({format_count})')
row_index += 1
# Summary checks for format refs
if not r.stopped:
if max_cell_format_ref <= format_count and max_row_format_ref <= format_count:
r.ok(f'Format refs: max cell={max_cell_format_ref}, max row={max_row_format_ref}, palette size={format_count}')
# Check column format indices
for cols in root.findall(f'{{{NS_D}}}columns'):
if r.stopped:
break
for ci in cols.findall(f'{{{NS_D}}}columnsItem'):
col = ci.find(f'{{{NS_D}}}column')
if col is not None:
fmt_node = col.find(f'{{{NS_D}}}formatIndex')
if fmt_node is not None and fmt_node.text:
val = int(fmt_node.text)
if val > format_count:
col_idx_node = ci.find(f'{{{NS_D}}}index')
col_idx_text = col_idx_node.text if col_idx_node is not None else '?'
r.error(f'Column {col_idx_text}: formatIndex={val} > format palette size ({format_count})')
# --- Check 5: column index summary ---
if not r.stopped:
r.ok(f'Column indices: max in default set={max_default_col_idx}, default column count={default_col_count}')
# --- Check 7, 8: named areas ---
for ni in root.findall(f'{{{NS_D}}}namedItem'):
if r.stopped:
break
ni_type = ni.get(f'{{{NS_XSI}}}type', '')
name_node = ni.find(f'{{{NS_D}}}name')
name = name_node.text if name_node is not None else ''
if 'NamedItemCells' in ni_type:
area = ni.find(f'{{{NS_D}}}area')
if area is None:
continue
begin_row = int_text(area.find(f'{{{NS_D}}}beginRow'))
end_row = int_text(area.find(f'{{{NS_D}}}endRow'))
# Check row bounds (skip -1 which means "all")
if begin_row != -1 and begin_row >= doc_height:
r.error(f"Area '{name}': beginRow={begin_row} >= height={doc_height}")
if end_row != -1 and end_row >= doc_height:
r.error(f"Area '{name}': endRow={end_row} >= height={doc_height}")
# Check columnsID reference
cols_id_node = area.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
cols_id = cols_id_node.text
if cols_id not in column_sets:
r.error(f"Area '{name}': columnsID '{cols_id[:8]}...' not found")
# --- Check 9: merge bounds ---
for merge in root.findall(f'{{{NS_D}}}merge'):
if r.stopped:
break
merge_r = int_text(merge.find(f'{{{NS_D}}}r'))
merge_c = int_text(merge.find(f'{{{NS_D}}}c'))
w_node = merge.find(f'{{{NS_D}}}w')
h_node = merge.find(f'{{{NS_D}}}h')
# r=-1 means all rows, skip bound check
if merge_r != -1 and merge_r >= doc_height:
r.error(f'Merge at row={merge_r}, col={merge_c}: row >= height ({doc_height})')
if h_node is not None and merge_r != -1:
h = int_text(h_node)
if (merge_r + h) >= doc_height:
r.error(f'Merge at row={merge_r}: extends to row {merge_r + h} >= height ({doc_height})')
# Check columnsID in merge
cols_id_node = merge.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
cols_id = cols_id_node.text
if cols_id not in column_sets:
r.error(f"Merge at row={merge_r}, col={merge_c}: columnsID '{cols_id[:8]}...' not found")
# --- Check 12: drawing picture indices ---
for drawing in root.findall(f'{{{NS_D}}}drawing'):
if r.stopped:
break
pic_idx_node = drawing.find(f'{{{NS_D}}}pictureIndex')
if pic_idx_node is not None and pic_idx_node.text:
pic_idx = int(pic_idx_node.text)
if pic_idx > picture_count:
draw_id_node = drawing.find(f'{{{NS_D}}}id')
draw_id = draw_id_node.text if draw_id_node is not None else '?'
r.error(f'Drawing id={draw_id}: pictureIndex={pic_idx} > picture count ({picture_count})')
# --- Summary ---
print()
print('---')
if r.stopped:
print(f'Stopped after {max_errors} errors. Fix and re-run.')
if r.errors == 0 and r.warnings == 0:
print('All checks passed.')
else:
print(f'Errors: {r.errors}, Warnings: {r.warnings}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# mxl-validate v1.0 — Validate 1C spreadsheet document Template.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges."""
import sys, os, argparse
from lxml import etree
NS_D = 'http://v8.1c.ru/8.2/data/spreadsheet'
NS_V8 = 'http://v8.1c.ru/8.1/data/core'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS = {
'd': NS_D,
'v8': NS_V8,
'xsi': NS_XSI,
}
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
def ok(self, msg):
print(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
print(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
print(f'[WARN] {msg}')
def int_text(node):
"""Return int from node text, or 0 if None."""
if node is not None and node.text:
return int(node.text)
return 0
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description='Validate 1C spreadsheet document Template.xml', allow_abbrev=False
)
parser.add_argument('-TemplatePath', dest='TemplatePath', default='')
parser.add_argument('-ProcessorName', dest='ProcessorName', default='')
parser.add_argument('-TemplateName', dest='TemplateName', default='')
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=20)
args = parser.parse_args()
template_path = args.TemplatePath
processor_name = args.ProcessorName
template_name_arg = args.TemplateName
src_dir = args.SrcDir
max_errors = args.MaxErrors
# --- Resolve template path ---
if not template_path:
if not processor_name or not template_name_arg:
print('Specify -TemplatePath or both -ProcessorName and -TemplateName', file=sys.stderr)
sys.exit(1)
template_path = os.path.join(src_dir, processor_name, 'Templates',
template_name_arg, 'Ext', 'Template.xml')
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
if not os.path.exists(template_path):
print(f'File not found: {template_path}', file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(template_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
root = xml_doc.getroot()
r = Reporter(max_errors)
# Derive template name from path: .../Templates/<Name>/Ext/Template.xml
# Go up 2 levels from Template.xml -> Ext -> <Name>
template_display_name = os.path.basename(os.path.dirname(os.path.dirname(resolved_path)))
print(f'=== Validation: {template_display_name} ===')
print()
# --- Collect palettes ---
line_nodes = root.findall(f'{{{NS_D}}}line')
line_count = len(line_nodes)
font_nodes = [node for node in root if isinstance(node.tag, str) and etree.QName(node.tag).localname == 'font']
font_count = len(font_nodes)
format_nodes = [node for node in root if isinstance(node.tag, str) and etree.QName(node.tag).localname == 'format']
format_count = len(format_nodes)
picture_nodes = root.findall(f'{{{NS_D}}}picture')
picture_count = len(picture_nodes)
# --- Collect column sets ---
column_sets = {} # id -> size
default_col_count = 0
for cols in root.findall(f'{{{NS_D}}}columns'):
size_node = cols.find(f'{{{NS_D}}}size')
id_node = cols.find(f'{{{NS_D}}}id')
size = int_text(size_node)
if id_node is not None and id_node.text:
column_sets[id_node.text] = size
else:
default_col_count = size
# --- Check 1: height vs actual rows ---
row_nodes = root.findall(f'{{{NS_D}}}rowsItem')
height_node = root.find(f'{{{NS_D}}}height')
doc_height = int_text(height_node)
max_row_index = -1
for ri in row_nodes:
idx_node = ri.find(f'{{{NS_D}}}index')
if idx_node is not None and idx_node.text:
idx = int(idx_node.text)
if idx > max_row_index:
max_row_index = idx
expected_min_height = max_row_index + 1
if doc_height >= expected_min_height:
r.ok(f'height ({doc_height}) >= max row index + 1 ({expected_min_height}), rowsItem count={len(row_nodes)}')
else:
r.error(f'height={doc_height} but max row index={max_row_index} (need at least {expected_min_height})')
# --- Check 2: vgRows <= height ---
vg_rows_node = root.find(f'{{{NS_D}}}vgRows')
if vg_rows_node is not None:
vg_rows = int_text(vg_rows_node)
if vg_rows <= doc_height:
r.ok(f'vgRows ({vg_rows}) <= height ({doc_height})')
else:
r.warn(f'vgRows ({vg_rows}) > height ({doc_height})')
# --- Build row data for checks ---
max_format_ref = 0
max_font_ref = 0
max_line_ref = 0
# Check format palette references in formats (font, border indices)
for fmt in format_nodes:
font_idx_node = fmt.find(f'{{{NS_D}}}font')
if font_idx_node is not None and font_idx_node.text:
val = int(font_idx_node.text)
if val > max_font_ref:
max_font_ref = val
for border_name in ('leftBorder', 'topBorder', 'rightBorder', 'bottomBorder', 'drawingBorder'):
border_node = fmt.find(f'{{{NS_D}}}{border_name}')
if border_node is not None and border_node.text:
val = int(border_node.text)
if val > max_line_ref:
max_line_ref = val
# --- Check 10: font indices in formats ---
if font_count > 0:
if max_font_ref < font_count:
r.ok(f'Font refs: max={max_font_ref}, palette size={font_count}')
else:
r.error(f'Font index {max_font_ref} exceeds palette size ({font_count})')
elif max_font_ref > 0:
r.error(f'Font index {max_font_ref} referenced but no fonts defined')
else:
r.ok('No font references')
# --- Check 11: line/border indices in formats ---
if line_count > 0:
if max_line_ref < line_count:
r.ok(f'Line/border refs: max={max_line_ref}, palette size={line_count}')
else:
r.error(f'Line index {max_line_ref} exceeds palette size ({line_count})')
elif max_line_ref > 0:
r.error(f'Line index {max_line_ref} referenced but no lines defined')
else:
r.ok('No line/border references')
# --- Check 3, 4, 5, 6: row/cell checks ---
max_cell_format_ref = 0
max_row_format_ref = 0
max_default_col_idx = 0
row_index = 0
for ri in row_nodes:
if r.stopped:
break
idx_node = ri.find(f'{{{NS_D}}}index')
if idx_node is not None and idx_node.text:
row_index = int(idx_node.text)
row = ri.find(f'{{{NS_D}}}row')
if row is None:
row_index += 1
continue
# Row formatIndex
row_fmt_node = row.find(f'{{{NS_D}}}formatIndex')
if row_fmt_node is not None and row_fmt_node.text:
val = int(row_fmt_node.text)
if val > max_row_format_ref:
max_row_format_ref = val
if val > format_count:
r.error(f'Row {row_index}: formatIndex={val} > format palette size ({format_count})')
# Check columnsID
row_cols_id = None
cols_id_node = row.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
row_cols_id = cols_id_node.text
if row_cols_id not in column_sets:
r.error(f"Row {row_index}: columnsID '{row_cols_id[:8]}...' not found in column sets")
# Determine column count for this row
row_col_count = default_col_count
if row_cols_id and row_cols_id in column_sets:
row_col_count = column_sets[row_cols_id]
# Cell checks
for c_group in row.findall(f'{{{NS_D}}}c'):
i_node = c_group.find(f'{{{NS_D}}}i')
col_idx = None
if i_node is not None and i_node.text:
col_idx = int(i_node.text)
# Track max index for default column set only
if row_cols_id is None and col_idx > max_default_col_idx:
max_default_col_idx = col_idx
# Check against row's column count
if row_col_count > 0 and col_idx >= row_col_count:
r.error(f'Row {row_index}: column index {col_idx} >= column count ({row_col_count})')
cell = c_group.find(f'{{{NS_D}}}c')
if cell is not None:
f_node = cell.find(f'{{{NS_D}}}f')
if f_node is not None and f_node.text:
val = int(f_node.text)
if val > max_cell_format_ref:
max_cell_format_ref = val
if val > format_count:
r.error(f'Row {row_index}: cell format index {val} > format palette size ({format_count})')
row_index += 1
# Summary checks for format refs
if not r.stopped:
if max_cell_format_ref <= format_count and max_row_format_ref <= format_count:
r.ok(f'Format refs: max cell={max_cell_format_ref}, max row={max_row_format_ref}, palette size={format_count}')
# Check column format indices
for cols in root.findall(f'{{{NS_D}}}columns'):
if r.stopped:
break
for ci in cols.findall(f'{{{NS_D}}}columnsItem'):
col = ci.find(f'{{{NS_D}}}column')
if col is not None:
fmt_node = col.find(f'{{{NS_D}}}formatIndex')
if fmt_node is not None and fmt_node.text:
val = int(fmt_node.text)
if val > format_count:
col_idx_node = ci.find(f'{{{NS_D}}}index')
col_idx_text = col_idx_node.text if col_idx_node is not None else '?'
r.error(f'Column {col_idx_text}: formatIndex={val} > format palette size ({format_count})')
# --- Check 5: column index summary ---
if not r.stopped:
r.ok(f'Column indices: max in default set={max_default_col_idx}, default column count={default_col_count}')
# --- Check 7, 8: named areas ---
for ni in root.findall(f'{{{NS_D}}}namedItem'):
if r.stopped:
break
ni_type = ni.get(f'{{{NS_XSI}}}type', '')
name_node = ni.find(f'{{{NS_D}}}name')
name = name_node.text if name_node is not None else ''
if 'NamedItemCells' in ni_type:
area = ni.find(f'{{{NS_D}}}area')
if area is None:
continue
begin_row = int_text(area.find(f'{{{NS_D}}}beginRow'))
end_row = int_text(area.find(f'{{{NS_D}}}endRow'))
# Check row bounds (skip -1 which means "all")
if begin_row != -1 and begin_row >= doc_height:
r.error(f"Area '{name}': beginRow={begin_row} >= height={doc_height}")
if end_row != -1 and end_row >= doc_height:
r.error(f"Area '{name}': endRow={end_row} >= height={doc_height}")
# Check columnsID reference
cols_id_node = area.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
cols_id = cols_id_node.text
if cols_id not in column_sets:
r.error(f"Area '{name}': columnsID '{cols_id[:8]}...' not found")
# --- Check 9: merge bounds ---
for merge in root.findall(f'{{{NS_D}}}merge'):
if r.stopped:
break
merge_r = int_text(merge.find(f'{{{NS_D}}}r'))
merge_c = int_text(merge.find(f'{{{NS_D}}}c'))
w_node = merge.find(f'{{{NS_D}}}w')
h_node = merge.find(f'{{{NS_D}}}h')
# r=-1 means all rows, skip bound check
if merge_r != -1 and merge_r >= doc_height:
r.error(f'Merge at row={merge_r}, col={merge_c}: row >= height ({doc_height})')
if h_node is not None and merge_r != -1:
h = int_text(h_node)
if (merge_r + h) >= doc_height:
r.error(f'Merge at row={merge_r}: extends to row {merge_r + h} >= height ({doc_height})')
# Check columnsID in merge
cols_id_node = merge.find(f'{{{NS_D}}}columnsID')
if cols_id_node is not None and cols_id_node.text:
cols_id = cols_id_node.text
if cols_id not in column_sets:
r.error(f"Merge at row={merge_r}, col={merge_c}: columnsID '{cols_id[:8]}...' not found")
# --- Check 12: drawing picture indices ---
for drawing in root.findall(f'{{{NS_D}}}drawing'):
if r.stopped:
break
pic_idx_node = drawing.find(f'{{{NS_D}}}pictureIndex')
if pic_idx_node is not None and pic_idx_node.text:
pic_idx = int(pic_idx_node.text)
if pic_idx > picture_count:
draw_id_node = drawing.find(f'{{{NS_D}}}id')
draw_id = draw_id_node.text if draw_id_node is not None else '?'
r.error(f'Drawing id={draw_id}: pictureIndex={pic_idx} > picture count ({picture_count})')
# --- Summary ---
print()
print('---')
if r.stopped:
print(f'Stopped after {max_errors} errors. Fix and re-run.')
if r.errors == 0 and r.warnings == 0:
print('All checks passed.')
else:
print(f'Errors: {r.errors}, Warnings: {r.warnings}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
+231 -229
View File
@@ -1,229 +1,231 @@
#!/usr/bin/env python3
# role-info v1.0 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from collections import OrderedDict
from lxml import etree
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False)
parser.add_argument("-RightsPath", required=True, help="Path to Rights.xml")
parser.add_argument("-ShowDenied", action="store_true", default=False, help="Show denied rights")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve paths ---
rights_path = args.RightsPath
if not os.path.isabs(rights_path):
rights_path = os.path.join(os.getcwd(), rights_path)
if not os.path.isfile(rights_path):
print(f"[ERROR] File not found: {rights_path}", file=sys.stderr)
sys.exit(1)
# --- Try to find metadata file for role name/synonym ---
role_name = ""
role_synonym = ""
ext_dir = os.path.dirname(rights_path) # .../Ext
role_dir = os.path.dirname(ext_dir) # .../RoleName
roles_dir = os.path.dirname(role_dir) # .../Roles
role_folder_name = os.path.basename(role_dir)
meta_path = os.path.join(roles_dir, f"{role_folder_name}.xml")
if os.path.isfile(meta_path):
try:
meta_tree = etree.parse(meta_path, etree.XMLParser(remove_blank_text=False))
meta_root = meta_tree.getroot()
meta_ns = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
}
name_node = meta_root.find(".//md:Role/md:Properties/md:Name", meta_ns)
if name_node is not None and name_node.text:
role_name = name_node.text
syn_node = meta_root.find(
".//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", meta_ns
)
if syn_node is not None and syn_node.text:
role_synonym = syn_node.text
except Exception:
pass
if not role_name:
role_name = role_folder_name
# --- Parse Rights.xml ---
tree = etree.parse(rights_path, etree.XMLParser(remove_blank_text=False))
root = tree.getroot()
rights_ns = "http://v8.1c.ru/8.2/roles"
NSMAP = {"r": rights_ns}
# Global flags
set_for_new = root.get("setForNewObjects", "")
set_for_attrs = root.get("setForAttributesByDefault", "")
independent_child = root.get("independentRightsOfChildObjects", "")
# --- Collect objects ---
allowed = OrderedDict() # type -> OrderedDict { shortName -> [rights] }
denied = OrderedDict()
rls_objects = []
total_allowed = 0
total_denied = 0
for obj in root.findall("r:object", NSMAP):
obj_name = ""
rights = []
for child in obj:
local = etree.QName(child.tag).localname
if local == "name" and child.tag == f"{{{rights_ns}}}name":
obj_name = child.text or ""
if local == "right" and child.tag == f"{{{rights_ns}}}right":
r_name = ""
r_value = ""
has_rls = False
for rc in child:
rc_local = etree.QName(rc.tag).localname
if rc_local == "name":
r_name = rc.text or ""
if rc_local == "value":
r_value = rc.text or ""
if rc_local == "restrictionByCondition":
has_rls = True
if r_name and r_value:
rights.append({"name": r_name, "value": r_value, "rls": has_rls})
if not obj_name or len(rights) == 0:
continue
dot_idx = obj_name.find(".")
if dot_idx < 0:
continue
type_prefix = obj_name[:dot_idx]
short_name = obj_name[dot_idx + 1:]
for r in rights:
if r["value"] == "true":
total_allowed += 1
if type_prefix not in allowed:
allowed[type_prefix] = OrderedDict()
if short_name not in allowed[type_prefix]:
allowed[type_prefix][short_name] = []
suffix = r["name"]
if r["rls"]:
suffix += " [RLS]"
rls_objects.append(f"{type_prefix}.{short_name} ({r['name']})")
allowed[type_prefix][short_name].append(suffix)
else:
total_denied += 1
if type_prefix not in denied:
denied[type_prefix] = OrderedDict()
if short_name not in denied[type_prefix]:
denied[type_prefix][short_name] = []
denied[type_prefix][short_name].append(r["name"])
# --- Restriction templates ---
templates = []
for tpl in root.findall("r:restrictionTemplate", NSMAP):
for child in tpl:
if etree.QName(child.tag).localname == "name":
t_name = child.text or ""
paren_idx = t_name.find("(")
if paren_idx > 0:
t_name = t_name[:paren_idx]
templates.append(t_name)
# --- Output ---
header = f"=== Role: {role_name}"
if role_synonym:
header += f' --- "{role_synonym}"'
header += " ==="
out(header)
out()
out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}")
out()
# Helper: output group
def out_group(obj_map, is_denied=False):
for short_name, rights_list in obj_map.items():
if is_denied:
rights_str = ", ".join(f"-{r}" for r in rights_list)
else:
rights_str = ", ".join(rights_list)
out(f" {short_name}: {rights_str}")
# Allowed rights grouped by type
if len(allowed) > 0:
out("Allowed rights:")
out()
for type_prefix, obj_map in allowed.items():
out(f" {type_prefix} ({len(obj_map)}):")
out_group(obj_map)
out()
else:
out("(no allowed rights)")
out()
# Denied rights
if args.ShowDenied and len(denied) > 0:
out("Denied rights:")
out()
for type_prefix, obj_map in denied.items():
out(f" {type_prefix} ({len(obj_map)}):")
out_group(obj_map, is_denied=True)
out()
elif total_denied > 0:
out(f"Denied: {total_denied} rights (use -ShowDenied to list)")
out()
# RLS summary
if len(rls_objects) > 0:
out(f"RLS: {len(rls_objects)} restrictions")
# Templates
if len(templates) > 0:
out(f"Templates: {', '.join(templates)}")
out()
out("---")
out(f"Total: {total_allowed} allowed, {total_denied} denied")
# --- Pagination and output ---
total_lines = len(lines_buf)
out_lines = lines_buf[:]
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
out_lines = out_lines[args.Offset:]
if args.Limit > 0 and len(out_lines) > args.Limit:
shown = out_lines[:args.Limit]
remaining = total_lines - args.Offset - args.Limit
shown.append("")
shown.append(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
out_lines = shown
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(out_lines))
print(f"Output written to {out_file}")
else:
for line in out_lines:
print(line)
#!/usr/bin/env python3
# role-info v1.0 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
from collections import OrderedDict
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False)
parser.add_argument("-RightsPath", required=True, help="Path to Rights.xml")
parser.add_argument("-ShowDenied", action="store_true", default=False, help="Show denied rights")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = parser.parse_args()
# --- Output helper (collect all, paginate at the end) ---
lines_buf = []
def out(text=""):
lines_buf.append(text)
# --- Resolve paths ---
rights_path = args.RightsPath
if not os.path.isabs(rights_path):
rights_path = os.path.join(os.getcwd(), rights_path)
if not os.path.isfile(rights_path):
print(f"[ERROR] File not found: {rights_path}", file=sys.stderr)
sys.exit(1)
# --- Try to find metadata file for role name/synonym ---
role_name = ""
role_synonym = ""
ext_dir = os.path.dirname(rights_path) # .../Ext
role_dir = os.path.dirname(ext_dir) # .../RoleName
roles_dir = os.path.dirname(role_dir) # .../Roles
role_folder_name = os.path.basename(role_dir)
meta_path = os.path.join(roles_dir, f"{role_folder_name}.xml")
if os.path.isfile(meta_path):
try:
meta_tree = etree.parse(meta_path, etree.XMLParser(remove_blank_text=False))
meta_root = meta_tree.getroot()
meta_ns = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
}
name_node = meta_root.find(".//md:Role/md:Properties/md:Name", meta_ns)
if name_node is not None and name_node.text:
role_name = name_node.text
syn_node = meta_root.find(
".//md:Role/md:Properties/md:Synonym/v8:item[v8:lang='ru']/v8:content", meta_ns
)
if syn_node is not None and syn_node.text:
role_synonym = syn_node.text
except Exception:
pass
if not role_name:
role_name = role_folder_name
# --- Parse Rights.xml ---
tree = etree.parse(rights_path, etree.XMLParser(remove_blank_text=False))
root = tree.getroot()
rights_ns = "http://v8.1c.ru/8.2/roles"
NSMAP = {"r": rights_ns}
# Global flags
set_for_new = root.get("setForNewObjects", "")
set_for_attrs = root.get("setForAttributesByDefault", "")
independent_child = root.get("independentRightsOfChildObjects", "")
# --- Collect objects ---
allowed = OrderedDict() # type -> OrderedDict { shortName -> [rights] }
denied = OrderedDict()
rls_objects = []
total_allowed = 0
total_denied = 0
for obj in root.findall("r:object", NSMAP):
obj_name = ""
rights = []
for child in obj:
local = etree.QName(child.tag).localname
if local == "name" and child.tag == f"{{{rights_ns}}}name":
obj_name = child.text or ""
if local == "right" and child.tag == f"{{{rights_ns}}}right":
r_name = ""
r_value = ""
has_rls = False
for rc in child:
rc_local = etree.QName(rc.tag).localname
if rc_local == "name":
r_name = rc.text or ""
if rc_local == "value":
r_value = rc.text or ""
if rc_local == "restrictionByCondition":
has_rls = True
if r_name and r_value:
rights.append({"name": r_name, "value": r_value, "rls": has_rls})
if not obj_name or len(rights) == 0:
continue
dot_idx = obj_name.find(".")
if dot_idx < 0:
continue
type_prefix = obj_name[:dot_idx]
short_name = obj_name[dot_idx + 1:]
for r in rights:
if r["value"] == "true":
total_allowed += 1
if type_prefix not in allowed:
allowed[type_prefix] = OrderedDict()
if short_name not in allowed[type_prefix]:
allowed[type_prefix][short_name] = []
suffix = r["name"]
if r["rls"]:
suffix += " [RLS]"
rls_objects.append(f"{type_prefix}.{short_name} ({r['name']})")
allowed[type_prefix][short_name].append(suffix)
else:
total_denied += 1
if type_prefix not in denied:
denied[type_prefix] = OrderedDict()
if short_name not in denied[type_prefix]:
denied[type_prefix][short_name] = []
denied[type_prefix][short_name].append(r["name"])
# --- Restriction templates ---
templates = []
for tpl in root.findall("r:restrictionTemplate", NSMAP):
for child in tpl:
if etree.QName(child.tag).localname == "name":
t_name = child.text or ""
paren_idx = t_name.find("(")
if paren_idx > 0:
t_name = t_name[:paren_idx]
templates.append(t_name)
# --- Output ---
header = f"=== Role: {role_name}"
if role_synonym:
header += f' --- "{role_synonym}"'
header += " ==="
out(header)
out()
out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}")
out()
# Helper: output group
def out_group(obj_map, is_denied=False):
for short_name, rights_list in obj_map.items():
if is_denied:
rights_str = ", ".join(f"-{r}" for r in rights_list)
else:
rights_str = ", ".join(rights_list)
out(f" {short_name}: {rights_str}")
# Allowed rights grouped by type
if len(allowed) > 0:
out("Allowed rights:")
out()
for type_prefix, obj_map in allowed.items():
out(f" {type_prefix} ({len(obj_map)}):")
out_group(obj_map)
out()
else:
out("(no allowed rights)")
out()
# Denied rights
if args.ShowDenied and len(denied) > 0:
out("Denied rights:")
out()
for type_prefix, obj_map in denied.items():
out(f" {type_prefix} ({len(obj_map)}):")
out_group(obj_map, is_denied=True)
out()
elif total_denied > 0:
out(f"Denied: {total_denied} rights (use -ShowDenied to list)")
out()
# RLS summary
if len(rls_objects) > 0:
out(f"RLS: {len(rls_objects)} restrictions")
# Templates
if len(templates) > 0:
out(f"Templates: {', '.join(templates)}")
out()
out("---")
out(f"Total: {total_allowed} allowed, {total_denied} denied")
# --- Pagination and output ---
total_lines = len(lines_buf)
out_lines = lines_buf[:]
if args.Offset > 0:
if args.Offset >= total_lines:
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
sys.exit(0)
out_lines = out_lines[args.Offset:]
if args.Limit > 0 and len(out_lines) > args.Limit:
shown = out_lines[:args.Limit]
remaining = total_lines - args.Offset - args.Limit
shown.append("")
shown.append(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
out_lines = shown
if args.OutFile:
out_file = args.OutFile
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(out_lines))
print(f"Output written to {out_file}")
else:
for line in out_lines:
print(line)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,286 +1,287 @@
#!/usr/bin/env python3
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import uuid
import xml.etree.ElementTree as ET
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def emit_mltext(lines, indent, tag, text):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def split_camel_case(name):
if not name:
return name
result = re.sub(r'([a-z\u0430-\u044f\u0451])([A-Z\u0410-\u042f\u0401])', r'\1 \2', name)
if len(result) > 1:
result = result[0] + result[1:].lower()
return result
def main():
parser = argparse.ArgumentParser(description='Compile 1C subsystem from JSON definition', allow_abbrev=False)
parser.add_argument('-DefinitionFile', type=str, default=None)
parser.add_argument('-Value', type=str, default=None)
parser.add_argument('-OutputDir', type=str, required=True)
parser.add_argument('-Parent', type=str, default=None)
parser.add_argument('-NoValidate', action='store_true', default=False)
args = parser.parse_args()
# --- 1. Load JSON ---
if args.DefinitionFile and args.Value:
print("Cannot use both -DefinitionFile and -Value", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Value:
print("Either -DefinitionFile or -Value is required", file=sys.stderr)
sys.exit(1)
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
if not os.path.exists(def_file):
print(f"Definition file not found: {def_file}", file=sys.stderr)
sys.exit(1)
with open(def_file, 'r', encoding='utf-8-sig') as f:
json_text = f.read()
else:
json_text = args.Value
defn = json.loads(json_text)
if not defn.get('name'):
print("JSON must have 'name' field", file=sys.stderr)
sys.exit(1)
obj_name = str(defn['name'])
# Resolve OutputDir
output_dir = args.OutputDir
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- 2. Resolve defaults ---
synonym = str(defn['synonym']) if defn.get('synonym') else split_camel_case(obj_name)
comment = str(defn['comment']) if defn.get('comment') else ''
include_help_in_contents = 'true'
include_in_ci = str(defn['includeInCommandInterface']).lower() if defn.get('includeInCommandInterface') is not None else 'true'
use_one_command = str(defn['useOneCommand']).lower() if defn.get('useOneCommand') is not None else 'false'
explanation = str(defn['explanation']) if defn.get('explanation') else ''
picture = str(defn['picture']) if defn.get('picture') else ''
content_items = []
if defn.get('content'):
for c in defn['content']:
content_items.append(str(c))
children = []
if defn.get('children'):
for ch in defn['children']:
children.append(str(ch))
# --- 3. Build XML ---
uid = new_uuid()
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">')
lines.append(f'\t<Subsystem uuid="{uid}">')
lines.append('\t\t<Properties>')
# Name
lines.append(f'\t\t\t<Name>{esc_xml(obj_name)}</Name>')
# Synonym
emit_mltext(lines, '\t\t\t', 'Synonym', synonym)
# Comment
if comment:
lines.append(f'\t\t\t<Comment>{esc_xml(comment)}</Comment>')
else:
lines.append('\t\t\t<Comment/>')
# Boolean properties
lines.append(f'\t\t\t<IncludeHelpInContents>{include_help_in_contents}</IncludeHelpInContents>')
lines.append(f'\t\t\t<IncludeInCommandInterface>{include_in_ci}</IncludeInCommandInterface>')
lines.append(f'\t\t\t<UseOneCommand>{use_one_command}</UseOneCommand>')
# Explanation
emit_mltext(lines, '\t\t\t', 'Explanation', explanation)
# Picture
if picture:
lines.append('\t\t\t<Picture>')
lines.append(f'\t\t\t\t<xr:Ref>{picture}</xr:Ref>')
lines.append('\t\t\t\t<xr:LoadTransparent>false</xr:LoadTransparent>')
lines.append('\t\t\t</Picture>')
else:
lines.append('\t\t\t<Picture/>')
# Content
if len(content_items) > 0:
lines.append('\t\t\t<Content>')
for item in content_items:
lines.append(f'\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(item)}</xr:Item>')
lines.append('\t\t\t</Content>')
else:
lines.append('\t\t\t<Content/>')
lines.append('\t\t</Properties>')
# ChildObjects
if len(children) > 0:
lines.append('\t\t<ChildObjects>')
for ch in children:
lines.append(f'\t\t\t<Subsystem>{esc_xml(ch)}</Subsystem>')
lines.append('\t\t</ChildObjects>')
else:
lines.append('\t\t<ChildObjects/>')
lines.append('\t</Subsystem>')
lines.append('</MetaDataObject>')
# --- 4. Write files ---
parent = args.Parent
if parent:
# Nested subsystem
if not os.path.isabs(parent):
parent = os.path.join(os.getcwd(), parent)
if not os.path.exists(parent):
print(f"Parent subsystem not found: {parent}", file=sys.stderr)
sys.exit(1)
parent_dir = os.path.dirname(parent)
parent_base_name = os.path.splitext(os.path.basename(parent))[0]
subs_dir = os.path.join(parent_dir, parent_base_name, 'Subsystems')
else:
# Top-level subsystem
subs_dir = os.path.join(output_dir, 'Subsystems')
os.makedirs(subs_dir, exist_ok=True)
target_xml = os.path.join(subs_dir, f'{obj_name}.xml')
# Write XML
xml_content = '\n'.join(lines) + '\n'
write_utf8_bom(target_xml, xml_content)
print(f"[OK] Created: {target_xml}")
# Create subdirectory if children exist
if len(children) > 0:
child_subs_dir = os.path.join(subs_dir, obj_name, 'Subsystems')
if not os.path.exists(child_subs_dir):
os.makedirs(child_subs_dir, exist_ok=True)
print(f"[OK] Created directory: {child_subs_dir}")
# --- 5. Register in parent ---
parent_xml_path = None
if parent:
parent_xml_path = parent
else:
config_xml = os.path.join(output_dir, 'Configuration.xml')
if os.path.exists(config_xml):
parent_xml_path = config_xml
if parent_xml_path and os.path.exists(parent_xml_path):
with open(parent_xml_path, 'r', encoding='utf-8-sig') as f:
raw_text = f.read()
doc = ET.ElementTree(ET.fromstring(raw_text))
root = doc.getroot()
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
# Find ChildObjects
child_objects = None
if parent:
for sub in root.iter(f'{{{md_ns}}}Subsystem'):
child_objects = sub.find(f'{{{md_ns}}}ChildObjects')
break
else:
for cfg in root.iter(f'{{{md_ns}}}Configuration'):
child_objects = cfg.find(f'{{{md_ns}}}ChildObjects')
break
if child_objects is not None:
# Check if already registered
already_exists = False
for child in child_objects:
if child.tag == f'{{{md_ns}}}Subsystem' and child.text == obj_name:
already_exists = True
break
if not already_exists:
new_el = ET.SubElement(child_objects, f'{{{md_ns}}}Subsystem')
new_el.text = obj_name
# Re-serialize with whitespace preservation via raw text manipulation instead
# Since ElementTree doesn't preserve whitespace well, use regex-based insertion
# Find </ChildObjects> or <ChildObjects/> and inject
pass # Fall through to raw text approach below
if not already_exists:
# Use raw text manipulation to preserve formatting
if '<ChildObjects/>' in raw_text:
replacement = f'<ChildObjects>\n\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n\t\t</ChildObjects>'
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
elif '</ChildObjects>' in raw_text:
insert_line = f'\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n'
raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1)
write_utf8_bom(parent_xml_path, raw_text)
print(f"[OK] Registered in: {parent_xml_path}")
else:
print(f"[SKIP] Already registered in: {parent_xml_path}")
else:
print(f"[WARN] ChildObjects not found in: {parent_xml_path}")
else:
print("[INFO] No parent XML to register in")
# --- 6. Auto-validate ---
if not args.NoValidate:
script_dir = os.path.dirname(os.path.abspath(__file__))
validate_script = os.path.normpath(os.path.join(script_dir, '..', '..', 'subsystem-validate', 'scripts', 'subsystem-validate.ps1'))
if os.path.exists(validate_script):
print()
print("--- Running subsystem-validate ---")
os.system(f'powershell.exe -NoProfile -File "{validate_script}" -SubsystemPath "{target_xml}"')
# --- 7. Summary ---
print()
print("=== subsystem-compile summary ===")
print(f" Name: {obj_name}")
print(f" UUID: {uid}")
print(f" Content: {len(content_items)} objects")
print(f" Children: {len(children)}")
print(f" File: {target_xml}")
sys.exit(0)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import uuid
import xml.etree.ElementTree as ET
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def emit_mltext(lines, indent, tag, text):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f"{indent}<{tag}>")
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def split_camel_case(name):
if not name:
return name
result = re.sub(r'([a-z\u0430-\u044f\u0451])([A-Z\u0410-\u042f\u0401])', r'\1 \2', name)
if len(result) > 1:
result = result[0] + result[1:].lower()
return result
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Compile 1C subsystem from JSON definition', allow_abbrev=False)
parser.add_argument('-DefinitionFile', type=str, default=None)
parser.add_argument('-Value', type=str, default=None)
parser.add_argument('-OutputDir', type=str, required=True)
parser.add_argument('-Parent', type=str, default=None)
parser.add_argument('-NoValidate', action='store_true', default=False)
args = parser.parse_args()
# --- 1. Load JSON ---
if args.DefinitionFile and args.Value:
print("Cannot use both -DefinitionFile and -Value", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Value:
print("Either -DefinitionFile or -Value is required", file=sys.stderr)
sys.exit(1)
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
if not os.path.exists(def_file):
print(f"Definition file not found: {def_file}", file=sys.stderr)
sys.exit(1)
with open(def_file, 'r', encoding='utf-8-sig') as f:
json_text = f.read()
else:
json_text = args.Value
defn = json.loads(json_text)
if not defn.get('name'):
print("JSON must have 'name' field", file=sys.stderr)
sys.exit(1)
obj_name = str(defn['name'])
# Resolve OutputDir
output_dir = args.OutputDir
if not os.path.isabs(output_dir):
output_dir = os.path.join(os.getcwd(), output_dir)
# --- 2. Resolve defaults ---
synonym = str(defn['synonym']) if defn.get('synonym') else split_camel_case(obj_name)
comment = str(defn['comment']) if defn.get('comment') else ''
include_help_in_contents = 'true'
include_in_ci = str(defn['includeInCommandInterface']).lower() if defn.get('includeInCommandInterface') is not None else 'true'
use_one_command = str(defn['useOneCommand']).lower() if defn.get('useOneCommand') is not None else 'false'
explanation = str(defn['explanation']) if defn.get('explanation') else ''
picture = str(defn['picture']) if defn.get('picture') else ''
content_items = []
if defn.get('content'):
for c in defn['content']:
content_items.append(str(c))
children = []
if defn.get('children'):
for ch in defn['children']:
children.append(str(ch))
# --- 3. Build XML ---
uid = new_uuid()
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">')
lines.append(f'\t<Subsystem uuid="{uid}">')
lines.append('\t\t<Properties>')
# Name
lines.append(f'\t\t\t<Name>{esc_xml(obj_name)}</Name>')
# Synonym
emit_mltext(lines, '\t\t\t', 'Synonym', synonym)
# Comment
if comment:
lines.append(f'\t\t\t<Comment>{esc_xml(comment)}</Comment>')
else:
lines.append('\t\t\t<Comment/>')
# Boolean properties
lines.append(f'\t\t\t<IncludeHelpInContents>{include_help_in_contents}</IncludeHelpInContents>')
lines.append(f'\t\t\t<IncludeInCommandInterface>{include_in_ci}</IncludeInCommandInterface>')
lines.append(f'\t\t\t<UseOneCommand>{use_one_command}</UseOneCommand>')
# Explanation
emit_mltext(lines, '\t\t\t', 'Explanation', explanation)
# Picture
if picture:
lines.append('\t\t\t<Picture>')
lines.append(f'\t\t\t\t<xr:Ref>{picture}</xr:Ref>')
lines.append('\t\t\t\t<xr:LoadTransparent>false</xr:LoadTransparent>')
lines.append('\t\t\t</Picture>')
else:
lines.append('\t\t\t<Picture/>')
# Content
if len(content_items) > 0:
lines.append('\t\t\t<Content>')
for item in content_items:
lines.append(f'\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(item)}</xr:Item>')
lines.append('\t\t\t</Content>')
else:
lines.append('\t\t\t<Content/>')
lines.append('\t\t</Properties>')
# ChildObjects
if len(children) > 0:
lines.append('\t\t<ChildObjects>')
for ch in children:
lines.append(f'\t\t\t<Subsystem>{esc_xml(ch)}</Subsystem>')
lines.append('\t\t</ChildObjects>')
else:
lines.append('\t\t<ChildObjects/>')
lines.append('\t</Subsystem>')
lines.append('</MetaDataObject>')
# --- 4. Write files ---
parent = args.Parent
if parent:
# Nested subsystem
if not os.path.isabs(parent):
parent = os.path.join(os.getcwd(), parent)
if not os.path.exists(parent):
print(f"Parent subsystem not found: {parent}", file=sys.stderr)
sys.exit(1)
parent_dir = os.path.dirname(parent)
parent_base_name = os.path.splitext(os.path.basename(parent))[0]
subs_dir = os.path.join(parent_dir, parent_base_name, 'Subsystems')
else:
# Top-level subsystem
subs_dir = os.path.join(output_dir, 'Subsystems')
os.makedirs(subs_dir, exist_ok=True)
target_xml = os.path.join(subs_dir, f'{obj_name}.xml')
# Write XML
xml_content = '\n'.join(lines) + '\n'
write_utf8_bom(target_xml, xml_content)
print(f"[OK] Created: {target_xml}")
# Create subdirectory if children exist
if len(children) > 0:
child_subs_dir = os.path.join(subs_dir, obj_name, 'Subsystems')
if not os.path.exists(child_subs_dir):
os.makedirs(child_subs_dir, exist_ok=True)
print(f"[OK] Created directory: {child_subs_dir}")
# --- 5. Register in parent ---
parent_xml_path = None
if parent:
parent_xml_path = parent
else:
config_xml = os.path.join(output_dir, 'Configuration.xml')
if os.path.exists(config_xml):
parent_xml_path = config_xml
if parent_xml_path and os.path.exists(parent_xml_path):
with open(parent_xml_path, 'r', encoding='utf-8-sig') as f:
raw_text = f.read()
doc = ET.ElementTree(ET.fromstring(raw_text))
root = doc.getroot()
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
# Find ChildObjects
child_objects = None
if parent:
for sub in root.iter(f'{{{md_ns}}}Subsystem'):
child_objects = sub.find(f'{{{md_ns}}}ChildObjects')
break
else:
for cfg in root.iter(f'{{{md_ns}}}Configuration'):
child_objects = cfg.find(f'{{{md_ns}}}ChildObjects')
break
if child_objects is not None:
# Check if already registered
already_exists = False
for child in child_objects:
if child.tag == f'{{{md_ns}}}Subsystem' and child.text == obj_name:
already_exists = True
break
if not already_exists:
new_el = ET.SubElement(child_objects, f'{{{md_ns}}}Subsystem')
new_el.text = obj_name
# Re-serialize with whitespace preservation via raw text manipulation instead
# Since ElementTree doesn't preserve whitespace well, use regex-based insertion
# Find </ChildObjects> or <ChildObjects/> and inject
pass # Fall through to raw text approach below
if not already_exists:
# Use raw text manipulation to preserve formatting
if '<ChildObjects/>' in raw_text:
replacement = f'<ChildObjects>\n\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n\t\t</ChildObjects>'
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
elif '</ChildObjects>' in raw_text:
insert_line = f'\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n'
raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1)
write_utf8_bom(parent_xml_path, raw_text)
print(f"[OK] Registered in: {parent_xml_path}")
else:
print(f"[SKIP] Already registered in: {parent_xml_path}")
else:
print(f"[WARN] ChildObjects not found in: {parent_xml_path}")
else:
print("[INFO] No parent XML to register in")
# --- 6. Auto-validate ---
if not args.NoValidate:
script_dir = os.path.dirname(os.path.abspath(__file__))
validate_script = os.path.normpath(os.path.join(script_dir, '..', '..', 'subsystem-validate', 'scripts', 'subsystem-validate.ps1'))
if os.path.exists(validate_script):
print()
print("--- Running subsystem-validate ---")
os.system(f'powershell.exe -NoProfile -File "{validate_script}" -SubsystemPath "{target_xml}"')
# --- 7. Summary ---
print()
print("=== subsystem-compile summary ===")
print(f" Name: {obj_name}")
print(f" UUID: {uid}")
print(f" Content: {len(content_items)} objects")
print(f" Children: {len(children)}")
print(f" File: {target_xml}")
sys.exit(0)
if __name__ == '__main__':
main()
@@ -1,462 +1,463 @@
#!/usr/bin/env python3
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from lxml import etree
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XS_NS = "http://www.w3.org/2001/XMLSchema"
NSMAP_WRAPPER = {
None: MD_NS,
"xsi": XSI_NS,
"v8": V8_NS,
"xr": XR_NS,
"xs": XS_NS,
}
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
"""Detect indentation of children inside a container element."""
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
for child in container:
if child.tail and "\n" in child.tail:
after_nl = child.tail.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
# Fallback: count depth
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
"""Insert new_el before the closing tag of container, with proper indentation."""
children = list(container)
if len(children) == 0:
# Empty element: set text to newline+indent, tail of new_el to newline+parent_indent
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def remove_with_indent(el):
"""Remove element and clean up surrounding whitespace."""
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
# Transfer el.tail to prev.tail
if el.tail and el.tail.strip() == "":
pass # just drop extra whitespace
prev.tail = el.tail if el.tail and el.tail.strip() else (prev.tail or "")
# Actually try to keep the prev's tail as the closing indent
# Better approach: set prev.tail to what el.tail was (newline+indent of next or closing)
if el.tail:
prev.tail = el.tail
else:
# First child: adjust parent.text
if el.tail:
parent.text = el.tail
parent.remove(el)
def expand_self_closing(container, parent_indent):
"""If container is self-closing (no children, no text), add closing whitespace."""
if len(container) == 0 and not (container.text and container.text.strip()):
container.text = "\r\n" + parent_indent
def import_fragment(xml_string, doc_root):
"""Parse an XML fragment in the MD namespace context and return elements."""
wrapper = (
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
nodes = []
for child in frag:
nodes.append(child)
return nodes
def parse_value_list(val):
"""Parse a string or JSON array into a list of strings."""
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Edit existing 1C subsystem XML", allow_abbrev=False)
parser.add_argument("-SubsystemPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["add-content", "remove-content", "add-child", "remove-child", "set-property"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Resolve path ---
subsystem_path = args.SubsystemPath
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
if os.path.isfile(candidate):
subsystem_path = candidate
elif os.path.isfile(sibling):
subsystem_path = sibling
else:
print(f"No {dir_name}.xml found in directory or as sibling", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
if os.path.isfile(c):
subsystem_path = c
if not os.path.isfile(subsystem_path):
print(f"File not found: {subsystem_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(subsystem_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
# --- Detect structure ---
sub = None
for child in xml_root:
if isinstance(child.tag, str) and localname(child) == "Subsystem":
sub = child
break
if sub is None:
print("No <Subsystem> element found", file=sys.stderr)
sys.exit(1)
props_el = None
child_objs_el = None
for child in sub:
if not isinstance(child.tag, str):
continue
if localname(child) == "Properties":
props_el = child
if localname(child) == "ChildObjects":
child_objs_el = child
obj_name = ""
if props_el is not None:
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Name":
obj_name = (child.text or "").strip()
break
info(f"Subsystem: {obj_name}")
# --- Operations ---
def do_add_content(items):
nonlocal add_count
content_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Content":
content_el = child
break
if content_el is None:
print("No <Content> element found", file=sys.stderr)
sys.exit(1)
existing = set()
for child in content_el:
if isinstance(child.tag, str) and localname(child) == "Item":
existing.add((child.text or "").strip())
props_indent = get_child_indent(props_el)
if len(content_el) == 0 and not (content_el.text and content_el.text.strip()):
expand_self_closing(content_el, props_indent)
content_indent = get_child_indent(content_el)
for item in items:
if item in existing:
warn(f"Content already contains: {item}")
continue
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{item}</xr:Item>'
nodes = import_fragment(frag_xml, xml_root)
if nodes:
insert_before_closing(content_el, nodes[0], content_indent)
add_count += 1
info(f"Added content: {item}")
def do_remove_content(items):
nonlocal remove_count
content_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Content":
content_el = child
break
if content_el is None:
print("No <Content> element found", file=sys.stderr)
sys.exit(1)
for item in items:
found = False
for child in list(content_el):
if isinstance(child.tag, str) and localname(child) == "Item" and (child.text or "").strip() == item:
remove_with_indent(child)
remove_count += 1
info(f"Removed content: {item}")
found = True
break
if not found:
warn(f"Content item not found: {item}")
def do_add_child(child_name):
nonlocal add_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
for child in child_objs_el:
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
warn(f"ChildObjects already contains: {child_name}")
return
sub_indent = get_child_indent(sub)
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
expand_self_closing(child_objs_el, sub_indent)
ci = get_child_indent(child_objs_el)
new_el = etree.SubElement(child_objs_el, f"{{{MD_NS}}}Subsystem")
# Actually we need to use insert_before_closing pattern
child_objs_el.remove(new_el)
new_el = etree.Element(f"{{{MD_NS}}}Subsystem")
new_el.text = child_name
insert_before_closing(child_objs_el, new_el, ci)
add_count += 1
info(f"Added child subsystem: {child_name}")
def do_remove_child(child_name):
nonlocal remove_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
found = False
for child in list(child_objs_el):
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
remove_with_indent(child)
remove_count += 1
info(f"Removed child subsystem: {child_name}")
found = True
break
if not found:
warn(f"Child subsystem not found: {child_name}")
def do_set_property(json_val):
nonlocal modify_count
prop_def = json.loads(json_val)
prop_name = str(prop_def["name"])
prop_value = str(prop_def.get("value", ""))
prop_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == prop_name:
prop_el = child
break
if prop_el is None:
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
sys.exit(1)
bool_props = ["IncludeInCommandInterface", "UseOneCommand", "IncludeHelpInContents"]
if prop_name in bool_props:
prop_el.text = prop_value.lower()
# Clear children
for ch in list(prop_el):
prop_el.remove(ch)
modify_count += 1
info(f"Set {prop_name} = {prop_value}")
return
ml_props = ["Synonym", "Explanation"]
if prop_name in ml_props:
if not prop_value:
# Clear - make self-closing
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = None
modify_count += 1
info(f"Cleared {prop_name}")
else:
for ch in list(prop_el):
prop_el.remove(ch)
indent = get_child_indent(props_el)
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
lang_el.text = "ru"
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
content_el.text = prop_value
# Set whitespace
prop_el.text = "\r\n" + indent + "\t"
item_el.text = "\r\n" + indent + "\t\t"
lang_el.tail = "\r\n" + indent + "\t\t"
content_el.tail = "\r\n" + indent + "\t"
item_el.tail = "\r\n" + indent
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
return
if prop_name == "Comment":
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
prop_el.text = prop_value
modify_count += 1
info(f'Set Comment = "{prop_value}"')
return
if prop_name == "Picture":
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
indent = get_child_indent(props_el)
ref_el = etree.SubElement(prop_el, f"{{{XR_NS}}}Ref")
ref_el.text = prop_value
load_el = etree.SubElement(prop_el, f"{{{XR_NS}}}LoadTransparent")
load_el.text = "false"
prop_el.text = "\r\n" + indent + "\t"
ref_el.tail = "\r\n" + indent + "\t"
load_el.tail = "\r\n" + indent
modify_count += 1
info(f'Set Picture = "{prop_value}"')
return
# Generic text property
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = prop_value
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "add-content":
do_add_content(parse_value_list(op_value))
elif op_name == "remove-content":
do_remove_content(parse_value_list(op_value))
elif op_name == "add-child":
do_add_child(op_value)
elif op_name == "remove-child":
do_remove_child(op_value)
elif op_name == "set-property":
do_set_property(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "subsystem-validate", "scripts", "subsystem-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running subsystem-validate ---")
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
# --- Summary ---
print()
print("=== subsystem-edit summary ===")
print(f" Subsystem: {obj_name}")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import subprocess
import sys
from lxml import etree
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XS_NS = "http://www.w3.org/2001/XMLSchema"
NSMAP_WRAPPER = {
None: MD_NS,
"xsi": XSI_NS,
"v8": V8_NS,
"xr": XR_NS,
"xs": XS_NS,
}
def localname(el):
return etree.QName(el.tag).localname
def info(msg):
print(f"[INFO] {msg}")
def warn(msg):
print(f"[WARN] {msg}")
def get_child_indent(container):
"""Detect indentation of children inside a container element."""
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
for child in container:
if child.tail and "\n" in child.tail:
after_nl = child.tail.rsplit("\n", 1)[-1]
if after_nl and not after_nl.strip():
return after_nl
# Fallback: count depth
depth = 0
current = container
while current is not None:
depth += 1
current = current.getparent()
return "\t" * depth
def insert_before_closing(container, new_el, child_indent):
"""Insert new_el before the closing tag of container, with proper indentation."""
children = list(container)
if len(children) == 0:
# Empty element: set text to newline+indent, tail of new_el to newline+parent_indent
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
container.text = "\r\n" + child_indent
new_el.tail = "\r\n" + parent_indent
container.append(new_el)
else:
last = children[-1]
new_el.tail = last.tail
last.tail = "\r\n" + child_indent
container.append(new_el)
def remove_with_indent(el):
"""Remove element and clean up surrounding whitespace."""
parent = el.getparent()
prev = el.getprevious()
if prev is not None:
# Transfer el.tail to prev.tail
if el.tail and el.tail.strip() == "":
pass # just drop extra whitespace
prev.tail = el.tail if el.tail and el.tail.strip() else (prev.tail or "")
# Actually try to keep the prev's tail as the closing indent
# Better approach: set prev.tail to what el.tail was (newline+indent of next or closing)
if el.tail:
prev.tail = el.tail
else:
# First child: adjust parent.text
if el.tail:
parent.text = el.tail
parent.remove(el)
def expand_self_closing(container, parent_indent):
"""If container is self-closing (no children, no text), add closing whitespace."""
if len(container) == 0 and not (container.text and container.text.strip()):
container.text = "\r\n" + parent_indent
def import_fragment(xml_string, doc_root):
"""Parse an XML fragment in the MD namespace context and return elements."""
wrapper = (
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
)
frag = etree.fromstring(wrapper.encode("utf-8"))
nodes = []
for child in frag:
nodes.append(child)
return nodes
def parse_value_list(val):
"""Parse a string or JSON array into a list of strings."""
val = val.strip()
if val.startswith("["):
arr = json.loads(val)
return [str(item) for item in arr]
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Edit existing 1C subsystem XML", allow_abbrev=False)
parser.add_argument("-SubsystemPath", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["add-content", "remove-content", "add-child", "remove-child", "set-property"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
# --- Mode validation ---
if args.DefinitionFile and args.Operation:
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Operation:
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
sys.exit(1)
# --- Resolve path ---
subsystem_path = args.SubsystemPath
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
if os.path.isfile(candidate):
subsystem_path = candidate
elif os.path.isfile(sibling):
subsystem_path = sibling
else:
print(f"No {dir_name}.xml found in directory or as sibling", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
if os.path.isfile(c):
subsystem_path = c
if not os.path.isfile(subsystem_path):
print(f"File not found: {subsystem_path}", file=sys.stderr)
sys.exit(1)
resolved_path = os.path.abspath(subsystem_path)
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
add_count = 0
remove_count = 0
modify_count = 0
# --- Detect structure ---
sub = None
for child in xml_root:
if isinstance(child.tag, str) and localname(child) == "Subsystem":
sub = child
break
if sub is None:
print("No <Subsystem> element found", file=sys.stderr)
sys.exit(1)
props_el = None
child_objs_el = None
for child in sub:
if not isinstance(child.tag, str):
continue
if localname(child) == "Properties":
props_el = child
if localname(child) == "ChildObjects":
child_objs_el = child
obj_name = ""
if props_el is not None:
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Name":
obj_name = (child.text or "").strip()
break
info(f"Subsystem: {obj_name}")
# --- Operations ---
def do_add_content(items):
nonlocal add_count
content_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Content":
content_el = child
break
if content_el is None:
print("No <Content> element found", file=sys.stderr)
sys.exit(1)
existing = set()
for child in content_el:
if isinstance(child.tag, str) and localname(child) == "Item":
existing.add((child.text or "").strip())
props_indent = get_child_indent(props_el)
if len(content_el) == 0 and not (content_el.text and content_el.text.strip()):
expand_self_closing(content_el, props_indent)
content_indent = get_child_indent(content_el)
for item in items:
if item in existing:
warn(f"Content already contains: {item}")
continue
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{item}</xr:Item>'
nodes = import_fragment(frag_xml, xml_root)
if nodes:
insert_before_closing(content_el, nodes[0], content_indent)
add_count += 1
info(f"Added content: {item}")
def do_remove_content(items):
nonlocal remove_count
content_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == "Content":
content_el = child
break
if content_el is None:
print("No <Content> element found", file=sys.stderr)
sys.exit(1)
for item in items:
found = False
for child in list(content_el):
if isinstance(child.tag, str) and localname(child) == "Item" and (child.text or "").strip() == item:
remove_with_indent(child)
remove_count += 1
info(f"Removed content: {item}")
found = True
break
if not found:
warn(f"Content item not found: {item}")
def do_add_child(child_name):
nonlocal add_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
for child in child_objs_el:
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
warn(f"ChildObjects already contains: {child_name}")
return
sub_indent = get_child_indent(sub)
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
expand_self_closing(child_objs_el, sub_indent)
ci = get_child_indent(child_objs_el)
new_el = etree.SubElement(child_objs_el, f"{{{MD_NS}}}Subsystem")
# Actually we need to use insert_before_closing pattern
child_objs_el.remove(new_el)
new_el = etree.Element(f"{{{MD_NS}}}Subsystem")
new_el.text = child_name
insert_before_closing(child_objs_el, new_el, ci)
add_count += 1
info(f"Added child subsystem: {child_name}")
def do_remove_child(child_name):
nonlocal remove_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
found = False
for child in list(child_objs_el):
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
remove_with_indent(child)
remove_count += 1
info(f"Removed child subsystem: {child_name}")
found = True
break
if not found:
warn(f"Child subsystem not found: {child_name}")
def do_set_property(json_val):
nonlocal modify_count
prop_def = json.loads(json_val)
prop_name = str(prop_def["name"])
prop_value = str(prop_def.get("value", ""))
prop_el = None
for child in props_el:
if isinstance(child.tag, str) and localname(child) == prop_name:
prop_el = child
break
if prop_el is None:
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
sys.exit(1)
bool_props = ["IncludeInCommandInterface", "UseOneCommand", "IncludeHelpInContents"]
if prop_name in bool_props:
prop_el.text = prop_value.lower()
# Clear children
for ch in list(prop_el):
prop_el.remove(ch)
modify_count += 1
info(f"Set {prop_name} = {prop_value}")
return
ml_props = ["Synonym", "Explanation"]
if prop_name in ml_props:
if not prop_value:
# Clear - make self-closing
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = None
modify_count += 1
info(f"Cleared {prop_name}")
else:
for ch in list(prop_el):
prop_el.remove(ch)
indent = get_child_indent(props_el)
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
lang_el.text = "ru"
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
content_el.text = prop_value
# Set whitespace
prop_el.text = "\r\n" + indent + "\t"
item_el.text = "\r\n" + indent + "\t\t"
lang_el.tail = "\r\n" + indent + "\t\t"
content_el.tail = "\r\n" + indent + "\t"
item_el.tail = "\r\n" + indent
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
return
if prop_name == "Comment":
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
prop_el.text = prop_value
modify_count += 1
info(f'Set Comment = "{prop_value}"')
return
if prop_name == "Picture":
for ch in list(prop_el):
prop_el.remove(ch)
if not prop_value:
prop_el.text = None
else:
indent = get_child_indent(props_el)
ref_el = etree.SubElement(prop_el, f"{{{XR_NS}}}Ref")
ref_el.text = prop_value
load_el = etree.SubElement(prop_el, f"{{{XR_NS}}}LoadTransparent")
load_el.text = "false"
prop_el.text = "\r\n" + indent + "\t"
ref_el.tail = "\r\n" + indent + "\t"
load_el.tail = "\r\n" + indent
modify_count += 1
info(f'Set Picture = "{prop_value}"')
return
# Generic text property
for ch in list(prop_el):
prop_el.remove(ch)
prop_el.text = prop_value
modify_count += 1
info(f'Set {prop_name} = "{prop_value}"')
# --- Execute operations ---
operations = []
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = json.loads(fh.read())
if isinstance(ops, list):
operations = ops
else:
operations = [ops]
else:
operations = [{"operation": args.Operation, "value": args.Value or ""}]
for op in operations:
op_name = op.get("operation", args.Operation or "")
op_value = op.get("value", args.Value or "")
if op_name == "add-content":
do_add_content(parse_value_list(op_value))
elif op_name == "remove-content":
do_remove_content(parse_value_list(op_value))
elif op_name == "add-child":
do_add_child(op_value)
elif op_name == "remove-child":
do_remove_child(op_value)
elif op_name == "set-property":
do_set_property(op_value)
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
# --- Save ---
save_xml_bom(tree, resolved_path)
info(f"Saved: {resolved_path}")
# --- Auto-validate ---
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "subsystem-validate", "scripts", "subsystem-validate.py"))
if os.path.isfile(validate_script):
print()
print("--- Running subsystem-validate ---")
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
# --- Summary ---
print()
print("=== subsystem-edit summary ===")
print(f" Subsystem: {obj_name}")
print(f" Added: {add_count}")
print(f" Removed: {remove_count}")
print(f" Modified: {modify_count}")
sys.exit(0)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -1,349 +1,350 @@
#!/usr/bin/env python3
# subsystem-validate v1.0 — Validate 1C subsystem XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates subsystem XML file structure, properties, content items, child objects."""
import sys, os, argparse, re
from lxml import etree
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
}
GUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
IDENT_PATTERN = re.compile(
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
)
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def find_duplicates(items):
seen = {}
dupes = []
for item in items:
seen[item] = seen.get(item, 0) + 1
for item, count in seen.items():
if count > 1 and item not in dupes:
dupes.append(item)
return dupes
def main():
parser = argparse.ArgumentParser(
description='Validate 1C subsystem XML structure', allow_abbrev=False
)
parser.add_argument('-SubsystemPath', dest='SubsystemPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
subsystem_path = args.SubsystemPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, dir_name + '.xml')
sibling = os.path.join(os.path.dirname(subsystem_path), dir_name + '.xml')
if os.path.exists(candidate):
subsystem_path = candidate
elif os.path.exists(sibling):
subsystem_path = sibling
else:
print(f'[ERROR] No {dir_name}.xml found in directory: {subsystem_path}')
sys.exit(1)
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
if not os.path.exists(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), fn + '.xml')
if os.path.exists(c):
subsystem_path = c
if not os.path.exists(subsystem_path):
print(f'[ERROR] File not found: {subsystem_path}')
sys.exit(1)
resolved_path = os.path.abspath(subsystem_path)
r = Reporter(max_errors)
# --- 1. XML well-formedness + root structure ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.error(f'1. XML parse error: {e}')
r.stopped = True
sub = None
version = ''
if not r.stopped:
root = xml_doc.getroot()
version = root.get('version', '')
sub_list = root.findall('md:Subsystem', NS)
sub = sub_list[0] if sub_list else None
if sub is None:
r.error('1. Root structure: expected MetaDataObject/Subsystem, not found')
r.stopped = True
else:
uuid_val = sub.get('uuid', '')
if uuid_val and GUID_PATTERN.match(uuid_val):
r.ok(f'1. Root structure: MetaDataObject/Subsystem, uuid={uuid_val}, version {version}')
else:
r.error('1. Root structure: invalid or missing uuid')
# --- Properties checks ---
props = None
if not r.stopped:
props_list = sub.findall('md:Properties', NS)
props = props_list[0] if props_list else None
if props is None:
r.error('2. Properties: <Properties> element not found')
r.stopped = True
sub_name = ''
if not r.stopped:
# --- 2. Required properties ---
required_props = [
'Name', 'Synonym', 'Comment', 'IncludeHelpInContents',
'IncludeInCommandInterface', 'UseOneCommand', 'Explanation',
'Picture', 'Content'
]
missing = []
for p in required_props:
el = props.find(f'md:{p}', NS)
if el is None:
missing.append(p)
if len(missing) == 0:
r.ok('2. Properties: all 9 required properties present')
else:
r.error(f'2. Properties: missing: {", ".join(missing)}')
# --- 3. Name ---
name_el = props.find('md:Name', NS)
sub_name = (name_el.text or '').strip() if name_el is not None else ''
r.out('')
r.out(f'=== Validation: Subsystem.{sub_name} ===')
# Re-insert header at position 0
header_line = f'=== Validation: Subsystem.{sub_name} ==='
r.lines.insert(0, '')
r.lines.insert(0, header_line)
if sub_name and IDENT_PATTERN.match(sub_name):
r.ok(f'3. Name: "{sub_name}" - valid identifier')
elif not sub_name:
r.error('3. Name: empty')
else:
r.error(f'3. Name: "{sub_name}" - invalid identifier')
# --- 4. Synonym ---
syn_el = props.find('md:Synonym', NS)
if syn_el is not None and len(syn_el) > 0:
items = syn_el.findall('v8:item', NS)
if len(items) > 0:
first_content = ''
for item in items:
c = item.find('v8:content', NS)
if c is not None and c.text:
first_content = c.text
break
r.ok(f'4. Synonym: "{first_content}" ({len(items)} lang(s))')
else:
r.warn('4. Synonym: element exists but no v8:item children')
else:
r.warn('4. Synonym: empty or missing')
# --- 5. Boolean properties ---
bool_props = ['IncludeHelpInContents', 'IncludeInCommandInterface', 'UseOneCommand']
bool_ok = True
bool_vals = {}
for bp in bool_props:
el = props.find(f'md:{bp}', NS)
if el is not None:
val = (el.text or '').strip()
bool_vals[bp] = val
if val not in ('true', 'false'):
r.error(f'5. Boolean property {bp} = "{val}" (expected true/false)')
bool_ok = False
if bool_ok:
r.ok('5. Boolean properties: valid')
# --- 6. Content items format ---
content_el = props.find('md:Content', NS)
content_items = []
if content_el is not None and len(content_el) > 0:
xr_items = content_el.findall('xr:Item', NS)
content_ok = True
for item in xr_items:
type_attr = item.get(f'{{{NS["xsi"]}}}type', '')
text = (item.text or '').strip()
content_items.append(text)
if type_attr != 'xr:MDObjectRef':
r.error(f'6. Content item "{text}": xsi:type="{type_attr}" (expected xr:MDObjectRef)')
content_ok = False
if not re.match(r'^[A-Za-z]+\..+$', text) and not GUID_PATTERN.match(text):
r.error(f'6. Content item "{text}": invalid format (expected Type.Name or UUID)')
content_ok = False
if content_ok:
r.ok(f'6. Content: {len(xr_items)} items, all valid MDObjectRef format')
else:
r.ok('6. Content: empty (no items)')
# --- 7. Content duplicates ---
if len(content_items) > 0:
dupes = find_duplicates(content_items)
if dupes:
r.warn(f'7. Content: duplicates found: {", ".join(dupes)}')
else:
r.ok('7. Content: no duplicates')
else:
r.ok('7. Content: no duplicates (empty)')
# --- 8. ChildObjects entries non-empty ---
child_objs = sub.find('md:ChildObjects', NS)
child_names = []
if child_objs is not None and len(child_objs) > 0:
child_ok = True
for child in child_objs:
if not isinstance(child.tag, str):
continue
local_name = etree.QName(child.tag).localname
if local_name != 'Subsystem':
r.error(f'8. ChildObjects: unexpected element <{local_name}>')
child_ok = False
elif not (child.text or '').strip():
r.error('8. ChildObjects: empty <Subsystem> element')
child_ok = False
else:
child_names.append((child.text or '').strip())
if child_ok:
r.ok(f'8. ChildObjects: {len(child_names)} entries, all non-empty')
else:
r.ok('8. ChildObjects: empty (leaf subsystem)')
# --- 9. ChildObjects duplicates ---
if len(child_names) > 0:
dupes = find_duplicates(child_names)
if dupes:
r.error(f'9. ChildObjects: duplicates: {", ".join(dupes)}')
else:
r.ok('9. ChildObjects: no duplicates')
else:
r.ok('9. ChildObjects: no duplicates (empty)')
# --- 10. ChildObjects files exist ---
if len(child_names) > 0:
parent_dir = os.path.dirname(resolved_path)
base_name = os.path.splitext(os.path.basename(resolved_path))[0]
subs_dir = os.path.join(parent_dir, base_name, 'Subsystems')
missing_files = []
for cn in child_names:
child_xml = os.path.join(subs_dir, cn + '.xml')
if not os.path.exists(child_xml):
missing_files.append(cn)
if len(missing_files) == 0:
r.ok(f'10. ChildObjects files: all {len(child_names)} files exist')
else:
r.warn(f'10. ChildObjects files: missing: {", ".join(missing_files)}')
else:
r.ok('10. ChildObjects files: n/a (no children)')
# --- 11. CommandInterface.xml ---
parent_dir2 = os.path.dirname(resolved_path)
base_name2 = os.path.splitext(os.path.basename(resolved_path))[0]
ci_path = os.path.join(parent_dir2, base_name2, 'Ext', 'CommandInterface.xml')
if os.path.exists(ci_path):
try:
etree.parse(ci_path, etree.XMLParser(remove_blank_text=False))
r.ok('11. CommandInterface: exists, well-formed')
except etree.XMLSyntaxError as e:
r.warn(f'11. CommandInterface: exists but NOT well-formed: {e}')
else:
r.ok('11. CommandInterface: not present')
# --- 12. Picture format ---
pic_el = props.find('md:Picture', NS)
if pic_el is not None and len(pic_el) > 0:
pic_ref = pic_el.find('xr:Ref', NS)
if pic_ref is not None and pic_ref.text:
ref_text = pic_ref.text
if ref_text.startswith('CommonPicture.'):
r.ok(f'12. Picture: {ref_text}')
else:
r.warn(f'12. Picture: "{ref_text}" (expected CommonPicture.XXX)')
else:
r.warn('12. Picture: has children but no xr:Ref content')
else:
r.ok('12. Picture: empty (not set)')
# --- 13. UseOneCommand constraint ---
use_one = bool_vals.get('UseOneCommand', '')
if use_one == 'true':
if len(content_items) == 1:
r.ok('13. UseOneCommand: true, Content has exactly 1 item')
else:
r.warn(f'13. UseOneCommand: true but Content has {len(content_items)} items (expected 1)')
else:
r.ok('13. UseOneCommand: false (no constraint)')
# --- Finalize ---
r.out('---')
r.out(f'Errors: {r.errors}, Warnings: {r.warnings}')
result = r.text()
print(result, end='')
if out_file:
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# subsystem-validate v1.0 — Validate 1C subsystem XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates subsystem XML file structure, properties, content items, child objects."""
import sys, os, argparse, re
from lxml import etree
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
}
GUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)
IDENT_PATTERN = re.compile(
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
)
class Reporter:
def __init__(self, max_errors):
self.errors = 0
self.warnings = 0
self.stopped = False
self.max_errors = max_errors
self.lines = []
def out(self, msg=''):
self.lines.append(msg)
def ok(self, msg):
self.lines.append(f'[OK] {msg}')
def error(self, msg):
self.errors += 1
self.lines.append(f'[ERROR] {msg}')
if self.errors >= self.max_errors:
self.stopped = True
def warn(self, msg):
self.warnings += 1
self.lines.append(f'[WARN] {msg}')
def text(self):
return '\r\n'.join(self.lines) + '\r\n'
def find_duplicates(items):
seen = {}
dupes = []
for item in items:
seen[item] = seen.get(item, 0) + 1
for item, count in seen.items():
if count > 1 and item not in dupes:
dupes.append(item)
return dupes
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description='Validate 1C subsystem XML structure', allow_abbrev=False
)
parser.add_argument('-SubsystemPath', dest='SubsystemPath', required=True)
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
args = parser.parse_args()
subsystem_path = args.SubsystemPath
max_errors = args.MaxErrors
out_file = args.OutFile
# --- Resolve path ---
if not os.path.isabs(subsystem_path):
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
if os.path.isdir(subsystem_path):
dir_name = os.path.basename(subsystem_path)
candidate = os.path.join(subsystem_path, dir_name + '.xml')
sibling = os.path.join(os.path.dirname(subsystem_path), dir_name + '.xml')
if os.path.exists(candidate):
subsystem_path = candidate
elif os.path.exists(sibling):
subsystem_path = sibling
else:
print(f'[ERROR] No {dir_name}.xml found in directory: {subsystem_path}')
sys.exit(1)
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
if not os.path.exists(subsystem_path):
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
pd = os.path.dirname(subsystem_path)
if fn == os.path.basename(pd):
c = os.path.join(os.path.dirname(pd), fn + '.xml')
if os.path.exists(c):
subsystem_path = c
if not os.path.exists(subsystem_path):
print(f'[ERROR] File not found: {subsystem_path}')
sys.exit(1)
resolved_path = os.path.abspath(subsystem_path)
r = Reporter(max_errors)
# --- 1. XML well-formedness + root structure ---
xml_doc = None
try:
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_doc = etree.parse(resolved_path, xml_parser)
except etree.XMLSyntaxError as e:
r.error(f'1. XML parse error: {e}')
r.stopped = True
sub = None
version = ''
if not r.stopped:
root = xml_doc.getroot()
version = root.get('version', '')
sub_list = root.findall('md:Subsystem', NS)
sub = sub_list[0] if sub_list else None
if sub is None:
r.error('1. Root structure: expected MetaDataObject/Subsystem, not found')
r.stopped = True
else:
uuid_val = sub.get('uuid', '')
if uuid_val and GUID_PATTERN.match(uuid_val):
r.ok(f'1. Root structure: MetaDataObject/Subsystem, uuid={uuid_val}, version {version}')
else:
r.error('1. Root structure: invalid or missing uuid')
# --- Properties checks ---
props = None
if not r.stopped:
props_list = sub.findall('md:Properties', NS)
props = props_list[0] if props_list else None
if props is None:
r.error('2. Properties: <Properties> element not found')
r.stopped = True
sub_name = ''
if not r.stopped:
# --- 2. Required properties ---
required_props = [
'Name', 'Synonym', 'Comment', 'IncludeHelpInContents',
'IncludeInCommandInterface', 'UseOneCommand', 'Explanation',
'Picture', 'Content'
]
missing = []
for p in required_props:
el = props.find(f'md:{p}', NS)
if el is None:
missing.append(p)
if len(missing) == 0:
r.ok('2. Properties: all 9 required properties present')
else:
r.error(f'2. Properties: missing: {", ".join(missing)}')
# --- 3. Name ---
name_el = props.find('md:Name', NS)
sub_name = (name_el.text or '').strip() if name_el is not None else ''
r.out('')
r.out(f'=== Validation: Subsystem.{sub_name} ===')
# Re-insert header at position 0
header_line = f'=== Validation: Subsystem.{sub_name} ==='
r.lines.insert(0, '')
r.lines.insert(0, header_line)
if sub_name and IDENT_PATTERN.match(sub_name):
r.ok(f'3. Name: "{sub_name}" - valid identifier')
elif not sub_name:
r.error('3. Name: empty')
else:
r.error(f'3. Name: "{sub_name}" - invalid identifier')
# --- 4. Synonym ---
syn_el = props.find('md:Synonym', NS)
if syn_el is not None and len(syn_el) > 0:
items = syn_el.findall('v8:item', NS)
if len(items) > 0:
first_content = ''
for item in items:
c = item.find('v8:content', NS)
if c is not None and c.text:
first_content = c.text
break
r.ok(f'4. Synonym: "{first_content}" ({len(items)} lang(s))')
else:
r.warn('4. Synonym: element exists but no v8:item children')
else:
r.warn('4. Synonym: empty or missing')
# --- 5. Boolean properties ---
bool_props = ['IncludeHelpInContents', 'IncludeInCommandInterface', 'UseOneCommand']
bool_ok = True
bool_vals = {}
for bp in bool_props:
el = props.find(f'md:{bp}', NS)
if el is not None:
val = (el.text or '').strip()
bool_vals[bp] = val
if val not in ('true', 'false'):
r.error(f'5. Boolean property {bp} = "{val}" (expected true/false)')
bool_ok = False
if bool_ok:
r.ok('5. Boolean properties: valid')
# --- 6. Content items format ---
content_el = props.find('md:Content', NS)
content_items = []
if content_el is not None and len(content_el) > 0:
xr_items = content_el.findall('xr:Item', NS)
content_ok = True
for item in xr_items:
type_attr = item.get(f'{{{NS["xsi"]}}}type', '')
text = (item.text or '').strip()
content_items.append(text)
if type_attr != 'xr:MDObjectRef':
r.error(f'6. Content item "{text}": xsi:type="{type_attr}" (expected xr:MDObjectRef)')
content_ok = False
if not re.match(r'^[A-Za-z]+\..+$', text) and not GUID_PATTERN.match(text):
r.error(f'6. Content item "{text}": invalid format (expected Type.Name or UUID)')
content_ok = False
if content_ok:
r.ok(f'6. Content: {len(xr_items)} items, all valid MDObjectRef format')
else:
r.ok('6. Content: empty (no items)')
# --- 7. Content duplicates ---
if len(content_items) > 0:
dupes = find_duplicates(content_items)
if dupes:
r.warn(f'7. Content: duplicates found: {", ".join(dupes)}')
else:
r.ok('7. Content: no duplicates')
else:
r.ok('7. Content: no duplicates (empty)')
# --- 8. ChildObjects entries non-empty ---
child_objs = sub.find('md:ChildObjects', NS)
child_names = []
if child_objs is not None and len(child_objs) > 0:
child_ok = True
for child in child_objs:
if not isinstance(child.tag, str):
continue
local_name = etree.QName(child.tag).localname
if local_name != 'Subsystem':
r.error(f'8. ChildObjects: unexpected element <{local_name}>')
child_ok = False
elif not (child.text or '').strip():
r.error('8. ChildObjects: empty <Subsystem> element')
child_ok = False
else:
child_names.append((child.text or '').strip())
if child_ok:
r.ok(f'8. ChildObjects: {len(child_names)} entries, all non-empty')
else:
r.ok('8. ChildObjects: empty (leaf subsystem)')
# --- 9. ChildObjects duplicates ---
if len(child_names) > 0:
dupes = find_duplicates(child_names)
if dupes:
r.error(f'9. ChildObjects: duplicates: {", ".join(dupes)}')
else:
r.ok('9. ChildObjects: no duplicates')
else:
r.ok('9. ChildObjects: no duplicates (empty)')
# --- 10. ChildObjects files exist ---
if len(child_names) > 0:
parent_dir = os.path.dirname(resolved_path)
base_name = os.path.splitext(os.path.basename(resolved_path))[0]
subs_dir = os.path.join(parent_dir, base_name, 'Subsystems')
missing_files = []
for cn in child_names:
child_xml = os.path.join(subs_dir, cn + '.xml')
if not os.path.exists(child_xml):
missing_files.append(cn)
if len(missing_files) == 0:
r.ok(f'10. ChildObjects files: all {len(child_names)} files exist')
else:
r.warn(f'10. ChildObjects files: missing: {", ".join(missing_files)}')
else:
r.ok('10. ChildObjects files: n/a (no children)')
# --- 11. CommandInterface.xml ---
parent_dir2 = os.path.dirname(resolved_path)
base_name2 = os.path.splitext(os.path.basename(resolved_path))[0]
ci_path = os.path.join(parent_dir2, base_name2, 'Ext', 'CommandInterface.xml')
if os.path.exists(ci_path):
try:
etree.parse(ci_path, etree.XMLParser(remove_blank_text=False))
r.ok('11. CommandInterface: exists, well-formed')
except etree.XMLSyntaxError as e:
r.warn(f'11. CommandInterface: exists but NOT well-formed: {e}')
else:
r.ok('11. CommandInterface: not present')
# --- 12. Picture format ---
pic_el = props.find('md:Picture', NS)
if pic_el is not None and len(pic_el) > 0:
pic_ref = pic_el.find('xr:Ref', NS)
if pic_ref is not None and pic_ref.text:
ref_text = pic_ref.text
if ref_text.startswith('CommonPicture.'):
r.ok(f'12. Picture: {ref_text}')
else:
r.warn(f'12. Picture: "{ref_text}" (expected CommonPicture.XXX)')
else:
r.warn('12. Picture: has children but no xr:Ref content')
else:
r.ok('12. Picture: empty (not set)')
# --- 13. UseOneCommand constraint ---
use_one = bool_vals.get('UseOneCommand', '')
if use_one == 'true':
if len(content_items) == 1:
r.ok('13. UseOneCommand: true, Content has exactly 1 item')
else:
r.warn(f'13. UseOneCommand: true but Content has {len(content_items)} items (expected 1)')
else:
r.ok('13. UseOneCommand: false (no constraint)')
# --- Finalize ---
r.out('---')
r.out(f'Errors: {r.errors}, Warnings: {r.warnings}')
result = r.text()
print(result, end='')
if out_file:
if not os.path.isabs(out_file):
out_file = os.path.join(os.getcwd(), out_file)
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
f.write(result)
print(f'Written to: {out_file}')
sys.exit(1 if r.errors > 0 else 0)
if __name__ == '__main__':
main()
@@ -1,249 +1,250 @@
#!/usr/bin/env python3
# add-template v1.0 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
TYPE_MAP = {
"HTML": {"TemplateType": "HTMLDocument", "Ext": ".html"},
"Text": {"TemplateType": "TextDocument", "Ext": ".txt"},
"SpreadsheetDocument": {"TemplateType": "SpreadsheetDocument", "Ext": ".xml"},
"BinaryData": {"TemplateType": "BinaryData", "Ext": ".bin"},
"DataCompositionSchema": {"TemplateType": "DataCompositionSchema", "Ext": ".xml"},
}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
parser = argparse.ArgumentParser(description="Add template to 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-TemplateName", required=True)
parser.add_argument("-TemplateType", required=True,
choices=["HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema"])
parser.add_argument("-Synonym", default=None)
parser.add_argument("-SrcDir", default="src")
parser.add_argument("-SetMainSKD", action="store_true")
args = parser.parse_args()
object_name = args.ObjectName
template_name = args.TemplateName
template_type = args.TemplateType
synonym = args.Synonym if args.Synonym is not None else template_name
src_dir = args.SrcDir
set_main_skd = args.SetMainSKD
tmpl = TYPE_MAP[template_type]
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
templates_dir = os.path.join(processor_dir, "Templates")
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
if os.path.exists(template_meta_path):
print(f"Макет уже существует: {template_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
template_ext_dir = os.path.join(templates_dir, template_name, "Ext")
os.makedirs(template_ext_dir, exist_ok=True)
# --- 1. Template metadata (Templates/<TemplateName>.xml) ---
template_uuid = str(uuid.uuid4())
template_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Template uuid="{template_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{template_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
f'\t\t\t<TemplateType>{tmpl["TemplateType"]}</TemplateType>\n'
'\t\t</Properties>\n'
'\t</Template>\n'
'</MetaDataObject>'
)
write_text_with_bom(template_meta_path, template_meta_xml)
# --- 2. Template content (Templates/<TemplateName>/Ext/Template.<ext>) ---
template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}")
if template_type == "HTML":
content = (
'<!DOCTYPE html>\n'
'<html>\n'
'<head>\n'
'\t<meta charset="UTF-8">\n'
'\t<title></title>\n'
'</head>\n'
'<body>\n'
'</body>\n'
'</html>'
)
write_text_with_bom(template_file_path, content)
elif template_type == "Text":
write_text_with_bom(template_file_path, "")
elif template_type == "SpreadsheetDocument":
content = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document"'
' xmlns:ss="http://v8.1c.ru/spreadsheet/document"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema">\n'
'</SpreadsheetDocument>'
)
write_text_with_bom(template_file_path, content)
elif template_type == "BinaryData":
with open(template_file_path, "wb") as f:
pass # empty file
elif template_type == "DataCompositionSchema":
content = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"\n'
'\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"\n'
'\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"\n'
'\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"\n'
'\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"\n'
'\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"\n'
'\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"\n'
'\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n'
'\t<dataSource>\n'
'\t\t<name>ИсточникДанных1</name>\n'
'\t\t<dataSourceType>Local</dataSourceType>\n'
'\t</dataSource>\n'
'</DataCompositionSchema>'
)
write_text_with_bom(template_file_path, content)
# --- 3. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Template> to end of ChildObjects
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
template_elem.text = template_name
# Remove auto-appended element to reinsert with proper whitespace
child_objects.remove(template_elem)
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
# last_child.tail is the trailing whitespace before </ChildObjects>
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = old_tail if old_tail else "\n\t\t"
else:
# Has text content but no element children
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
main_dcs_updated = False
if template_type == "DataCompositionSchema":
report_like_types = ["ExternalReport", "Report"]
object_type_node = None
object_type_name = None
for rt in report_like_types:
node = root.find(f".//md:{rt}", NSMAP)
if node is not None:
object_type_node = node
object_type_name = rt
break
if object_type_node is not None:
main_dcs = root.find(f".//md:{object_type_name}/md:Properties/md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None:
is_empty = main_dcs.text is None or main_dcs.text.strip() == ""
if is_empty or set_main_skd:
obj_name_node = root.find(f".//md:{object_type_name}/md:Properties/md:Name", NSMAP)
obj_name = obj_name_node.text if obj_name_node is not None else ""
main_dcs.text = f"{object_type_name}.{obj_name}.Template.{template_name}"
main_dcs_updated = True
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создан макет: {template_name} ({template_type})")
print(f" Метаданные: {template_meta_path}")
print(f" Содержимое: {template_file_path}")
if main_dcs_updated:
print(f" MainDataCompositionSchema: {main_dcs.text}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# add-template v1.0 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
TYPE_MAP = {
"HTML": {"TemplateType": "HTMLDocument", "Ext": ".html"},
"Text": {"TemplateType": "TextDocument", "Ext": ".txt"},
"SpreadsheetDocument": {"TemplateType": "SpreadsheetDocument", "Ext": ".xml"},
"BinaryData": {"TemplateType": "BinaryData", "Ext": ".bin"},
"DataCompositionSchema": {"TemplateType": "DataCompositionSchema", "Ext": ".xml"},
}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Add template to 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-TemplateName", required=True)
parser.add_argument("-TemplateType", required=True,
choices=["HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema"])
parser.add_argument("-Synonym", default=None)
parser.add_argument("-SrcDir", default="src")
parser.add_argument("-SetMainSKD", action="store_true")
args = parser.parse_args()
object_name = args.ObjectName
template_name = args.TemplateName
template_type = args.TemplateType
synonym = args.Synonym if args.Synonym is not None else template_name
src_dir = args.SrcDir
set_main_skd = args.SetMainSKD
tmpl = TYPE_MAP[template_type]
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
templates_dir = os.path.join(processor_dir, "Templates")
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
if os.path.exists(template_meta_path):
print(f"Макет уже существует: {template_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
template_ext_dir = os.path.join(templates_dir, template_name, "Ext")
os.makedirs(template_ext_dir, exist_ok=True)
# --- 1. Template metadata (Templates/<TemplateName>.xml) ---
template_uuid = str(uuid.uuid4())
template_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Template uuid="{template_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{template_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
f'\t\t\t<TemplateType>{tmpl["TemplateType"]}</TemplateType>\n'
'\t\t</Properties>\n'
'\t</Template>\n'
'</MetaDataObject>'
)
write_text_with_bom(template_meta_path, template_meta_xml)
# --- 2. Template content (Templates/<TemplateName>/Ext/Template.<ext>) ---
template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}")
if template_type == "HTML":
content = (
'<!DOCTYPE html>\n'
'<html>\n'
'<head>\n'
'\t<meta charset="UTF-8">\n'
'\t<title></title>\n'
'</head>\n'
'<body>\n'
'</body>\n'
'</html>'
)
write_text_with_bom(template_file_path, content)
elif template_type == "Text":
write_text_with_bom(template_file_path, "")
elif template_type == "SpreadsheetDocument":
content = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document"'
' xmlns:ss="http://v8.1c.ru/spreadsheet/document"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema">\n'
'</SpreadsheetDocument>'
)
write_text_with_bom(template_file_path, content)
elif template_type == "BinaryData":
with open(template_file_path, "wb") as f:
pass # empty file
elif template_type == "DataCompositionSchema":
content = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"\n'
'\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"\n'
'\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"\n'
'\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"\n'
'\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"\n'
'\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"\n'
'\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"\n'
'\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n'
'\t<dataSource>\n'
'\t\t<name>ИсточникДанных1</name>\n'
'\t\t<dataSourceType>Local</dataSourceType>\n'
'\t</dataSource>\n'
'</DataCompositionSchema>'
)
write_text_with_bom(template_file_path, content)
# --- 3. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Template> to end of ChildObjects
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
template_elem.text = template_name
# Remove auto-appended element to reinsert with proper whitespace
child_objects.remove(template_elem)
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
# last_child.tail is the trailing whitespace before </ChildObjects>
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = old_tail if old_tail else "\n\t\t"
else:
# Has text content but no element children
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
main_dcs_updated = False
if template_type == "DataCompositionSchema":
report_like_types = ["ExternalReport", "Report"]
object_type_node = None
object_type_name = None
for rt in report_like_types:
node = root.find(f".//md:{rt}", NSMAP)
if node is not None:
object_type_node = node
object_type_name = rt
break
if object_type_node is not None:
main_dcs = root.find(f".//md:{object_type_name}/md:Properties/md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None:
is_empty = main_dcs.text is None or main_dcs.text.strip() == ""
if is_empty or set_main_skd:
obj_name_node = root.find(f".//md:{object_type_name}/md:Properties/md:Name", NSMAP)
obj_name = obj_name_node.text if obj_name_node is not None else ""
main_dcs.text = f"{object_type_name}.{obj_name}.Template.{template_name}"
main_dcs_updated = True
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создан макет: {template_name} ({template_type})")
print(f" Метаданные: {template_meta_path}")
print(f" Содержимое: {template_file_path}")
if main_dcs_updated:
print(f" MainDataCompositionSchema: {main_dcs.text}")
if __name__ == "__main__":
main()
@@ -1,98 +1,99 @@
#!/usr/bin/env python3
# remove-template v1.0 — Remove template from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
parser = argparse.ArgumentParser(description="Remove template from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-TemplateName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
template_name = args.TemplateName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
templates_dir = os.path.join(processor_dir, "Templates")
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
template_dir = os.path.join(templates_dir, template_name)
if not os.path.exists(template_meta_path):
print(f"Метаданные макета не найдены: {template_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(template_dir):
shutil.rmtree(template_dir)
print(f"[OK] Удалён каталог: {template_dir}")
os.remove(template_meta_path)
print(f"[OK] Удалён файл: {template_meta_path}")
# --- Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
# Remove <Template>TemplateName</Template> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Template", NSMAP):
if node.text and node.text.strip() == template_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
break
# Clear MainDataCompositionSchema if it pointed to this template
main_dcs = root.find(".//md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None and main_dcs.text:
if re.search(rf"Template\.{re.escape(template_name)}$", main_dcs.text):
main_dcs.text = ""
print("[OK] Очищён MainDataCompositionSchema")
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Макет {template_name} удалён из {root_xml_path}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# remove-template v1.0 — Remove template from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import shutil
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Remove template from 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-TemplateName", required=True)
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
template_name = args.TemplateName
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, object_name)
templates_dir = os.path.join(processor_dir, "Templates")
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
template_dir = os.path.join(templates_dir, template_name)
if not os.path.exists(template_meta_path):
print(f"Метаданные макета не найдены: {template_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Delete files ---
if os.path.isdir(template_dir):
shutil.rmtree(template_dir)
print(f"[OK] Удалён каталог: {template_dir}")
os.remove(template_meta_path)
print(f"[OK] Удалён файл: {template_meta_path}")
# --- Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
# Remove <Template>TemplateName</Template> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Template", NSMAP):
if node.text and node.text.strip() == template_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
break
# Clear MainDataCompositionSchema if it pointed to this template
main_dcs = root.find(".//md:MainDataCompositionSchema", NSMAP)
if main_dcs is not None and main_dcs.text:
if re.search(rf"Template\.{re.escape(template_name)}$", main_dcs.text):
main_dcs.text = ""
print("[OK] Очищён MainDataCompositionSchema")
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Макет {template_name} удалён из {root_xml_path}")
if __name__ == "__main__":
main()
+159 -158
View File
@@ -1,158 +1,159 @@
#!/usr/bin/env python3
# web-info v1.0 — Apache & 1C publication status
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Статус Apache HTTP Server и публикаций 1С.
Показывает состояние Apache, список опубликованных баз
и последние ошибки из error.log.
"""
import argparse
import os
import re
import sys
import psutil
def get_httpd_by_exe(httpd_exe_norm):
"""Get httpd processes matching our exe path."""
ours = []
foreign = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
ours.append(p)
else:
foreign.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return ours, foreign
def main():
parser = argparse.ArgumentParser(description='Apache & 1C publication status', allow_abbrev=False)
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Check Apache installation ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
print('=== Apache Web Server ===')
if not os.path.exists(httpd_exe):
print('Status: Не установлен')
print(f'Path: {apache_path} (не найден)')
print('')
print('Используйте /web-publish для установки Apache.')
sys.exit(0)
# --- Check process (only our Apache) ---
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
our_proc, foreign_proc = get_httpd_by_exe(httpd_exe_norm)
if our_proc:
pids = ', '.join(str(p.pid) for p in our_proc)
print(f'Status: Запущен (PID: {pids})')
else:
print('Status: Остановлен')
if foreign_proc:
fp = foreign_proc[0]
try:
fpath = fp.info['exe'] or '?'
except Exception:
fpath = '?'
print(f'[WARN] Обнаружен сторонний Apache (PID: {fp.pid}, {fpath})')
print(f'Path: {apache_path}')
# --- Parse httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print('Config: httpd.conf не найден')
sys.exit(0)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
# Extract port from global block
port = '\u2014'
m = re.search(r'(?m)^Listen\s+(\d+)', conf_content)
if m:
port = m.group(1)
print(f'Port: {port}')
# Extract wsap24 path
m = re.search(r'LoadModule\s+_1cws_module\s+"([^"]+)"', conf_content)
if m:
print(f'Module: {m.group(1)}')
# --- Publications ---
print('')
print('=== Опубликованные базы ===')
pub_pattern = r'# --- 1C Publication: (.+?) ---'
pub_matches = re.findall(pub_pattern, conf_content)
if not pub_matches:
print('(нет публикаций)')
else:
for app_name in pub_matches:
# Read default.vrd for this publication
vrd_path = os.path.join(apache_path, 'publish', app_name, 'default.vrd')
ib_info = '\u2014'
vrd_content = ''
if os.path.exists(vrd_path):
with open(vrd_path, 'r', encoding='utf-8-sig') as f:
vrd_content = f.read()
m = re.search(r'ib="([^"]*)"', vrd_content)
if m:
ib_info = m.group(1).replace('&quot;', '"')
# Detect published services
svc_tags = []
if vrd_content:
if re.search(r'<ws\s', vrd_content):
svc_tags.append('WS')
if re.search(r'<httpServices\s', vrd_content):
svc_tags.append('HTTP')
if re.search(r'enableStandardOdata\s*=\s*"true"', vrd_content):
svc_tags.append('OData')
svc_label = ' [' + ' '.join(svc_tags) + ']' if svc_tags else ''
url = f'http://localhost:{port}/{app_name}'
print(f' {app_name} {url} {ib_info}{svc_label}')
# --- Error log ---
print('')
print('=== Последние ошибки ===')
error_log = os.path.join(apache_path, 'logs', 'error.log')
if os.path.exists(error_log):
try:
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
all_lines = f.readlines()
tail_lines = all_lines[-5:] if len(all_lines) >= 5 else all_lines
if tail_lines:
for line in tail_lines:
print(f' {line.rstrip()}')
else:
print('(пусто)')
except Exception:
print('(ошибка чтения)')
else:
print('(нет файла)')
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# web-info v1.0 — Apache & 1C publication status
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Статус Apache HTTP Server и публикаций 1С.
Показывает состояние Apache, список опубликованных баз
и последние ошибки из error.log.
"""
import argparse
import os
import re
import sys
import psutil
def get_httpd_by_exe(httpd_exe_norm):
"""Get httpd processes matching our exe path."""
ours = []
foreign = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
ours.append(p)
else:
foreign.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return ours, foreign
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Apache & 1C publication status', allow_abbrev=False)
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Check Apache installation ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
print('=== Apache Web Server ===')
if not os.path.exists(httpd_exe):
print('Status: Не установлен')
print(f'Path: {apache_path} (не найден)')
print('')
print('Используйте /web-publish для установки Apache.')
sys.exit(0)
# --- Check process (only our Apache) ---
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
our_proc, foreign_proc = get_httpd_by_exe(httpd_exe_norm)
if our_proc:
pids = ', '.join(str(p.pid) for p in our_proc)
print(f'Status: Запущен (PID: {pids})')
else:
print('Status: Остановлен')
if foreign_proc:
fp = foreign_proc[0]
try:
fpath = fp.info['exe'] or '?'
except Exception:
fpath = '?'
print(f'[WARN] Обнаружен сторонний Apache (PID: {fp.pid}, {fpath})')
print(f'Path: {apache_path}')
# --- Parse httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print('Config: httpd.conf не найден')
sys.exit(0)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
# Extract port from global block
port = '\u2014'
m = re.search(r'(?m)^Listen\s+(\d+)', conf_content)
if m:
port = m.group(1)
print(f'Port: {port}')
# Extract wsap24 path
m = re.search(r'LoadModule\s+_1cws_module\s+"([^"]+)"', conf_content)
if m:
print(f'Module: {m.group(1)}')
# --- Publications ---
print('')
print('=== Опубликованные базы ===')
pub_pattern = r'# --- 1C Publication: (.+?) ---'
pub_matches = re.findall(pub_pattern, conf_content)
if not pub_matches:
print('(нет публикаций)')
else:
for app_name in pub_matches:
# Read default.vrd for this publication
vrd_path = os.path.join(apache_path, 'publish', app_name, 'default.vrd')
ib_info = '\u2014'
vrd_content = ''
if os.path.exists(vrd_path):
with open(vrd_path, 'r', encoding='utf-8-sig') as f:
vrd_content = f.read()
m = re.search(r'ib="([^"]*)"', vrd_content)
if m:
ib_info = m.group(1).replace('&quot;', '"')
# Detect published services
svc_tags = []
if vrd_content:
if re.search(r'<ws\s', vrd_content):
svc_tags.append('WS')
if re.search(r'<httpServices\s', vrd_content):
svc_tags.append('HTTP')
if re.search(r'enableStandardOdata\s*=\s*"true"', vrd_content):
svc_tags.append('OData')
svc_label = ' [' + ' '.join(svc_tags) + ']' if svc_tags else ''
url = f'http://localhost:{port}/{app_name}'
print(f' {app_name} {url} {ib_info}{svc_label}')
# --- Error log ---
print('')
print('=== Последние ошибки ===')
error_log = os.path.join(apache_path, 'logs', 'error.log')
if os.path.exists(error_log):
try:
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
all_lines = f.readlines()
tail_lines = all_lines[-5:] if len(all_lines) >= 5 else all_lines
if tail_lines:
for line in tail_lines:
print(f' {line.rstrip()}')
else:
print('(пусто)')
except Exception:
print('(ошибка чтения)')
else:
print('(нет файла)')
if __name__ == '__main__':
main()
+399 -398
View File
@@ -1,398 +1,399 @@
#!/usr/bin/env python3
# web-publish v1.0 — Publish 1C infobase via Apache
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Публикация информационной базы 1С через Apache HTTP Server.
Генерирует default.vrd и настраивает httpd.conf для веб-доступа
к информационной базе 1С. При необходимости скачивает portable Apache.
Идемпотентный повторный вызов обновляет конфигурацию.
"""
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import zipfile
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def get_all_httpd():
"""Get all httpd processes."""
result = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def check_port_in_use(port):
"""Check if a port is in use and return the owning PID, or None."""
for conn in psutil.net_connections(kind='tcp'):
if conn.laddr and conn.laddr.port == port and conn.status == 'LISTEN':
return conn.pid
return None
def main():
parser = argparse.ArgumentParser(description='Publish 1C infobase via Apache', allow_abbrev=False)
parser.add_argument('-V8Path', type=str, default='', help='Path to 1C platform bin directory (for wsap24.dll)')
parser.add_argument('-InfoBasePath', type=str, default='', help='Path to file infobase')
parser.add_argument('-InfoBaseServer', type=str, default='', help='1C server (for server infobase)')
parser.add_argument('-InfoBaseRef', type=str, default='', help='Infobase name on server')
parser.add_argument('-UserName', type=str, default='', help='1C user name')
parser.add_argument('-Password', type=str, default='', help='1C password')
parser.add_argument('-AppName', type=str, default='', help='Publication name (default: from infobase folder name)')
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
parser.add_argument('-Port', type=int, default=8081, help='Port (default: 8081)')
parser.add_argument('-Manual', action='store_true', help='Do not download Apache — only check and give instructions')
args = parser.parse_args()
# --- Resolve V8Path ---
v8_path = args.V8Path
if not v8_path:
candidates = glob.glob(r'C:\Program Files\1cv8\*\bin\1cv8.exe')
candidates.sort(reverse=True)
if candidates:
v8_path = os.path.dirname(candidates[0])
else:
print('Error: платформа 1С не найдена. Укажите -V8Path', file=sys.stderr)
sys.exit(1)
elif os.path.isfile(v8_path):
v8_path = os.path.dirname(v8_path)
# Validate wsap24.dll
wsap_dll = os.path.join(v8_path, 'wsap24.dll')
if not os.path.exists(wsap_dll):
print(f'Error: wsap24.dll не найден в {v8_path}', file=sys.stderr)
sys.exit(1)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef', file=sys.stderr)
sys.exit(1)
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
port = args.Port
# --- Check / Install Apache ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if not os.path.exists(httpd_exe):
if args.Manual:
print(f'Apache не найден: {apache_path}')
print('')
print('Установите Apache вручную:')
print(' 1. Скачайте Apache Lounge (x64) с https://www.apachelounge.com/download/')
print(f' 2. Распакуйте содержимое Apache24\\ в: {apache_path}')
print(' 3. Запустите скрипт повторно')
sys.exit(1)
print('Apache не найден. Скачиваю...')
zip_url = 'https://www.apachelounge.com/download/VS18/binaries/httpd-2.4.66-260131-Win64-VS18.zip'
tmp_zip = os.path.join(tempfile.gettempdir(), 'apache24.zip')
tmp_dir = os.path.join(tempfile.gettempdir(), 'apache24_extract')
try:
urllib.request.urlretrieve(zip_url, tmp_zip)
except Exception as e:
print(f'Error: не удалось скачать Apache: {e}', file=sys.stderr)
print('Скачайте вручную: https://www.apachelounge.com/download/')
sys.exit(1)
print('Распаковка...')
if os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir, ignore_errors=True)
with zipfile.ZipFile(tmp_zip, 'r') as zf:
zf.extractall(tmp_dir)
# Move Apache24 contents up to ApachePath
inner_dir = os.path.join(tmp_dir, 'Apache24')
if not os.path.isdir(inner_dir):
# Try to find Apache24 in nested folder
found_inner = None
for root, dirs, files in os.walk(tmp_dir):
if 'Apache24' in dirs:
found_inner = os.path.join(root, 'Apache24')
break
if found_inner:
inner_dir = found_inner
else:
print('Error: каталог Apache24 не найден в архиве', file=sys.stderr)
sys.exit(1)
os.makedirs(apache_path, exist_ok=True)
# Copy contents of inner_dir to apache_path
for item in os.listdir(inner_dir):
src = os.path.join(inner_dir, item)
dst = os.path.join(apache_path, item)
if os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
# Cleanup
try:
os.remove(tmp_zip)
except OSError:
pass
try:
shutil.rmtree(tmp_dir, ignore_errors=True)
except OSError:
pass
# Patch ServerRoot in httpd.conf
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if os.path.exists(conf_file):
apache_path_fwd = apache_path.replace('\\', '/')
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
conf_content = re.sub(
r'(?m)^Define SRVROOT .*$',
f'Define SRVROOT "{apache_path_fwd}"',
conf_content,
)
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
print(f'ServerRoot обновлён: {apache_path_fwd}')
print(f'Apache установлен: {apache_path}')
# --- Derive AppName ---
app_name = args.AppName
if not app_name:
if args.InfoBasePath:
app_name = re.sub(r'[^\w]', '', os.path.basename(args.InfoBasePath))
else:
app_name = re.sub(r'[^\w]', '', args.InfoBaseRef)
app_name = app_name.lower()
app_name = app_name.lower()
if not app_name:
print('Error: не удалось определить имя публикации. Укажите -AppName', file=sys.stderr)
sys.exit(1)
print(f'Публикация: {app_name}')
# --- Create publish directory ---
publish_dir = os.path.join(apache_path, 'publish', app_name)
os.makedirs(publish_dir, exist_ok=True)
# --- Generate default.vrd ---
vrd_path = os.path.join(publish_dir, 'default.vrd')
ib_parts = []
if args.InfoBaseServer and args.InfoBaseRef:
ib_parts.append(f'Srvr=&quot;{args.InfoBaseServer}&quot;')
ib_parts.append(f'Ref=&quot;{args.InfoBaseRef}&quot;')
else:
ib_parts.append(f'File=&quot;{args.InfoBasePath}&quot;')
if args.UserName:
ib_parts.append(f'Usr=&quot;{args.UserName}&quot;')
if args.Password:
ib_parts.append(f'Pwd=&quot;{args.Password}&quot;')
ib_string = ';'.join(ib_parts) + ';'
vrd_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<point xmlns="http://v8.1c.ru/8.2/virtual-resource-system"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
base="/{app_name}"
ib="{ib_string}"
enableStandardOdata="true">
<ws pointEnableCommon="true"/>
<httpServices publishByDefault="true"/>
</point>'''
with open(vrd_path, 'wb') as f:
f.write(b'\xef\xbb\xbf')
f.write(vrd_content.encode('utf-8'))
print(f'default.vrd: {vrd_path}')
# --- Update httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
sys.exit(1)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
apache_path_fwd = apache_path.replace('\\', '/')
wsap_dll_fwd = wsap_dll.replace('\\', '/')
publish_dir_fwd = publish_dir.replace('\\', '/')
vrd_path_fwd = vrd_path.replace('\\', '/')
# --- Global block (Listen + LoadModule) ---
global_marker_start = '# --- 1C: global ---'
global_marker_end = '# --- End: global ---'
global_block = (
f'{global_marker_start}\n'
f'Listen {port}\n'
f'LoadModule _1cws_module "{wsap_dll_fwd}"\n'
f'{global_marker_end}'
)
if re.search(re.escape(global_marker_start), conf_content):
# Replace existing global block
pattern = re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end)
conf_content = re.sub(pattern, global_block, conf_content)
else:
# Comment out default Listen to avoid port conflict
conf_content = re.sub(r'(?m)^(Listen\s+\d+)', r'#\1 # commented by web-publish', conf_content)
# Append global block
conf_content = conf_content.rstrip() + '\n\n' + global_block + '\n'
# --- Publication block ---
pub_marker_start = f'# --- 1C Publication: {app_name} ---'
pub_marker_end = f'# --- End: {app_name} ---'
pub_block = (
f'{pub_marker_start}\n'
f'Alias "/{app_name}" "{publish_dir_fwd}"\n'
f'<Directory "{publish_dir_fwd}">\n'
f' AllowOverride All\n'
f' Require all granted\n'
f' SetHandler 1c-application\n'
f' ManagedApplicationDescriptor "{vrd_path_fwd}"\n'
f'</Directory>\n'
f'{pub_marker_end}'
)
if re.search(re.escape(pub_marker_start), conf_content):
# Replace existing publication block
pattern = re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end)
conf_content = re.sub(pattern, pub_block, conf_content)
else:
# Append publication block
conf_content = conf_content.rstrip() + '\n\n' + pub_block + '\n'
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
print('httpd.conf обновлён')
# --- Normalize httpd_exe for process matching ---
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Check port availability ---
holder_pid = check_port_in_use(port)
if holder_pid:
our_proc = get_our_httpd(httpd_exe_norm)
if not our_proc:
# Port is held by someone else
try:
holder_proc = psutil.Process(holder_pid)
holder_name = f'{holder_proc.name()} (PID: {holder_pid})'
except (psutil.NoSuchProcess, psutil.AccessDenied):
holder_name = f'PID {holder_pid}'
print(f'Error: порт {port} занят процессом {holder_name}', file=sys.stderr)
print('Укажите другой порт: -Port 9090')
sys.exit(1)
# --- Start Apache if not running ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if httpd_proc:
first_pid = httpd_proc[0].pid
print(f'Apache уже запущен (PID: {first_pid})')
print('Перезапуск для применения конфигурации...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
else:
# Check if a foreign httpd holds the port
foreign_httpd = get_all_httpd()
if foreign_httpd:
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign_httpd[0].pid})')
print(f' Наш Apache: {httpd_exe}')
print('Запуск Apache...')
subprocess.Popen(
[httpd_exe],
cwd=apache_path,
creationflags=subprocess.CREATE_NO_WINDOW,
)
time.sleep(2)
httpd_check = get_our_httpd(httpd_exe_norm)
if httpd_check:
print(f'Apache запущен (PID: {httpd_check[0].pid})')
else:
print('Apache не удалось запустить', file=sys.stderr)
# Run config test for diagnostics
try:
result = subprocess.run(
[httpd_exe, '-t'],
capture_output=True,
text=True,
timeout=10,
)
test_output = (result.stdout + result.stderr).strip()
if test_output:
print('--- httpd -t ---')
for line in test_output.splitlines():
print(f' {line}')
except Exception:
pass
error_log = os.path.join(apache_path, 'logs', 'error.log')
if os.path.exists(error_log):
print('--- error.log (последние 10 строк) ---')
try:
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
all_lines = f.readlines()
for line in all_lines[-10:]:
print(line.rstrip())
except Exception:
pass
sys.exit(1)
# --- Result ---
print('')
print('=== Публикация готова ===')
print(f'URL: http://localhost:{port}/{app_name}')
print(f'OData: http://localhost:{port}/{app_name}/odata/standard.odata')
print(f'HTTP-сервисы: http://localhost:{port}/{app_name}/hs/<RootUrl>/...')
print(f'Web-сервисы: http://localhost:{port}/{app_name}/ws/<Имя>?wsdl')
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# web-publish v1.0 — Publish 1C infobase via Apache
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Публикация информационной базы 1С через Apache HTTP Server.
Генерирует default.vrd и настраивает httpd.conf для веб-доступа
к информационной базе 1С. При необходимости скачивает portable Apache.
Идемпотентный повторный вызов обновляет конфигурацию.
"""
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import zipfile
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def get_all_httpd():
"""Get all httpd processes."""
result = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def check_port_in_use(port):
"""Check if a port is in use and return the owning PID, or None."""
for conn in psutil.net_connections(kind='tcp'):
if conn.laddr and conn.laddr.port == port and conn.status == 'LISTEN':
return conn.pid
return None
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Publish 1C infobase via Apache', allow_abbrev=False)
parser.add_argument('-V8Path', type=str, default='', help='Path to 1C platform bin directory (for wsap24.dll)')
parser.add_argument('-InfoBasePath', type=str, default='', help='Path to file infobase')
parser.add_argument('-InfoBaseServer', type=str, default='', help='1C server (for server infobase)')
parser.add_argument('-InfoBaseRef', type=str, default='', help='Infobase name on server')
parser.add_argument('-UserName', type=str, default='', help='1C user name')
parser.add_argument('-Password', type=str, default='', help='1C password')
parser.add_argument('-AppName', type=str, default='', help='Publication name (default: from infobase folder name)')
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
parser.add_argument('-Port', type=int, default=8081, help='Port (default: 8081)')
parser.add_argument('-Manual', action='store_true', help='Do not download Apache — only check and give instructions')
args = parser.parse_args()
# --- Resolve V8Path ---
v8_path = args.V8Path
if not v8_path:
candidates = glob.glob(r'C:\Program Files\1cv8\*\bin\1cv8.exe')
candidates.sort(reverse=True)
if candidates:
v8_path = os.path.dirname(candidates[0])
else:
print('Error: платформа 1С не найдена. Укажите -V8Path', file=sys.stderr)
sys.exit(1)
elif os.path.isfile(v8_path):
v8_path = os.path.dirname(v8_path)
# Validate wsap24.dll
wsap_dll = os.path.join(v8_path, 'wsap24.dll')
if not os.path.exists(wsap_dll):
print(f'Error: wsap24.dll не найден в {v8_path}', file=sys.stderr)
sys.exit(1)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef', file=sys.stderr)
sys.exit(1)
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
port = args.Port
# --- Check / Install Apache ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if not os.path.exists(httpd_exe):
if args.Manual:
print(f'Apache не найден: {apache_path}')
print('')
print('Установите Apache вручную:')
print(' 1. Скачайте Apache Lounge (x64) с https://www.apachelounge.com/download/')
print(f' 2. Распакуйте содержимое Apache24\\ в: {apache_path}')
print(' 3. Запустите скрипт повторно')
sys.exit(1)
print('Apache не найден. Скачиваю...')
zip_url = 'https://www.apachelounge.com/download/VS18/binaries/httpd-2.4.66-260131-Win64-VS18.zip'
tmp_zip = os.path.join(tempfile.gettempdir(), 'apache24.zip')
tmp_dir = os.path.join(tempfile.gettempdir(), 'apache24_extract')
try:
urllib.request.urlretrieve(zip_url, tmp_zip)
except Exception as e:
print(f'Error: не удалось скачать Apache: {e}', file=sys.stderr)
print('Скачайте вручную: https://www.apachelounge.com/download/')
sys.exit(1)
print('Распаковка...')
if os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir, ignore_errors=True)
with zipfile.ZipFile(tmp_zip, 'r') as zf:
zf.extractall(tmp_dir)
# Move Apache24 contents up to ApachePath
inner_dir = os.path.join(tmp_dir, 'Apache24')
if not os.path.isdir(inner_dir):
# Try to find Apache24 in nested folder
found_inner = None
for root, dirs, files in os.walk(tmp_dir):
if 'Apache24' in dirs:
found_inner = os.path.join(root, 'Apache24')
break
if found_inner:
inner_dir = found_inner
else:
print('Error: каталог Apache24 не найден в архиве', file=sys.stderr)
sys.exit(1)
os.makedirs(apache_path, exist_ok=True)
# Copy contents of inner_dir to apache_path
for item in os.listdir(inner_dir):
src = os.path.join(inner_dir, item)
dst = os.path.join(apache_path, item)
if os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
# Cleanup
try:
os.remove(tmp_zip)
except OSError:
pass
try:
shutil.rmtree(tmp_dir, ignore_errors=True)
except OSError:
pass
# Patch ServerRoot in httpd.conf
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if os.path.exists(conf_file):
apache_path_fwd = apache_path.replace('\\', '/')
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
conf_content = re.sub(
r'(?m)^Define SRVROOT .*$',
f'Define SRVROOT "{apache_path_fwd}"',
conf_content,
)
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
print(f'ServerRoot обновлён: {apache_path_fwd}')
print(f'Apache установлен: {apache_path}')
# --- Derive AppName ---
app_name = args.AppName
if not app_name:
if args.InfoBasePath:
app_name = re.sub(r'[^\w]', '', os.path.basename(args.InfoBasePath))
else:
app_name = re.sub(r'[^\w]', '', args.InfoBaseRef)
app_name = app_name.lower()
app_name = app_name.lower()
if not app_name:
print('Error: не удалось определить имя публикации. Укажите -AppName', file=sys.stderr)
sys.exit(1)
print(f'Публикация: {app_name}')
# --- Create publish directory ---
publish_dir = os.path.join(apache_path, 'publish', app_name)
os.makedirs(publish_dir, exist_ok=True)
# --- Generate default.vrd ---
vrd_path = os.path.join(publish_dir, 'default.vrd')
ib_parts = []
if args.InfoBaseServer and args.InfoBaseRef:
ib_parts.append(f'Srvr=&quot;{args.InfoBaseServer}&quot;')
ib_parts.append(f'Ref=&quot;{args.InfoBaseRef}&quot;')
else:
ib_parts.append(f'File=&quot;{args.InfoBasePath}&quot;')
if args.UserName:
ib_parts.append(f'Usr=&quot;{args.UserName}&quot;')
if args.Password:
ib_parts.append(f'Pwd=&quot;{args.Password}&quot;')
ib_string = ';'.join(ib_parts) + ';'
vrd_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<point xmlns="http://v8.1c.ru/8.2/virtual-resource-system"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
base="/{app_name}"
ib="{ib_string}"
enableStandardOdata="true">
<ws pointEnableCommon="true"/>
<httpServices publishByDefault="true"/>
</point>'''
with open(vrd_path, 'wb') as f:
f.write(b'\xef\xbb\xbf')
f.write(vrd_content.encode('utf-8'))
print(f'default.vrd: {vrd_path}')
# --- Update httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
sys.exit(1)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
apache_path_fwd = apache_path.replace('\\', '/')
wsap_dll_fwd = wsap_dll.replace('\\', '/')
publish_dir_fwd = publish_dir.replace('\\', '/')
vrd_path_fwd = vrd_path.replace('\\', '/')
# --- Global block (Listen + LoadModule) ---
global_marker_start = '# --- 1C: global ---'
global_marker_end = '# --- End: global ---'
global_block = (
f'{global_marker_start}\n'
f'Listen {port}\n'
f'LoadModule _1cws_module "{wsap_dll_fwd}"\n'
f'{global_marker_end}'
)
if re.search(re.escape(global_marker_start), conf_content):
# Replace existing global block
pattern = re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end)
conf_content = re.sub(pattern, global_block, conf_content)
else:
# Comment out default Listen to avoid port conflict
conf_content = re.sub(r'(?m)^(Listen\s+\d+)', r'#\1 # commented by web-publish', conf_content)
# Append global block
conf_content = conf_content.rstrip() + '\n\n' + global_block + '\n'
# --- Publication block ---
pub_marker_start = f'# --- 1C Publication: {app_name} ---'
pub_marker_end = f'# --- End: {app_name} ---'
pub_block = (
f'{pub_marker_start}\n'
f'Alias "/{app_name}" "{publish_dir_fwd}"\n'
f'<Directory "{publish_dir_fwd}">\n'
f' AllowOverride All\n'
f' Require all granted\n'
f' SetHandler 1c-application\n'
f' ManagedApplicationDescriptor "{vrd_path_fwd}"\n'
f'</Directory>\n'
f'{pub_marker_end}'
)
if re.search(re.escape(pub_marker_start), conf_content):
# Replace existing publication block
pattern = re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end)
conf_content = re.sub(pattern, pub_block, conf_content)
else:
# Append publication block
conf_content = conf_content.rstrip() + '\n\n' + pub_block + '\n'
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
print('httpd.conf обновлён')
# --- Normalize httpd_exe for process matching ---
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Check port availability ---
holder_pid = check_port_in_use(port)
if holder_pid:
our_proc = get_our_httpd(httpd_exe_norm)
if not our_proc:
# Port is held by someone else
try:
holder_proc = psutil.Process(holder_pid)
holder_name = f'{holder_proc.name()} (PID: {holder_pid})'
except (psutil.NoSuchProcess, psutil.AccessDenied):
holder_name = f'PID {holder_pid}'
print(f'Error: порт {port} занят процессом {holder_name}', file=sys.stderr)
print('Укажите другой порт: -Port 9090')
sys.exit(1)
# --- Start Apache if not running ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if httpd_proc:
first_pid = httpd_proc[0].pid
print(f'Apache уже запущен (PID: {first_pid})')
print('Перезапуск для применения конфигурации...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
else:
# Check if a foreign httpd holds the port
foreign_httpd = get_all_httpd()
if foreign_httpd:
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign_httpd[0].pid})')
print(f' Наш Apache: {httpd_exe}')
print('Запуск Apache...')
subprocess.Popen(
[httpd_exe],
cwd=apache_path,
creationflags=subprocess.CREATE_NO_WINDOW,
)
time.sleep(2)
httpd_check = get_our_httpd(httpd_exe_norm)
if httpd_check:
print(f'Apache запущен (PID: {httpd_check[0].pid})')
else:
print('Apache не удалось запустить', file=sys.stderr)
# Run config test for diagnostics
try:
result = subprocess.run(
[httpd_exe, '-t'],
capture_output=True,
text=True,
timeout=10,
)
test_output = (result.stdout + result.stderr).strip()
if test_output:
print('--- httpd -t ---')
for line in test_output.splitlines():
print(f' {line}')
except Exception:
pass
error_log = os.path.join(apache_path, 'logs', 'error.log')
if os.path.exists(error_log):
print('--- error.log (последние 10 строк) ---')
try:
with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
all_lines = f.readlines()
for line in all_lines[-10:]:
print(line.rstrip())
except Exception:
pass
sys.exit(1)
# --- Result ---
print('')
print('=== Публикация готова ===')
print(f'URL: http://localhost:{port}/{app_name}')
print(f'OData: http://localhost:{port}/{app_name}/odata/standard.odata')
print(f'HTTP-сервисы: http://localhost:{port}/{app_name}/hs/<RootUrl>/...')
print(f'Web-сервисы: http://localhost:{port}/{app_name}/ws/<Имя>?wsdl')
if __name__ == '__main__':
main()
+118 -117
View File
@@ -1,117 +1,118 @@
#!/usr/bin/env python3
# web-stop v1.0 — Stop Apache HTTP Server
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Остановка Apache HTTP Server.
Сначала пытается graceful shutdown, при неудаче принудительная остановка.
"""
import argparse
import os
import sys
import time
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def get_all_httpd():
"""Get all httpd processes."""
result = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def main():
parser = argparse.ArgumentParser(description='Stop Apache HTTP Server', allow_abbrev=False)
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Helper: normalize httpd exe path ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Check process (only our Apache) ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if not httpd_proc:
foreign = get_all_httpd()
if foreign:
print('Наш Apache не запущен')
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign[0].pid})')
else:
print('Apache не запущен')
sys.exit(0)
pids = ', '.join(str(p.pid) for p in httpd_proc)
print(f'Останавливаю Apache (PID: {pids})...')
# --- Stop our processes ---
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# --- Wait for shutdown ---
max_wait = 5
elapsed = 0
while elapsed < max_wait:
time.sleep(1)
elapsed += 1
check = get_our_httpd(httpd_exe_norm)
if not check:
print('Apache остановлен')
print('Публикации сохранены. Перезапуск: /web-publish <база> Удаление: /web-unpublish --all')
sys.exit(0)
# --- Fallback: force kill ---
remaining = get_our_httpd(httpd_exe_norm)
if remaining:
print('Принудительная остановка...')
for p in remaining:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
final = get_our_httpd(httpd_exe_norm)
if final:
print('Error: не удалось остановить Apache', file=sys.stderr)
sys.exit(1)
print('Apache остановлен')
print('Публикации сохранены. Перезапуск: /web-publish <база> Удаление: /web-unpublish --all')
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# web-stop v1.0 — Stop Apache HTTP Server
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Остановка Apache HTTP Server.
Сначала пытается graceful shutdown, при неудаче принудительная остановка.
"""
import argparse
import os
import sys
import time
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def get_all_httpd():
"""Get all httpd processes."""
result = []
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Stop Apache HTTP Server', allow_abbrev=False)
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Helper: normalize httpd exe path ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Check process (only our Apache) ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if not httpd_proc:
foreign = get_all_httpd()
if foreign:
print('Наш Apache не запущен')
print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign[0].pid})')
else:
print('Apache не запущен')
sys.exit(0)
pids = ', '.join(str(p.pid) for p in httpd_proc)
print(f'Останавливаю Apache (PID: {pids})...')
# --- Stop our processes ---
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# --- Wait for shutdown ---
max_wait = 5
elapsed = 0
while elapsed < max_wait:
time.sleep(1)
elapsed += 1
check = get_our_httpd(httpd_exe_norm)
if not check:
print('Apache остановлен')
print('Публикации сохранены. Перезапуск: /web-publish <база> Удаление: /web-unpublish --all')
sys.exit(0)
# --- Fallback: force kill ---
remaining = get_our_httpd(httpd_exe_norm)
if remaining:
print('Принудительная остановка...')
for p in remaining:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
final = get_our_httpd(httpd_exe_norm)
if final:
print('Error: не удалось остановить Apache', file=sys.stderr)
sys.exit(1)
print('Apache остановлен')
print('Публикации сохранены. Перезапуск: /web-publish <база> Удаление: /web-unpublish --all')
if __name__ == '__main__':
main()
@@ -1,160 +1,161 @@
#!/usr/bin/env python3
# web-unpublish v1.0 — Remove 1C web publication
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Удаление веб-публикации 1С из Apache.
Удаляет маркерный блок из httpd.conf и каталог публикации.
Если Apache запущен перезапускает для применения.
С флагом -All удаляет все публикации и останавливает Apache.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import time
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def main():
parser = argparse.ArgumentParser(description='Remove 1C web publication', allow_abbrev=False)
parser.add_argument('-AppName', type=str, default='', help='Publication name')
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
parser.add_argument('-All', action='store_true', help='Remove all publications')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Validate params ---
if not args.All and not args.AppName:
print('Error: укажите -AppName или -All', file=sys.stderr)
sys.exit(1)
# --- Read httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
sys.exit(1)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
# --- Helper: our httpd process ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Collect app names to remove ---
if args.All:
pub_pattern = r'# --- 1C Publication: (.+?) ---'
pub_matches = re.findall(pub_pattern, conf_content)
if not pub_matches:
print('Нет публикаций для удаления')
sys.exit(0)
app_names = pub_matches
print(f'Удаление всех публикаций: {", ".join(app_names)}')
else:
app_names = [args.AppName]
# --- Remove marker blocks ---
for name in app_names:
pub_marker_start = f'# --- 1C Publication: {name} ---'
pub_marker_end = f'# --- End: {name} ---'
if re.search(re.escape(pub_marker_start), conf_content):
pattern = r'\r?\n?' + re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end) + r'\r?\n?'
conf_content = re.sub(pattern, '\n', conf_content)
print(f"httpd.conf: блок публикации '{name}' удалён")
else:
print(f"Публикация '{name}' не найдена в httpd.conf")
# --- Check if any publications remain; if not, remove global block ---
remaining_pubs = re.findall(r'# --- 1C Publication: .+? ---', conf_content)
if not remaining_pubs:
global_marker_start = '# --- 1C: global ---'
global_marker_end = '# --- End: global ---'
if re.search(re.escape(global_marker_start), conf_content):
global_pattern = r'\r?\n?' + re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end) + r'\r?\n?'
conf_content = re.sub(global_pattern, '\n', conf_content)
print('httpd.conf: глобальный блок 1C удалён (нет публикаций)')
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
# --- Remove publish directories ---
for name in app_names:
publish_dir = os.path.join(apache_path, 'publish', name)
if os.path.exists(publish_dir):
shutil.rmtree(publish_dir, ignore_errors=True)
print(f'Каталог удалён: {publish_dir}')
else:
print(f'Каталог не найден: {publish_dir}')
# --- Restart/Stop Apache if running (only our instance) ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if httpd_proc:
if remaining_pubs:
print('Перезапуск Apache...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
subprocess.Popen(
[httpd_exe],
cwd=apache_path,
creationflags=subprocess.CREATE_NO_WINDOW,
)
time.sleep(2)
check = get_our_httpd(httpd_exe_norm)
if check:
print('Apache перезапущен')
else:
print('Error: Apache не удалось перезапустить', file=sys.stderr)
sys.exit(1)
else:
print('Публикаций не осталось — останавливаю Apache...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
print('Apache остановлен')
print('')
if args.All:
print(f'Все публикации удалены ({len(app_names)} шт.)')
else:
print(f"Публикация '{args.AppName}' удалена")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# web-unpublish v1.0 — Remove 1C web publication
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""
Удаление веб-публикации 1С из Apache.
Удаляет маркерный блок из httpd.conf и каталог публикации.
Если Apache запущен перезапускает для применения.
С флагом -All удаляет все публикации и останавливает Apache.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import time
import psutil
def get_our_httpd(httpd_exe_norm):
"""Filter httpd processes by our ApachePath."""
result = []
if not httpd_exe_norm:
return result
for p in psutil.process_iter(['pid', 'name', 'exe']):
try:
if p.info['name'] and 'httpd' in p.info['name'].lower():
if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
result.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return result
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Remove 1C web publication', allow_abbrev=False)
parser.add_argument('-AppName', type=str, default='', help='Publication name')
parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
parser.add_argument('-All', action='store_true', help='Remove all publications')
args = parser.parse_args()
# --- Resolve ApachePath ---
apache_path = args.ApachePath
if not apache_path:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
apache_path = os.path.join(project_root, 'tools', 'apache24')
# --- Validate params ---
if not args.All and not args.AppName:
print('Error: укажите -AppName или -All', file=sys.stderr)
sys.exit(1)
# --- Read httpd.conf ---
conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
if not os.path.exists(conf_file):
print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr)
sys.exit(1)
with open(conf_file, 'r', encoding='utf-8-sig') as f:
conf_content = f.read()
# --- Helper: our httpd process ---
httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')
if os.path.exists(httpd_exe):
httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
else:
httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))
# --- Collect app names to remove ---
if args.All:
pub_pattern = r'# --- 1C Publication: (.+?) ---'
pub_matches = re.findall(pub_pattern, conf_content)
if not pub_matches:
print('Нет публикаций для удаления')
sys.exit(0)
app_names = pub_matches
print(f'Удаление всех публикаций: {", ".join(app_names)}')
else:
app_names = [args.AppName]
# --- Remove marker blocks ---
for name in app_names:
pub_marker_start = f'# --- 1C Publication: {name} ---'
pub_marker_end = f'# --- End: {name} ---'
if re.search(re.escape(pub_marker_start), conf_content):
pattern = r'\r?\n?' + re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end) + r'\r?\n?'
conf_content = re.sub(pattern, '\n', conf_content)
print(f"httpd.conf: блок публикации '{name}' удалён")
else:
print(f"Публикация '{name}' не найдена в httpd.conf")
# --- Check if any publications remain; if not, remove global block ---
remaining_pubs = re.findall(r'# --- 1C Publication: .+? ---', conf_content)
if not remaining_pubs:
global_marker_start = '# --- 1C: global ---'
global_marker_end = '# --- End: global ---'
if re.search(re.escape(global_marker_start), conf_content):
global_pattern = r'\r?\n?' + re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end) + r'\r?\n?'
conf_content = re.sub(global_pattern, '\n', conf_content)
print('httpd.conf: глобальный блок 1C удалён (нет публикаций)')
with open(conf_file, 'w', encoding='utf-8') as f:
f.write(conf_content)
# --- Remove publish directories ---
for name in app_names:
publish_dir = os.path.join(apache_path, 'publish', name)
if os.path.exists(publish_dir):
shutil.rmtree(publish_dir, ignore_errors=True)
print(f'Каталог удалён: {publish_dir}')
else:
print(f'Каталог не найден: {publish_dir}')
# --- Restart/Stop Apache if running (only our instance) ---
httpd_proc = get_our_httpd(httpd_exe_norm)
if httpd_proc:
if remaining_pubs:
print('Перезапуск Apache...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
subprocess.Popen(
[httpd_exe],
cwd=apache_path,
creationflags=subprocess.CREATE_NO_WINDOW,
)
time.sleep(2)
check = get_our_httpd(httpd_exe_norm)
if check:
print('Apache перезапущен')
else:
print('Error: Apache не удалось перезапустить', file=sys.stderr)
sys.exit(1)
else:
print('Публикаций не осталось — останавливаю Apache...')
for p in httpd_proc:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
print('Apache остановлен')
print('')
if args.All:
print(f'Все публикации удалены ({len(app_names)} шт.)')
else:
print(f"Публикация '{args.AppName}' удалена")
if __name__ == '__main__':
main()