mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 17:04:57 +03:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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}")
|
||||
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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
@@ -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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
+1905
-1903
File diff suppressed because it is too large
Load Diff
+1680
-1679
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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -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('"', '"')
|
||||
|
||||
# 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('"', '"')
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -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="{args.InfoBaseServer}"')
|
||||
ib_parts.append(f'Ref="{args.InfoBaseRef}"')
|
||||
else:
|
||||
ib_parts.append(f'File="{args.InfoBasePath}"')
|
||||
if args.UserName:
|
||||
ib_parts.append(f'Usr="{args.UserName}"')
|
||||
if args.Password:
|
||||
ib_parts.append(f'Pwd="{args.Password}"')
|
||||
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="{args.InfoBaseServer}"')
|
||||
ib_parts.append(f'Ref="{args.InfoBaseRef}"')
|
||||
else:
|
||||
ib_parts.append(f'File="{args.InfoBasePath}"')
|
||||
if args.UserName:
|
||||
ib_parts.append(f'Usr="{args.UserName}"')
|
||||
if args.Password:
|
||||
ib_parts.append(f'Pwd="{args.Password}"')
|
||||
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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user