mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-08 04:30:19 +03:00
Навык писал шапку многострочно (каждое объявление своей строкой) и отступы четырьмя пробелами, тогда как платформа пишет шапку одной строкой и отступы табами. На каждой роли это давало диff во всю длину файла — шум, из-за которого реальные расхождения не видны. Сверено с выгрузкой УНФ 8.5: скелет Role.xml совпал с платформенным полностью (с точностью до uuid и текстов). 1С-сертификация: 10/10 кейсов загружаются. Снэпшоты обновлены — дрейф только ожидаемый (склейка шапки и пробелы→табы). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
851 lines
40 KiB
Python
851 lines
40 KiB
Python
#!/usr/bin/env python3
|
||
# role-compile v1.18 — Compile 1C role from JSON
|
||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
import argparse
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
import uuid
|
||
|
||
from lxml import etree
|
||
|
||
|
||
# ============================================================
|
||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||
# ============================================================
|
||
|
||
def _sg_root_uuid(xml_path):
|
||
if not os.path.isfile(xml_path):
|
||
return None
|
||
try:
|
||
mx = etree.parse(xml_path).getroot()
|
||
for child in mx:
|
||
if isinstance(child.tag, str) and child.get("uuid"):
|
||
return child.get("uuid")
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
|
||
def _sg_is_external_root(xml_path):
|
||
if not os.path.isfile(xml_path):
|
||
return False
|
||
try:
|
||
mx = etree.parse(xml_path).getroot()
|
||
for child in mx:
|
||
if isinstance(child.tag, str):
|
||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||
except Exception:
|
||
return False
|
||
return False
|
||
|
||
def _sg_find_v8project(start_dir):
|
||
d = start_dir
|
||
for _ in range(20):
|
||
if not d:
|
||
break
|
||
pj = os.path.join(d, ".v8-project.json")
|
||
if os.path.isfile(pj):
|
||
return pj
|
||
parent = os.path.dirname(d)
|
||
if parent == d:
|
||
break
|
||
d = parent
|
||
return None
|
||
|
||
|
||
def _sg_get_edit_mode(cfg_dir):
|
||
try:
|
||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||
if not pj:
|
||
return "deny"
|
||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||
for db in proj.get("databases", []):
|
||
src = db.get("configSrc")
|
||
if src:
|
||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||
if db.get("editingAllowedCheck"):
|
||
return db["editingAllowedCheck"]
|
||
if proj.get("editingAllowedCheck"):
|
||
return proj["editingAllowedCheck"]
|
||
return "deny"
|
||
except Exception:
|
||
return "deny"
|
||
|
||
|
||
def assert_edit_allowed(target_path, require):
|
||
try:
|
||
rp = os.path.abspath(target_path)
|
||
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||
if _sg_is_external_root(rp):
|
||
return
|
||
elem_uuid = _sg_root_uuid(rp)
|
||
cfg_dir = None
|
||
bin_path = None
|
||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||
for _ in range(12):
|
||
if not d:
|
||
break
|
||
if _sg_is_external_root(d + ".xml"):
|
||
return
|
||
if not elem_uuid:
|
||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||
if not cfg_dir:
|
||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||
cfg_dir = d
|
||
bin_path = cand
|
||
if elem_uuid and cfg_dir:
|
||
break
|
||
parent = os.path.dirname(d)
|
||
if parent == d:
|
||
break
|
||
d = parent
|
||
if not elem_uuid and cfg_dir:
|
||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||
if not bin_path or not os.path.exists(bin_path):
|
||
return
|
||
data = open(bin_path, "rb").read()
|
||
if len(data) <= 32:
|
||
return
|
||
if data[:3] == b"\xef\xbb\xbf":
|
||
data = data[3:]
|
||
text = data.decode("utf-8", "replace")
|
||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||
if not h:
|
||
return
|
||
g = int(h.group(1))
|
||
k = int(h.group(2))
|
||
if k == 0:
|
||
return
|
||
best = None
|
||
if elem_uuid:
|
||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||
f1 = int(m.group(1))
|
||
if best is None or f1 < best:
|
||
best = f1
|
||
blocked = False
|
||
code = ""
|
||
reason = ""
|
||
if g == 1:
|
||
blocked = True
|
||
code = "capability-off"
|
||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||
elif require == "removed":
|
||
if best is not None and best != 2:
|
||
blocked = True
|
||
code = "not-removed"
|
||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||
else:
|
||
if best is not None and best == 0:
|
||
blocked = True
|
||
code = "locked"
|
||
reason = "объект на замке — редактирование сломает обновления"
|
||
if not blocked:
|
||
return
|
||
mode = _sg_get_edit_mode(cfg_dir)
|
||
if mode == "off":
|
||
return
|
||
if mode == "warn":
|
||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||
return
|
||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||
if code == "capability-off":
|
||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||
fix = (
|
||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||
)
|
||
elif code == "not-removed":
|
||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||
fix = (
|
||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||
)
|
||
else:
|
||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||
fix = (
|
||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||
)
|
||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||
sys.exit(1)
|
||
except SystemExit:
|
||
raise
|
||
except Exception:
|
||
return
|
||
|
||
|
||
def detect_format_version(d):
|
||
while d:
|
||
cfg_path = os.path.join(d, "Configuration.xml")
|
||
if os.path.isfile(cfg_path):
|
||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||
head = f.read(2000)
|
||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||
if m:
|
||
return m.group(1)
|
||
parent = os.path.dirname(d)
|
||
if parent == d:
|
||
break
|
||
d = parent
|
||
return "2.17"
|
||
|
||
|
||
def format_rank(ver):
|
||
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||
|
||
|
||
def detect_eol(text):
|
||
# Перевод строки ВСТАВКИ берём из самого файла: канон CRLF относится к файлам,
|
||
# которые мы создаём, а правка существующего сохраняет его стиль (#44/#46/#47).
|
||
# Семантика та же, что у _detect_xml_style в остальных портах: есть CRLF → CRLF.
|
||
# Мажоритарное правило здесь было расхождением — на смешанном входе оно давало
|
||
# другой ответ, чем канон, при том же назначении.
|
||
return '\r\n' if '\r\n' in text else '\n'
|
||
|
||
def esc_xml(s):
|
||
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
|
||
(92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно."""
|
||
return s.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)
|
||
|
||
|
||
# --- Russian synonyms -> canonical English names ---
|
||
|
||
TYPE_ALIASES = {
|
||
"Справочник": "Catalog",
|
||
"Документ": "Document",
|
||
"РегистрСведений": "InformationRegister",
|
||
"РегистрНакопления": "AccumulationRegister",
|
||
"РегистрБухгалтерии": "AccountingRegister",
|
||
"РегистрРасчета": "CalculationRegister",
|
||
"Константа": "Constant",
|
||
"ПланСчетов": "ChartOfAccounts",
|
||
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
|
||
"ПланВидовРасчета": "ChartOfCalculationTypes",
|
||
"ПланОбмена": "ExchangePlan",
|
||
"БизнесПроцесс": "BusinessProcess",
|
||
"Задача": "Task",
|
||
"Обработка": "DataProcessor",
|
||
"Отчет": "Report",
|
||
"ОбщаяФорма": "CommonForm",
|
||
"ОбщаяКоманда": "CommonCommand",
|
||
"Подсистема": "Subsystem",
|
||
"КритерийОтбора": "FilterCriterion",
|
||
"ЖурналДокументов": "DocumentJournal",
|
||
"Последовательность": "Sequence",
|
||
"ВебСервис": "WebService",
|
||
"HTTPСервис": "HTTPService",
|
||
"СервисИнтеграции": "IntegrationService",
|
||
"ПараметрСеанса": "SessionParameter",
|
||
"ОбщийРеквизит": "CommonAttribute",
|
||
"Конфигурация": "Configuration",
|
||
"Перечисление": "Enum",
|
||
# Nested
|
||
"Реквизит": "Attribute",
|
||
"СтандартныйРеквизит": "StandardAttribute",
|
||
"ТабличнаяЧасть": "TabularSection",
|
||
"Измерение": "Dimension",
|
||
"Ресурс": "Resource",
|
||
"Команда": "Command",
|
||
"РеквизитАдресации": "AddressingAttribute",
|
||
}
|
||
|
||
RIGHT_ALIASES = {
|
||
"Чтение": "Read",
|
||
"Добавление": "Insert",
|
||
"Изменение": "Update",
|
||
"Удаление": "Delete",
|
||
"Просмотр": "View",
|
||
"Редактирование": "Edit",
|
||
"ВводПоСтроке": "InputByString",
|
||
"Проведение": "Posting",
|
||
"ОтменаПроведения": "UndoPosting",
|
||
"ИнтерактивноеДобавление": "InteractiveInsert",
|
||
"ИнтерактивнаяПометкаУдаления": "InteractiveSetDeletionMark",
|
||
"ИнтерактивноеСнятиеПометкиУдаления": "InteractiveClearDeletionMark",
|
||
"ИнтерактивноеУдаление": "InteractiveDelete",
|
||
"ИнтерактивноеУдалениеПомеченных": "InteractiveDeleteMarked",
|
||
"ИнтерактивноеПроведение": "InteractivePosting",
|
||
"ИнтерактивноеПроведениеНеоперативное": "InteractivePostingRegular",
|
||
"ИнтерактивнаяОтменаПроведения": "InteractiveUndoPosting",
|
||
"ИнтерактивноеИзменениеПроведенных": "InteractiveChangeOfPosted",
|
||
"Использование": "Use",
|
||
"Получение": "Get",
|
||
"Установка": "Set",
|
||
"Старт": "Start",
|
||
"ИнтерактивныйСтарт": "InteractiveStart",
|
||
"ИнтерактивнаяАктивация": "InteractiveActivate",
|
||
"Выполнение": "Execute",
|
||
"ИнтерактивноеВыполнение": "InteractiveExecute",
|
||
"УправлениеИтогами": "TotalsControl",
|
||
"Администрирование": "Administration",
|
||
"АдминистрированиеДанных": "DataAdministration",
|
||
"ТонкийКлиент": "ThinClient",
|
||
"ВебКлиент": "WebClient",
|
||
"ТолстыйКлиент": "ThickClient",
|
||
"ВнешнееСоединение": "ExternalConnection",
|
||
"Вывод": "Output",
|
||
"СохранениеДанныхПользователя": "SaveUserData",
|
||
"МобильныйКлиент": "MobileClient",
|
||
}
|
||
|
||
# --- Known rights per object type ---
|
||
|
||
KNOWN_RIGHTS = {
|
||
"Configuration": [
|
||
"Administration", "DataAdministration", "UpdateDataBaseConfiguration",
|
||
"ConfigurationExtensionsAdministration", "ActiveUsers", "EventLog", "ExclusiveMode",
|
||
"ThinClient", "ThickClient", "WebClient", "MobileClient", "ExternalConnection",
|
||
"Automation", "Output", "SaveUserData", "TechnicalSpecialistMode",
|
||
"InteractiveOpenExtDataProcessors", "InteractiveOpenExtReports",
|
||
"AnalyticsSystemClient", "CollaborationSystemInfoBaseRegistration",
|
||
"MainWindowModeNormal", "MainWindowModeWorkplace",
|
||
"MainWindowModeEmbeddedWorkplace", "MainWindowModeFullscreenWorkplace", "MainWindowModeKiosk",
|
||
],
|
||
"Catalog": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete", "InteractiveDeleteMarked",
|
||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
||
],
|
||
"Document": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"Posting", "UndoPosting",
|
||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete", "InteractiveDeleteMarked",
|
||
"InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting",
|
||
"InteractiveChangeOfPosted",
|
||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
||
],
|
||
"InformationRegister": [
|
||
"Read", "Update", "View", "Edit", "TotalsControl",
|
||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
||
],
|
||
"AccumulationRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
|
||
"AccountingRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
|
||
"CalculationRegister": ["Read", "View"],
|
||
"Constant": [
|
||
"Read", "Update", "View", "Edit",
|
||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
||
],
|
||
"ChartOfAccounts": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete",
|
||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
||
"ReadDataHistory", "ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistory", "UpdateDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
],
|
||
"ChartOfCharacteristicTypes": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete", "InteractiveDeleteMarked",
|
||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
||
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
||
],
|
||
"ChartOfCalculationTypes": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete",
|
||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
||
],
|
||
"ExchangePlan": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete", "InteractiveDeleteMarked",
|
||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
||
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
||
],
|
||
"BusinessProcess": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete", "InteractiveActivate", "InteractiveStart",
|
||
],
|
||
"Task": [
|
||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
||
"Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
||
"InteractiveDelete", "InteractiveActivate", "InteractiveExecute",
|
||
],
|
||
"DataProcessor": ["Use", "View"],
|
||
"Report": ["Use", "View"],
|
||
"CommonForm": ["View"],
|
||
"CommonCommand": ["View"],
|
||
"Subsystem": ["View"],
|
||
"FilterCriterion": ["View"],
|
||
"DocumentJournal": ["Read", "View"],
|
||
"Sequence": ["Read", "Update"],
|
||
"WebService": ["Use"],
|
||
"HTTPService": ["Use"],
|
||
"IntegrationService": ["Use"],
|
||
"SessionParameter": ["Get", "Set"],
|
||
"CommonAttribute": ["View", "Edit"],
|
||
}
|
||
|
||
NESTED_RIGHTS = ["View", "Edit"]
|
||
COMMAND_RIGHTS = ["View"]
|
||
|
||
# --- Presets ---
|
||
|
||
PRESETS = {
|
||
"view": {
|
||
"Catalog": ["Read", "View", "InputByString"],
|
||
"ExchangePlan": ["Read", "View", "InputByString"],
|
||
"Document": ["Read", "View", "InputByString"],
|
||
"ChartOfAccounts": ["Read", "View", "InputByString"],
|
||
"ChartOfCharacteristicTypes": ["Read", "View", "InputByString"],
|
||
"ChartOfCalculationTypes": ["Read", "View", "InputByString"],
|
||
"BusinessProcess": ["Read", "View", "InputByString"],
|
||
"Task": ["Read", "View", "InputByString"],
|
||
"InformationRegister": ["Read", "View"],
|
||
"AccumulationRegister": ["Read", "View"],
|
||
"AccountingRegister": ["Read", "View"],
|
||
"CalculationRegister": ["Read", "View"],
|
||
"Constant": ["Read", "View"],
|
||
"DocumentJournal": ["Read", "View"],
|
||
"Sequence": ["Read"],
|
||
"CommonForm": ["View"],
|
||
"CommonCommand": ["View"],
|
||
"Subsystem": ["View"],
|
||
"FilterCriterion": ["View"],
|
||
"SessionParameter": ["Get"],
|
||
"CommonAttribute": ["View"],
|
||
"DataProcessor": ["Use", "View"],
|
||
"Report": ["Use", "View"],
|
||
"Configuration": ["ThinClient", "WebClient", "Output", "SaveUserData", "MainWindowModeNormal"],
|
||
},
|
||
"edit": {
|
||
"Catalog": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
||
"ExchangePlan": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
||
"Document": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Posting", "UndoPosting", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting", "InteractiveChangeOfPosted"],
|
||
"ChartOfAccounts": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
||
"ChartOfCharacteristicTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
||
"ChartOfCalculationTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
||
"BusinessProcess": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveStart"],
|
||
"Task": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveExecute"],
|
||
"InformationRegister": ["Read", "Update", "View", "Edit"],
|
||
"AccumulationRegister": ["Read", "Update", "View", "Edit"],
|
||
"AccountingRegister": ["Read", "Update", "View", "Edit"],
|
||
"Constant": ["Read", "Update", "View", "Edit"],
|
||
"DocumentJournal": ["Read", "View"],
|
||
"Sequence": ["Read", "Update"],
|
||
"SessionParameter": ["Get", "Set"],
|
||
"CommonAttribute": ["View", "Edit"],
|
||
},
|
||
}
|
||
|
||
|
||
def translate_object_name(name):
|
||
parts = name.split('.')
|
||
result = []
|
||
for p in parts:
|
||
result.append(TYPE_ALIASES.get(p, p))
|
||
return '.'.join(result)
|
||
|
||
|
||
def translate_right_name(name):
|
||
return RIGHT_ALIASES.get(name, name)
|
||
|
||
|
||
def get_object_type(object_name):
|
||
dot_idx = object_name.find('.')
|
||
if dot_idx < 0:
|
||
return object_name
|
||
return object_name[:dot_idx]
|
||
|
||
|
||
def is_nested_object(object_name):
|
||
return len(object_name.split('.')) >= 3
|
||
|
||
|
||
def resolve_preset(object_type, preset_name):
|
||
preset = preset_name.lstrip('@')
|
||
if preset not in PRESETS:
|
||
print(f"WARNING: Unknown preset '@{preset}'. Known: @view, @edit", file=sys.stderr)
|
||
return []
|
||
type_map = PRESETS[preset]
|
||
if object_type not in type_map:
|
||
available = []
|
||
for k in PRESETS:
|
||
if object_type in PRESETS[k]:
|
||
available.append(f'@{k}')
|
||
avail_str = ', '.join(available) if available else 'none'
|
||
print(f"WARNING: Preset '@{preset}' not defined for type '{object_type}'. Available: {avail_str}", file=sys.stderr)
|
||
return []
|
||
return list(type_map[object_type])
|
||
|
||
|
||
def validate_right_name(object_name, right_name):
|
||
object_type = get_object_type(object_name)
|
||
|
||
if is_nested_object(object_name):
|
||
if '.Command.' in object_name:
|
||
if right_name not in COMMAND_RIGHTS:
|
||
print(f"WARNING: {object_name}: '{right_name}' not valid for commands (only: View)", file=sys.stderr)
|
||
return False
|
||
else:
|
||
if right_name not in NESTED_RIGHTS:
|
||
print(f"WARNING: {object_name}: '{right_name}' not valid for nested objects (only: View, Edit)", file=sys.stderr)
|
||
return False
|
||
return True
|
||
|
||
if object_type not in KNOWN_RIGHTS:
|
||
print(f"WARNING: {object_name}: unknown object type '{object_type}'", file=sys.stderr)
|
||
return True
|
||
|
||
valid_rights = KNOWN_RIGHTS[object_type]
|
||
if right_name not in valid_rights:
|
||
suggestions = [r for r in valid_rights if right_name in r or r in right_name]
|
||
sug_str = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
|
||
print(f"WARNING: {object_name}: unknown right '{right_name}'.{sug_str}", file=sys.stderr)
|
||
return False
|
||
|
||
return True
|
||
|
||
|
||
def parse_object_entry(entry):
|
||
# --- String shorthand ---
|
||
if isinstance(entry, str):
|
||
colon_idx = entry.find(':')
|
||
if colon_idx < 0:
|
||
print(f"WARNING: Invalid string '{entry}' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'", file=sys.stderr)
|
||
return None
|
||
obj_name = translate_object_name(entry[:colon_idx].strip())
|
||
rights_str = entry[colon_idx + 1:].strip()
|
||
object_type = get_object_type(obj_name)
|
||
|
||
if rights_str.startswith('@'):
|
||
right_names = resolve_preset(object_type, rights_str)
|
||
else:
|
||
right_names = [translate_right_name(r.strip()) for r in rights_str.split(',') if r.strip()]
|
||
for r in right_names:
|
||
validate_right_name(obj_name, r)
|
||
|
||
rights = []
|
||
for r in right_names:
|
||
rights.append({'Name': r, 'Value': 'true', 'Condition': None})
|
||
return {'Name': obj_name, 'Rights': rights}
|
||
|
||
# --- Object form ---
|
||
obj_name = translate_object_name(str(entry.get('name', '')))
|
||
if not obj_name:
|
||
print("WARNING: Object entry missing 'name' field", file=sys.stderr)
|
||
return None
|
||
|
||
object_type = get_object_type(obj_name)
|
||
# Use a list of tuples to preserve insertion order
|
||
rights_map = {} # name -> {Value, Condition}
|
||
rights_order = [] # preserve order
|
||
|
||
# 1) Start with preset
|
||
if entry.get('preset'):
|
||
preset_rights = resolve_preset(object_type, str(entry['preset']))
|
||
for r in preset_rights:
|
||
if r not in rights_map:
|
||
rights_order.append(r)
|
||
rights_map[r] = {'Value': 'true', 'Condition': None}
|
||
|
||
# 2) Apply explicit rights
|
||
if entry.get('rights') is not None:
|
||
if isinstance(entry['rights'], list):
|
||
for r in entry['rights']:
|
||
r_name = translate_right_name(str(r))
|
||
validate_right_name(obj_name, r_name)
|
||
if r_name not in rights_map:
|
||
rights_order.append(r_name)
|
||
rights_map[r_name] = {'Value': 'true', 'Condition': None}
|
||
elif isinstance(entry['rights'], dict):
|
||
for p_name, p_value in entry['rights'].items():
|
||
r_name = translate_right_name(p_name)
|
||
validate_right_name(obj_name, r_name)
|
||
bool_val = 'true' if p_value is True or str(p_value) == 'True' else 'false'
|
||
if r_name not in rights_map:
|
||
rights_order.append(r_name)
|
||
rights_map[r_name] = {'Value': bool_val, 'Condition': None}
|
||
|
||
# 3) Apply RLS conditions
|
||
if entry.get('rls'):
|
||
for p_name, p_value in entry['rls'].items():
|
||
rls_right = translate_right_name(p_name)
|
||
if rls_right in rights_map:
|
||
rights_map[rls_right]['Condition'] = str(p_value)
|
||
else:
|
||
print(f"WARNING: {obj_name}: RLS for '{rls_right}' but this right is not in the rights list", file=sys.stderr)
|
||
|
||
# Convert to array
|
||
rights = []
|
||
for k in rights_order:
|
||
rights.append({
|
||
'Name': k,
|
||
'Value': rights_map[k]['Value'],
|
||
'Condition': rights_map[k]['Condition'],
|
||
})
|
||
|
||
return {'Name': obj_name, 'Rights': rights}
|
||
|
||
|
||
def main():
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
sys.stderr.reconfigure(encoding="utf-8")
|
||
parser = argparse.ArgumentParser(description='Compile 1C role from JSON', allow_abbrev=False)
|
||
parser.add_argument('-JsonPath', type=str, required=True)
|
||
parser.add_argument('-OutputDir', type=str, required=True)
|
||
args = parser.parse_args()
|
||
|
||
# --- 1. Load and validate JSON ---
|
||
json_path = args.JsonPath
|
||
if not os.path.exists(json_path):
|
||
print(f"File not found: {json_path}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
||
defn = json.load(f)
|
||
|
||
if not defn.get('name'):
|
||
print("JSON must have 'name' field (role programmatic name)", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
role_name = str(defn['name'])
|
||
synonym = str(defn['synonym']) if defn.get('synonym') else role_name
|
||
comment = str(defn['comment']) if defn.get('comment') else ''
|
||
|
||
# Synonym: accept "rights" as alias for "objects"
|
||
if not defn.get('objects') and defn.get('rights'):
|
||
defn['objects'] = defn['rights']
|
||
|
||
out_dir_resolved = args.OutputDir if os.path.isabs(args.OutputDir) else os.path.join(os.getcwd(), args.OutputDir)
|
||
assert_edit_allowed(out_dir_resolved, "editable")
|
||
format_version = detect_format_version(out_dir_resolved)
|
||
|
||
# --- 2. Parse all object entries ---
|
||
parsed_objects = []
|
||
if defn.get('objects'):
|
||
for entry in defn['objects']:
|
||
parsed = parse_object_entry(entry)
|
||
if parsed:
|
||
parsed_objects.append(parsed)
|
||
|
||
# --- 3. Generate UUID ---
|
||
uid = new_uuid()
|
||
|
||
# --- 4. Emit metadata XML (Roles/Name.xml) ---
|
||
lines = []
|
||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
||
# Объявления пространств имён — одной переменной и одной строкой, как пишет платформа.
|
||
xmlns_decl = ('xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
||
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
|
||
# style): платформа держит объявления по алфавиту. В Rights.xml палитра НЕ идёт.
|
||
if format_rank(format_version) >= 221:
|
||
xmlns_decl = xmlns_decl.replace(
|
||
' xmlns:style=',
|
||
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||
lines.append(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
|
||
lines.append(f'\t<Role uuid="{uid}">')
|
||
lines.append('\t\t<Properties>')
|
||
lines.append(f'\t\t\t<Name>{role_name}</Name>')
|
||
lines.append('\t\t\t<Synonym>')
|
||
lines.append('\t\t\t\t<v8:item>')
|
||
lines.append('\t\t\t\t\t<v8:lang>ru</v8:lang>')
|
||
lines.append(f'\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>')
|
||
lines.append('\t\t\t\t</v8:item>')
|
||
lines.append('\t\t\t</Synonym>')
|
||
if comment:
|
||
lines.append(f'\t\t\t<Comment>{esc_xml(comment)}</Comment>')
|
||
else:
|
||
lines.append('\t\t\t<Comment/>')
|
||
lines.append('\t\t</Properties>')
|
||
lines.append('\t</Role>')
|
||
lines.append('</MetaDataObject>')
|
||
|
||
metadata_xml = '\r\n'.join(lines)
|
||
|
||
# --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
|
||
lines = []
|
||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
||
lines.append('<Rights xmlns="http://v8.1c.ru/8.2/roles" xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Rights"'
|
||
f' version="{format_version}">')
|
||
|
||
# Global flags
|
||
sfno = str(defn['setForNewObjects']).lower() if defn.get('setForNewObjects') is not None else 'false'
|
||
sfab = str(defn['setForAttributesByDefault']).lower() if defn.get('setForAttributesByDefault') is not None else 'true'
|
||
irco = str(defn['independentRightsOfChildObjects']).lower() if defn.get('independentRightsOfChildObjects') is not None else 'false'
|
||
|
||
lines.append(f'\t<setForNewObjects>{sfno}</setForNewObjects>')
|
||
lines.append(f'\t<setForAttributesByDefault>{sfab}</setForAttributesByDefault>')
|
||
lines.append(f'\t<independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>')
|
||
|
||
# Object blocks
|
||
total_rights = 0
|
||
for obj in parsed_objects:
|
||
lines.append('\t<object>')
|
||
lines.append(f'\t\t<name>{obj["Name"]}</name>')
|
||
for right in obj['Rights']:
|
||
lines.append('\t\t<right>')
|
||
lines.append(f'\t\t\t<name>{right["Name"]}</name>')
|
||
lines.append(f'\t\t\t<value>{right["Value"]}</value>')
|
||
if right['Condition']:
|
||
lines.append('\t\t\t<restrictionByCondition>')
|
||
lines.append(f'\t\t\t\t<condition>{esc_xml(right["Condition"])}</condition>')
|
||
lines.append('\t\t\t</restrictionByCondition>')
|
||
lines.append('\t\t</right>')
|
||
total_rights += 1
|
||
lines.append('\t</object>')
|
||
|
||
# RLS restriction templates
|
||
template_count = 0
|
||
if defn.get('templates'):
|
||
for tpl in defn['templates']:
|
||
lines.append('\t<restrictionTemplate>')
|
||
lines.append(f'\t\t<name>{esc_xml(str(tpl["name"]))}</name>')
|
||
lines.append(f'\t\t<condition>{esc_xml(str(tpl["condition"]))}</condition>')
|
||
lines.append('\t</restrictionTemplate>')
|
||
template_count += 1
|
||
|
||
lines.append('</Rights>')
|
||
|
||
rights_xml = '\r\n'.join(lines)
|
||
|
||
# --- 6. Write output files ---
|
||
out_dir = args.OutputDir
|
||
if not os.path.isabs(out_dir):
|
||
out_dir = os.path.join(os.getcwd(), out_dir)
|
||
|
||
# Determine Roles dir and config root
|
||
# Back-compat: if OutputDir leaf is "Roles", use as-is; otherwise treat as config root
|
||
leaf = os.path.basename(out_dir.rstrip(os.sep).rstrip('/'))
|
||
if leaf == 'Roles':
|
||
roles_dir = out_dir
|
||
config_dir = os.path.dirname(out_dir)
|
||
else:
|
||
roles_dir = os.path.join(out_dir, 'Roles')
|
||
config_dir = out_dir
|
||
|
||
# Metadata: Roles/RoleName.xml
|
||
metadata_path = os.path.join(roles_dir, f'{role_name}.xml')
|
||
os.makedirs(roles_dir, exist_ok=True)
|
||
|
||
# Rights: Roles/RoleName/Ext/Rights.xml
|
||
role_sub_dir = os.path.join(roles_dir, role_name)
|
||
ext_dir = os.path.join(role_sub_dir, 'Ext')
|
||
rights_path = os.path.join(ext_dir, 'Rights.xml')
|
||
os.makedirs(ext_dir, exist_ok=True)
|
||
|
||
write_utf8_bom(metadata_path, metadata_xml)
|
||
write_utf8_bom(rights_path, rights_xml)
|
||
|
||
# --- 7. Register in Configuration.xml ---
|
||
config_xml_path = os.path.join(config_dir, 'Configuration.xml')
|
||
reg_result = None
|
||
|
||
if os.path.exists(config_xml_path):
|
||
# newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
|
||
# в LF при чтении и файл будет переписан в LF.
|
||
with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
|
||
raw_text = f.read()
|
||
|
||
eol = detect_eol(raw_text)
|
||
|
||
# Check if already registered
|
||
if f'<Role>{role_name}</Role>' in raw_text:
|
||
reg_result = 'already'
|
||
else:
|
||
# Find last <Role>...</Role> and insert after it
|
||
role_pattern = re.compile(r'(<Role>[^<]*</Role>)')
|
||
matches = list(role_pattern.finditer(raw_text))
|
||
new_role_tag = f'<Role>{role_name}</Role>'
|
||
|
||
if matches:
|
||
# Insert after last existing <Role>
|
||
last_match = matches[-1]
|
||
insert_pos = last_match.end()
|
||
raw_text = raw_text[:insert_pos] + eol + f'\t\t\t{new_role_tag}' + raw_text[insert_pos:]
|
||
else:
|
||
# No existing roles — insert before </ChildObjects>
|
||
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка
|
||
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
|
||
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
|
||
raw_text = re.sub(r'([ \t]*)</ChildObjects>',
|
||
lambda m: m.group(1) + '\t' + new_role_tag + eol + m.group(1) + '</ChildObjects>',
|
||
raw_text, count=1)
|
||
|
||
write_utf8_bom(config_xml_path, raw_text)
|
||
reg_result = 'added'
|
||
else:
|
||
reg_result = 'no-config'
|
||
|
||
# --- 8. Summary ---
|
||
print(f"[OK] Role '{role_name}' compiled")
|
||
print(f" UUID: {uid}")
|
||
print(f" Metadata: {metadata_path}")
|
||
print(f" Rights: {rights_path}")
|
||
print(f" Objects: {len(parsed_objects)}, Rights: {total_rights}, Templates: {template_count}")
|
||
if reg_result == 'added':
|
||
print(f" Configuration.xml: <Role>{role_name}</Role> added to ChildObjects")
|
||
elif reg_result == 'already':
|
||
print(f" Configuration.xml: <Role>{role_name}</Role> already registered")
|
||
elif reg_result == 'no-childobj':
|
||
print(f"WARNING: Configuration.xml found but <ChildObjects> not found", file=sys.stderr)
|
||
elif reg_result == 'no-config':
|
||
print(f"WARNING: Configuration.xml not found at {config_xml_path} -- register manually", file=sys.stderr)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|