mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-11 13:33:41 +03:00
Auto-build: opencode (powershell) from 78b5b73
This commit is contained in:
@@ -0,0 +1,867 @@
|
||||
# role-compile v1.7 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$JsonPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$OutputDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- 1. Load and validate JSON ---
|
||||
|
||||
if (-not (Test-Path $JsonPath)) {
|
||||
Write-Error "File not found: $JsonPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
||||
$def = $json | ConvertFrom-Json
|
||||
|
||||
if (-not $def.name) {
|
||||
Write-Error "JSON must have 'name' field (role programmatic name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$roleName = "$($def.name)"
|
||||
$synonym = if ($def.synonym) { "$($def.synonym)" } else { $roleName }
|
||||
$comment = if ($def.comment) { "$($def.comment)" } else { "" }
|
||||
|
||||
# --- 2. XML helpers ---
|
||||
|
||||
$script:xmlBuf = $null
|
||||
|
||||
function X {
|
||||
param([string]$text)
|
||||
$script:xmlBuf.AppendLine($text) | Out-Null
|
||||
}
|
||||
|
||||
function Esc-Xml {
|
||||
param([string]$s)
|
||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||
}
|
||||
|
||||
# --- 3. Russian synonyms → canonical English names ---
|
||||
|
||||
$script:typeAliases = @{
|
||||
"Справочник" = "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"
|
||||
}
|
||||
|
||||
$script:rightAliases = @{
|
||||
"Чтение" = "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"
|
||||
}
|
||||
|
||||
# Translate Russian object name to English (e.g. "Справочник.Контрагенты" → "Catalog.Контрагенты")
|
||||
function Translate-ObjectName {
|
||||
param([string]$name)
|
||||
$parts = $name.Split(".")
|
||||
$result = @()
|
||||
foreach ($p in $parts) {
|
||||
if ($script:typeAliases.ContainsKey($p)) {
|
||||
$result += $script:typeAliases[$p]
|
||||
} else {
|
||||
$result += $p
|
||||
}
|
||||
}
|
||||
return $result -join "."
|
||||
}
|
||||
|
||||
# Translate Russian right name to English (e.g. "Чтение" → "Read")
|
||||
function Translate-RightName {
|
||||
param([string]$name)
|
||||
if ($script:rightAliases.ContainsKey($name)) {
|
||||
return $script:rightAliases[$name]
|
||||
}
|
||||
return $name
|
||||
}
|
||||
|
||||
# --- 4. Known rights per object type (source: docs/1c-role-spec.md) ---
|
||||
|
||||
$script:knownRights = @{
|
||||
"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 objects: Attribute, StandardAttribute, TabularSection, Dimension, Resource, AddressingAttribute
|
||||
$script:nestedRights = @("View","Edit")
|
||||
$script:commandRights = @("View")
|
||||
|
||||
# --- 4. Presets (@view, @edit) ---
|
||||
|
||||
$script: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")
|
||||
}
|
||||
}
|
||||
|
||||
# --- 5. Helpers ---
|
||||
|
||||
function Get-ObjectType {
|
||||
param([string]$objectName)
|
||||
$dotIdx = $objectName.IndexOf(".")
|
||||
if ($dotIdx -lt 0) { return $objectName }
|
||||
return $objectName.Substring(0, $dotIdx)
|
||||
}
|
||||
|
||||
function Is-NestedObject {
|
||||
param([string]$objectName)
|
||||
return ($objectName.Split(".").Count -ge 3)
|
||||
}
|
||||
|
||||
function Resolve-Preset {
|
||||
param([string]$objectType, [string]$presetName)
|
||||
|
||||
$preset = $presetName.TrimStart('@')
|
||||
|
||||
if (-not $script:presets.ContainsKey($preset)) {
|
||||
Write-Warning "Unknown preset '@$preset'. Known: @view, @edit"
|
||||
return @()
|
||||
}
|
||||
|
||||
$typeMap = $script:presets[$preset]
|
||||
if (-not $typeMap.ContainsKey($objectType)) {
|
||||
$available = @()
|
||||
foreach ($k in $script:presets.Keys) {
|
||||
if ($script:presets[$k].ContainsKey($objectType)) {
|
||||
$available += "@$k"
|
||||
}
|
||||
}
|
||||
$availStr = if ($available.Count -gt 0) { $available -join ", " } else { "none" }
|
||||
Write-Warning "Preset '@$preset' not defined for type '$objectType'. Available: $availStr"
|
||||
return @()
|
||||
}
|
||||
|
||||
return @($typeMap[$objectType])
|
||||
}
|
||||
|
||||
function Validate-RightName {
|
||||
param([string]$objectName, [string]$rightName)
|
||||
|
||||
$objectType = Get-ObjectType $objectName
|
||||
|
||||
if (Is-NestedObject $objectName) {
|
||||
if ($objectName -match '\.Command\.') {
|
||||
if ($rightName -notin $script:commandRights) {
|
||||
Write-Warning "${objectName}: '$rightName' not valid for commands (only: View)"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
if ($rightName -notin $script:nestedRights) {
|
||||
Write-Warning "${objectName}: '$rightName' not valid for nested objects (only: View, Edit)"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not $script:knownRights.ContainsKey($objectType)) {
|
||||
Write-Warning "${objectName}: unknown object type '$objectType'"
|
||||
return $true
|
||||
}
|
||||
|
||||
$validRights = $script:knownRights[$objectType]
|
||||
if ($rightName -notin $validRights) {
|
||||
$suggestions = @($validRights | Where-Object {
|
||||
$_ -like "*$rightName*" -or $rightName -like "*$_*"
|
||||
})
|
||||
$sugStr = if ($suggestions.Count -gt 0) { " Did you mean: $($suggestions -join ', ')?" } else { "" }
|
||||
Write-Warning "${objectName}: unknown right '$rightName'.$sugStr"
|
||||
return $false
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
# --- 6. Parse object entries ---
|
||||
|
||||
function Parse-ObjectEntry {
|
||||
param($entry)
|
||||
|
||||
# --- String shorthand ---
|
||||
if ($entry -is [string]) {
|
||||
$colonIdx = $entry.IndexOf(':')
|
||||
if ($colonIdx -lt 0) {
|
||||
Write-Warning "Invalid string '$entry' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'"
|
||||
return $null
|
||||
}
|
||||
$objName = Translate-ObjectName ($entry.Substring(0, $colonIdx).Trim())
|
||||
$rightsStr = $entry.Substring($colonIdx + 1).Trim()
|
||||
$objectType = Get-ObjectType $objName
|
||||
|
||||
if ($rightsStr.StartsWith('@')) {
|
||||
$rightNames = @(Resolve-Preset -objectType $objectType -presetName $rightsStr)
|
||||
} else {
|
||||
$rightNames = @($rightsStr -split ',\s*' | ForEach-Object { Translate-RightName $_.Trim() } | Where-Object { $_ })
|
||||
foreach ($r in $rightNames) {
|
||||
Validate-RightName -objectName $objName -rightName $r | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
$rights = @()
|
||||
foreach ($r in $rightNames) {
|
||||
$rights += ,@{Name=$r; Value="true"; Condition=$null}
|
||||
}
|
||||
return @{ Name = $objName; Rights = $rights }
|
||||
}
|
||||
|
||||
# --- Object form ---
|
||||
$objName = Translate-ObjectName "$($entry.name)"
|
||||
if (-not $objName) {
|
||||
Write-Warning "Object entry missing 'name' field"
|
||||
return $null
|
||||
}
|
||||
|
||||
$objectType = Get-ObjectType $objName
|
||||
$rightsMap = [ordered]@{}
|
||||
|
||||
# 1) Start with preset
|
||||
if ($entry.preset) {
|
||||
$presetRights = @(Resolve-Preset -objectType $objectType -presetName "$($entry.preset)")
|
||||
foreach ($r in $presetRights) {
|
||||
$rightsMap[$r] = @{Value="true"; Condition=$null}
|
||||
}
|
||||
}
|
||||
|
||||
# 2) Apply explicit rights
|
||||
if ($entry.rights) {
|
||||
if ($entry.rights -is [array]) {
|
||||
foreach ($r in $entry.rights) {
|
||||
$rName = Translate-RightName "$r"
|
||||
Validate-RightName -objectName $objName -rightName $rName | Out-Null
|
||||
$rightsMap[$rName] = @{Value="true"; Condition=$null}
|
||||
}
|
||||
} else {
|
||||
foreach ($p in $entry.rights.PSObject.Properties) {
|
||||
$rName = Translate-RightName $p.Name
|
||||
Validate-RightName -objectName $objName -rightName $rName | Out-Null
|
||||
$boolVal = $p.Value
|
||||
if ($boolVal -eq $true -or "$boolVal" -eq "True") {
|
||||
$rightsMap[$rName] = @{Value="true"; Condition=$null}
|
||||
} else {
|
||||
$rightsMap[$rName] = @{Value="false"; Condition=$null}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 3) Apply RLS conditions
|
||||
if ($entry.rls) {
|
||||
foreach ($p in $entry.rls.PSObject.Properties) {
|
||||
$rlsRight = Translate-RightName $p.Name
|
||||
if ($rightsMap.Contains($rlsRight)) {
|
||||
$rightsMap[$rlsRight].Condition = "$($p.Value)"
|
||||
} else {
|
||||
Write-Warning "${objName}: RLS for '$rlsRight' but this right is not in the rights list"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Convert to array
|
||||
$rights = @()
|
||||
foreach ($k in $rightsMap.Keys) {
|
||||
$rights += ,@{
|
||||
Name = $k
|
||||
Value = $rightsMap[$k].Value
|
||||
Condition = $rightsMap[$k].Condition
|
||||
}
|
||||
}
|
||||
|
||||
return @{ Name = $objName; Rights = $rights }
|
||||
}
|
||||
|
||||
# --- 7. Parse all object entries ---
|
||||
|
||||
# Synonym: accept "rights" as alias for "objects"
|
||||
if (-not $def.objects -and $def.rights) { $def | Add-Member -NotePropertyName objects -NotePropertyValue $def.rights }
|
||||
|
||||
$parsedObjects = @()
|
||||
if ($def.objects) {
|
||||
foreach ($entry in $def.objects) {
|
||||
$parsed = Parse-ObjectEntry -entry $entry
|
||||
if ($parsed) {
|
||||
$parsedObjects += ,$parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Detect format version ---
|
||||
|
||||
function Detect-FormatVersion([string]$dir) {
|
||||
$d = $dir
|
||||
while ($d) {
|
||||
$cfgPath = Join-Path $d "Configuration.xml"
|
||||
if (Test-Path $cfgPath) {
|
||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||
}
|
||||
$parent = Split-Path $d -Parent
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return "2.17"
|
||||
}
|
||||
|
||||
$resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir }
|
||||
Assert-EditAllowed $resolvedOutputDir 'editable'
|
||||
$formatVersion = Detect-FormatVersion $resolvedOutputDir
|
||||
|
||||
# --- 8. Generate UUID ---
|
||||
|
||||
$uuid = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- 9. Emit metadata XML (Roles/Name.xml) ---
|
||||
|
||||
$script:xmlBuf = New-Object System.Text.StringBuilder 4096
|
||||
|
||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
X '<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||
X ' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||
X ' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||
X ' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||
X ' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||
X ' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||
X ' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||
X ' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||
X ' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||
X ' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||
X ' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||
X ' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||
X ' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||
X ' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||
X ' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
X " version=`"$formatVersion`">"
|
||||
X " <Role uuid=`"$uuid`">"
|
||||
X ' <Properties>'
|
||||
X " <Name>$roleName</Name>"
|
||||
X ' <Synonym>'
|
||||
X ' <v8:item>'
|
||||
X ' <v8:lang>ru</v8:lang>'
|
||||
X " <v8:content>$(Esc-Xml $synonym)</v8:content>"
|
||||
X ' </v8:item>'
|
||||
X ' </Synonym>'
|
||||
if ($comment) {
|
||||
X " <Comment>$(Esc-Xml $comment)</Comment>"
|
||||
} else {
|
||||
X ' <Comment/>'
|
||||
}
|
||||
X ' </Properties>'
|
||||
X ' </Role>'
|
||||
X '</MetaDataObject>'
|
||||
|
||||
$metadataXml = $script:xmlBuf.ToString()
|
||||
|
||||
# --- 10. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
|
||||
|
||||
$script:xmlBuf = New-Object System.Text.StringBuilder 8192
|
||||
|
||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
X '<Rights xmlns="http://v8.1c.ru/8.2/roles"'
|
||||
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
X " xsi:type=`"Rights`" version=`"$formatVersion`">"
|
||||
|
||||
# Global flags (defaults match typical 1C roles)
|
||||
$sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" }
|
||||
$sfab = if ($null -ne $def.setForAttributesByDefault) { "$($def.setForAttributesByDefault)".ToLower() } else { "true" }
|
||||
$irco = if ($null -ne $def.independentRightsOfChildObjects) { "$($def.independentRightsOfChildObjects)".ToLower() } else { "false" }
|
||||
|
||||
X " <setForNewObjects>$sfno</setForNewObjects>"
|
||||
X " <setForAttributesByDefault>$sfab</setForAttributesByDefault>"
|
||||
X " <independentRightsOfChildObjects>$irco</independentRightsOfChildObjects>"
|
||||
|
||||
# Object blocks
|
||||
$totalRights = 0
|
||||
foreach ($obj in $parsedObjects) {
|
||||
X ' <object>'
|
||||
X " <name>$($obj.Name)</name>"
|
||||
foreach ($right in $obj.Rights) {
|
||||
X ' <right>'
|
||||
X " <name>$($right.Name)</name>"
|
||||
X " <value>$($right.Value)</value>"
|
||||
if ($right.Condition) {
|
||||
X ' <restrictionByCondition>'
|
||||
X " <condition>$(Esc-Xml $right.Condition)</condition>"
|
||||
X ' </restrictionByCondition>'
|
||||
}
|
||||
X ' </right>'
|
||||
$totalRights++
|
||||
}
|
||||
X ' </object>'
|
||||
}
|
||||
|
||||
# RLS restriction templates
|
||||
$templateCount = 0
|
||||
if ($def.templates) {
|
||||
foreach ($tpl in $def.templates) {
|
||||
X ' <restrictionTemplate>'
|
||||
X " <name>$(Esc-Xml "$($tpl.name)")</name>"
|
||||
X " <condition>$(Esc-Xml "$($tpl.condition)")</condition>"
|
||||
X ' </restrictionTemplate>'
|
||||
$templateCount++
|
||||
}
|
||||
}
|
||||
|
||||
X '</Rights>'
|
||||
|
||||
$rightsXml = $script:xmlBuf.ToString()
|
||||
|
||||
# --- 11. Write output files ---
|
||||
|
||||
$outDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir
|
||||
} else {
|
||||
Join-Path (Get-Location) $OutputDir
|
||||
}
|
||||
|
||||
# Determine Roles dir and config root
|
||||
# Back-compat: if OutputDir leaf is "Roles", use as-is; otherwise treat as config root
|
||||
$leaf = Split-Path $outDir -Leaf
|
||||
if ($leaf -eq "Roles") {
|
||||
$rolesDir = $outDir
|
||||
$configDir = Split-Path $outDir -Parent
|
||||
} else {
|
||||
$rolesDir = Join-Path $outDir "Roles"
|
||||
$configDir = $outDir
|
||||
}
|
||||
|
||||
# Metadata: Roles/RoleName.xml
|
||||
$metadataPath = Join-Path $rolesDir "$roleName.xml"
|
||||
if (-not (Test-Path $rolesDir)) {
|
||||
New-Item -ItemType Directory -Path $rolesDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Rights: Roles/RoleName/Ext/Rights.xml
|
||||
$roleSubDir = Join-Path $rolesDir $roleName
|
||||
$extDir = Join-Path $roleSubDir "Ext"
|
||||
$rightsPath = Join-Path $extDir "Rights.xml"
|
||||
if (-not (Test-Path $extDir)) {
|
||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($metadataPath, $metadataXml, $enc)
|
||||
[System.IO.File]::WriteAllText($rightsPath, $rightsXml, $enc)
|
||||
|
||||
# --- 12. Register in Configuration.xml ---
|
||||
|
||||
$configXmlPath = Join-Path $configDir "Configuration.xml"
|
||||
$regResult = $null
|
||||
|
||||
if (Test-Path $configXmlPath) {
|
||||
$configDoc = New-Object System.Xml.XmlDocument
|
||||
$configDoc.PreserveWhitespace = $true
|
||||
$configDoc.Load($configXmlPath)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
|
||||
if ($childObjects) {
|
||||
$existing = $childObjects.SelectNodes("md:Role", $nsMgr)
|
||||
$alreadyExists = $false
|
||||
foreach ($r in $existing) {
|
||||
if ($r.InnerText -eq $roleName) {
|
||||
$alreadyExists = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($alreadyExists) {
|
||||
$regResult = "already"
|
||||
} else {
|
||||
$roleElem = $configDoc.CreateElement("Role", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$roleElem.InnerText = $roleName
|
||||
|
||||
if ($existing.Count -gt 0) {
|
||||
# Insert after last existing <Role>
|
||||
$lastRole = $existing[$existing.Count - 1]
|
||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertAfter($newWs, $lastRole) | Out-Null
|
||||
$childObjects.InsertAfter($roleElem, $newWs) | Out-Null
|
||||
} else {
|
||||
# No existing roles — insert before closing whitespace
|
||||
$lastChild = $childObjects.LastChild
|
||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
|
||||
$childObjects.InsertBefore($roleElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($roleElem) | Out-Null
|
||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Save
|
||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgSettings.Indent = $false
|
||||
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create)
|
||||
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings)
|
||||
$configDoc.Save($writer)
|
||||
$writer.Close()
|
||||
$stream.Close()
|
||||
|
||||
$regResult = "added"
|
||||
}
|
||||
} else {
|
||||
$regResult = "no-childobj"
|
||||
}
|
||||
} else {
|
||||
$regResult = "no-config"
|
||||
}
|
||||
|
||||
# --- 13. Summary ---
|
||||
|
||||
Write-Host "[OK] Role '$roleName' compiled"
|
||||
Write-Host " UUID: $uuid"
|
||||
Write-Host " Metadata: $metadataPath"
|
||||
Write-Host " Rights: $rightsPath"
|
||||
Write-Host " Objects: $($parsedObjects.Count), Rights: $totalRights, Templates: $templateCount"
|
||||
switch ($regResult) {
|
||||
"added" { Write-Host " Configuration.xml: <Role>$roleName</Role> added to ChildObjects" }
|
||||
"already" { Write-Host " Configuration.xml: <Role>$roleName</Role> already registered" }
|
||||
"no-childobj" { Write-Warning "Configuration.xml found but <ChildObjects> not found" }
|
||||
"no-config" { Write-Warning "Configuration.xml not found at $configXmlPath — register manually" }
|
||||
}
|
||||
@@ -0,0 +1,818 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-compile v1.7 — 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_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)
|
||||
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 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 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)
|
||||
|
||||
|
||||
# --- 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"?>')
|
||||
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"')
|
||||
lines.append(' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"')
|
||||
lines.append(' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"')
|
||||
lines.append(' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"')
|
||||
lines.append(' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"')
|
||||
lines.append(' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"')
|
||||
lines.append(' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"')
|
||||
lines.append(' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"')
|
||||
lines.append(' xmlns:v8="http://v8.1c.ru/8.1/data/core"')
|
||||
lines.append(' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"')
|
||||
lines.append(' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"')
|
||||
lines.append(' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"')
|
||||
lines.append(' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"')
|
||||
lines.append(' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"')
|
||||
lines.append(' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"')
|
||||
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
||||
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
||||
lines.append(f' version="{format_version}">')
|
||||
lines.append(f' <Role uuid="{uid}">')
|
||||
lines.append(' <Properties>')
|
||||
lines.append(f' <Name>{role_name}</Name>')
|
||||
lines.append(' <Synonym>')
|
||||
lines.append(' <v8:item>')
|
||||
lines.append(' <v8:lang>ru</v8:lang>')
|
||||
lines.append(f' <v8:content>{esc_xml(synonym)}</v8:content>')
|
||||
lines.append(' </v8:item>')
|
||||
lines.append(' </Synonym>')
|
||||
if comment:
|
||||
lines.append(f' <Comment>{esc_xml(comment)}</Comment>')
|
||||
else:
|
||||
lines.append(' <Comment/>')
|
||||
lines.append(' </Properties>')
|
||||
lines.append(' </Role>')
|
||||
lines.append('</MetaDataObject>')
|
||||
|
||||
metadata_xml = '\n'.join(lines) + '\n'
|
||||
|
||||
# --- 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"')
|
||||
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
||||
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
||||
lines.append(f' xsi:type="Rights" 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' <setForNewObjects>{sfno}</setForNewObjects>')
|
||||
lines.append(f' <setForAttributesByDefault>{sfab}</setForAttributesByDefault>')
|
||||
lines.append(f' <independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>')
|
||||
|
||||
# Object blocks
|
||||
total_rights = 0
|
||||
for obj in parsed_objects:
|
||||
lines.append(' <object>')
|
||||
lines.append(f' <name>{obj["Name"]}</name>')
|
||||
for right in obj['Rights']:
|
||||
lines.append(' <right>')
|
||||
lines.append(f' <name>{right["Name"]}</name>')
|
||||
lines.append(f' <value>{right["Value"]}</value>')
|
||||
if right['Condition']:
|
||||
lines.append(' <restrictionByCondition>')
|
||||
lines.append(f' <condition>{esc_xml(right["Condition"])}</condition>')
|
||||
lines.append(' </restrictionByCondition>')
|
||||
lines.append(' </right>')
|
||||
total_rights += 1
|
||||
lines.append(' </object>')
|
||||
|
||||
# RLS restriction templates
|
||||
template_count = 0
|
||||
if defn.get('templates'):
|
||||
for tpl in defn['templates']:
|
||||
lines.append(' <restrictionTemplate>')
|
||||
lines.append(f' <name>{esc_xml(str(tpl["name"]))}</name>')
|
||||
lines.append(f' <condition>{esc_xml(str(tpl["condition"]))}</condition>')
|
||||
lines.append(' </restrictionTemplate>')
|
||||
template_count += 1
|
||||
|
||||
lines.append('</Rights>')
|
||||
|
||||
rights_xml = '\n'.join(lines) + '\n'
|
||||
|
||||
# --- 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):
|
||||
with open(config_xml_path, 'r', encoding='utf-8-sig') as f:
|
||||
raw_text = f.read()
|
||||
|
||||
# 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] + f'\n\t\t\t{new_role_tag}' + raw_text[insert_pos:]
|
||||
else:
|
||||
# No existing roles — insert before </ChildObjects>
|
||||
raw_text = raw_text.replace('</ChildObjects>', f'\t\t\t{new_role_tag}\n\t\t</ChildObjects>')
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user