mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-07 18:50:53 +03:00
Compare commits
125
Commits
@@ -34,6 +34,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -Confi
|
||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||
| `sort-childObjects` | вид, напр. `Catalog` (batch `;;`), либо пусто | Упорядочить ChildObjects по имени внутри вида. Без значения — все виды, кроме четырёх (см. reference) |
|
||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||
|
||||
@@ -39,6 +39,20 @@
|
||||
|
||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||
|
||||
## sort-childObjects
|
||||
|
||||
Упорядочивает объекты в `<ChildObjects>` по имени **внутри вида**. Значение — имя вида (`Catalog`, `Role`, …), batch через `;;`. Без значения обрабатываются все виды, какие есть в файле.
|
||||
|
||||
```
|
||||
-Operation sort-childObjects — все виды, кроме перечисленных ниже
|
||||
-Operation sort-childObjects -Value "Catalog" — только справочники
|
||||
-Operation sort-childObjects -Value "Catalog ;; Role"
|
||||
```
|
||||
|
||||
Не сортируются, пока вид не назван явно: `CommonAttribute`, `Subsystem`, `CommandGroup`, `Language`.
|
||||
|
||||
Вызов без значения дополнительно ставит группы видов в канонический порядок; вызов с явным видом трогает только имена внутри него.
|
||||
|
||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||
|
||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||
|
||||
@@ -1,15 +1,80 @@
|
||||
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.29 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||
[string]$DefinitionFile,
|
||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
|
||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page","sort-childObjects")]
|
||||
[string]$Operation,
|
||||
[string]$Value,
|
||||
[switch]$NoValidate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Разбор пользовательского JSON ---
|
||||
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
|
||||
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
|
||||
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
|
||||
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
|
||||
# Возврат через -NoEnumerate: без него одноэлементный
|
||||
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
|
||||
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
|
||||
try {
|
||||
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
|
||||
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
|
||||
$parsed = $text | ConvertFrom-Json
|
||||
} catch {
|
||||
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
|
||||
if ($Inline) {
|
||||
$got = ($text -replace '\s+', ' ').Trim()
|
||||
$label = 'got'
|
||||
if (-not $got) { $got = '(empty)' }
|
||||
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
|
||||
$what = "${what}, ${label}: ${got}"
|
||||
}
|
||||
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
|
||||
exit 1
|
||||
}
|
||||
Write-Output -NoEnumerate $parsed
|
||||
}
|
||||
|
||||
# --- Чтение входного JSON-файла ---
|
||||
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
|
||||
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
|
||||
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
|
||||
# угаданное имя уйдёт в метаданные так же молча.
|
||||
function Read-JsonInputFile([string]$path) {
|
||||
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
|
||||
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
|
||||
# проверкой срабатывают раньше и сохраняют свой текст.
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
[Console]::Error.WriteLine("[ERROR] File not found: $path")
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
|
||||
exit 1
|
||||
}
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
||||
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
try {
|
||||
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
|
||||
} catch {
|
||||
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
|
||||
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Mode validation ---
|
||||
@@ -212,27 +277,27 @@ foreach ($child in $script:propsEl.ChildNodes) {
|
||||
}
|
||||
Info "Configuration: $($script:objName)"
|
||||
|
||||
# --- Canonical type order for ChildObjects (44 types) ---
|
||||
# --- Canonical type order for ChildObjects (46 types) ---
|
||||
$script:typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# --- Type → on-disk directory name (plural) ---
|
||||
$script:typeToDir = @{
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||
@@ -241,7 +306,7 @@ $script:typeToDir = @{
|
||||
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# --- XML manipulation helpers (from subsystem-edit pattern) ---
|
||||
@@ -311,6 +376,21 @@ function Import-Fragment([string]$xmlString) {
|
||||
}
|
||||
|
||||
# --- Parse batch value (split by ;;) ---
|
||||
|
||||
# Имя вида из пользовательского ввода → каноническое имя или $null.
|
||||
# Ввод прощающий: регистр не важен, принимается имя каталога выгрузки (Catalogs → Catalog)
|
||||
# и русское имя вида в единственном и множественном числе.
|
||||
function Resolve-TypeName([string]$token) {
|
||||
$key = "$token".Trim()
|
||||
if (-not $key) { return $null }
|
||||
foreach ($canon in $script:typeOrder) { if ($canon -eq $key) { return $canon } }
|
||||
$byDir = $script:dirToType[$key.ToLowerInvariant()]
|
||||
if ($byDir) { return $byDir }
|
||||
$ru = $script:ruTypeMap[$key.ToLowerInvariant()]
|
||||
if ($ru) { return $ru }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Parse-BatchValue([string]$val) {
|
||||
$items = @()
|
||||
foreach ($part in $val.Split(";;")) {
|
||||
@@ -376,6 +456,220 @@ function Do-ModifyProperty([string]$batchVal) {
|
||||
}
|
||||
|
||||
# --- Operation: add-childObject ---
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Сортировка имён компаратором Compare-MetadataNames. В py-порту ту же роль играет
|
||||
# functools.cmp_to_key — штатный способ отсортировать компаратором; в PS 5.1 его нет,
|
||||
# поэтому слияние вручную. Порядок обоих портов задаёт один и тот же компаратор.
|
||||
function Sort-MetadataNames([string[]]$names) {
|
||||
# Возврат без запятой-обёртки: приёмная сторона всегда пишет @(...), и одноэлементный
|
||||
# результат остаётся массивом. С `return ,@(...)` @() собрал бы ОДИН объект-массив.
|
||||
if ($names.Count -le 1) { return $names }
|
||||
$mid = [int]($names.Count / 2)
|
||||
$left = @(Sort-MetadataNames $names[0..($mid - 1)])
|
||||
$right = @(Sort-MetadataNames $names[$mid..($names.Count - 1)])
|
||||
$out = New-Object System.Collections.ArrayList
|
||||
$i = 0; $j = 0
|
||||
while ($i -lt $left.Count -and $j -lt $right.Count) {
|
||||
if ((Compare-MetadataNames $left[$i] $right[$j]) -le 0) { [void]$out.Add($left[$i]); $i++ }
|
||||
else { [void]$out.Add($right[$j]); $j++ }
|
||||
}
|
||||
while ($i -lt $left.Count) { [void]$out.Add($left[$i]); $i++ }
|
||||
while ($j -lt $right.Count) { [void]$out.Add($right[$j]); $j++ }
|
||||
return $out.ToArray()
|
||||
}
|
||||
|
||||
# Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
|
||||
# Виды из Test-OrderSensitiveType по имени не сортируются, пока не названы явно.
|
||||
# Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
|
||||
# починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
|
||||
# ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
|
||||
# остаются как были, в дифе только перестановка строк.
|
||||
function Do-SortChildObjects([string]$batchVal) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
# Ввод прощающий: регистр не важен, принимается и имя каталога (Catalogs → Catalog) —
|
||||
# в дереве выгрузки виды видны именно во множественном числе.
|
||||
# Без @(...) на приёме: Parse-BatchValue возвращает ,$items — обёртка, которую @()
|
||||
# собрал бы как ОДИН объект-массив, и вид не нашёлся бы в $script:typeOrder.
|
||||
$tokens = @()
|
||||
if ("$batchVal".Trim()) { $tokens = Parse-BatchValue $batchVal }
|
||||
$requested = @()
|
||||
foreach ($token in $tokens) {
|
||||
$canon = Resolve-TypeName $token
|
||||
if (-not $canon) { Write-Error "Unknown type '$token'. Valid: $($script:typeOrder -join ', ')"; exit 1 }
|
||||
$requested += $canon
|
||||
}
|
||||
|
||||
$groups = New-Object System.Collections.Specialized.OrderedDictionary
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$ln = $child.get_LocalName()
|
||||
if (-not $groups.Contains($ln)) { $groups[$ln] = New-Object System.Collections.ArrayList }
|
||||
[void]$groups[$ln].Add($child)
|
||||
}
|
||||
|
||||
$targets = if ($requested.Count -gt 0) { $requested } else { @($groups.Keys | Where-Object { -not (Test-OrderSensitiveType $_) }) }
|
||||
foreach ($typeName in $targets) {
|
||||
if (-not $groups.Contains($typeName)) { continue }
|
||||
$els = $groups[$typeName]
|
||||
if ($els.Count -lt 2) { continue }
|
||||
$names = @(foreach ($e in $els) { $e.InnerText })
|
||||
$ordered = @(Sort-MetadataNames $names)
|
||||
$same = $true
|
||||
for ($i = 0; $i -lt $names.Count; $i++) { if ($names[$i] -cne $ordered[$i]) { $same = $false; break } }
|
||||
if ($same) { continue }
|
||||
for ($i = 0; $i -lt $els.Count; $i++) { $els[$i].InnerText = $ordered[$i] }
|
||||
$script:modifyCount++
|
||||
Info "Sorted: $typeName ($($els.Count))"
|
||||
}
|
||||
|
||||
if ($requested.Count -gt 0) { return }
|
||||
|
||||
# Без аргумента приводим в порядок и сами группы видов: собранная навыками конфигурация
|
||||
# может держать их не в каноне, и первая же выгрузка платформы даст диф. Переставляем
|
||||
# содержимое существующих узлов, а не узлы, поэтому отступы и структура файла не меняются —
|
||||
# в дифе только перестановка строк. Имя тега у XmlElement неизменяемо, поэтому там, где вид
|
||||
# меняется, узел заменяется через ReplaceChild: он сохраняет окружающие пробельные узлы.
|
||||
$elems = @()
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element') { $elems += $child }
|
||||
}
|
||||
$tags = @(); $texts = @()
|
||||
foreach ($e in $elems) { $tags += $e.get_LocalName(); $texts += $e.InnerText }
|
||||
$rank = @()
|
||||
for ($i = 0; $i -lt $tags.Count; $i++) {
|
||||
$r = $script:typeOrder.IndexOf($tags[$i])
|
||||
if ($r -lt 0) { $r = $script:typeOrder.Count }
|
||||
$rank += $r
|
||||
}
|
||||
# Порядок стабильный: вторым ключом идёт исходная позиция
|
||||
$order = @(0..($tags.Count - 1) | Sort-Object @{e={$rank[$_]}}, @{e={$_}})
|
||||
$same = $true
|
||||
for ($i = 0; $i -lt $order.Count; $i++) { if ($order[$i] -ne $i) { $same = $false; break } }
|
||||
if ($same) { return }
|
||||
|
||||
for ($i = 0; $i -lt $elems.Count; $i++) {
|
||||
$srcIdx = $order[$i]
|
||||
if ($tags[$i] -ceq $tags[$srcIdx]) {
|
||||
$elems[$i].InnerText = $texts[$srcIdx]
|
||||
continue
|
||||
}
|
||||
$newEl = $script:xmlDoc.CreateElement($tags[$srcIdx], $script:mdNs)
|
||||
$newEl.InnerText = $texts[$srcIdx]
|
||||
[void]$script:childObjsEl.ReplaceChild($newEl, $elems[$i])
|
||||
}
|
||||
$script:modifyCount++
|
||||
Info "Reordered type groups: $($elems.Count) entries"
|
||||
}
|
||||
|
||||
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
# финальный перенос. $null → файл новый (сохранить текущее поведение).
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Detect-XmlStyle([string]$path) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||
$raw = [System.IO.File]::ReadAllBytes($path)
|
||||
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
|
||||
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
|
||||
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
|
||||
$m = [regex]::Match($head, 'encoding="([^"]+)"')
|
||||
return @{
|
||||
bom = $bom
|
||||
crlf = $body.Contains("`r`n")
|
||||
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
|
||||
finalNl = $body.EndsWith("`n")
|
||||
}
|
||||
}
|
||||
|
||||
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
|
||||
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Finalize-XmlText([string]$text, $style) {
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
|
||||
if ($style -and $style.finalNl) { $text += "`n" }
|
||||
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
|
||||
return $text
|
||||
}
|
||||
|
||||
function Do-AddChildObject([string]$batchVal) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
@@ -395,6 +689,8 @@ function Do-AddChildObject([string]$batchVal) {
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$canonType = Resolve-TypeName $typeName
|
||||
if ($canonType) { $typeName = $canonType }
|
||||
$objNameVal = $item.Substring($dotIdx + 1)
|
||||
|
||||
# Check type is valid
|
||||
@@ -439,11 +735,11 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
||||
continue
|
||||
}
|
||||
|
||||
# Find insertion point: after last element of same type, or after last element of preceding type
|
||||
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition.
|
||||
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $script:configDir) -eq "byName")
|
||||
$insertBefore = $null
|
||||
$lastSameType = $null
|
||||
$lastPrecedingType = $null
|
||||
$currentTypeIdx = -1
|
||||
$firstLaterType = $null
|
||||
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
@@ -451,17 +747,29 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
||||
if ($childTypeIdx -lt 0) { continue }
|
||||
|
||||
if ($child.LocalName -eq $typeName) {
|
||||
# Same type — check alphabetical order
|
||||
if ($child.InnerText -gt $objNameVal -and -not $insertBefore) {
|
||||
# Insert before this element (alphabetical)
|
||||
# Внутри вида — по newObjectPosition: end (по умолчанию) кладёт после последнего
|
||||
# объекта того же вида, byName — по имени. Subsystem по имени не упорядочиваем
|
||||
# никогда: порядок подсистем в дереве задаёт порядок разделов в панели.
|
||||
$lastSameType = $child
|
||||
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objNameVal) -gt 0) {
|
||||
$insertBefore = $child
|
||||
}
|
||||
$lastSameType = $child
|
||||
} elseif ($childTypeIdx -lt $typeIdx) {
|
||||
$lastPrecedingType = $child
|
||||
} elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) {
|
||||
# First element of a later type — insert before it
|
||||
$insertBefore = $child
|
||||
} elseif ($childTypeIdx -gt $typeIdx -and -not $firstLaterType) {
|
||||
$firstLaterType = $child
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $insertBefore) {
|
||||
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
|
||||
# то есть перед его следующим соседом. Через $firstLaterType этого не сделать:
|
||||
# если видов старше в файле нет, запись уехала бы в самый конец блока,
|
||||
# за пределы своей группы.
|
||||
if ($lastSameType) {
|
||||
$next = $lastSameType.NextSibling
|
||||
while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling }
|
||||
$insertBefore = $next
|
||||
} else {
|
||||
$insertBefore = $firstLaterType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +801,8 @@ function Do-RemoveChildObject([string]$batchVal) {
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$canonType = Resolve-TypeName $typeName
|
||||
if ($canonType) { $typeName = $canonType }
|
||||
$objNameVal = $item.Substring($dotIdx + 1)
|
||||
|
||||
$found = $false
|
||||
@@ -639,10 +949,7 @@ function Do-SetPanels($valArg) {
|
||||
# Accept string (JSON), PSCustomObject, or hashtable
|
||||
$layout = $valArg
|
||||
if ($layout -is [string]) {
|
||||
try { $layout = $layout | ConvertFrom-Json } catch {
|
||||
Write-Error "set-panels value must be valid JSON object, got: $valArg"
|
||||
exit 1
|
||||
}
|
||||
$layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-panels'" "a JSON object with panel layout" -Inline
|
||||
}
|
||||
if (-not $layout) {
|
||||
Write-Error "set-panels value is empty"
|
||||
@@ -725,6 +1032,29 @@ $script:ruTypeMap = @{
|
||||
"бот" = "Bot"
|
||||
"планобмена" = "ExchangePlan"
|
||||
"хранилищенастроек" = "SettingsStorage"
|
||||
# Множественное число: в дереве конфигурации виды подписаны именно так.
|
||||
"справочники" = "Catalog"
|
||||
"документы" = "Document"
|
||||
"перечисления" = "Enum"
|
||||
"отчёты" = "Report"
|
||||
"отчеты" = "Report"
|
||||
"обработки" = "DataProcessor"
|
||||
"общиеформы" = "CommonForm"
|
||||
"журналыдокументов" = "DocumentJournal"
|
||||
"планывидовхарактеристик" = "ChartOfCharacteristicTypes"
|
||||
"планысчетов" = "ChartOfAccounts"
|
||||
"планывидоврасчета" = "ChartOfCalculationTypes"
|
||||
"планывидоврасчёта" = "ChartOfCalculationTypes"
|
||||
"регистрысведений" = "InformationRegister"
|
||||
"регистрынакопления" = "AccumulationRegister"
|
||||
"регистрыбухгалтерии" = "AccountingRegister"
|
||||
"регистрырасчета" = "CalculationRegister"
|
||||
"регистрырасчёта" = "CalculationRegister"
|
||||
"бизнеспроцессы" = "BusinessProcess"
|
||||
"задачи" = "Task"
|
||||
"боты" = "Bot"
|
||||
"планыобмена" = "ExchangePlan"
|
||||
"хранилищанастроек" = "SettingsStorage"
|
||||
}
|
||||
# plural folder → singular type
|
||||
$script:dirToType = @{}
|
||||
@@ -826,9 +1156,7 @@ $indent</Item>
|
||||
function Do-SetHomePage($valArg) {
|
||||
$layout = $valArg
|
||||
if ($layout -is [string]) {
|
||||
try { $layout = $layout | ConvertFrom-Json } catch {
|
||||
Write-Error "set-home-page value must be valid JSON object"; exit 1
|
||||
}
|
||||
$layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-home-page'" "a JSON object with home page layout" -Inline
|
||||
}
|
||||
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
|
||||
|
||||
@@ -942,8 +1270,8 @@ if ($DefinitionFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
||||
}
|
||||
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
||||
$ops = $jsonText | ConvertFrom-Json
|
||||
$jsonText = Read-JsonInputFile $DefinitionFile
|
||||
$ops = ConvertFrom-JsonInput $jsonText $DefinitionFile
|
||||
if ($ops -is [System.Array]) {
|
||||
foreach ($op in $ops) { $operations += $op }
|
||||
} else {
|
||||
@@ -968,11 +1296,16 @@ foreach ($op in $operations) {
|
||||
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
||||
"set-panels" { Do-SetPanels $opValue }
|
||||
"set-home-page" { Do-SetHomePage $opValue }
|
||||
"sort-childObjects" { Do-SortChildObjects $opValueStr }
|
||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Save ---
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$xmlStyle = Detect-XmlStyle $resolvedPath
|
||||
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
@@ -983,22 +1316,12 @@ $writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$script:xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
$text = Finalize-XmlText $text $xmlStyle
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||
$writeBom = ($null -eq $xmlStyle) -or $xmlStyle.bom
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
|
||||
Info "Saved: $resolvedPath"
|
||||
|
||||
# --- Auto-validate ---
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.29 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -14,6 +15,68 @@ from lxml import etree
|
||||
|
||||
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||
# регистр не различают, в argparse совпадение точное.
|
||||
|
||||
def parse_json_input(text, source, expected=None, inline=False):
|
||||
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
|
||||
|
||||
expected заполняем только для полиморфного входа: у файла подсказка
|
||||
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
|
||||
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
|
||||
|
||||
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
|
||||
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
|
||||
"""
|
||||
import json as _pj
|
||||
import sys as _psys
|
||||
try:
|
||||
if not str(text).strip():
|
||||
raise ValueError("input is empty")
|
||||
return _pj.loads(text)
|
||||
except ValueError as exc:
|
||||
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
|
||||
if inline:
|
||||
got = " ".join(str(text).split())
|
||||
label = "got"
|
||||
if not got:
|
||||
got = "(empty)"
|
||||
elif len(got) > 60:
|
||||
label = "got (first 60 chars)"
|
||||
got = got[:60]
|
||||
what = "%s, %s: %s" % (what, label, got)
|
||||
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
def read_json_file(path):
|
||||
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
|
||||
|
||||
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
|
||||
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
|
||||
"""
|
||||
import os as _pos
|
||||
import sys as _psys
|
||||
if not _pos.path.exists(path):
|
||||
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
if _pos.path.isdir(path):
|
||||
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
with open(path, "rb") as _fh:
|
||||
data = _fh.read()
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
return data[3:].decode("utf-8")
|
||||
if data[:2] == b"\xff\xfe":
|
||||
return data[2:].decode("utf-16-le")
|
||||
if data[:2] == b"\xfe\xff":
|
||||
return data[2:].decode("utf-16-be")
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
|
||||
% (path, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
class CIDict(dict):
|
||||
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||
@@ -254,27 +317,27 @@ XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
# Canonical type order for ChildObjects (44 types)
|
||||
# Canonical type order for ChildObjects (46 types)
|
||||
TYPE_ORDER = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
|
||||
]
|
||||
|
||||
# Type → on-disk directory name (plural)
|
||||
TYPE_TO_DIR = {
|
||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||
@@ -283,7 +346,7 @@ TYPE_TO_DIR = {
|
||||
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
|
||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
|
||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
|
||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "ExternalDataSource": "ExternalDataSources", "IntegrationService": "IntegrationServices",
|
||||
}
|
||||
|
||||
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
||||
@@ -291,6 +354,137 @@ SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCat
|
||||
REF_PROPS = ["DefaultLanguage"]
|
||||
|
||||
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
RU_TYPE_MAP = {
|
||||
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||
"плансчетов": "ChartOfAccounts",
|
||||
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||
"регистрсведений": "InformationRegister",
|
||||
"регистрнакопления": "AccumulationRegister",
|
||||
"регистрбухгалтерии": "AccountingRegister",
|
||||
"регистррасчета": "CalculationRegister",
|
||||
"регистррасчёта": "CalculationRegister",
|
||||
"бизнеспроцесс": "BusinessProcess",
|
||||
"бот": "Bot",
|
||||
"задача": "Task", "планобмена": "ExchangePlan",
|
||||
"хранилищенастроек": "SettingsStorage",
|
||||
# Множественное число: в дереве конфигурации виды подписаны именно так.
|
||||
"справочники": "Catalog", "документы": "Document", "перечисления": "Enum",
|
||||
"отчёты": "Report", "отчеты": "Report", "обработки": "DataProcessor",
|
||||
"общиеформы": "CommonForm", "журналыдокументов": "DocumentJournal",
|
||||
"планывидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||
"планысчетов": "ChartOfAccounts",
|
||||
"планывидоврасчета": "ChartOfCalculationTypes",
|
||||
"планывидоврасчёта": "ChartOfCalculationTypes",
|
||||
"регистрысведений": "InformationRegister",
|
||||
"регистрынакопления": "AccumulationRegister",
|
||||
"регистрыбухгалтерии": "AccountingRegister",
|
||||
"регистррасчета": "CalculationRegister", "регистрырасчета": "CalculationRegister",
|
||||
"регистрырасчёта": "CalculationRegister",
|
||||
"бизнеспроцессы": "BusinessProcess",
|
||||
"боты": "Bot",
|
||||
"задачи": "Task", "планыобмена": "ExchangePlan",
|
||||
"хранилищанастроек": "SettingsStorage",
|
||||
}
|
||||
|
||||
|
||||
def resolve_type_name(token):
|
||||
"""Имя вида из пользовательского ввода → каноническое имя или None.
|
||||
|
||||
Ввод прощающий: регистр не важен, принимается имя каталога выгрузки
|
||||
(Catalogs → Catalog) и русское имя вида в единственном и множественном числе.
|
||||
"""
|
||||
key = (token or "").strip().lower()
|
||||
if not key:
|
||||
return None
|
||||
for canon in TYPE_ORDER:
|
||||
if canon.lower() == key:
|
||||
return canon
|
||||
for canon, dir_name in TYPE_TO_DIR.items():
|
||||
if dir_name.lower() == key:
|
||||
return canon
|
||||
return RU_TYPE_MAP.get(key)
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
@@ -436,7 +630,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||
parser.add_argument("-ConfigPath", "-Path", required=True)
|
||||
parser.add_argument("-DefinitionFile", default=None)
|
||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page", "sort-childObjects"])
|
||||
parser.add_argument("-Value", default=None)
|
||||
parser.add_argument("-NoValidate", action="store_true")
|
||||
args = ci_parse_args(parser)
|
||||
@@ -575,7 +769,7 @@ def main():
|
||||
if dot_idx < 1:
|
||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_name = item[:dot_idx]
|
||||
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
|
||||
obj_name_val = item[dot_idx + 1:]
|
||||
|
||||
if type_name not in TYPE_ORDER:
|
||||
@@ -612,8 +806,15 @@ def main():
|
||||
warn(f"Already exists: {type_name}.{obj_name_val}")
|
||||
continue
|
||||
|
||||
# Find insertion point
|
||||
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
|
||||
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
|
||||
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт
|
||||
# порядок разделов в панели, пока их не перечислили в <SubsystemsOrder>.
|
||||
by_name = (not is_order_sensitive_type(type_name)
|
||||
and get_new_object_position(config_dir) == "byName")
|
||||
insert_before = None
|
||||
last_same = None
|
||||
first_later = None
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
@@ -623,10 +824,24 @@ def main():
|
||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||
|
||||
if child_type_name == type_name:
|
||||
if (child.text or "") > obj_name_val and insert_before is None:
|
||||
last_same = child
|
||||
if (by_name and insert_before is None
|
||||
and compare_metadata_names(child.text or "", obj_name_val) > 0):
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and insert_before is None:
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and first_later is None:
|
||||
first_later = child
|
||||
|
||||
if insert_before is None:
|
||||
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
|
||||
# то есть перед его следующим соседом. Через first_later этого не сделать:
|
||||
# если видов старше в файле нет, запись уехала бы в самый конец блока,
|
||||
# за пределы своей группы.
|
||||
if last_same is not None:
|
||||
siblings = [c for c in child_objs_el if isinstance(c.tag, str)]
|
||||
pos = siblings.index(last_same)
|
||||
insert_before = siblings[pos + 1] if pos + 1 < len(siblings) else None
|
||||
else:
|
||||
insert_before = first_later
|
||||
|
||||
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
||||
new_el.text = obj_name_val
|
||||
@@ -639,6 +854,69 @@ def main():
|
||||
add_count += 1
|
||||
info(f"Added: {type_name}.{obj_name_val}")
|
||||
|
||||
def do_sort_child_objects(batch_val):
|
||||
"""Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
|
||||
|
||||
Виды из is_order_sensitive_type по имени не сортируются, пока не названы явно.
|
||||
Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
|
||||
починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
|
||||
ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
|
||||
остаются как были, в дифе только перестановка строк.
|
||||
"""
|
||||
nonlocal modify_count
|
||||
if child_objs_el is None:
|
||||
print("No <ChildObjects> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
requested = []
|
||||
for token in (parse_batch_value(batch_val) if str(batch_val or "").strip() else []):
|
||||
canon = resolve_type_name(token)
|
||||
if canon is None:
|
||||
print(f"Unknown type '{token}'. Valid: {', '.join(TYPE_ORDER)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
requested.append(canon)
|
||||
|
||||
groups = {}
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
groups.setdefault(localname(child), []).append(child)
|
||||
|
||||
targets = requested or [t for t in groups if not is_order_sensitive_type(t)]
|
||||
for type_name in targets:
|
||||
els = groups.get(type_name, [])
|
||||
if len(els) < 2:
|
||||
continue
|
||||
names = [e.text or "" for e in els]
|
||||
ordered = sorted(names, key=functools.cmp_to_key(compare_metadata_names))
|
||||
if names == ordered:
|
||||
continue
|
||||
for el, name in zip(els, ordered):
|
||||
el.text = name
|
||||
modify_count += 1
|
||||
info(f"Sorted: {type_name} ({len(els)})")
|
||||
|
||||
if requested:
|
||||
# Вид назван явно — точечная операция: взаимный порядок групп не трогаем.
|
||||
return
|
||||
|
||||
# Без аргумента приводим в порядок и сами группы видов: собранная навыками
|
||||
# конфигурация может держать их не в каноне, и первая же выгрузка платформы даст
|
||||
# диф. Переставляем содержимое существующих узлов, а не узлы, поэтому отступы и
|
||||
# структура файла не меняются — в дифе только перестановка строк.
|
||||
elems = [c for c in child_objs_el if isinstance(c.tag, str)]
|
||||
pairs = [(localname(c), c.text or "") for c in elems]
|
||||
ranked = sorted(range(len(pairs)),
|
||||
key=lambda i: (TYPE_ORDER.index(pairs[i][0]) if pairs[i][0] in TYPE_ORDER else len(TYPE_ORDER), i))
|
||||
wanted = [pairs[i] for i in ranked]
|
||||
if wanted == pairs:
|
||||
return
|
||||
for el, (tag, text) in zip(elems, wanted):
|
||||
el.tag = f'{{{MD_NS}}}{tag}'
|
||||
el.text = text
|
||||
modify_count += 1
|
||||
info(f"Reordered type groups: {len(elems)} entries")
|
||||
|
||||
def do_remove_child_object(batch_val):
|
||||
nonlocal remove_count
|
||||
if child_objs_el is None:
|
||||
@@ -651,7 +929,7 @@ def main():
|
||||
if dot_idx < 1:
|
||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_name = item[:dot_idx]
|
||||
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
|
||||
obj_name_val = item[dot_idx + 1:]
|
||||
|
||||
found = False
|
||||
@@ -821,11 +1099,8 @@ def main():
|
||||
nonlocal modify_count
|
||||
layout = value
|
||||
if isinstance(layout, str):
|
||||
try:
|
||||
layout = ci_json(json.loads(layout))
|
||||
except json.JSONDecodeError:
|
||||
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
layout = ci_json(parse_json_input(
|
||||
layout, "-Value for operation 'set-panels'", "a JSON object with panel layout", inline=True))
|
||||
if not isinstance(layout, dict) or not layout:
|
||||
print("set-panels value must be non-empty object", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -874,24 +1149,6 @@ def main():
|
||||
info(f"Wrote panel layout: {cai_path}")
|
||||
|
||||
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
||||
RU_TYPE_MAP = {
|
||||
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||
"плансчетов": "ChartOfAccounts",
|
||||
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||
"регистрсведений": "InformationRegister",
|
||||
"регистрнакопления": "AccumulationRegister",
|
||||
"регистрбухгалтерии": "AccountingRegister",
|
||||
"регистррасчета": "CalculationRegister",
|
||||
"регистррасчёта": "CalculationRegister",
|
||||
"бизнеспроцесс": "BusinessProcess",
|
||||
"бот": "Bot",
|
||||
"задача": "Task", "планобмена": "ExchangePlan",
|
||||
"хранилищенастроек": "SettingsStorage",
|
||||
}
|
||||
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
||||
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||
|
||||
@@ -976,11 +1233,8 @@ def main():
|
||||
nonlocal modify_count
|
||||
layout = value
|
||||
if isinstance(layout, str):
|
||||
try:
|
||||
layout = ci_json(json.loads(layout))
|
||||
except json.JSONDecodeError:
|
||||
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
layout = ci_json(parse_json_input(
|
||||
layout, "-Value for operation 'set-home-page'", "a JSON object with home page layout", inline=True))
|
||||
if not isinstance(layout, dict) or not layout:
|
||||
print("set-home-page value must be non-empty object", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -1044,8 +1298,7 @@ def main():
|
||||
def_file = args.DefinitionFile
|
||||
if not os.path.isabs(def_file):
|
||||
def_file = os.path.join(os.getcwd(), def_file)
|
||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||
ops = ci_json(json.loads(fh.read()))
|
||||
ops = ci_json(parse_json_input(read_json_file(def_file), def_file))
|
||||
if isinstance(ops, list):
|
||||
operations = ops
|
||||
else:
|
||||
@@ -1075,6 +1328,8 @@ def main():
|
||||
do_set_panels(op_value)
|
||||
elif op_key == "set-home-page":
|
||||
do_set_home_page(op_value)
|
||||
elif op_key == "sort-childobjects":
|
||||
do_sort_child_objects(op_value if isinstance(op_value, str) else str(op_value))
|
||||
else:
|
||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||
# cf-info v1.9 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||
[Parameter(Mandatory=$true, Position=0)][Alias('Path')][string]$ConfigPath,
|
||||
[ValidateSet("overview","brief","full")]
|
||||
[string]$Mode = "overview",
|
||||
[Alias('Name')]
|
||||
@@ -85,20 +86,20 @@ function Get-PropML([string]$propName) {
|
||||
return (Get-MLText $n)
|
||||
}
|
||||
|
||||
# --- Type name maps (canonical order, 44 types) ---
|
||||
# --- Type name maps (canonical order, 46 types) ---
|
||||
$typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
$typeRuNames = @{
|
||||
@@ -119,7 +120,7 @@ $typeRuNames = @{
|
||||
"ChartOfCharacteristicTypes"="ПВХ"; "ChartOfAccounts"="Планы счетов"
|
||||
"AccountingRegister"="Регистры бухгалтерии"; "ChartOfCalculationTypes"="ПВР"
|
||||
"CalculationRegister"="Регистры расчёта"; "BusinessProcess"="Бизнес-процессы"
|
||||
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
||||
"Task"="Задачи"; "ExternalDataSource"="Внешние источники данных"; "IntegrationService"="Сервисы интеграции"
|
||||
}
|
||||
|
||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||
# cf-info v1.9 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -61,11 +61,11 @@ if os.path.isdir(config_path):
|
||||
if os.path.isfile(candidate):
|
||||
config_path = candidate
|
||||
else:
|
||||
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
|
||||
print(f"[ERROR] No Configuration.xml found in directory: {config_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isfile(config_path):
|
||||
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
|
||||
print(f"[ERROR] File not found: {config_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Load XML ---
|
||||
@@ -82,12 +82,12 @@ NS = {
|
||||
|
||||
md_root = xml_root # root is MetaDataObject itself
|
||||
if etree.QName(md_root.tag).localname != "MetaDataObject":
|
||||
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
|
||||
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)")
|
||||
sys.exit(1)
|
||||
|
||||
cfg_node = md_root.find("md:Configuration", NS)
|
||||
if cfg_node is None:
|
||||
print("[ERROR] No <Configuration> element found", file=sys.stderr)
|
||||
print("[ERROR] No <Configuration> element found")
|
||||
sys.exit(1)
|
||||
|
||||
version = md_root.get("version", "")
|
||||
@@ -113,20 +113,20 @@ def get_prop_ml(prop_name):
|
||||
n = props_node.find(f"md:{prop_name}", NS)
|
||||
return get_ml_text(n)
|
||||
|
||||
# --- Type name maps (canonical order, 44 types) ---
|
||||
# --- Type name maps (canonical order, 46 types) ---
|
||||
type_order = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
|
||||
]
|
||||
|
||||
type_ru_names = {
|
||||
@@ -147,7 +147,7 @@ type_ru_names = {
|
||||
"ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов",
|
||||
"AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР",
|
||||
"CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы",
|
||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
||||
"Task": "Задачи", "ExternalDataSource": "Внешние источники данных", "IntegrationService": "Сервисы интеграции",
|
||||
}
|
||||
|
||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Generates minimal XML source files for a 1C configuration."""
|
||||
import sys, os, argparse, re, uuid
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# cf-validate v1.7 — Validate 1C configuration root structure
|
||||
# cf-validate v1.10 — Validate 1C configuration root structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Parameter(Mandatory, Position=0)]
|
||||
[Alias('Path')]
|
||||
[string]$ConfigPath,
|
||||
|
||||
@@ -121,16 +122,16 @@ $validClassIds = @(
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# Type -> directory mapping
|
||||
@@ -139,6 +140,7 @@ $childTypeDirMap = @{
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||
"Bot"="Bots"
|
||||
"PaletteColor"="PaletteColors"
|
||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
@@ -155,7 +157,7 @@ $childTypeDirMap = @{
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"IntegrationService"="IntegrationServices"
|
||||
"ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# Valid enum values for Configuration properties
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-validate v1.7 — Validate 1C configuration XML structure
|
||||
# cf-validate v1.10 — Validate 1C configuration XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||
import sys, os, argparse, re
|
||||
@@ -59,16 +59,16 @@ VALID_CLASS_IDS = [
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'IntegrationService',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
# Type -> directory mapping
|
||||
@@ -76,7 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||
'Bot': 'Bots',
|
||||
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
|
||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||
@@ -93,6 +93,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
|
||||
'CalculationRegister': 'CalculationRegisters',
|
||||
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
|
||||
'ExternalDataSource': 'ExternalDataSources',
|
||||
'IntegrationService': 'IntegrationServices',
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ allowed-tools:
|
||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||
| `Module` | Создать пустые модули объекта: `ObjectModule`, `ManagerModule`, `RecordSetModule`, `ValueManagerModule` (через запятую) или `None`. У типов с единственным модулем (`CommonModule`, `HTTPService`, `WebService`) он создаётся и без параметра |
|
||||
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||
|
||||
## Формат -Object
|
||||
@@ -65,7 +66,7 @@ allowed-tools:
|
||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||
|
||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
||||
**Защита существующих данных**: уже заимствованный объект не перезаписывается — добавляется только недостающее. Повторный вызов безопасен: собственные реквизиты расширения, заимствованные подобъекты и код в модулях сохраняются.
|
||||
|
||||
## Команда
|
||||
|
||||
@@ -79,6 +80,12 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -Ex
|
||||
# Заимствовать один объект
|
||||
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||
|
||||
# Заимствовать справочник вместе с модулями объекта и менеджера
|
||||
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты" -Module ObjectModule,ManagerModule
|
||||
|
||||
# Общий модуль без файла модуля
|
||||
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "CommonModule.РаботаСФайлами" -Module None
|
||||
|
||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.37 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||
[Parameter(Mandatory)][string]$ConfigPath,
|
||||
[Parameter(Mandatory)][string]$Object,
|
||||
[string]$BorrowMainAttribute
|
||||
[string]$BorrowMainAttribute,
|
||||
[string]$Module
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -255,12 +257,38 @@ $childTypeDirMap = @{
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
||||
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "Language"="Languages"
|
||||
}
|
||||
|
||||
# --- 4a. Модули заимствованных объектов ---
|
||||
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
|
||||
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
|
||||
$script:moduleKindsByType = @{
|
||||
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
|
||||
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
|
||||
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
|
||||
"ExchangePlan"=@("ObjectModule","ManagerModule")
|
||||
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
|
||||
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
|
||||
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
|
||||
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
|
||||
"InformationRegister"=@("RecordSetModule","ManagerModule")
|
||||
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
|
||||
"AccountingRegister"=@("RecordSetModule","ManagerModule")
|
||||
"CalculationRegister"=@("RecordSetModule","ManagerModule")
|
||||
"Sequence"=@("RecordSetModule","ManagerModule")
|
||||
"Constant"=@("ValueManagerModule","ManagerModule")
|
||||
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
|
||||
"FilterCriterion"=@("ManagerModule")
|
||||
}
|
||||
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
|
||||
# Отказ — `-Module None`.
|
||||
$script:autoModuleTypes = @("CommonModule", "HTTPService", "WebService")
|
||||
$script:moduleKindNames = @("Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule")
|
||||
|
||||
# --- 4b. Russian synonym → English type ---
|
||||
$synonymMap = @{
|
||||
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
|
||||
@@ -282,20 +310,20 @@ $synonymMap = @{
|
||||
"HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService"
|
||||
}
|
||||
|
||||
# --- 5. Canonical type order (44 types) ---
|
||||
# --- 5. Canonical type order (46 types) ---
|
||||
$script:typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# --- 6. GeneratedType patterns per type ---
|
||||
@@ -427,6 +455,11 @@ $script:generatedTypes = @{
|
||||
"DefinedType" = @(
|
||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||
)
|
||||
"ExternalDataSource" = @(
|
||||
@{ prefix = "ExternalDataSourceManager"; category = "Manager" }
|
||||
@{ prefix = "ExternalDataSourceTablesManager"; category = "TablesManager" }
|
||||
@{ prefix = "ExternalDataSourceCubesManager"; category = "CubesManager" }
|
||||
)
|
||||
"Sequence" = @(
|
||||
@{ prefix = "SequenceRecord"; category = "Record" }
|
||||
@{ prefix = "SequenceManager"; category = "Manager" }
|
||||
@@ -591,6 +624,50 @@ if ($BorrowMainAttribute) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- 9c. Validate -Module ---
|
||||
$script:requestedModules = @()
|
||||
$script:noModule = $false
|
||||
if ($Module) {
|
||||
foreach ($raw in ($Module -split '[,;]')) {
|
||||
$kind = $raw.Trim()
|
||||
if (-not $kind) { continue }
|
||||
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно (-ieq): в py-порте это отдельная ветка, и молчаливое
|
||||
# расхождение портов на «none» ловится только глазами.
|
||||
if ($kind -ieq "None") { $script:noModule = $true; continue }
|
||||
$canon = @($script:moduleKindNames | Where-Object { $_ -ieq $kind })
|
||||
if ($canon.Count -eq 0) {
|
||||
Write-Error "Неизвестный вид модуля '$kind'. Допустимо: $($script:moduleKindNames -join ', '), None"
|
||||
exit 1
|
||||
}
|
||||
$script:requestedModules += $canon[0]
|
||||
}
|
||||
if ($script:noModule -and $script:requestedModules.Count -gt 0) {
|
||||
Write-Error "-Module None нельзя сочетать с видами модулей"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
|
||||
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
|
||||
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
|
||||
function Resolve-ModuleKinds {
|
||||
param([string]$typeName)
|
||||
|
||||
if ($script:noModule) { return @() }
|
||||
$allowed = @($script:moduleKindsByType[$typeName])
|
||||
if ($allowed.Count -eq 0) { return @() }
|
||||
|
||||
if ($script:autoModuleTypes -contains $typeName) { return @($allowed[0]) }
|
||||
if ($script:requestedModules.Count -eq 0) { return @() }
|
||||
|
||||
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
|
||||
$selected = @($allowed | Where-Object { $script:requestedModules -contains $_ })
|
||||
if ($selected.Count -eq 0) {
|
||||
Warn " Тип $typeName не имеет запрошенных модулей — пропущено. Допустимо: $($allowed -join ', ')"
|
||||
}
|
||||
return $selected
|
||||
}
|
||||
|
||||
# --- 10. Helper: read source object XML ---
|
||||
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
||||
@@ -1275,32 +1352,22 @@ function Register-FormInObject {
|
||||
}
|
||||
|
||||
# Save object XML
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$style2 = Detect-XmlStyle $objFile
|
||||
$settings2 = New-Object System.Xml.XmlWriterSettings
|
||||
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings2.Indent = $false
|
||||
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream2 = New-Object System.IO.MemoryStream
|
||||
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
|
||||
$objDoc.Save($writer2)
|
||||
$writer2.Flush(); $writer2.Close()
|
||||
|
||||
$bytes2 = $memStream2.ToArray()
|
||||
$text2 = [System.Text.Encoding]::UTF8.GetString($memStream2.ToArray())
|
||||
$memStream2.Close()
|
||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
|
||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
||||
$text2 = Finalize-XmlText $text2 $style2
|
||||
$writeBom2 = ($null -eq $style2) -or $style2.bom
|
||||
[System.IO.File]::WriteAllText($objFile, $text2, (New-Object System.Text.UTF8Encoding($writeBom2)))
|
||||
Info " Registered form in: $objFile"
|
||||
}
|
||||
|
||||
@@ -1313,6 +1380,81 @@ function Test-ObjectBorrowed {
|
||||
return (Test-Path $objFile)
|
||||
}
|
||||
|
||||
# --- 10f. Helper: пометка расширенного свойства (<xr:PropertyState>) ---
|
||||
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке — эмитим, чтобы исходники навыка
|
||||
# совпадали с эталоном. Имя свойства = базовое имя файла модуля (Module / ObjectModule / …),
|
||||
# у заимствованной формы — Form. Ставит тот, кто создал файл модуля (или форму).
|
||||
function Build-PropertyStateXml {
|
||||
param([string]$propertyName, [string]$indent)
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||
return $sb.ToString()
|
||||
}
|
||||
|
||||
function Set-PropertyStateFlag {
|
||||
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||
|
||||
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||
if (-not (Test-Path $objFile)) { return }
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||
|
||||
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||
# но они лежат ниже, внутри <ChildObjects>.
|
||||
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||
|
||||
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||
$ind = $empty.Groups[1].Value
|
||||
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||
} elseif ($open.Success) {
|
||||
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||
$ind = $open.Groups[1].Value
|
||||
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||
}
|
||||
|
||||
# --- 10g. Helper: пустой модуль заимствованного объекта ---
|
||||
function New-BorrowedModuleFile {
|
||||
param([string]$typeName, [string]$objName, [string]$moduleKind)
|
||||
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
$objDir = Join-Path (Join-Path $extDir $dirName) $objName
|
||||
$moduleDir = Join-Path $objDir "Ext"
|
||||
if (-not (Test-Path $moduleDir)) { New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null }
|
||||
|
||||
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный код
|
||||
# (то же правило, что у модуля формы).
|
||||
$moduleFile = Join-Path $moduleDir "${moduleKind}.bsl"
|
||||
if (Test-Path $moduleFile) {
|
||||
Info " Preserved existing ${moduleKind}.bsl"
|
||||
} else {
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($moduleFile, "", $enc)
|
||||
Info " Created: $moduleFile"
|
||||
}
|
||||
|
||||
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
|
||||
Set-PropertyStateFlag (Join-Path (Join-Path $extDir $dirName) "${objName}.xml") $moduleKind $script:formatVersion
|
||||
return $moduleFile
|
||||
}
|
||||
|
||||
# --- 11. Helper: generate InternalInfo XML ---
|
||||
function Build-InternalInfoXml {
|
||||
param([string]$typeName, [string]$objName, [string]$indent)
|
||||
@@ -1738,6 +1880,9 @@ function Merge-AttributesIntoObject {
|
||||
}
|
||||
|
||||
# Save via text manipulation to avoid namespace issues with InnerXml
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$style3 = Detect-XmlStyle $objFile
|
||||
$settings3 = New-Object System.Xml.XmlWriterSettings
|
||||
$settings3.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings3.Indent = $false
|
||||
@@ -1746,28 +1891,15 @@ function Merge-AttributesIntoObject {
|
||||
$writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3)
|
||||
$objDoc.Save($writer3)
|
||||
$writer3.Flush(); $writer3.Close()
|
||||
$bytes3 = $memStream3.ToArray()
|
||||
$text3 = [System.Text.Encoding]::UTF8.GetString($memStream3.ToArray())
|
||||
$memStream3.Close()
|
||||
$text3 = [System.Text.Encoding]::UTF8.GetString($bytes3)
|
||||
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
|
||||
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
|
||||
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
|
||||
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
||||
# Стоит ДО Finalize-XmlText, чтобы схлопывание пустых тегов накрыло и вставленные реквизиты.
|
||||
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
|
||||
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
|
||||
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
|
||||
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
||||
$text3 = Finalize-XmlText $text3 $style3
|
||||
$writeBom3 = ($null -eq $style3) -or $style3.bom
|
||||
[System.IO.File]::WriteAllText($objFile, $text3, (New-Object System.Text.UTF8Encoding($writeBom3)))
|
||||
Info " Merged $added attribute(s) into: $objFile"
|
||||
}
|
||||
}
|
||||
@@ -2083,6 +2215,127 @@ function Build-BorrowedObjectXml {
|
||||
}
|
||||
|
||||
# --- 13. Helper: add object to extension ChildObjects ---
|
||||
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
# финальный перенос. $null → файл новый (сохранить текущее поведение).
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Detect-XmlStyle([string]$path) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||
$raw = [System.IO.File]::ReadAllBytes($path)
|
||||
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
|
||||
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
|
||||
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
|
||||
$m = [regex]::Match($head, 'encoding="([^"]+)"')
|
||||
return @{
|
||||
bom = $bom
|
||||
crlf = $body.Contains("`r`n")
|
||||
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
|
||||
finalNl = $body.EndsWith("`n")
|
||||
}
|
||||
}
|
||||
|
||||
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
|
||||
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Finalize-XmlText([string]$text, $style) {
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
|
||||
if ($style -and $style.finalNl) { $text += "`n" }
|
||||
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
|
||||
return $text
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
function Add-ToChildObjects {
|
||||
param([string]$typeName, [string]$objName)
|
||||
|
||||
@@ -2108,7 +2361,12 @@ function Add-ToChildObjects {
|
||||
}
|
||||
}
|
||||
|
||||
# Find insertion point: after last element of same type, or before first element of later type
|
||||
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition: end
|
||||
# (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени. Так же
|
||||
# заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects не отсортирован.
|
||||
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт порядок
|
||||
# разделов в панели.
|
||||
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $extDir) -eq "byName")
|
||||
$insertBefore = $null
|
||||
$lastSameType = $null
|
||||
|
||||
@@ -2118,8 +2376,7 @@ function Add-ToChildObjects {
|
||||
if ($childTypeIdx -lt 0) { continue }
|
||||
|
||||
if ($child.LocalName -eq $typeName) {
|
||||
# Same type -- check alphabetical order
|
||||
if ($child.InnerText -gt $objName -and -not $insertBefore) {
|
||||
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objName) -gt 0) {
|
||||
$insertBefore = $child
|
||||
}
|
||||
$lastSameType = $child
|
||||
@@ -2204,6 +2461,9 @@ foreach ($item in $items) {
|
||||
$hasBMA = [bool]$BorrowMainAttribute
|
||||
$formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA
|
||||
$script:borrowedFiles += $formFiles
|
||||
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
|
||||
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
|
||||
Set-PropertyStateFlag $formFiles[0] "Form" $script:formatVersion
|
||||
$borrowedCount++
|
||||
|
||||
# Borrow main attribute if requested
|
||||
@@ -2212,26 +2472,37 @@ foreach ($item in $items) {
|
||||
}
|
||||
} else {
|
||||
# --- Object borrowing (existing logic) ---
|
||||
Info "Borrowing ${typeName}.${objName}..."
|
||||
|
||||
$src = Read-SourceObject $typeName $objName
|
||||
Info " Source UUID: $($src.Uuid)"
|
||||
|
||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||
|
||||
$targetDir = Join-Path $extDir $dirName
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$targetFile = Join-Path $targetDir "${objName}.xml"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||
Info " Created: $targetFile"
|
||||
|
||||
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
|
||||
# расширения, заимствованные подобъекты и состояния, которые из источника не выводятся.
|
||||
# Повторный вызов — законный способ доделать модуль (-Module), а не переиздать заготовку.
|
||||
if (Test-ObjectBorrowed $typeName $objName) {
|
||||
Info "Already borrowed: ${typeName}.${objName} — XML сохранён без изменений"
|
||||
} else {
|
||||
Info "Borrowing ${typeName}.${objName}..."
|
||||
|
||||
$src = Read-SourceObject $typeName $objName
|
||||
Info " Source UUID: $($src.Uuid)"
|
||||
|
||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||
Info " Created: $targetFile"
|
||||
}
|
||||
|
||||
Add-ToChildObjects $typeName $objName
|
||||
|
||||
$script:borrowedFiles += $targetFile
|
||||
foreach ($kind in (Resolve-ModuleKinds $typeName)) {
|
||||
$script:borrowedFiles += (New-BorrowedModuleFile $typeName $objName $kind)
|
||||
}
|
||||
$borrowedCount++
|
||||
}
|
||||
}
|
||||
@@ -2278,32 +2549,22 @@ while ($true) {
|
||||
}
|
||||
|
||||
# --- 15. Save modified Configuration.xml ---
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$style = Detect-XmlStyle $extResolvedPath
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$script:xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
||||
$text = Finalize-XmlText $text $style
|
||||
$writeBom = ($null -eq $style) -or $style.bom
|
||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
|
||||
Info "Saved: $extResolvedPath"
|
||||
|
||||
# --- 16. Summary ---
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.37 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -202,6 +203,97 @@ def decode_numeric_entities(s):
|
||||
return s
|
||||
|
||||
|
||||
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 get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
@@ -233,13 +325,39 @@ CHILD_TYPE_DIR_MAP = {
|
||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs",
|
||||
"SettingsStorage": "SettingsStorages", "FilterCriterion": "FilterCriteria",
|
||||
"CommandGroup": "CommandGroups", "DocumentNumerator": "DocumentNumerators",
|
||||
"Sequence": "Sequences", "IntegrationService": "IntegrationServices",
|
||||
"Sequence": "Sequences", "ExternalDataSource": "ExternalDataSources", "IntegrationService": "IntegrationServices",
|
||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
|
||||
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||
"CommonAttribute": "CommonAttributes", "Style": "Styles",
|
||||
"Bot": "Bots", "Language": "Languages",
|
||||
"Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "Language": "Languages",
|
||||
}
|
||||
|
||||
# --- Модули заимствованных объектов ---
|
||||
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
|
||||
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
|
||||
MODULE_KINDS_BY_TYPE = {
|
||||
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
|
||||
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
|
||||
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
|
||||
"ExchangePlan": ["ObjectModule", "ManagerModule"],
|
||||
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
|
||||
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
|
||||
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
|
||||
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
|
||||
"InformationRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"Sequence": ["RecordSetModule", "ManagerModule"],
|
||||
"Constant": ["ValueManagerModule", "ManagerModule"],
|
||||
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
|
||||
"FilterCriterion": ["ManagerModule"],
|
||||
}
|
||||
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
|
||||
# Отказ — `-Module None`.
|
||||
AUTO_MODULE_TYPES = ["CommonModule", "HTTPService", "WebService"]
|
||||
MODULE_KIND_NAMES = ["Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule"]
|
||||
|
||||
SYNONYM_MAP = {
|
||||
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
|
||||
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
|
||||
@@ -281,16 +399,16 @@ SYNONYM_MAP = {
|
||||
TYPE_ORDER = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
|
||||
]
|
||||
|
||||
GENERATED_TYPES = {
|
||||
@@ -421,6 +539,11 @@ GENERATED_TYPES = {
|
||||
"DefinedType": [
|
||||
{"prefix": "DefinedType", "category": "DefinedType"},
|
||||
],
|
||||
"ExternalDataSource": [
|
||||
{"prefix": "ExternalDataSourceManager", "category": "Manager"},
|
||||
{"prefix": "ExternalDataSourceTablesManager", "category": "TablesManager"},
|
||||
{"prefix": "ExternalDataSourceCubesManager", "category": "CubesManager"},
|
||||
],
|
||||
"Sequence": [
|
||||
{"prefix": "SequenceRecord", "category": "Record"},
|
||||
{"prefix": "SequenceManager", "category": "Manager"},
|
||||
@@ -516,6 +639,55 @@ def format_rank(ver):
|
||||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||
|
||||
|
||||
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||
# Копии этих функций есть в cfe-patch-method (навыки автономны); держать их одинаковыми — сознательно.
|
||||
def build_property_state_xml(property_name, indent):
|
||||
return "\n".join([
|
||||
f"{indent}<xr:PropertyState>",
|
||||
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||
f"{indent}</xr:PropertyState>",
|
||||
])
|
||||
|
||||
|
||||
def set_property_state_flag(obj_file, property_name, format_version):
|
||||
if format_rank(format_version) < 219:
|
||||
return
|
||||
if not os.path.isfile(obj_file):
|
||||
return
|
||||
|
||||
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||
text = fh.read()
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
|
||||
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||
# но они лежат ниже, внутри <ChildObjects>.
|
||||
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||
|
||||
if empty and (not opened or empty.start() < opened.start()):
|
||||
ind = empty.group(1)
|
||||
block = build_property_state_xml(property_name, ind + "\t")
|
||||
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||
elif opened:
|
||||
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||
return
|
||||
ind = opened.group(1)
|
||||
block = build_property_state_xml(property_name, ind + "\t")
|
||||
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||
text = text[:close_at] + block + nl + text[close_at:]
|
||||
else:
|
||||
return
|
||||
|
||||
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write(text)
|
||||
|
||||
|
||||
def apply_pal_ns(format_version):
|
||||
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||
@@ -655,6 +827,7 @@ def main():
|
||||
parser.add_argument("-ConfigPath", required=True)
|
||||
parser.add_argument("-Object", required=True)
|
||||
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
||||
parser.add_argument("-Module", default=None)
|
||||
args = ci_parse_args(parser)
|
||||
|
||||
# --- 1. Resolve paths ---
|
||||
@@ -860,6 +1033,25 @@ def main():
|
||||
sys.exit(1)
|
||||
return src_uuid
|
||||
|
||||
# --- Пустой модуль заимствованного объекта ---
|
||||
def new_borrowed_module_file(type_name, obj_name, module_kind):
|
||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||
module_dir = os.path.join(ext_dir, dir_name, obj_name, "Ext")
|
||||
os.makedirs(module_dir, exist_ok=True)
|
||||
|
||||
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный
|
||||
# код (то же правило, что у модуля формы).
|
||||
module_file = os.path.join(module_dir, f"{module_kind}.bsl")
|
||||
if os.path.isfile(module_file):
|
||||
info(f" Preserved existing {module_kind}.bsl")
|
||||
else:
|
||||
write_utf8_bom(module_file, "")
|
||||
info(f" Created: {module_file}")
|
||||
|
||||
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
|
||||
set_property_state_flag(os.path.join(ext_dir, dir_name, f"{obj_name}.xml"), module_kind, format_version)
|
||||
return module_file
|
||||
|
||||
def build_internal_info_xml(type_name, obj_name, indent):
|
||||
types = GENERATED_TYPES.get(type_name)
|
||||
if not types:
|
||||
@@ -940,6 +1132,13 @@ def main():
|
||||
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
|
||||
return
|
||||
|
||||
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
|
||||
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
|
||||
# Так же, как заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects
|
||||
# не отсортирован. Subsystem по имени не упорядочиваем никогда: порядок подсистем
|
||||
# в дереве задаёт порядок разделов в панели.
|
||||
by_name = (not is_order_sensitive_type(type_name)
|
||||
and get_new_object_position(ext_dir) == "byName")
|
||||
insert_before = None
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
@@ -950,7 +1149,8 @@ def main():
|
||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||
|
||||
if child_type_name == type_name:
|
||||
if (child.text or "") > obj_name and insert_before is None:
|
||||
if (by_name and insert_before is None
|
||||
and compare_metadata_names(child.text or "", obj_name) > 0):
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and insert_before is None:
|
||||
insert_before = child
|
||||
@@ -2019,6 +2219,47 @@ def main():
|
||||
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- 9c. Validate -Module ---
|
||||
requested_modules = []
|
||||
no_module = False
|
||||
if args.Module:
|
||||
for raw in re.split(r"[,;]", args.Module):
|
||||
kind = raw.strip()
|
||||
if not kind:
|
||||
continue
|
||||
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно: в ps1-порте `-ieq`, и молчаливое расхождение
|
||||
# портов на «none» ловится только глазами.
|
||||
if kind.lower() == "none":
|
||||
no_module = True
|
||||
continue
|
||||
canon = [k for k in MODULE_KIND_NAMES if k.lower() == kind.lower()]
|
||||
if not canon:
|
||||
print(f"Неизвестный вид модуля '{kind}'. Допустимо: {', '.join(MODULE_KIND_NAMES)}, None", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
requested_modules.append(canon[0])
|
||||
if no_module and requested_modules:
|
||||
print("-Module None нельзя сочетать с видами модулей", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
|
||||
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
|
||||
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
|
||||
def resolve_module_kinds(type_name):
|
||||
if no_module:
|
||||
return []
|
||||
allowed = MODULE_KINDS_BY_TYPE.get(type_name, [])
|
||||
if not allowed:
|
||||
return []
|
||||
if type_name in AUTO_MODULE_TYPES:
|
||||
return [allowed[0]]
|
||||
if not requested_modules:
|
||||
return []
|
||||
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
|
||||
selected = [k for k in allowed if k in requested_modules]
|
||||
if not selected:
|
||||
warn(f" Тип {type_name} не имеет запрошенных модулей — пропущено. Допустимо: {', '.join(allowed)}")
|
||||
return selected
|
||||
|
||||
# --- 10. Process each item ---
|
||||
borrowed_count = 0
|
||||
|
||||
@@ -2070,6 +2311,9 @@ def main():
|
||||
has_bma = borrow_main_attribute_mode is not None
|
||||
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
|
||||
borrowed_files.extend(form_files)
|
||||
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
|
||||
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
|
||||
set_property_state_flag(form_files[0], "Form", format_version)
|
||||
borrowed_count += 1
|
||||
|
||||
# Borrow main attribute if requested
|
||||
@@ -2077,23 +2321,32 @@ def main():
|
||||
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
|
||||
else:
|
||||
# --- Object borrowing ---
|
||||
info(f"Borrowing {type_name}.{obj_name}...")
|
||||
|
||||
src = read_source_object(type_name, obj_name)
|
||||
info(f" Source UUID: {src['Uuid']}")
|
||||
|
||||
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
|
||||
|
||||
target_dir = os.path.join(ext_dir, dir_name)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
||||
write_xml_file(target_file, borrowed_xml)
|
||||
info(f" Created: {target_file}")
|
||||
|
||||
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
|
||||
# расширения, заимствованные подобъекты и состояния, которые из источника не
|
||||
# выводятся. Повторный вызов — законный способ доделать модуль (-Module), а не
|
||||
# переиздать заготовку.
|
||||
if test_object_borrowed(type_name, obj_name):
|
||||
info(f"Already borrowed: {type_name}.{obj_name} — XML сохранён без изменений")
|
||||
else:
|
||||
info(f"Borrowing {type_name}.{obj_name}...")
|
||||
|
||||
src = read_source_object(type_name, obj_name)
|
||||
info(f" Source UUID: {src['Uuid']}")
|
||||
|
||||
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
|
||||
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
write_xml_file(target_file, borrowed_xml)
|
||||
info(f" Created: {target_file}")
|
||||
|
||||
add_to_child_objects(type_name, obj_name)
|
||||
|
||||
borrowed_files.append(target_file)
|
||||
for kind in resolve_module_kinds(type_name):
|
||||
borrowed_files.append(new_borrowed_module_file(type_name, obj_name, kind))
|
||||
borrowed_count += 1
|
||||
|
||||
# --- Владельцы заимствованных справочников ---
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Parameter(Mandatory, Position=0)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
@@ -48,10 +49,11 @@ $childTypeDirMap = @{
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"Bot"="Bots"
|
||||
"PaletteColor"="PaletteColors"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -80,6 +80,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
"CommandGroup": "CommandGroups",
|
||||
"DocumentNumerator": "DocumentNumerators",
|
||||
"Sequence": "Sequences",
|
||||
"ExternalDataSource": "ExternalDataSources",
|
||||
"IntegrationService": "IntegrationServices",
|
||||
"CommonAttribute": "CommonAttributes",
|
||||
"Style": "Styles",
|
||||
@@ -88,6 +89,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
"HTTPService": "HTTPServices",
|
||||
"WSReference": "WSReferences",
|
||||
"Bot": "Bots",
|
||||
"PaletteColor": "PaletteColors",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# cfe-init v1.11 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# cfe-init v1.11 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||
import sys, os, re, argparse, uuid
|
||||
|
||||
@@ -88,7 +88,7 @@ allowed-tools:
|
||||
|
||||
Правила:
|
||||
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Дословно — включая комментарии, регистр и пробелы внутри строки (`Х = Х + 1` ≠ `Х=Х+1`); свободны только отступ и пустые строки. Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||
|
||||
## Актуализация
|
||||
@@ -107,6 +107,8 @@ allowed-tools:
|
||||
|
||||
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
|
||||
|
||||
`-Check` смотрит исходники. Вердикт платформы — уже после загрузки в базу: `/db-cfe-admin check`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
@@ -361,6 +362,22 @@ function Get-Normalized {
|
||||
return (($line -replace '\s+', ' ').Trim())
|
||||
}
|
||||
|
||||
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
|
||||
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
|
||||
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
|
||||
function Get-ControlKey {
|
||||
param($lines)
|
||||
return (@($lines | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join "`n")
|
||||
}
|
||||
|
||||
# Parameter count of a signature params text. The platform compares only the number of
|
||||
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
|
||||
function Get-ParamCount {
|
||||
param([string]$paramsText)
|
||||
if ([string]::IsNullOrWhiteSpace($paramsText)) { return 0 }
|
||||
return @(Split-TopLevel $paramsText | Where-Object { $_.Trim() -ne '' }).Count
|
||||
}
|
||||
|
||||
# Reconstruct v1 body and edit ops from a marked body
|
||||
function Parse-MarkedBody {
|
||||
param($bodyLines)
|
||||
@@ -650,7 +667,14 @@ function Invoke-Resync {
|
||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||
|
||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
||||
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
|
||||
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
|
||||
$extParamCount = Get-ParamCount $sig.ParamsText
|
||||
$srcParamCount = Get-ParamCount $method.ParamsText
|
||||
$paramsDrift = ($extParamCount -ne $srcParamCount)
|
||||
$paramsReason = if ($paramsDrift) { "список параметров: в оригинале $srcParamCount, в перехватчике $extParamCount" } else { '' }
|
||||
|
||||
if (-not $paramsDrift -and [string]::Equals((Get-ControlKey $v1), (Get-ControlKey $v2), 'Ordinal')) {
|
||||
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
|
||||
}
|
||||
|
||||
@@ -700,7 +724,9 @@ function Invoke-Resync {
|
||||
|
||||
if ($ReportOnly) {
|
||||
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'ДРЕЙФ' }
|
||||
if ($paramsDrift -and $st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { $st = 'ДРЕЙФ' }
|
||||
$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации' } else { '' }
|
||||
if ($paramsDrift) { $rsn = if ($rsn) { "$paramsReason; $rsn" } else { $paramsReason } }
|
||||
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
|
||||
}
|
||||
|
||||
@@ -788,6 +814,100 @@ if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $Exte
|
||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
|
||||
|
||||
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||
function Get-FormatRank([string]$ver) {
|
||||
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||
return 0
|
||||
}
|
||||
|
||||
function Detect-FormatVersion([string]$dir) {
|
||||
$d = $dir
|
||||
while ($d) {
|
||||
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||
$extPath = "$d.xml"
|
||||
if (Test-Path $extPath) {
|
||||
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||
}
|
||||
$cfgPath = Join-Path $d "Configuration.xml"
|
||||
if (Test-Path $cfgPath) {
|
||||
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.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"
|
||||
}
|
||||
|
||||
function Build-PropertyStateXml {
|
||||
param([string]$propertyName, [string]$indent)
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||
return $sb.ToString()
|
||||
}
|
||||
|
||||
function Set-PropertyStateFlag {
|
||||
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||
|
||||
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||
if (-not (Test-Path $objFile)) { return }
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||
|
||||
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||
# но они лежат ниже, внутри <ChildObjects>.
|
||||
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||
|
||||
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||
$ind = $empty.Groups[1].Value
|
||||
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||
} elseif ($open.Success) {
|
||||
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||
$ind = $open.Groups[1].Value
|
||||
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||
}
|
||||
|
||||
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||
function Get-ModuleFlagTarget {
|
||||
param([string[]]$relParts, [string]$extRoot)
|
||||
|
||||
if ($relParts.Count -ne 4 -or $relParts[2] -ne "Ext") { return $null }
|
||||
$prop = [System.IO.Path]::GetFileNameWithoutExtension($relParts[3])
|
||||
return @{
|
||||
File = (Join-Path (Join-Path $extRoot $relParts[0]) "$($relParts[1]).xml")
|
||||
Property = $prop
|
||||
}
|
||||
}
|
||||
|
||||
# --- Read NamePrefix ---
|
||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgDoc.PreserveWhitespace = $false
|
||||
@@ -1084,6 +1204,12 @@ if ($reuseRegionIdx -ge 0) {
|
||||
}
|
||||
}
|
||||
|
||||
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||
$flagTarget = Get-ModuleFlagTarget $relParts $ExtensionPath
|
||||
if ($flagTarget) {
|
||||
Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
|
||||
}
|
||||
|
||||
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
|
||||
Write-Host " Файл: $extBsl"
|
||||
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -75,6 +75,99 @@ CONTEXT_RE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||
def detect_format_version(d):
|
||||
while d:
|
||||
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||
ext_path = d + ".xml"
|
||||
if os.path.isfile(ext_path):
|
||||
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||
ext_head = f.read(2000)
|
||||
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||
if m:
|
||||
return m.group(1)
|
||||
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 build_property_state_xml(property_name, indent):
|
||||
return "\n".join([
|
||||
f"{indent}<xr:PropertyState>",
|
||||
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||
f"{indent}</xr:PropertyState>",
|
||||
])
|
||||
|
||||
|
||||
def set_property_state_flag(obj_file, property_name, format_version):
|
||||
if format_rank(format_version) < 219:
|
||||
return
|
||||
if not os.path.isfile(obj_file):
|
||||
return
|
||||
|
||||
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||
text = fh.read()
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
|
||||
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||
# но они лежат ниже, внутри <ChildObjects>.
|
||||
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||
|
||||
if empty and (not opened or empty.start() < opened.start()):
|
||||
ind = empty.group(1)
|
||||
block = build_property_state_xml(property_name, ind + "\t")
|
||||
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||
elif opened:
|
||||
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||
return
|
||||
ind = opened.group(1)
|
||||
block = build_property_state_xml(property_name, ind + "\t")
|
||||
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||
text = text[:close_at] + block + nl + text[close_at:]
|
||||
else:
|
||||
return
|
||||
|
||||
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write(text)
|
||||
|
||||
|
||||
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||
def get_module_flag_target(rel_parts, ext_root):
|
||||
if len(rel_parts) != 4 or rel_parts[2] != "Ext":
|
||||
return None
|
||||
prop = os.path.splitext(rel_parts[3])[0]
|
||||
return {
|
||||
"file": os.path.join(ext_root, rel_parts[0], f"{rel_parts[1]}.xml"),
|
||||
"property": prop,
|
||||
}
|
||||
|
||||
|
||||
def get_module_rel_path(module_path):
|
||||
parts = module_path.split(".")
|
||||
if len(parts) < 2:
|
||||
@@ -398,6 +491,21 @@ def normalize(line):
|
||||
return re.sub(r'\s+', ' ', line).strip()
|
||||
|
||||
|
||||
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
|
||||
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
|
||||
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
|
||||
def control_key(lines):
|
||||
return "\n".join([k for k in (x.strip() for x in lines) if k != ""])
|
||||
|
||||
|
||||
# Parameter count of a signature params text. The platform compares only the number of
|
||||
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
|
||||
def param_count(params_text):
|
||||
if not params_text or not params_text.strip():
|
||||
return 0
|
||||
return len([p for p in split_top_level(params_text) if p.strip()])
|
||||
|
||||
|
||||
def parse_marked_body(body_lines):
|
||||
v1 = []
|
||||
ops = []
|
||||
@@ -841,6 +949,12 @@ def main():
|
||||
|
||||
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
||||
|
||||
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||
flag_target = get_module_flag_target(rel_parts, extension_path)
|
||||
if flag_target:
|
||||
set_property_state_flag(flag_target["file"], flag_target["property"],
|
||||
detect_format_version(extension_path))
|
||||
|
||||
# emit summary
|
||||
placement = place_new.placement
|
||||
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
||||
@@ -1030,7 +1144,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
||||
sig = read_signature(ext_lines, sig_line_idx)
|
||||
if not sig:
|
||||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
|
||||
_params, sig_end = sig
|
||||
ext_params_text, sig_end = sig
|
||||
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
|
||||
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
|
||||
block_end = -1
|
||||
@@ -1047,7 +1161,15 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
||||
v1norm = [normalize(x) for x in v1]
|
||||
v2norm = [normalize(x) for x in v2]
|
||||
|
||||
if "\n".join(v1norm) == "\n".join(v2norm):
|
||||
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
|
||||
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
|
||||
ext_param_count = param_count(ext_params_text)
|
||||
src_param_count = param_count(method["params_text"])
|
||||
params_drift = ext_param_count != src_param_count
|
||||
params_reason = ("список параметров: в оригинале %d, в перехватчике %d"
|
||||
% (src_param_count, ext_param_count)) if params_drift else ""
|
||||
|
||||
if not params_drift and control_key(v1) == control_key(v2):
|
||||
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
|
||||
|
||||
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
|
||||
@@ -1109,7 +1231,11 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
||||
st = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
|
||||
else:
|
||||
st = "ДРЕЙФ"
|
||||
if params_drift and st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ":
|
||||
st = "ДРЕЙФ"
|
||||
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации" if st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
|
||||
if params_drift:
|
||||
rsn = ("%s; %s" % (params_reason, rsn)) if rsn else params_reason
|
||||
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
|
||||
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ allowed-tools:
|
||||
|
||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты, права ролей. Аналог `/cf-validate`, но для расширений.
|
||||
|
||||
Проверяются исходники. Применимость — уже после загрузки в базу: `/db-cfe-admin check`.
|
||||
|
||||
## Параметры
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# cfe-validate v1.10 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||
# cfe-validate v1.16 — Validate 1C configuration extension structure (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Parameter(Mandatory, Position=0)]
|
||||
[Alias('Path')]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
@@ -107,7 +108,28 @@ function Get-FormatRank([string]$ver) {
|
||||
}
|
||||
|
||||
# --- Reference tables ---
|
||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
|
||||
$moduleKindsByType = @{
|
||||
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
|
||||
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
|
||||
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
|
||||
"ExchangePlan"=@("ObjectModule","ManagerModule")
|
||||
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
|
||||
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
|
||||
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
|
||||
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
|
||||
"InformationRegister"=@("RecordSetModule","ManagerModule")
|
||||
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
|
||||
"AccountingRegister"=@("RecordSetModule","ManagerModule")
|
||||
"CalculationRegister"=@("RecordSetModule","ManagerModule")
|
||||
"Sequence"=@("RecordSetModule","ManagerModule")
|
||||
"Constant"=@("ValueManagerModule","ManagerModule")
|
||||
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
|
||||
"FilterCriterion"=@("ManagerModule")
|
||||
}
|
||||
|
||||
$guidPattern ='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||
|
||||
# 7 fixed ClassIds for Configuration
|
||||
@@ -121,25 +143,25 @@ $validClassIds = @(
|
||||
"fb282519-d103-4dd3-bc12-cb271d631dfc"
|
||||
)
|
||||
|
||||
# 44 types in canonical order
|
||||
# 46 types in canonical order
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# Type -> directory mapping
|
||||
$childTypeDirMap = @{
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||
@@ -158,7 +180,7 @@ $childTypeDirMap = @{
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"IntegrationService"="IntegrationServices"
|
||||
"ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||
@@ -186,6 +208,7 @@ $generatedTypeCategories = @{
|
||||
"Sequence" = @("Record","Manager","RecordSet")
|
||||
"FilterCriterion" = @("Manager","List")
|
||||
"SettingsStorage" = @("Manager")
|
||||
"ExternalDataSource" = @("Manager","TablesManager","CubesManager")
|
||||
"IntegrationService" = @("Manager")
|
||||
"WSReference" = @("Manager")
|
||||
"DefinedType" = @("DefinedType")
|
||||
@@ -1169,8 +1192,113 @@ if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
|
||||
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
|
||||
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
|
||||
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
|
||||
$defaultRoleNodes = @($cfgNode.SelectNodes("md:Properties/md:DefaultRoles/xr:Item", $ns))
|
||||
if ($defaultRoleNodes.Count -gt 0) {
|
||||
$adoptedCache = @{}
|
||||
|
||||
function Test-ObjectAdopted {
|
||||
param([string]$typeName, [string]$objName)
|
||||
$key = "$typeName.$objName"
|
||||
if ($adoptedCache.ContainsKey($key)) { return $adoptedCache[$key] }
|
||||
$adoptedCache[$key] = $false
|
||||
if ($childTypeDirMap.ContainsKey($typeName)) {
|
||||
$objPath = Join-Path (Join-Path $configDir $childTypeDirMap[$typeName]) "$objName.xml"
|
||||
if (Test-Path $objPath) {
|
||||
try {
|
||||
$objDoc = New-Object System.Xml.XmlDocument
|
||||
$objDoc.Load($objPath)
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ob = $objDoc.SelectSingleNode("/md:MetaDataObject/md:$typeName/md:Properties/md:ObjectBelonging", $objNs)
|
||||
if ($ob -and $ob.InnerText -eq "Adopted") { $adoptedCache[$key] = $true }
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return $adoptedCache[$key]
|
||||
}
|
||||
|
||||
$check15Ok = $true
|
||||
$check15Count = 0
|
||||
foreach ($rn in $defaultRoleNodes) {
|
||||
$roleRef = $rn.InnerText
|
||||
if ($roleRef -notmatch '^Role\.(.+)$') { continue }
|
||||
$defRoleName = $Matches[1]
|
||||
$rightsPath = Join-Path (Join-Path (Join-Path $configDir "Roles") $defRoleName) "Ext\Rights.xml"
|
||||
if (-not (Test-Path $rightsPath)) { continue }
|
||||
try {
|
||||
$rDoc = New-Object System.Xml.XmlDocument
|
||||
$rDoc.Load($rightsPath)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
$rNs = New-Object System.Xml.XmlNamespaceManager($rDoc.NameTable)
|
||||
$rNs.AddNamespace("r", "http://v8.1c.ru/8.2/roles")
|
||||
foreach ($nameNode in $rDoc.SelectNodes("/r:Rights/r:object/r:name", $rNs)) {
|
||||
$fullName = $nameNode.InnerText
|
||||
$segs = $fullName.Split(".")
|
||||
# Configuration.* — права самого расширения, не объект; заимствования там нет.
|
||||
if ($segs.Count -lt 2 -or $segs[0] -eq "Configuration") { continue }
|
||||
$check15Count++
|
||||
if (Test-ObjectAdopted $segs[0] $segs[1]) {
|
||||
Report-Error ("15. Роль '$defRoleName' входит в DefaultRoles и даёт права на заимствованный $($segs[0]).$($segs[1]) " +
|
||||
"($fullName): платформа это запрещает. Вынесите такие права в отдельную роль вне DefaultRoles.")
|
||||
$check15Ok = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($check15Ok -and $check15Count -gt 0) {
|
||||
Report-OK "15. Основные роли: прав на заимствованные объекты нет ($check15Count checked)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
|
||||
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
|
||||
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
|
||||
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на стенде),
|
||||
# но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
|
||||
if ($versionRank -ge 219 -and $childObjNode) {
|
||||
$stateIssues = @()
|
||||
$stateChecked = 0
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$typeName = $child.LocalName
|
||||
if (-not $moduleKindsByType.ContainsKey($typeName)) { continue }
|
||||
if (-not $childTypeDirMap.ContainsKey($typeName)) { continue }
|
||||
$stateObjName = $child.InnerText.Trim()
|
||||
if (-not $stateObjName) { continue }
|
||||
$typeDir = Join-Path $configDir $childTypeDirMap[$typeName]
|
||||
$objFile = Join-Path $typeDir "$stateObjName.xml"
|
||||
if (-not (Test-Path $objFile)) { continue }
|
||||
$objText = [System.IO.File]::ReadAllText($objFile, [System.Text.Encoding]::UTF8)
|
||||
if ($objText -notmatch '<ObjectBelonging>Adopted</ObjectBelonging>') { continue }
|
||||
|
||||
foreach ($kind in $moduleKindsByType[$typeName]) {
|
||||
$stateChecked++
|
||||
$hasFile = Test-Path (Join-Path (Join-Path (Join-Path $typeDir $stateObjName) "Ext") "$kind.bsl")
|
||||
$hasFlag = $objText -match "<xr:Property>$kind</xr:Property>"
|
||||
if ($hasFile -and -not $hasFlag) {
|
||||
$stateIssues += "$typeName.$stateObjName — есть $kind.bsl, но нет <xr:PropertyState> для $kind"
|
||||
} elseif ($hasFlag -and -not $hasFile) {
|
||||
$stateIssues += "$typeName.$stateObjName — есть <xr:PropertyState> для $kind, но нет $kind.bsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($stateChecked -gt 0) {
|
||||
if ($stateIssues.Count -eq 0) {
|
||||
Report-OK "16. Модули заимствованных объектов: пометки расширенных свойств согласованы ($stateChecked)"
|
||||
} else {
|
||||
foreach ($issue in $stateIssues) { Report-Warn "16. $issue" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||
$extRootDir = Split-Path $resolvedPath -Parent
|
||||
$ctrlCount = 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-validate v1.10 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||
# cfe-validate v1.16 — Validate 1C configuration extension XML structure (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||
import sys, os, argparse, re
|
||||
@@ -55,28 +55,49 @@ VALID_CLASS_IDS = [
|
||||
'fb282519-d103-4dd3-bc12-cb271d631dfc',
|
||||
]
|
||||
|
||||
# 44 types in canonical order
|
||||
# 46 types in canonical order
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'IntegrationService',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
|
||||
MODULE_KINDS_BY_TYPE = {
|
||||
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
|
||||
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
|
||||
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
|
||||
"ExchangePlan": ["ObjectModule", "ManagerModule"],
|
||||
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
|
||||
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
|
||||
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
|
||||
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
|
||||
"InformationRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
|
||||
"Sequence": ["RecordSetModule", "ManagerModule"],
|
||||
"Constant": ["ValueManagerModule", "ManagerModule"],
|
||||
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
|
||||
"FilterCriterion": ["ManagerModule"],
|
||||
}
|
||||
|
||||
# Type -> directory mapping
|
||||
CHILD_TYPE_DIR_MAP = {
|
||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||
'Bot': 'Bots',
|
||||
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
|
||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||
@@ -93,6 +114,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
|
||||
'CalculationRegister': 'CalculationRegisters',
|
||||
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
|
||||
'ExternalDataSource': 'ExternalDataSources',
|
||||
'IntegrationService': 'IntegrationServices',
|
||||
}
|
||||
|
||||
@@ -121,6 +143,7 @@ GENERATED_TYPE_CATEGORIES = {
|
||||
'Sequence': ['Record', 'Manager', 'RecordSet'],
|
||||
'FilterCriterion': ['Manager', 'List'],
|
||||
'SettingsStorage': ['Manager'],
|
||||
'ExternalDataSource': ['Manager', 'TablesManager', 'CubesManager'],
|
||||
'IntegrationService': ['Manager'],
|
||||
'WSReference': ['Manager'],
|
||||
'DefinedType': ['DefinedType'],
|
||||
@@ -1141,10 +1164,108 @@ def main():
|
||||
if check14_ok:
|
||||
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
|
||||
|
||||
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
|
||||
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
|
||||
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
|
||||
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
|
||||
default_role_nodes = cfg_node.findall('md:Properties/md:DefaultRoles/xr:Item', NS)
|
||||
if default_role_nodes:
|
||||
adopted_cache = {}
|
||||
|
||||
def is_object_adopted(type_name, obj_name):
|
||||
key = f"{type_name}.{obj_name}"
|
||||
if key in adopted_cache:
|
||||
return adopted_cache[key]
|
||||
adopted_cache[key] = False
|
||||
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||
if dir_name:
|
||||
obj_path = os.path.join(config_dir, dir_name, obj_name + '.xml')
|
||||
if os.path.isfile(obj_path):
|
||||
try:
|
||||
obj_root = etree.parse(obj_path).getroot()
|
||||
ob = obj_root.find(f'md:{type_name}/md:Properties/md:ObjectBelonging', NS)
|
||||
if ob is not None and (ob.text or '') == 'Adopted':
|
||||
adopted_cache[key] = True
|
||||
except Exception:
|
||||
pass
|
||||
return adopted_cache[key]
|
||||
|
||||
check15_ok = True
|
||||
check15_count = 0
|
||||
roles_ns = {'r': 'http://v8.1c.ru/8.2/roles'}
|
||||
for rn in default_role_nodes:
|
||||
m = re.match(r'^Role\.(.+)$', rn.text or '')
|
||||
if not m:
|
||||
continue
|
||||
def_role_name = m.group(1)
|
||||
rights_path = os.path.join(config_dir, 'Roles', def_role_name, 'Ext', 'Rights.xml')
|
||||
if not os.path.isfile(rights_path):
|
||||
continue
|
||||
try:
|
||||
rights_root = etree.parse(rights_path).getroot()
|
||||
except Exception:
|
||||
continue
|
||||
for name_node in rights_root.findall('r:object/r:name', roles_ns):
|
||||
full_name = name_node.text or ''
|
||||
segs = full_name.split('.')
|
||||
# Configuration.* — права самого расширения, не объект; заимствования там нет.
|
||||
if len(segs) < 2 or segs[0] == 'Configuration':
|
||||
continue
|
||||
check15_count += 1
|
||||
if is_object_adopted(segs[0], segs[1]):
|
||||
r.error(f"15. Роль '{def_role_name}' входит в DefaultRoles и даёт права на заимствованный "
|
||||
f"{segs[0]}.{segs[1]} ({full_name}): платформа это запрещает. "
|
||||
"Вынесите такие права в отдельную роль вне DefaultRoles.")
|
||||
check15_ok = False
|
||||
if check15_ok and check15_count > 0:
|
||||
r.ok(f'15. Основные роли: прав на заимствованные объекты нет ({check15_count} checked)')
|
||||
|
||||
if r.stopped:
|
||||
r.finalize(out_file)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
|
||||
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
|
||||
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
|
||||
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на
|
||||
# стенде), но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
|
||||
if version_rank >= 219 and child_obj_node is not None:
|
||||
state_issues = []
|
||||
state_checked = 0
|
||||
for child in child_obj_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
type_name = etree.QName(child.tag).localname
|
||||
if type_name not in MODULE_KINDS_BY_TYPE or type_name not in CHILD_TYPE_DIR_MAP:
|
||||
continue
|
||||
obj_name_val = (child.text or '').strip()
|
||||
if not obj_name_val:
|
||||
continue
|
||||
type_dir = os.path.join(config_dir, CHILD_TYPE_DIR_MAP[type_name])
|
||||
obj_file = os.path.join(type_dir, f'{obj_name_val}.xml')
|
||||
if not os.path.isfile(obj_file):
|
||||
continue
|
||||
with open(obj_file, 'r', encoding='utf-8-sig') as f:
|
||||
obj_text = f.read()
|
||||
if '<ObjectBelonging>Adopted</ObjectBelonging>' not in obj_text:
|
||||
continue
|
||||
|
||||
for kind in MODULE_KINDS_BY_TYPE[type_name]:
|
||||
state_checked += 1
|
||||
has_file = os.path.isfile(os.path.join(type_dir, obj_name_val, 'Ext', f'{kind}.bsl'))
|
||||
has_flag = f'<xr:Property>{kind}</xr:Property>' in obj_text
|
||||
if has_file and not has_flag:
|
||||
state_issues.append(f'{type_name}.{obj_name_val} — есть {kind}.bsl, но нет <xr:PropertyState> для {kind}')
|
||||
elif has_flag and not has_file:
|
||||
state_issues.append(f'{type_name}.{obj_name_val} — есть <xr:PropertyState> для {kind}, но нет {kind}.bsl')
|
||||
|
||||
if state_checked > 0:
|
||||
if not state_issues:
|
||||
r.ok(f'16. Модули заимствованных объектов: пометки расширенных свойств согласованы ({state_checked})')
|
||||
else:
|
||||
for issue in state_issues:
|
||||
r.warn(f'16. {issue}')
|
||||
|
||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||
ctrl_count = 0
|
||||
for dp, _dn, files in os.walk(config_dir):
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: db-cfe-admin
|
||||
description: Управление расширениями конфигурации в информационной базе 1С. Используй когда нужно узнать какие расширения подключены к базе, выполнить проверку применимости или синтаксическую проверку, изменить безопасный режим или активность, удалить расширение из базы
|
||||
argument-hint: <list|check|set-properties|delete> [database] [-Name <Имя>]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-cfe-admin — Управление расширениями конфигурации
|
||||
|
||||
Расширения **на стороне базы**: состав и свойства подключения, проверки, удаление.
|
||||
Про исходники расширения — другие навыки, см. «Смежное».
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-cfe-admin list [database]
|
||||
/db-cfe-admin check [database] [-Name Расш1]
|
||||
/db-cfe-admin set-properties [database] -Name Расш1 -SafeMode off
|
||||
/db-cfe-admin delete [database] -Name Расш1
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-cfe-admin.ps1" -Command <команда> <параметры>
|
||||
```
|
||||
|
||||
### Общие параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-Command <команда>` | да | `list` / `check` / `set-properties` / `delete` |
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Пользователь базы |
|
||||
| `-Password <пароль>` | нет | Пароль пользователя |
|
||||
| `-Name <имя>` | усл. | Расширение. Обязателен для `set-properties`; в `delete` — вместо `-All`. Без него `list` и `check` работают по всем расширениям |
|
||||
| `-All` | усл. | Только для `delete`: удалить все расширения. Вместо `-Name`, а не вместе с ним |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Параметры `check`
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-Checks <список>` | `apply` — применимость расширения, `modules` — синтаксическая проверка, `config` — проверки конфигурации (целостность, ссылки, неиспользуемые процедуры и обработчики). Через запятую, по умолчанию `apply,modules` |
|
||||
| `-Context <список>` | Контексты синтаксической проверки: `ThinClient`, `WebClient`, `Server`, `ExternalConnection`, `ThickClientManagedApplication`, `ThickClientOrdinaryApplication`, `MobileClient` и др. Через запятую, по умолчанию `ThinClient,Server` |
|
||||
|
||||
Применимость и синтаксис проверяют разное и друг друга не заменяют. Проверяется только
|
||||
расширение: ошибки самой конфигурации сюда не попадают.
|
||||
|
||||
### Свойства `set-properties`
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-SafeMode <on/off>` | Безопасный режим |
|
||||
| `-Active <on/off>` | Активность расширения |
|
||||
| `-UnsafeActionProtection <on/off>` | Защита от опасных действий |
|
||||
| `-UsedInDistributedInfobase <on/off>` | Использование в распределённой ИБ |
|
||||
| `-Scope <область>` | `infobase` / `data-separation` |
|
||||
| `-SecurityProfile <имя>` | Профиль безопасности |
|
||||
|
||||
Передавай только то, что меняешь: не указанное свойство остаётся как было. Расширение, впервые
|
||||
попавшее в базу загрузкой, создаётся с включённым безопасным режимом, а в нём расширение модуля не
|
||||
применяется.
|
||||
|
||||
Свойствами управляет `ibcmd` — он есть не в каждой установке платформы и работает с файловой базой;
|
||||
остальные команды работают всегда.
|
||||
|
||||
## Смежное
|
||||
|
||||
| Задача | Навык |
|
||||
|--------|-------|
|
||||
| Создать расширение, заимствовать объекты, перехватить метод | `/cfe-init`, `/cfe-borrow`, `/cfe-patch-method`, `/cfe-validate` |
|
||||
| Загрузить исходники расширения в базу | `/db-load-xml -Extension` (из коммита Git — `/db-load-git`) |
|
||||
| Загрузить готовый `.cfe` | `/db-load-cf -Extension` |
|
||||
| Выгрузить расширение из базы | `/db-dump-xml -Extension`, `/db-dump-cf -Extension` |
|
||||
| Обновить конфигурацию базы после загрузки | `/db-update -Extension` |
|
||||
| Проверить дрейф контролируемых методов по исходникам | `/cfe-patch-method -Check` |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Что подключено к базе
|
||||
... -Command list -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
# Проверить расширение
|
||||
... -Command check -InfoBasePath "C:\Bases\MyDB" -Name "Расш1"
|
||||
|
||||
# Синтаксическая проверка в контексте веб-клиента
|
||||
... -Command check -InfoBasePath "C:\Bases\MyDB" -Checks modules -Context WebClient,Server
|
||||
|
||||
# Снять безопасный режим
|
||||
... -Command set-properties -InfoBasePath "C:\Bases\MyDB" -Name "Расш1" -SafeMode off
|
||||
|
||||
# Отключить, не удаляя
|
||||
... -Command set-properties -InfoBasePath "C:\Bases\MyDB" -Name "Расш1" -Active off
|
||||
|
||||
# Убрать расширение из базы
|
||||
... -Command delete -InfoBasePath "C:\Bases\MyDB" -Name "Расш1"
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# db-create v1.11 — Create 1C information base
|
||||
# db-create v1.14 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -46,7 +46,7 @@
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -88,6 +88,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -138,6 +149,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-create v1.11 — Create 1C information base
|
||||
# db-create v1.14 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -292,7 +307,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -313,7 +328,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -332,11 +347,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -401,15 +436,15 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate template ---
|
||||
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||
print(f"Error: template file not found: {args.UseTemplate}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- ibcmd branch (file infobase only) ---
|
||||
@@ -436,10 +471,9 @@ def main():
|
||||
print(
|
||||
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||
"— information base was not created",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error creating information base (code: {exit_code})")
|
||||
print_platform_output(result)
|
||||
sys.exit(exit_code)
|
||||
|
||||
@@ -496,10 +530,9 @@ def main():
|
||||
print(
|
||||
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||
"— information base was not created",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error creating information base (code: {exit_code})")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
||||
# db-dump-cf v1.16 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -49,7 +49,7 @@
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -104,6 +104,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -154,6 +165,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
||||
# db-dump-cf v1.16 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -422,10 +457,10 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
@@ -436,7 +471,7 @@ def main():
|
||||
# --- ibcmd branch (file infobase only) ---
|
||||
if engine == "ibcmd":
|
||||
if args.AllExtensions:
|
||||
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)")
|
||||
sys.exit(1)
|
||||
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
||||
if args.Extension:
|
||||
@@ -459,9 +494,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped")
|
||||
else:
|
||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping configuration (code: {exit_code})")
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -509,9 +544,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped")
|
||||
else:
|
||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping configuration (code: {exit_code})")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
||||
# db-dump-dt v1.15 — Dump 1C information base to DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -39,7 +39,7 @@
|
||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -88,6 +88,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -138,6 +149,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
||||
# db-dump-dt v1.15 — Dump 1C information base to DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -420,10 +455,10 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
@@ -452,9 +487,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped")
|
||||
else:
|
||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping information base (code: {exit_code})")
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -496,9 +531,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped")
|
||||
else:
|
||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping information base (code: {exit_code})")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -33,6 +33,7 @@ allowed-tools:
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||
Для `-Extension` каталог берётся из `extensions[].src` записи базы, если он там указан.
|
||||
|
||||
## Команда
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
||||
# db-dump-xml v1.21 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -61,7 +61,7 @@
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -85,8 +85,10 @@ param(
|
||||
[string]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
|
||||
[string]$Mode = "Changes",
|
||||
# Пустое значение = режим не задан. Прежнее умолчание Changes подставляется ниже, после
|
||||
# того как станет видно, перечислены ли объекты.
|
||||
[ValidateSet("", "Full", "Changes", "Partial", "UpdateInfo")]
|
||||
[string]$Mode = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Objects,
|
||||
@@ -101,6 +103,18 @@ param(
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ObjectsFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryUser,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPassword,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -111,6 +125,90 @@ param(
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||
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 Test-SamePath {
|
||||
param([string]$A, [string]$B)
|
||||
if (-not $A -or -not $B) { return $false }
|
||||
try {
|
||||
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
function Find-ProjectDatabase {
|
||||
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if (-not $pf) { return $null }
|
||||
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||
if (-not $proj.databases) { return $null }
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-RepositorySettings {
|
||||
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||
$dbRec = Find-ProjectDatabase
|
||||
$rec = $null
|
||||
if ($dbRec) {
|
||||
if ($Extension) {
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
if ($dbRec.extensions) {
|
||||
foreach ($ext in $dbRec.extensions) {
|
||||
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$rec = $ext.repository
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$rec = $dbRec.repository
|
||||
}
|
||||
}
|
||||
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||
return @{
|
||||
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||
User = $user
|
||||
Password = $pwd
|
||||
FromRegistry = [bool]($rec -and $rec.path)
|
||||
DbRecord = $dbRec
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepositoryArgs {
|
||||
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||
param([hashtable]$Repo)
|
||||
$a = @()
|
||||
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return $a
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||
param([string]$Text, [string[]]$Secrets)
|
||||
@@ -127,12 +225,23 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
'--user', '--password'
|
||||
)
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||
|
||||
function Test-ArgKeyMatch {
|
||||
@@ -177,6 +286,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -415,8 +532,33 @@ if ($engine -eq "ibcmd") {
|
||||
}
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
|
||||
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
|
||||
if ($ObjectsFile) {
|
||||
if (-not (Test-Path $ObjectsFile)) {
|
||||
Write-Host "Error: -ObjectsFile not found: $ObjectsFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$fromFile = @([System.IO.File]::ReadAllLines($ObjectsFile, [System.Text.Encoding]::UTF8) |
|
||||
ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
|
||||
$Objects = (@(@($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $fromFile) -join ',')
|
||||
}
|
||||
# Перечислены объекты — операция частичная. Иначе список молча игнорировался бы: умолчание
|
||||
# Changes выгружает «изменённое с прошлой выгрузки», а не то, что просили.
|
||||
if ($Objects) {
|
||||
if ($Mode -eq "UpdateInfo") {
|
||||
# Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов.
|
||||
Write-Host "Error: -Mode UpdateInfo does not take an object list — it only refreshes ConfigDumpInfo.xml" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Mode -eq "Full" -or $Mode -eq "Changes") {
|
||||
Write-Host "[note] перечислены объекты — выгружаются только они; -Mode $Mode не применён" -ForegroundColor Yellow
|
||||
}
|
||||
$Mode = "Partial"
|
||||
}
|
||||
if (-not $Mode) { $Mode = "Changes" }
|
||||
if ($Mode -eq "Partial" -and -not $Objects) {
|
||||
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
|
||||
Write-Host "Error: -Objects or -ObjectsFile required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -486,6 +628,11 @@ try {
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$arguments += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
@@ -530,7 +677,7 @@ try {
|
||||
$arguments += $extraArgs
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||
$exitCode = $__v8.ExitCode
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
||||
# db-dump-xml v1.21 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,15 +67,111 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -120,15 +216,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +298,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +345,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +383,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +400,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +419,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -352,7 +472,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -388,14 +508,18 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
||||
parser.add_argument(
|
||||
"-Mode",
|
||||
default="Changes",
|
||||
choices=["Full", "Changes", "Partial", "UpdateInfo"],
|
||||
default="",
|
||||
choices=["", "Full", "Changes", "Partial", "UpdateInfo"],
|
||||
help="Dump mode (default: Changes)",
|
||||
)
|
||||
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
||||
parser.add_argument("-ObjectsFile", default="")
|
||||
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
||||
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
||||
parser.add_argument(
|
||||
@@ -436,15 +560,40 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
|
||||
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
|
||||
if args.ObjectsFile:
|
||||
if not os.path.exists(args.ObjectsFile):
|
||||
print("Error: -ObjectsFile not found: %s" % args.ObjectsFile)
|
||||
sys.exit(1)
|
||||
with open(args.ObjectsFile, encoding="utf-8-sig") as f:
|
||||
from_file = [s.strip() for s in f.read().splitlines()
|
||||
if s.strip() and not s.strip().startswith("#")]
|
||||
inline = [s.strip() for s in args.Objects.split(",") if s.strip()]
|
||||
args.Objects = ",".join(inline + from_file)
|
||||
# Перечислены объекты — операция частичная. Иначе список молча игнорировался бы: умолчание
|
||||
# Changes выгружает «изменённое с прошлой выгрузки», а не то, что просили.
|
||||
if args.Objects:
|
||||
if args.Mode == "UpdateInfo":
|
||||
# Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов.
|
||||
print("Error: -Mode UpdateInfo does not take an object list — it only refreshes "
|
||||
"ConfigDumpInfo.xml")
|
||||
sys.exit(1)
|
||||
if args.Mode in ("Full", "Changes"):
|
||||
print("[note] перечислены объекты — выгружаются только они; -Mode %s не применён"
|
||||
% args.Mode)
|
||||
args.Mode = "Partial"
|
||||
if not args.Mode:
|
||||
args.Mode = "Changes"
|
||||
if args.Mode == "Partial" and not args.Objects:
|
||||
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
||||
print("Error: -Objects or -ObjectsFile required for Partial mode")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Create output dir if needed ---
|
||||
@@ -455,12 +604,12 @@ def main():
|
||||
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||
if engine == "ibcmd":
|
||||
if args.Format == "Plain":
|
||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||
sys.exit(1)
|
||||
if args.AllExtensions:
|
||||
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||
elif args.Mode == "UpdateInfo":
|
||||
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
||||
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8")
|
||||
sys.exit(1)
|
||||
elif args.Mode == "Partial":
|
||||
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
||||
@@ -490,9 +639,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported")
|
||||
else:
|
||||
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error exporting configuration (code: {exit_code})")
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -513,6 +662,11 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
|
||||
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
|
||||
arguments += ["-Format", args.Format]
|
||||
|
||||
@@ -551,7 +705,7 @@ def main():
|
||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
|
||||
@@ -564,9 +718,9 @@ def main():
|
||||
print("Dump completed successfully")
|
||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped")
|
||||
else:
|
||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping configuration (code: {exit_code})")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -40,7 +40,19 @@ allowed-tools:
|
||||
"password": "",
|
||||
"aliases": ["dev", "разработка"],
|
||||
"branches": ["dev", "develop", "feature/*"],
|
||||
"configSrc": "C:\\WS\\myapp\\cfsrc"
|
||||
"configSrc": "C:\\WS\\myapp\\cfsrc",
|
||||
"repository": {
|
||||
"path": "\\\\srv01\\repo\\MyApp",
|
||||
"user": "Ivanov",
|
||||
"password": ""
|
||||
},
|
||||
"extensions": [
|
||||
{
|
||||
"name": "МоёРасширение",
|
||||
"src": "src\\cfe\\МоёРасширение",
|
||||
"repository": { "path": "\\\\srv01\\repo\\MyApp_Ext", "user": "Ivanov", "password": "" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "test",
|
||||
@@ -64,6 +76,9 @@ allowed-tools:
|
||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
|
||||
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
|
||||
| `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` |
|
||||
| `extensionApplyCheck` | bool | Проверять ли применимость расширения после загрузки в базу (по умолчанию `true`). Разово отключается ключом `-NoApplyCheck` |
|
||||
| `externalCheck` | bool | Проверять ли исходники внешней обработки/отчёта платформой перед сборкой (по умолчанию `true`). Разово отключается ключом `-Checks off` |
|
||||
| `databases` | array | Массив баз данных |
|
||||
| `default` | string | id базы по умолчанию |
|
||||
|
||||
@@ -82,6 +97,35 @@ allowed-tools:
|
||||
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
|
||||
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
|
||||
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
|
||||
| `repository` | object | нет | Хранилище конфигурации: `path`, `user`, `password` (см. ниже) |
|
||||
| `extensions` | array | нет | Расширения: `name`, `src`, необязательное `repository` (см. ниже) |
|
||||
|
||||
### Хранилище конфигурации
|
||||
|
||||
База, подключённая к хранилищу конфигурации 1С, **не принимает ни одной операции конфигуратора**
|
||||
без реквизитов доступа к хранилищу — это касается не только `/db-repo`, но и `/db-load-xml`,
|
||||
`/db-dump-xml`, `/db-update`, `/db-load-git`. Реквизиты берутся из `repository` записи базы,
|
||||
передавать их в каждом вызове не нужно.
|
||||
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|:------------:|----------|
|
||||
| `repository.path` | string | да | Каталог хранилища или `tcp://<хост>[:<порт>]/<имя>` |
|
||||
| `repository.user` | string | нет | Пользователь **хранилища**. Не наследуется от `user` базы |
|
||||
| `repository.password` | string | нет | Пароль пользователя хранилища |
|
||||
|
||||
У расширения **своё хранилище** со своим путём, поэтому одного `repository` мало:
|
||||
|
||||
| Поле | Тип | Обязательное | Описание |
|
||||
|------|-----|:------------:|----------|
|
||||
| `extensions[].name` | string | да | Имя расширения, как в конфигурации |
|
||||
| `extensions[].src` | string | нет | Каталог XML-исходников расширения |
|
||||
| `extensions[].repository` | object | нет | Хранилище расширения. Расширение без хранилища — обычный случай |
|
||||
|
||||
Пароль хранилища — такой же секрет, как `password` базы; `.v8-project.json` в `.gitignore`.
|
||||
|
||||
> **Сетевое хранилище.** Адрес — `tcp://<хост>[:<порт>]/<имя>`, порт по умолчанию 1542.
|
||||
> Обслуживается сервером хранилища. Если он недоступен, платформа отвечает «Соединение с
|
||||
> хранилищем конфигурации не установлено» — тем же сообщением, что и при отсутствии реквизитов.
|
||||
|
||||
## Алгоритм разрешения базы данных
|
||||
|
||||
@@ -128,6 +172,7 @@ test Тестовая server srv01/MyApp_Test
|
||||
- path (для file) или server + ref (для server)
|
||||
- user, password (необязательно)
|
||||
- aliases, branches (необязательно)
|
||||
- если база под хранилищем конфигурации — `repository`: путь, пользователь, пароль
|
||||
|
||||
Добавь в массив `databases`. Если это первая база — установи как `default`.
|
||||
|
||||
@@ -159,3 +204,10 @@ test Тестовая server srv01/MyApp_Test
|
||||
```
|
||||
|
||||
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
|
||||
|
||||
**Хранилище конфигурации** (если у базы задан `repository`) — скрипты навыков подставляют
|
||||
сами, сопоставляя параметры соединения с записью реестра:
|
||||
```
|
||||
/ConfigurationRepositoryF"<path>" /ConfigurationRepositoryN"<user>" /ConfigurationRepositoryP"<password>"
|
||||
```
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
||||
| `-Extension <имя>` | нет | Загрузить как расширение |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-cf v1.14 — Load 1C configuration from CF file
|
||||
# db-load-cf v1.17 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -49,7 +49,7 @@
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -78,6 +78,16 @@ param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но расширение при этом неприменимо.
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -121,6 +131,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -171,6 +192,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -384,6 +413,85 @@ function Write-PlatformOutput {
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -434,22 +542,32 @@ try {
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
Write-PlatformOutput $output
|
||||
exit $exitCode
|
||||
}
|
||||
|
||||
# --- 1cv8 branch ---
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/LoadCfg", "`"$InputFile`""
|
||||
|
||||
@@ -488,6 +606,11 @@ try {
|
||||
}
|
||||
Write-PlatformOutput $__v8.Output
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-cf v1.14 — Load 1C configuration from CF file
|
||||
# db-load-cf v1.17 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -344,6 +379,91 @@ def print_platform_output(result):
|
||||
print("--- End ---")
|
||||
|
||||
|
||||
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 run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -352,7 +472,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -409,6 +529,12 @@ def main():
|
||||
parser.add_argument("-InputFile", required=True)
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но расширение при этом неприменимо.
|
||||
parser.add_argument("-StrictLog", action="store_true")
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -440,21 +566,21 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate input file ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
print(f"Error: input file not found: {args.InputFile}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- ibcmd branch (file infobase only) ---
|
||||
if engine == "ibcmd":
|
||||
if args.AllExtensions:
|
||||
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)")
|
||||
sys.exit(1)
|
||||
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
||||
if args.Extension:
|
||||
@@ -473,7 +599,19 @@ def main():
|
||||
if result.returncode == 0:
|
||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
exit_code = result.returncode
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -482,17 +620,20 @@ def main():
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
else:
|
||||
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
conn_args.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
|
||||
|
||||
@@ -517,7 +658,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
@@ -530,6 +671,12 @@ def main():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
print_platform_output(result)
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-dt v1.13 — Load 1C information base from DT file
|
||||
# db-load-dt v1.16 — Load 1C information base from DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -46,7 +46,7 @@
|
||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -118,6 +118,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -168,6 +179,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-dt v1.13 — Load 1C information base from DT file
|
||||
# db-load-dt v1.16 — Load 1C information base from DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -440,15 +475,15 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate input file ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
print(f"Error: input file not found: {args.InputFile}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- ibcmd branch (file infobase only) ---
|
||||
@@ -470,7 +505,7 @@ def main():
|
||||
if result.returncode == 0:
|
||||
print(f"Information base restored successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -512,7 +547,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Information base restored successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -55,6 +55,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
||||
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
|
||||
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
|
||||
| `-Extension <имя>` | нет | Загрузить в расширение |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-git v1.20 — Load Git changes into 1C database
|
||||
# db-load-git v1.26 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -64,7 +64,7 @@
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -116,6 +116,19 @@ param(
|
||||
# но в логе есть отбраковка.
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryUser,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPassword,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -126,6 +139,115 @@ param(
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||
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 Test-SamePath {
|
||||
param([string]$A, [string]$B)
|
||||
if (-not $A -or -not $B) { return $false }
|
||||
try {
|
||||
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
function Find-ProjectDatabase {
|
||||
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if (-not $pf) { return $null }
|
||||
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||
if (-not $proj.databases) { return $null }
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-RepositorySettings {
|
||||
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||
$dbRec = Find-ProjectDatabase
|
||||
$rec = $null
|
||||
if ($dbRec) {
|
||||
if ($Extension) {
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
if ($dbRec.extensions) {
|
||||
foreach ($ext in $dbRec.extensions) {
|
||||
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$rec = $ext.repository
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$rec = $dbRec.repository
|
||||
}
|
||||
}
|
||||
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||
return @{
|
||||
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||
User = $user
|
||||
Password = $pwd
|
||||
FromRegistry = [bool]($rec -and $rec.path)
|
||||
DbRecord = $dbRec
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepositoryArgs {
|
||||
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||
param([hashtable]$Repo)
|
||||
$a = @()
|
||||
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return $a
|
||||
}
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
function Write-RepositoryHints {
|
||||
param([string]$LogText)
|
||||
if (-not $LogText) { return }
|
||||
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
|
||||
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
|
||||
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
|
||||
}
|
||||
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
|
||||
$obj = $m.Groups[1].Value
|
||||
if ($obj -eq 'Configuration') {
|
||||
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
|
||||
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
|
||||
Write-Host "[hint] соединение с хранилищем не установлено. Две причины:" -ForegroundColor Yellow
|
||||
Write-Host " реквизиты неизвестны — добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list);" -ForegroundColor Yellow
|
||||
Write-Host " либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||
param([string]$Text, [string[]]$Secrets)
|
||||
@@ -142,12 +264,23 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
'--user', '--password'
|
||||
)
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||
|
||||
function Test-ArgKeyMatch {
|
||||
@@ -192,6 +325,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -436,7 +577,74 @@ function Find-SilentRejections {
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
if ($engine -eq "ibcmd") {
|
||||
if (-not $InfoBasePath) {
|
||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||
@@ -645,6 +853,13 @@ try {
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
Write-PlatformOutput $applyOut
|
||||
}
|
||||
exit $exitCode
|
||||
@@ -657,16 +872,24 @@ try {
|
||||
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
|
||||
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$connArgs += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
@@ -695,7 +918,7 @@ try {
|
||||
# --- Execute ---
|
||||
Write-Host ""
|
||||
Write-Host "Executing partial configuration load..."
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||
|
||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||
$exitCode = $__v8.ExitCode
|
||||
@@ -718,6 +941,7 @@ try {
|
||||
}
|
||||
}
|
||||
Write-PlatformOutput $__v8.Output
|
||||
Write-RepositoryHints $logContent
|
||||
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
@@ -729,6 +953,11 @@ try {
|
||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-git v1.20 — Load Git changes into 1C database
|
||||
# db-load-git v1.26 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,15 +67,132 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
def write_repository_hints(log_text):
|
||||
if not log_text:
|
||||
return
|
||||
if "текущая конфигурация помещена в хранилище" in log_text:
|
||||
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
|
||||
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
|
||||
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
|
||||
obj = m.group(1)
|
||||
if obj == "Configuration":
|
||||
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
|
||||
print(' /db-repo lock <база> -Objects "Конфигурация"')
|
||||
else:
|
||||
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
|
||||
if "Соединение с хранилищем конфигурации не установлено" in log_text:
|
||||
print("[hint] соединение с хранилищем не установлено. Две причины:")
|
||||
print(' реквизиты неизвестны — добавьте "repository" в запись базы в .v8-project.json (см. /db-list);')
|
||||
print(" либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт.")
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -120,15 +237,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +319,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +366,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +404,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +421,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +440,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -376,6 +517,76 @@ def find_silent_rejections(log_text):
|
||||
return found
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -384,7 +595,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -460,6 +671,9 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
|
||||
parser.add_argument(
|
||||
"-Source",
|
||||
@@ -482,6 +696,8 @@ def main():
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но в логе есть отбраковка.
|
||||
parser.add_argument("-StrictLog", action="store_true")
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -506,10 +722,10 @@ def main():
|
||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -526,19 +742,19 @@ def main():
|
||||
|
||||
# --- Validate config dir ---
|
||||
if not os.path.exists(args.ConfigDir):
|
||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
||||
print(f"Error: config directory not found: {args.ConfigDir}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Commit mode ---
|
||||
if args.Source == "Commit" and not args.CommitRange:
|
||||
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
|
||||
print("Error: -CommitRange required for Source=Commit")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check git ---
|
||||
try:
|
||||
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Error: git not found in PATH", file=sys.stderr)
|
||||
print("Error: git not found in PATH")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Get changed files from Git ---
|
||||
@@ -617,10 +833,10 @@ def main():
|
||||
config_files.append(rel_path)
|
||||
|
||||
if support_skipped:
|
||||
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
||||
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):")
|
||||
for sf in support_skipped:
|
||||
print(f" - {sf}", file=sys.stderr)
|
||||
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
||||
print(f" - {sf}")
|
||||
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).")
|
||||
|
||||
if len(config_files) == 0:
|
||||
print("No configuration files found in changes")
|
||||
@@ -644,10 +860,10 @@ def main():
|
||||
if engine == "ibcmd":
|
||||
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||
if args.Format == "Plain":
|
||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||
sys.exit(1)
|
||||
if args.AllExtensions:
|
||||
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
||||
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)")
|
||||
sys.exit(1)
|
||||
arguments = ["infobase", "config", "import", "files"] + config_files
|
||||
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||
@@ -664,7 +880,7 @@ def main():
|
||||
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||
if result.returncode != 0:
|
||||
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
sys.exit(result.returncode)
|
||||
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||
exit_code = 0
|
||||
@@ -682,8 +898,19 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Database configuration updated successfully")
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
print_platform_output(ar)
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Write list file (UTF-8 with BOM) ---
|
||||
@@ -692,17 +919,25 @@ def main():
|
||||
f.write("\n".join(config_files))
|
||||
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
else:
|
||||
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||
conn_args += ["/F", f'"{args.InfoBasePath}"']
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
conn_args.extend(repository_args(repo))
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||
arguments += ["-listFile", f'"{list_file}"']
|
||||
@@ -729,7 +964,7 @@ def main():
|
||||
# --- Execute ---
|
||||
print("")
|
||||
print("Executing partial configuration load...")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
@@ -739,7 +974,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Load completed successfully")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
|
||||
log_content = ""
|
||||
if os.path.isfile(out_file):
|
||||
@@ -754,6 +989,7 @@ def main():
|
||||
pass
|
||||
|
||||
print_platform_output(result)
|
||||
write_repository_hints(log_content)
|
||||
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
@@ -769,6 +1005,12 @@ def main():
|
||||
if args.StrictLog and exit_code == 0:
|
||||
exit_code = 1
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -34,6 +34,7 @@ allowed-tools:
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||
Для `-Extension` каталог берётся из `extensions[].src` записи базы, если он там указан.
|
||||
|
||||
## Команда
|
||||
|
||||
@@ -57,6 +58,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
|
||||
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
|
||||
| `-Extension <имя>` | нет | Загрузить в расширение |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-xml v1.21 — Load 1C configuration from XML files
|
||||
# db-load-xml v1.28 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -61,7 +61,7 @@
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -85,8 +85,10 @@ param(
|
||||
[string]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Partial")]
|
||||
[string]$Mode = "Full",
|
||||
# Пустое значение = режим не задан. Прежнее умолчание Full подставляется ниже, после того
|
||||
# как станет видно, перечислены ли файлы.
|
||||
[ValidateSet("", "Full", "Partial")]
|
||||
[string]$Mode = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Files,
|
||||
@@ -110,6 +112,19 @@ param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryUser,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPassword,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -120,6 +135,115 @@ param(
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||
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 Test-SamePath {
|
||||
param([string]$A, [string]$B)
|
||||
if (-not $A -or -not $B) { return $false }
|
||||
try {
|
||||
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
function Find-ProjectDatabase {
|
||||
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if (-not $pf) { return $null }
|
||||
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||
if (-not $proj.databases) { return $null }
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-RepositorySettings {
|
||||
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||
$dbRec = Find-ProjectDatabase
|
||||
$rec = $null
|
||||
if ($dbRec) {
|
||||
if ($Extension) {
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
if ($dbRec.extensions) {
|
||||
foreach ($ext in $dbRec.extensions) {
|
||||
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$rec = $ext.repository
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$rec = $dbRec.repository
|
||||
}
|
||||
}
|
||||
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||
return @{
|
||||
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||
User = $user
|
||||
Password = $pwd
|
||||
FromRegistry = [bool]($rec -and $rec.path)
|
||||
DbRecord = $dbRec
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepositoryArgs {
|
||||
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||
param([hashtable]$Repo)
|
||||
$a = @()
|
||||
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return $a
|
||||
}
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
function Write-RepositoryHints {
|
||||
param([string]$LogText)
|
||||
if (-not $LogText) { return }
|
||||
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
|
||||
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
|
||||
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
|
||||
}
|
||||
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
|
||||
$obj = $m.Groups[1].Value
|
||||
if ($obj -eq 'Configuration') {
|
||||
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
|
||||
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
|
||||
Write-Host "[hint] соединение с хранилищем не установлено. Две причины:" -ForegroundColor Yellow
|
||||
Write-Host " реквизиты неизвестны — добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list);" -ForegroundColor Yellow
|
||||
Write-Host " либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||
param([string]$Text, [string[]]$Secrets)
|
||||
@@ -153,12 +277,23 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
'--user', '--password'
|
||||
)
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||
|
||||
function Test-ArgKeyMatch {
|
||||
@@ -203,6 +338,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -452,6 +595,73 @@ function Find-SilentRejections {
|
||||
}
|
||||
|
||||
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -475,6 +685,16 @@ if (-not (Test-Path $ConfigDir)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Перечислены файлы — загрузка частичная. Иначе список молча игнорировался бы, а умолчание Full
|
||||
# заменило бы всю конфигурацию базы.
|
||||
if ($Files -or $ListFile) {
|
||||
if ($Mode -eq "Full") {
|
||||
Write-Host "[note] перечислены файлы — загружаются только они; -Mode Full не применён" -ForegroundColor Yellow
|
||||
}
|
||||
$Mode = "Partial"
|
||||
}
|
||||
if (-not $Mode) { $Mode = "Full" }
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
||||
@@ -494,7 +714,7 @@ try {
|
||||
}
|
||||
if ($AllExtensions) {
|
||||
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
|
||||
} elseif ($Mode -eq "Partial") {
|
||||
# partial: import specific files (relative to ConfigDir)
|
||||
$fileList = @()
|
||||
if ($ListFile) {
|
||||
@@ -551,21 +771,36 @@ try {
|
||||
}
|
||||
Write-PlatformOutput $applyOut
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
exit $exitCode
|
||||
}
|
||||
|
||||
# --- 1cv8 branch ---
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$connArgs += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
|
||||
@@ -631,7 +866,7 @@ try {
|
||||
$arguments += $extraArgs
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||
$exitCode = $__v8.ExitCode
|
||||
|
||||
@@ -660,6 +895,7 @@ try {
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
Write-PlatformOutput $__v8.Output
|
||||
Write-RepositoryHints $logContent
|
||||
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
@@ -670,6 +906,11 @@ try {
|
||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
# Расширение могло загрузиться «успешно» и при этом остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-xml v1.21 — Load 1C configuration from XML files
|
||||
# db-load-xml v1.28 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,15 +67,132 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
def write_repository_hints(log_text):
|
||||
if not log_text:
|
||||
return
|
||||
if "текущая конфигурация помещена в хранилище" in log_text:
|
||||
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
|
||||
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
|
||||
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
|
||||
obj = m.group(1)
|
||||
if obj == "Configuration":
|
||||
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
|
||||
print(' /db-repo lock <база> -Objects "Конфигурация"')
|
||||
else:
|
||||
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
|
||||
if "Соединение с хранилищем конфигурации не установлено" in log_text:
|
||||
print("[hint] соединение с хранилищем не установлено. Две причины:")
|
||||
print(' реквизиты неизвестны — добавьте "repository" в запись базы в .v8-project.json (см. /db-list);')
|
||||
print(" либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт.")
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -120,15 +237,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +319,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +366,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +404,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +421,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +440,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -376,6 +517,76 @@ def find_silent_rejections(log_text):
|
||||
return found
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -384,7 +595,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -438,11 +649,14 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
||||
parser.add_argument(
|
||||
"-Mode",
|
||||
default="Full",
|
||||
choices=["Full", "Partial"],
|
||||
default="",
|
||||
choices=["", "Full", "Partial"],
|
||||
help="Load mode (default: Full)",
|
||||
)
|
||||
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
|
||||
@@ -461,6 +675,8 @@ def main():
|
||||
action="store_true",
|
||||
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
||||
)
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -495,34 +711,42 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate config dir ---
|
||||
if not os.path.exists(args.ConfigDir):
|
||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
||||
print(f"Error: config directory not found: {args.ConfigDir}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
# Перечислены файлы — загрузка частичная. Иначе список молча игнорировался бы, а умолчание
|
||||
# Full заменило бы всю конфигурацию базы.
|
||||
if args.Files or args.ListFile:
|
||||
if args.Mode == "Full":
|
||||
print("[note] перечислены файлы — загружаются только они; -Mode Full не применён")
|
||||
args.Mode = "Partial"
|
||||
if not args.Mode:
|
||||
args.Mode = "Full"
|
||||
if args.Mode == "Partial" and not args.Files and not args.ListFile:
|
||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||
print("Error: -Files or -ListFile required for Partial mode")
|
||||
sys.exit(1)
|
||||
|
||||
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||
if engine == "ibcmd":
|
||||
if args.Format == "Plain":
|
||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||
sys.exit(1)
|
||||
if args.AllExtensions:
|
||||
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||
elif args.Mode == "Partial" or args.Files or args.ListFile:
|
||||
elif args.Mode == "Partial":
|
||||
# partial: import specific files (relative to ConfigDir)
|
||||
if args.ListFile:
|
||||
if not os.path.isfile(args.ListFile):
|
||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||
print(f"Error: list file not found: {args.ListFile}")
|
||||
sys.exit(1)
|
||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||
file_list = [ln.strip() for ln in f if ln.strip()]
|
||||
@@ -531,7 +755,7 @@ def main():
|
||||
else:
|
||||
file_list = []
|
||||
if not file_list:
|
||||
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
|
||||
print("Error: -Files or -ListFile required for partial import")
|
||||
sys.exit(1)
|
||||
arguments = ["infobase", "config", "import", "files"] + file_list
|
||||
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||
@@ -553,7 +777,7 @@ def main():
|
||||
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||
if result.returncode != 0:
|
||||
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
sys.exit(result.returncode)
|
||||
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||
exit_code = 0
|
||||
@@ -571,8 +795,19 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Database configuration updated successfully")
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
print_platform_output(ar)
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -581,17 +816,25 @@ def main():
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
else:
|
||||
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||
conn_args += ["/F", f'"{args.InfoBasePath}"']
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
conn_args.extend(repository_args(repo))
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||
|
||||
@@ -603,7 +846,7 @@ def main():
|
||||
# Build list file
|
||||
if args.ListFile:
|
||||
if not os.path.isfile(args.ListFile):
|
||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||
print(f"Error: list file not found: {args.ListFile}")
|
||||
sys.exit(1)
|
||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||
raw_list = [ln.strip() for ln in f if ln.strip()]
|
||||
@@ -615,12 +858,12 @@ def main():
|
||||
support_files = [x for x in raw_list if support_re.search(x)]
|
||||
file_list = [x for x in raw_list if not support_re.search(x)]
|
||||
if support_files:
|
||||
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
|
||||
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):")
|
||||
for sf in support_files:
|
||||
print(f" - {sf}", file=sys.stderr)
|
||||
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
|
||||
print(f" - {sf}")
|
||||
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.")
|
||||
if not file_list:
|
||||
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
|
||||
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.")
|
||||
sys.exit(1)
|
||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||
@@ -652,7 +895,7 @@ def main():
|
||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
|
||||
@@ -677,7 +920,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Load completed successfully")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
@@ -685,6 +928,7 @@ def main():
|
||||
print("--- End ---")
|
||||
|
||||
print_platform_output(result)
|
||||
write_repository_hints(log_content)
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
|
||||
@@ -701,6 +945,12 @@ def main():
|
||||
if args.StrictLog and exit_code == 0:
|
||||
exit_code = 1
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
name: db-repo
|
||||
description: Работа с хранилищем конфигурации 1С. Используй когда нужно захватить объекты, поместить изменения в хранилище конфигурации, получить изменения из него, подключить базу к хранилищу
|
||||
argument-hint: <lock|unlock|commit|update> [database] -Objects "<объекты>"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-repo — Хранилище конфигурации 1С
|
||||
|
||||
Захват и помещение объектов, получение изменений, подключение базы, история версий,
|
||||
администрирование хранилища.
|
||||
|
||||
> Хранилище конфигурации 1С, а не Git-репозиторий.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-repo lock [database] -Objects "Справочник.Номенклатура"
|
||||
/db-repo commit [database] -Objects "Справочник.Номенклатура" -Comment "Добавлен Артикул"
|
||||
/db-repo unlock [database] -Objects "Справочник.Номенклатура"
|
||||
/db-repo update [database]
|
||||
```
|
||||
|
||||
## Порядок работы
|
||||
|
||||
В базу, подключённую к хранилищу, исходники грузятся **только частично** и **только по захваченным**
|
||||
объектам. Выполняй строго по шагам:
|
||||
|
||||
```
|
||||
0. /db-repo update <база> — начать с актуального состояния
|
||||
1. /db-repo lock <база> -Objects "Справочник.Номенклатура"
|
||||
2. если шаг 0 или 1 напечатал «локальная конфигурация изменена, получено объектов из хранилища: N» —
|
||||
выгрузи названные объекты: /db-dump-xml <база> <каталог> -Mode Partial -ObjectsFile "<файл из вывода>"
|
||||
3. правки в исходниках: /meta-edit, /form-edit, /skd-edit, /meta-compile и т. д.
|
||||
4. /db-load-xml <каталог> <база> -Mode Partial -Files "Catalogs/Номенклатура.xml,…" -UpdateDB
|
||||
5. /db-repo commit <база> -Objects "Справочник.Номенклатура" -Comment "…"
|
||||
```
|
||||
|
||||
Шаг 0 стоит делать всегда, когда работа не продолжается сразу после предыдущего цикла: правки
|
||||
должны опираться на актуальное состояние — в том числе тех объектов, которые ты не меняешь, но
|
||||
используешь.
|
||||
|
||||
Шаг 2 пропускать нельзя: захват и обновление подтягивают из хранилища свежие версии, и загрузка
|
||||
исходников, снятых раньше, откатит чужие изменения — молча, без ошибки.
|
||||
|
||||
**Что вообще захватывается.** Отдельные объекты хранилища — сам объект, а также его **формы,
|
||||
макеты и команды**. Реквизиты, табличные части, измерения и ресурсы отдельными объектами **не
|
||||
являются**: они правятся в составе владельца.
|
||||
|
||||
| Что правишь | Что захватывать |
|
||||
|-------------|-----------------|
|
||||
| Реквизит, табличную часть, измерение, ресурс, модуль объекта | сам объект: `Справочник.Контрагенты` |
|
||||
| Существующую форму, макет, команду | её саму: `Справочник.Контрагенты.Форма.ФормаЭлемента` |
|
||||
| Добавляешь новую форму, макет, команду | объект-владельца; при помещении назови и новый объект |
|
||||
| Добавляешь новый объект конфигурации | только корень: `Конфигурация`. Самого объекта ещё нет — захватить его нельзя; при помещении назови и его |
|
||||
|
||||
Захватывай минимум того, что правишь: чем шире захват, тем больше конфликтов с коллегами.
|
||||
Захват объекта его формы и макеты не захватывает — для этого есть `-WithChildren`.
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
1. Если пользователь указал параметры подключения — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
|
||||
Реквизиты хранилища передавать не нужно: запись базы находится по переданным параметрам
|
||||
соединения (`-InfoBasePath` либо `-InfoBaseServer` + `-InfoBaseRef`), реквизиты берутся из её
|
||||
`repository`. Задать их явно можно параметрами `-Repository*`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command <подкоманда> <параметры>
|
||||
```
|
||||
|
||||
### Рабочий цикл
|
||||
|
||||
| Подкоманда | Что делает |
|
||||
|------------|------------|
|
||||
| `lock` | Захватить объекты |
|
||||
| `unlock` | Отменить захват |
|
||||
| `commit` | Поместить изменения в хранилище |
|
||||
| `update` | Получить изменения из хранилища |
|
||||
|
||||
### Параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Пользователь базы |
|
||||
| `-Password <пароль>` | нет | Пароль пользователя базы |
|
||||
| `-Objects <список>` | усл. | Объекты через запятую. Для `lock`, `unlock`, `commit` обязателен, если не задан `-All` |
|
||||
| `-ObjectsFile <путь>` | нет | Файл со списком объектов, одно имя на строку |
|
||||
| `-All` | нет | Операция над всей конфигурацией — вместо `-Objects`, а не вместе с ним |
|
||||
| `-WithChildren` | нет | Вместе с подчинёнными объектами на полную глубину |
|
||||
| `-Comment <текст>` | нет | Комментарий к помещению (`commit`). Многострочный — как есть, с переводами строк |
|
||||
| `-KeepLocked` | нет | Оставить объекты захваченными после помещения |
|
||||
| `-Revised` | нет | Получать захваченные объекты, если потребуется |
|
||||
| `-Force` | нет | Разное по подкомандам — см. ниже |
|
||||
| `-Extension <имя>` | нет | Работать с хранилищем расширения |
|
||||
| `-RepositoryPath <путь>` | нет | Хранилище явно, вместо реестра |
|
||||
| `-RepositoryUser <имя>` | нет | Пользователь хранилища явно |
|
||||
| `-RepositoryPassword <пароль>` | нет | Пароль пользователя хранилища явно |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### `-Force`
|
||||
|
||||
| Подкоманда | Что делает |
|
||||
|------------|------------|
|
||||
| `unlock` | **Теряет локальные правки**: объекты перезаписываются версией из хранилища |
|
||||
| `commit` | Пытается очистить ссылки на удалённые объекты вместо ошибки |
|
||||
| `update` | Подтверждает добавление и удаление объектов конфигурации |
|
||||
|
||||
### Имена объектов
|
||||
|
||||
Объект — `Справочник.Номенклатура`. Форма, макет, команда — полным путём:
|
||||
`Документ.ЗаказПокупателя.Форма.ФормаДокумента`, `Справочник.Номенклатура.Макет.Печать`.
|
||||
Корень конфигурации — `Конфигурация`.
|
||||
|
||||
Если объект «не найден», это не всегда опечатка: он мог появиться в хранилище позже, чем
|
||||
обновлялась база (`/db-repo update`), либо это вовсе не объект хранилища — реквизит или
|
||||
табличная часть.
|
||||
|
||||
## Результат
|
||||
|
||||
Нулевой код не означает, что что-то изменилось. Под нулём приходят «уже захвачено», «обновлять
|
||||
нечего», «помещать нечего» и частичный захват — когда часть объектов занята другими, а остальное
|
||||
захвачено и его можно править.
|
||||
|
||||
**Читай текст вывода, а не только код.** Там же приходит список полученных из хранилища объектов,
|
||||
который требует перевыгрузки перед правкой.
|
||||
|
||||
## Требуют подтверждения пользователя
|
||||
|
||||
Перед этими операциями **спроси подтверждение**:
|
||||
|
||||
| Операция | Почему |
|
||||
|----------|--------|
|
||||
| `lock -All` | Захватывает **всю конфигурацию**: на большой базе идёт долго и блокирует работу всей команде |
|
||||
| `unlock -Force` | Теряются локальные правки захваченных объектов |
|
||||
| `disconnect` | Теряется подключение базы к хранилищу, в том числе на стороне хранилища |
|
||||
| `connect -ForceReplaceCfg` | Конфигурация базы заменяется конфигурацией из хранилища |
|
||||
|
||||
`update` не выполнится, если у базы в реестре не объявлено `repository`, а реквизиты не заданы
|
||||
явно: на неподключённой к хранилищу базе эта команда заменяет всю конфигурацию его содержимым и
|
||||
рапортует успех.
|
||||
|
||||
## Расширения
|
||||
|
||||
У расширения своё хранилище со своим путём. Укажи `-Extension "<Имя>"` — реквизиты возьмутся из
|
||||
`extensions[].repository` записи базы. Подкоманды работают одинаково для основной конфигурации и
|
||||
для расширения.
|
||||
|
||||
## Остальные задачи
|
||||
|
||||
| Файл | Про что |
|
||||
|------|---------|
|
||||
| [connect.md](references/connect.md) | Подключение и отключение базы от хранилища |
|
||||
| [history.md](references/history.md) | История версий, отчёт, выгрузка версии в CF |
|
||||
| [admin.md](references/admin.md) | Создание хранилища, пользователи и права |
|
||||
| [service.md](references/service.md) | Метки версий, оптимизация, очистка кеша |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Захватить справочник вместе с подчинёнными объектами
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren
|
||||
|
||||
# Захватить корень — он нужен, чтобы добавить или удалить объект конфигурации
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация"
|
||||
|
||||
# Поместить новый объект: он уже существует, поэтому называется вместе с корнем
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады" -Comment "Добавлен справочник Склады"
|
||||
|
||||
# Поместить с комментарием, оставив захват
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked
|
||||
|
||||
# Получить изменения из хранилища
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command update -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
# Серверная база, расширение
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура"
|
||||
```
|
||||
|
||||
## После выполнения
|
||||
|
||||
- `lock` или `update` сообщил о полученных объектах — выполни `/db-dump-xml -Mode Partial` с
|
||||
указанным в выводе файлом, и только потом правь исходники
|
||||
- после `lock` правки идут через `/db-load-xml -Mode Partial` и `/db-update`
|
||||
- изменения готовы — предложи `/db-repo commit` с комментарием
|
||||
@@ -0,0 +1,45 @@
|
||||
# Администрирование хранилища
|
||||
|
||||
## create — создать хранилище
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-NoBind` | Не подключать базу к созданному хранилищу |
|
||||
| `-AllowConfigurationChanges` | Включить возможность изменения, если конфигурация на поддержке без неё |
|
||||
| `-ChangesAllowedRule <правило>` | Правило для объектов, изменения которых разрешены поставщиком |
|
||||
| `-ChangesNotRecommendedRule <правило>` | То же для «изменения не рекомендуются» |
|
||||
|
||||
Правила: `ObjectNotEditable`, `ObjectIsEditableSupportEnabled`, `ObjectNotSupported`.
|
||||
|
||||
Без `-NoBind` база сразу подключается к созданному хранилищу. Создание — это версия 1.
|
||||
|
||||
Для расширения: `-Extension "<Имя>"` и отдельный путь — у расширения своё хранилище.
|
||||
|
||||
## add-user — создать пользователя
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects
|
||||
```
|
||||
|
||||
| Право | Что даёт |
|
||||
|-------|----------|
|
||||
| `ReadOnly` | Просмотр |
|
||||
| `LockObjects` | Захват объектов |
|
||||
| `ManageConfigurationVersions` | Изменение состава версий |
|
||||
| `Administration` | Административные функции |
|
||||
|
||||
`-RestoreDeletedUser` — восстановить одноимённого удалённого. Если пользователь с таким именем
|
||||
существует, он **не** будет добавлен. Выполняющий должен иметь административные права.
|
||||
|
||||
## copy-users — скопировать пользователей из другого хранилища
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…"
|
||||
```
|
||||
|
||||
`-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи
|
||||
не копируются; существующие не перезаписываются.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Подключение базы к хранилищу
|
||||
|
||||
## connect — подключить
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-ForceReplaceCfg` | Конфигурация базы непустая — подтвердить замену её конфигурацией из хранилища. **Спроси подтверждение у пользователя** |
|
||||
| `-ForceBindAlreadyBindedUser` | Подключить, даже если у этого пользователя уже есть конфигурация, связанная с хранилищем |
|
||||
|
||||
На пустой базе `-ForceReplaceCfg` не нужен.
|
||||
|
||||
**Переподключение** базы, которая уже была подключена, требует обоих флагов: конфигурация в базе
|
||||
не пустая (`-ForceReplaceCfg`), а за пользователем хранилища всё ещё числится эта база
|
||||
(`-ForceBindAlreadyBindedUser`).
|
||||
|
||||
После подключения добавь `repository` в запись базы в `.v8-project.json` — иначе остальные
|
||||
подкоманды придётся каждый раз звать с явными реквизитами, а `update` откажется работать.
|
||||
|
||||
## disconnect — отключить
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command disconnect -InfoBasePath "C:\Bases\MyDB"
|
||||
```
|
||||
|
||||
**Спроси подтверждение у пользователя.** Отключение снимает связь и на стороне самого хранилища:
|
||||
запись о подключении удаляется. Подключить базу обратно можно, но это уже не рядовая операция —
|
||||
понадобятся оба флага `connect` из раздела выше.
|
||||
|
||||
Если в базе есть захваченные и изменённые объекты, операция не выполнится. `-Force` выполняет её
|
||||
всё равно, и эти изменения теряются.
|
||||
|
||||
## Расширения
|
||||
|
||||
У расширения своё хранилище: `-Extension "<Имя>"` указывай вместе с путём именно к нему, а не
|
||||
к хранилищу основной конфигурации.
|
||||
@@ -0,0 +1,31 @@
|
||||
# История версий хранилища
|
||||
|
||||
## report — отчёт по версиям
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-OutputFile <путь>` | Куда сохранить отчёт. Необязателен |
|
||||
| `-NBegin <номер>` | С какой версии. `-1` — только последняя |
|
||||
| `-NEnd <номер>` | По какую версию |
|
||||
| `-DateBegin` / `-DateEnd` | Границы по датам |
|
||||
| `-GroupByObject` | Группировать по объектам |
|
||||
| `-GroupByComment` | Группировать по комментарию |
|
||||
| `-ReportFormat <txt\|mxl>` | По умолчанию `txt` |
|
||||
|
||||
`txt` — с разделителем-табуляцией, разбирается построчно.
|
||||
|
||||
> На боевом хранилище полный отчёт строить не надо — тысячи версий. Нужна головная
|
||||
> версия — `-NBegin -1`. Длинный отчёт в вывод не печатается: сузьте выборку
|
||||
> параметрами ниже.
|
||||
|
||||
## dump-cfg — выгрузить версию в CF
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120
|
||||
```
|
||||
|
||||
Без `-Version` (или при `-1`) выгружается последняя версия.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Сервисные операции
|
||||
|
||||
## set-label — метка на версию
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест"
|
||||
```
|
||||
|
||||
Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка.
|
||||
|
||||
## optimize — оптимизация хранения
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command optimize -InfoBasePath "C:\Bases\MyDB"
|
||||
```
|
||||
|
||||
Оптимизирует хранение данных в хранилище. Операция долгая.
|
||||
|
||||
## clear-cache — очистка кеша
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local
|
||||
```
|
||||
|
||||
| `-CacheScope` | Что чистит |
|
||||
|---------------|------------|
|
||||
| `local` (по умолчанию) | Локальный кеш версий конфигурации |
|
||||
| `global` | Глобальный кеш версий |
|
||||
| `db` | Локальную базу данных хранилища |
|
||||
|
||||
Пригождается, когда хранилище ведёт себя странно после сбоя сети или отката версии.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# db-run v1.8 — Launch 1C:Enterprise
|
||||
# db-run v1.10 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -52,7 +52,7 @@
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -107,6 +107,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -157,6 +168,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-run v1.8 — Launch 1C:Enterprise
|
||||
# db-run v1.10 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -64,6 +64,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -117,15 +128,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -193,14 +210,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -225,7 +240,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -260,14 +275,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -327,7 +342,7 @@ def main():
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Build arguments ---
|
||||
@@ -377,7 +392,7 @@ def main():
|
||||
time.sleep(0.2)
|
||||
rc = proc.poll()
|
||||
if rc is not None:
|
||||
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
|
||||
print(f"Error: 1C:Enterprise exited immediately (code: {rc})")
|
||||
sys.exit(rc if rc and rc > 0 else 1)
|
||||
print(f"PID: {proc.pid}")
|
||||
print("1C:Enterprise launched")
|
||||
|
||||
@@ -11,14 +11,16 @@ allowed-tools:
|
||||
|
||||
# /db-update — Обновление конфигурации БД
|
||||
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`) —
|
||||
отдельным шагом после загрузки. У `/db-load-xml` и `/db-load-git` то же самое делает
|
||||
ключ `-UpdateDB`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-update [database]
|
||||
/db-update dev
|
||||
/db-update dev -Dynamic+
|
||||
/db-update dev -Dynamic on
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
@@ -50,7 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-Extension <имя>` | нет | Обновить расширение |
|
||||
| `-AllExtensions` | нет | Обновить все расширения |
|
||||
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после обновления |
|
||||
| `-Dynamic <on/off>` | нет | `on` — динамическое обновление, без монопольного доступа к базе; `off` — отключить |
|
||||
| `-Server` | нет | Обновление на стороне сервера |
|
||||
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
@@ -68,20 +71,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
||||
| `-BackgroundSuspend` | Приостановить |
|
||||
| `-BackgroundResume` | Возобновить |
|
||||
|
||||
## Предупреждения
|
||||
|
||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
|
||||
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обычное обновление (файловая база)
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||
|
||||
# Динамическое обновление (серверная база)
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||
# Динамическое обновление
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic on
|
||||
|
||||
# Обновление расширения
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-update v1.15 — Update 1C database configuration
|
||||
# db-update v1.20 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -34,7 +34,7 @@
|
||||
Обновить все расширения
|
||||
|
||||
.PARAMETER Dynamic
|
||||
Динамическое обновление: "+" включить, "-" отключить
|
||||
Динамическое обновление: on включить, off отключить
|
||||
|
||||
.PARAMETER Server
|
||||
Обновление на стороне сервера
|
||||
@@ -55,7 +55,7 @@
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -81,8 +81,10 @@ param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
# on/off, а не +/-: значение "-" через powershell.exe -File парсер не связывает и молча
|
||||
# выходит с кодом 2, без единого сообщения. "+"/"-" принимаются, но в инструкции не значатся.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("+", "-")]
|
||||
[ValidateSet("on", "off", "yes", "no", "+", "-")]
|
||||
[string]$Dynamic,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
@@ -97,6 +99,19 @@ param(
|
||||
# но в логе есть отбраковка.
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryUser,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPassword,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -104,9 +119,95 @@ param(
|
||||
[string[]]$AdditionalIbcmdArguments = @()
|
||||
)
|
||||
|
||||
if ($Dynamic) { $Dynamic = if (@('on', 'yes', '+') -contains $Dynamic.ToLower()) { '+' } else { '-' } }
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||
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 Test-SamePath {
|
||||
param([string]$A, [string]$B)
|
||||
if (-not $A -or -not $B) { return $false }
|
||||
try {
|
||||
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
function Find-ProjectDatabase {
|
||||
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if (-not $pf) { return $null }
|
||||
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||
if (-not $proj.databases) { return $null }
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-RepositorySettings {
|
||||
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||
$dbRec = Find-ProjectDatabase
|
||||
$rec = $null
|
||||
if ($dbRec) {
|
||||
if ($Extension) {
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
if ($dbRec.extensions) {
|
||||
foreach ($ext in $dbRec.extensions) {
|
||||
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$rec = $ext.repository
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$rec = $dbRec.repository
|
||||
}
|
||||
}
|
||||
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||
return @{
|
||||
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||
User = $user
|
||||
Password = $pwd
|
||||
FromRegistry = [bool]($rec -and $rec.path)
|
||||
DbRecord = $dbRec
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepositoryArgs {
|
||||
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||
param([hashtable]$Repo)
|
||||
$a = @()
|
||||
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return $a
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||
param([string]$Text, [string[]]$Secrets)
|
||||
@@ -140,12 +241,23 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
'--user', '--password'
|
||||
)
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||
|
||||
function Test-ArgKeyMatch {
|
||||
@@ -190,6 +302,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -437,6 +557,73 @@ function Find-SilentRejections {
|
||||
}
|
||||
|
||||
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -482,22 +669,37 @@ try {
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
Write-PlatformOutput $output
|
||||
exit $exitCode
|
||||
}
|
||||
|
||||
# --- 1cv8 branch ---
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$connArgs += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/UpdateDBCfg"
|
||||
|
||||
@@ -526,7 +728,7 @@ try {
|
||||
$arguments += $extraArgs
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||
$exitCode = $__v8.ExitCode
|
||||
|
||||
@@ -558,6 +760,11 @@ try {
|
||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-update v1.15 — Update 1C database configuration
|
||||
# db-update v1.20 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,15 +67,111 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -120,15 +216,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +298,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +345,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +383,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +400,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +419,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -376,6 +496,76 @@ def find_silent_rejections(log_text):
|
||||
return found
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -384,7 +574,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -438,15 +628,22 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||
# on/off, а не +/-: значение "-" через powershell.exe -File парсер PS не связывает и молча
|
||||
# выходит с кодом 2. "+"/"-" принимаются, но в инструкции не значатся.
|
||||
parser.add_argument("-Dynamic", default="", choices=["", "on", "off", "yes", "no", "+", "-"])
|
||||
parser.add_argument("-Server", action="store_true")
|
||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но в логе есть отбраковка.
|
||||
parser.add_argument("-StrictLog", action="store_true")
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -455,6 +652,9 @@ def main():
|
||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||
args = ci_parse_args(parser, argv)
|
||||
|
||||
if args.Dynamic:
|
||||
args.Dynamic = "+" if args.Dynamic.lower() in ("on", "yes", "+") else "-"
|
||||
|
||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||
assert_infobase_exists(args.InfoBasePath)
|
||||
@@ -478,16 +678,16 @@ def main():
|
||||
# --- Validate connection ---
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||
sys.exit(1)
|
||||
|
||||
# --- ibcmd branch (file infobase only) ---
|
||||
if engine == "ibcmd":
|
||||
if args.AllExtensions:
|
||||
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)")
|
||||
sys.exit(1)
|
||||
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||
if args.Dynamic == "+":
|
||||
@@ -509,7 +709,19 @@ def main():
|
||||
if result.returncode == 0:
|
||||
print("Database configuration updated successfully")
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
exit_code = result.returncode
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -518,17 +730,25 @@ def main():
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
else:
|
||||
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
conn_args.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
conn_args.extend(repository_args(repo))
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments.append("/UpdateDBCfg")
|
||||
|
||||
@@ -553,7 +773,7 @@ def main():
|
||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
|
||||
@@ -561,7 +781,7 @@ def main():
|
||||
if exit_code == 0:
|
||||
print("Database configuration updated successfully")
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
|
||||
log_content = ""
|
||||
if os.path.isfile(out_file):
|
||||
@@ -591,6 +811,12 @@ def main():
|
||||
if args.StrictLog and exit_code == 0:
|
||||
exit_code = 1
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -25,7 +25,8 @@ allowed-tools:
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
|
||||
под проверку исходников, если она не отключена.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
@@ -55,11 +56,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
||||
| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` |
|
||||
| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Проверка перед сборкой
|
||||
|
||||
Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм.
|
||||
Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение
|
||||
платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off`
|
||||
или ключом `"externalCheck": false` в `.v8-project.json`.
|
||||
|
||||
Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -46,7 +46,7 @@
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -72,6 +72,15 @@ param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile,
|
||||
|
||||
# Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers,
|
||||
# unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Checks,
|
||||
|
||||
# Контексты синтаксической проверки. По умолчанию ThinClient,Server.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Context,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -98,6 +107,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -148,6 +168,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -369,6 +397,122 @@ function Test-OutputNonEmpty {
|
||||
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# --- Проверка исходников платформой ---
|
||||
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
|
||||
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
|
||||
# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она
|
||||
# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create
|
||||
# -EmbedSourceFile) и спрашивается штатной /CheckConfig.
|
||||
#
|
||||
# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке
|
||||
# DESIGNER выполняет лишь последнюю пакетную команду.
|
||||
function Get-CheckFlags {
|
||||
param([string[]]$Checks, [string[]]$Contexts)
|
||||
$flags = @()
|
||||
if ($Checks -contains 'modules') { foreach ($c in $Contexts) { $flags += "-$c" } }
|
||||
if ($Checks -contains 'handlers') { $flags += '-HandlersExistence' }
|
||||
if ($Checks -contains 'unreferenced') { $flags += '-UnreferenceProcedures' }
|
||||
if ($Checks -contains 'empty-handlers') { $flags += '-EmptyHandlers' }
|
||||
if ($Checks -contains 'config') { $flags += '-ConfigLogIntegrity', '-IncorrectReferences' }
|
||||
return $flags
|
||||
}
|
||||
|
||||
# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику.
|
||||
# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл:
|
||||
# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет.
|
||||
function Resolve-SourcePath {
|
||||
param([string]$Line, [string]$SourceDir)
|
||||
$candidate = $null
|
||||
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(?:Форма|Form)\.([^.]+)\.')
|
||||
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value (Join-Path "Forms" (Join-Path $m.Groups[2].Value "Ext\Form\Module.bsl")))) }
|
||||
if (-not $candidate) {
|
||||
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульОбъекта|ObjectModule)')
|
||||
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ObjectModule.bsl")) }
|
||||
}
|
||||
if (-not $candidate) {
|
||||
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульМенеджера|ManagerModule)')
|
||||
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ManagerModule.bsl")) }
|
||||
}
|
||||
if ($candidate -and (Test-Path $candidate -PathType Leaf)) { return $candidate }
|
||||
return $null
|
||||
}
|
||||
|
||||
# $true, если платформа нашла проблемы — вызывающий не собирает артефакт.
|
||||
function Invoke-SourceCheck {
|
||||
param([string]$Exe, [string]$BasePath, [string[]]$Flags, [string]$SourceDir, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) {
|
||||
Write-Host "[note] source check skipped: 1cv8 not found at $v8" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
$dir = Join-Path $env:TEMP "epf_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a = @("DESIGNER", "/F", "`"$BasePath`"", "/CheckConfig") + $Flags + @("/Out", "`"$outFile`"", "/DisableStartupDialogs") + $ExtraArgs
|
||||
Write-Host "Running: 1cv8.exe $($a -join ' ')"
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | Where-Object { $_.Trim() -ne '' }) }
|
||||
}
|
||||
# Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0.
|
||||
if ($res.ExitCode -eq 0) { return $false }
|
||||
Write-Host "Error: платформа нашла проблемы в исходниках — сборка отменена" -ForegroundColor Red
|
||||
# Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя.
|
||||
if ($lines.Count -eq 0) { Write-Host " платформа вернула код $($res.ExitCode) без сообщений" -ForegroundColor Red }
|
||||
foreach ($l in $lines) {
|
||||
Write-Host " $($l.TrimEnd())" -ForegroundColor Red
|
||||
$srcPath = Resolve-SourcePath $l $SourceDir
|
||||
if ($srcPath) { Write-Host " -> $srcPath" -ForegroundColor Red }
|
||||
}
|
||||
return $true
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck.
|
||||
function Get-SourceCheckList {
|
||||
param([string]$Checks)
|
||||
$known = @('modules', 'handlers', 'unreferenced', 'empty-handlers', 'config')
|
||||
if ($Checks) {
|
||||
$list = @($Checks -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
|
||||
if ($list -contains 'off') { return @() }
|
||||
foreach ($c in $list) {
|
||||
if ($known -notcontains $c) {
|
||||
Write-Host "Error: unknown check '$c' (expected: $($known -join ', ') or off)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
return $list
|
||||
}
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.externalCheck -and -not [bool]$proj.externalCheck) { return @() }
|
||||
} catch {}
|
||||
}
|
||||
return @('modules', 'handlers')
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -379,33 +523,69 @@ if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
# --- Что проверяем в исходниках перед сборкой ---
|
||||
$checkList = @(Get-SourceCheckList $Checks)
|
||||
$contextList = @()
|
||||
if ($Context) { $contextList = @($Context -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
|
||||
if ($contextList.Count -eq 0) { $contextList = @('ThinClient', 'Server') }
|
||||
elseif ($checkList.Count -gt 0 -and $checkList -notcontains 'modules') {
|
||||
Write-Host "Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
|
||||
function New-StubBase {
|
||||
# Стаб запускает свои процессы платформы (CREATEINFOBASE, LoadConfigFromFiles, UpdateDBCfg) —
|
||||
# им нужны те же дополнительные аргументы, что и сборке. Передаются только явные: файл проекта
|
||||
# стаб читает сам. Вызов через -Command, не -File: -File берёт хвост буквально, и массивный
|
||||
# параметр пришёл бы одним склеенным токеном.
|
||||
param([string]$BasePath, [switch]$Embed)
|
||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||
# Invoked via -Command, not -File: -File takes the tail literally, so an array
|
||||
# parameter would arrive as a single comma-glued token.
|
||||
$q = { param($s) "'" + ($s -replace "'", "''") + "'" }
|
||||
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)"
|
||||
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $BasePath)"
|
||||
if ($Embed) { $stubCmd += " -EmbedSourceFile $(& $q $SourceFile)" }
|
||||
if ($AdditionalV8Arguments.Count -gt 0) {
|
||||
$stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',')
|
||||
}
|
||||
if ($AdditionalIbcmdArguments.Count -gt 0) {
|
||||
$stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',')
|
||||
}
|
||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
|
||||
if ($stubProc.ExitCode -ne 0) {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
$p = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
|
||||
return $p.ExitCode
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
$checkBase = $null
|
||||
$checkBasePath = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
if ((New-StubBase $autoBasePath -Embed:($checkList.Count -gt 0)) -ne 0) {
|
||||
# С внедрённой обработкой база падает прежде всего из-за самих исходников
|
||||
# (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда.
|
||||
if ($checkList.Count -gt 0) {
|
||||
Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red
|
||||
Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red
|
||||
} else {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
$InfoBasePath = $autoBasePath
|
||||
$autoCreatedBase = $autoBasePath
|
||||
if ($checkList.Count -gt 0) { $checkBasePath = $autoBasePath }
|
||||
} elseif ($checkList.Count -gt 0) {
|
||||
# Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под
|
||||
# проверку поднимается своя временная база, а сборка идёт на указанной.
|
||||
$checkBase = Join-Path $env:TEMP "epf_check_db_$(Get-Random)"
|
||||
Write-Host "Creating temporary database for the source check..."
|
||||
if ((New-StubBase $checkBase -Embed) -ne 0) {
|
||||
Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red
|
||||
Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$checkBasePath = $checkBase
|
||||
}
|
||||
|
||||
# --- Validate source file ---
|
||||
@@ -414,6 +594,18 @@ if (-not (Test-Path $SourceFile)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен ---
|
||||
if ($checkList.Count -gt 0 -and $checkBasePath) {
|
||||
# Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём.
|
||||
$checkExtra = if ($engine -eq "ibcmd") { @() } else { $extraArgs }
|
||||
$found = Invoke-SourceCheck $V8Path $checkBasePath (Get-CheckFlags $checkList $contextList) $sourceDir $checkExtra
|
||||
if ($found) {
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if ($checkBase -and (Test-Path $checkBase)) { Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
$outDir = Split-Path $OutputFile -Parent
|
||||
if ($outDir -and -not (Test-Path $outDir)) {
|
||||
@@ -507,4 +699,7 @@ try {
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
|
||||
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($checkBase -and (Test-Path $checkBase)) {
|
||||
Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
@@ -67,6 +68,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +132,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +214,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +261,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +299,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +316,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +335,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -352,7 +388,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -375,6 +411,136 @@ def _redact(text, *secrets):
|
||||
return text
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# --- Проверка исходников платформой ---
|
||||
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
|
||||
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
|
||||
# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она
|
||||
# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create
|
||||
# -EmbedSourceFile) и спрашивается штатной /CheckConfig.
|
||||
#
|
||||
# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке
|
||||
# DESIGNER выполняет лишь последнюю пакетную команду.
|
||||
def get_check_flags(checks, contexts):
|
||||
flags = []
|
||||
if 'modules' in checks:
|
||||
for c in contexts:
|
||||
flags.append('-' + c)
|
||||
if 'handlers' in checks:
|
||||
flags.append('-HandlersExistence')
|
||||
if 'unreferenced' in checks:
|
||||
flags.append('-UnreferenceProcedures')
|
||||
if 'empty-handlers' in checks:
|
||||
flags.append('-EmptyHandlers')
|
||||
if 'config' in checks:
|
||||
flags += ['-ConfigLogIntegrity', '-IncorrectReferences']
|
||||
return flags
|
||||
|
||||
|
||||
# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику.
|
||||
# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл:
|
||||
# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет.
|
||||
def resolve_source_path(line, source_dir):
|
||||
candidate = None
|
||||
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u0424\u043e\u0440\u043c\u0430|Form)\.([^.]+)\.', line)
|
||||
if m:
|
||||
candidate = os.path.join(source_dir, m.group(1), 'Forms', m.group(2), 'Ext', 'Form', 'Module.bsl')
|
||||
if candidate is None:
|
||||
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041e\u0431\u044a\u0435\u043a\u0442\u0430|ObjectModule)', line)
|
||||
if m:
|
||||
candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ObjectModule.bsl')
|
||||
if candidate is None:
|
||||
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430|ManagerModule)', line)
|
||||
if m:
|
||||
candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ManagerModule.bsl')
|
||||
if candidate and os.path.isfile(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
# True, если платформа нашла проблемы — вызывающий не собирает артефакт.
|
||||
def invoke_source_check(exe, base_path, flags, source_dir, extra_args):
|
||||
exe_dir = os.path.dirname(exe)
|
||||
exe_leaf = os.path.basename(exe)
|
||||
if exe_leaf.lower().startswith('ibcmd'):
|
||||
v8 = os.path.join(exe_dir, '1cv8' + os.path.splitext(exe)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.exists(v8):
|
||||
print(f'[note] source check skipped: 1cv8 not found at {v8}')
|
||||
return False
|
||||
d = os.path.join(tempfile.gettempdir(), f'epf_check_{random.randint(0, 999999)}')
|
||||
os.makedirs(d, exist_ok=True)
|
||||
try:
|
||||
out_file = os.path.join(d, 'check_log.txt')
|
||||
a = (['DESIGNER', '/F', f'"{base_path}"', '/CheckConfig'] + flags
|
||||
+ ['/Out', f'"{out_file}"', '/DisableStartupDialogs']
|
||||
+ [quote_if_needed(x) for x in extra_args])
|
||||
print(f'Running: 1cv8.exe {" ".join(a)}')
|
||||
result = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with io.open(out_file, encoding='utf-8-sig', errors='replace') as fh:
|
||||
raw = fh.read()
|
||||
lines = [l for l in raw.splitlines() if l.strip()]
|
||||
except Exception:
|
||||
lines = []
|
||||
# Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0.
|
||||
if result.returncode == 0:
|
||||
return False
|
||||
print('Error: \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043d\u0430\u0448\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0432 \u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0430\u0445 \u2014 \u0441\u0431\u043e\u0440\u043a\u0430 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430')
|
||||
# Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя.
|
||||
if not lines:
|
||||
print(f' платформа вернула код {result.returncode} без сообщений')
|
||||
for l in lines:
|
||||
print(f' {l.rstrip()}')
|
||||
src_path = resolve_source_path(l, source_dir)
|
||||
if src_path:
|
||||
print(f' -> {src_path}')
|
||||
return True
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck.
|
||||
def get_source_check_list(checks):
|
||||
known = ['modules', 'handlers', 'unreferenced', 'empty-handlers', 'config']
|
||||
if checks:
|
||||
lst = [c.strip().lower() for c in checks.split(',') if c.strip()]
|
||||
if 'off' in lst:
|
||||
return []
|
||||
for c in lst:
|
||||
if c not in known:
|
||||
print(f'Error: unknown check \'{c}\' (expected: {", ".join(known)} or off)')
|
||||
sys.exit(1)
|
||||
return lst
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding='utf-8-sig') as fh:
|
||||
proj = json.load(fh)
|
||||
if 'externalCheck' in proj and not proj['externalCheck']:
|
||||
return []
|
||||
except Exception:
|
||||
pass
|
||||
return ['modules', 'handlers']
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -390,6 +556,11 @@ def main():
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
||||
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
||||
# Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers,
|
||||
# unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers.
|
||||
parser.add_argument("-Checks", default="")
|
||||
# Контексты синтаксической проверки. По умолчанию ThinClient,Server.
|
||||
parser.add_argument("-Context", default="")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -420,37 +591,83 @@ def main():
|
||||
}
|
||||
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
auto_created_base = None
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||
print("No database specified. Creating temporary stub database...")
|
||||
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
|
||||
"-TempBasePath", auto_base_path]
|
||||
# --- Что проверяем в исходниках перед сборкой ---
|
||||
check_list = get_source_check_list(args.Checks)
|
||||
context_list = [c.strip() for c in args.Context.split(',') if c.strip()] if args.Context else []
|
||||
if not context_list:
|
||||
context_list = ['ThinClient', 'Server']
|
||||
elif check_list and 'modules' not in check_list:
|
||||
print('Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней')
|
||||
sys.exit(1)
|
||||
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||
|
||||
def new_stub_base(base_path, embed):
|
||||
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
|
||||
"-TempBasePath", base_path]
|
||||
if embed:
|
||||
stub_cmd += ["-EmbedSourceFile", args.SourceFile]
|
||||
if v8_extra:
|
||||
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
|
||||
if ibcmd_extra:
|
||||
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
|
||||
result = subprocess.run(stub_cmd, capture_output=False)
|
||||
if result.returncode != 0:
|
||||
print("Error: failed to create stub database", file=sys.stderr)
|
||||
return subprocess.run(stub_cmd, capture_output=False).returncode
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
auto_created_base = None
|
||||
check_base = None
|
||||
check_base_path = None
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||
print("No database specified. Creating temporary stub database...")
|
||||
if new_stub_base(auto_base_path, bool(check_list)) != 0:
|
||||
# С внедрённой обработкой база падает прежде всего из-за самих исходников
|
||||
# (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда.
|
||||
if check_list:
|
||||
print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена')
|
||||
print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт')
|
||||
else:
|
||||
print("Error: failed to create stub database")
|
||||
sys.exit(1)
|
||||
args.InfoBasePath = auto_base_path
|
||||
auto_created_base = auto_base_path
|
||||
if check_list:
|
||||
check_base_path = auto_base_path
|
||||
elif check_list:
|
||||
# Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под
|
||||
# проверку поднимается своя временная база, а сборка идёт на указанной.
|
||||
check_base = os.path.join(tempfile.gettempdir(), f"epf_check_db_{random.randint(0, 999999)}")
|
||||
print("Creating temporary database for the source check...")
|
||||
if new_stub_base(check_base, True) != 0:
|
||||
print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена')
|
||||
print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт')
|
||||
sys.exit(1)
|
||||
check_base_path = check_base
|
||||
|
||||
# --- Validate source file ---
|
||||
if not os.path.isfile(args.SourceFile):
|
||||
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
|
||||
print(f"Error: source file not found: {args.SourceFile}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен ---
|
||||
if check_list and check_base_path:
|
||||
# Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём.
|
||||
check_extra = [] if engine == "ibcmd" else extra_args
|
||||
found = invoke_source_check(v8path, check_base_path,
|
||||
get_check_flags(check_list, context_list), source_dir, check_extra)
|
||||
if found:
|
||||
if auto_created_base and os.path.exists(auto_created_base):
|
||||
shutil.rmtree(auto_created_base, ignore_errors=True)
|
||||
if check_base and os.path.exists(check_base):
|
||||
shutil.rmtree(check_base, ignore_errors=True)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
out_dir = os.path.dirname(args.OutputFile)
|
||||
if out_dir and not os.path.exists(out_dir):
|
||||
@@ -482,9 +699,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"External data processor/report built successfully: {args.OutputFile}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output")
|
||||
else:
|
||||
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error building external data processor/report (code: {exit_code})")
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Build arguments ---
|
||||
@@ -521,9 +738,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Build completed successfully: {args.OutputFile}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output")
|
||||
else:
|
||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error building (code: {exit_code})")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
@@ -544,6 +761,8 @@ def main():
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
if auto_created_base and os.path.exists(auto_created_base):
|
||||
shutil.rmtree(auto_created_base, ignore_errors=True)
|
||||
if check_base and os.path.exists(check_base):
|
||||
shutil.rmtree(check_base, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -9,6 +9,10 @@ param(
|
||||
|
||||
[string]$TempBasePath,
|
||||
|
||||
# XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа
|
||||
# смогла проверить его штатными проверками. Без параметра стаб работает как раньше.
|
||||
[string]$EmbedSourceFile,
|
||||
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
[string[]]$AdditionalIbcmdArguments = @()
|
||||
@@ -49,6 +53,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -99,6 +114,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -331,6 +354,9 @@ foreach ($f in $xmlFiles) {
|
||||
}
|
||||
|
||||
$hasRefTypes = $typeMap.Count -gt 0
|
||||
# Конфигурация нужна и тогда, когда ссылочных типов нет: в неё кладётся сам объект.
|
||||
$embedRequested = -not [string]::IsNullOrWhiteSpace($EmbedSourceFile)
|
||||
$needCfg = $hasRefTypes -or $embedRequested
|
||||
|
||||
# --- 2. Determine TempBasePath ---
|
||||
if (-not $TempBasePath) {
|
||||
@@ -351,13 +377,110 @@ if ($needsRegistrator) {
|
||||
$typeMap["Document"]["ЗаглушкаРегистратора"] = $true
|
||||
}
|
||||
|
||||
# --- Внедрение проверяемого объекта в конфигурацию-заглушку ---
|
||||
# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles
|
||||
# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а
|
||||
# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем
|
||||
# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим
|
||||
# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и
|
||||
# макеты, а формат может расти без правок здесь.
|
||||
#
|
||||
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
|
||||
# такой же текст был бы кодом, и трогать его нельзя.
|
||||
function Add-SourceObjectToConfig {
|
||||
param([string]$SourceXml, [string]$CfgDir)
|
||||
|
||||
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
|
||||
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
|
||||
# XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные
|
||||
# между её файлами (ссылки внутри объекта идут по идентификатору).
|
||||
$guidMap = @{}
|
||||
$reGuid = [regex]'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
|
||||
$reissue = {
|
||||
param($m)
|
||||
$k = $m.Value.ToLower()
|
||||
if (-not $guidMap.ContainsKey($k)) { $guidMap[$k] = [guid]::NewGuid().ToString() }
|
||||
$guidMap[$k]
|
||||
}
|
||||
|
||||
$text = [IO.File]::ReadAllText($SourceXml, [Text.Encoding]::UTF8)
|
||||
if ($text -match '<ExternalDataProcessor[\s>]') {
|
||||
$extTag = 'ExternalDataProcessor'; $cfgTag = 'DataProcessor'; $folder = 'DataProcessors'
|
||||
} elseif ($text -match '<ExternalReport[\s>]') {
|
||||
$extTag = 'ExternalReport'; $cfgTag = 'Report'; $folder = 'Reports'
|
||||
} else {
|
||||
return $null
|
||||
}
|
||||
|
||||
$name = if ($text -match '<Name>([^<]+)</Name>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($SourceXml) }
|
||||
|
||||
$conv = $reGuid.Replace($text, $reissue)
|
||||
$conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("</$extTag>", "</$cfgTag>")
|
||||
$conv = $conv.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
|
||||
|
||||
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
|
||||
# без него платформа отвечает «отсутствует один или более типов объекта».
|
||||
$mgr = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Manager.$name`" category=`"Manager`">`r`n" +
|
||||
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
|
||||
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
|
||||
"`t`t`t</xr:GeneratedType>`r`n"
|
||||
if ($conv -match '</InternalInfo>') {
|
||||
$conv = [regex]::Replace($conv, '(\s*)</InternalInfo>', ("`r`n" + $mgr + "`t`t</InternalInfo>"), 1)
|
||||
} else {
|
||||
$objType = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Object.$name`" category=`"Object`">`r`n" +
|
||||
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
|
||||
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
|
||||
"`t`t`t</xr:GeneratedType>`r`n"
|
||||
$conv = [regex]::Replace($conv, "(<$cfgTag[^>]*>)", ("`$1`r`n`t`t<InternalInfo>`r`n" + $objType + $mgr + "`t`t</InternalInfo>"), 1)
|
||||
}
|
||||
|
||||
$objDir = Join-Path $CfgDir $folder
|
||||
New-Item -ItemType Directory -Path $objDir -Force | Out-Null
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
[IO.File]::WriteAllText((Join-Path $objDir "$name.xml"), $conv, $encBom)
|
||||
|
||||
# Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт.
|
||||
$srcContent = Join-Path (Split-Path $SourceXml -Parent) $name
|
||||
if (Test-Path $srcContent) {
|
||||
# Длину префикса берём у РАЗРЕШЁННОГО пути, а не у переданной строки: путь может
|
||||
# прийти с коротким именем (C:\Users\NSHIRO~1\…), а Get-ChildItem отдаёт полное — тогда
|
||||
# отрезание по длине исходной строки оставляет в относительном пути чужие символы.
|
||||
$srcRoot = (Get-Item -LiteralPath $srcContent).FullName.TrimEnd('\', '/')
|
||||
$dstContent = Join-Path $objDir $name
|
||||
foreach ($f in (Get-ChildItem -LiteralPath $srcRoot -Recurse -File)) {
|
||||
$rel = $f.FullName.Substring($srcRoot.Length).TrimStart('\', '/')
|
||||
$dst = Join-Path $dstContent $rel
|
||||
New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null
|
||||
if ($f.Extension -ieq '.xml') {
|
||||
$t = [IO.File]::ReadAllText($f.FullName, [Text.Encoding]::UTF8)
|
||||
$t = $reGuid.Replace($t, $reissue)
|
||||
$t = $t.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
|
||||
[IO.File]::WriteAllText($dst, $t, $encBom)
|
||||
} else {
|
||||
Copy-Item -Path $f.FullName -Destination $dst -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @{ Tag = $cfgTag; Name = $name }
|
||||
}
|
||||
|
||||
# --- 4. Generate configuration XML ---
|
||||
|
||||
if ($hasRefTypes) {
|
||||
if ($needCfg) {
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgDir = Join-Path $TempBasePath "cfg"
|
||||
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
|
||||
|
||||
$embedded = $null
|
||||
if ($embedRequested) {
|
||||
$embedded = Add-SourceObjectToConfig $EmbedSourceFile $cfgDir
|
||||
if (-not $embedded) {
|
||||
Write-Host "Error: $EmbedSourceFile is neither ExternalDataProcessor nor ExternalReport" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
|
||||
# одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
|
||||
# заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
|
||||
@@ -547,6 +670,7 @@ if ($hasRefTypes) {
|
||||
$childXml += "`r`n`t`t`t<$tag>$name</$tag>"
|
||||
}
|
||||
}
|
||||
if ($embedded) { $childXml += "`r`n`t`t`t<$($embedded.Tag)>$($embedded.Name)</$($embedded.Tag)>" }
|
||||
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -1529,7 +1653,7 @@ if ($stubEngine -eq "ibcmd") {
|
||||
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
|
||||
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||
if ($needCfg) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||
$ibArgs += "--data=$ibData"
|
||||
$ibArgs += $extraArgs
|
||||
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
|
||||
@@ -1541,7 +1665,7 @@ if ($stubEngine -eq "ibcmd") {
|
||||
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||
exit 1
|
||||
}
|
||||
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if ($needCfg) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Write-Host "[OK] Stub database created: $TempBasePath"
|
||||
Write-Host $TempBasePath
|
||||
exit 0
|
||||
@@ -1557,8 +1681,8 @@ if ($proc.ExitCode -ne 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 6. Load config and update DB if ref types exist ---
|
||||
if ($hasRefTypes) {
|
||||
# --- 6. Load config and update DB if there is one ---
|
||||
if ($needCfg) {
|
||||
$cfgDir = Join-Path $TempBasePath "cfg"
|
||||
# LoadConfigFromFiles
|
||||
Write-Host "Loading configuration from files..."
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
@@ -64,11 +66,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -114,6 +136,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -167,15 +200,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -243,14 +282,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -1083,6 +1120,94 @@ def write_bom(path, content):
|
||||
f.write(content)
|
||||
|
||||
|
||||
# --- Внедрение проверяемого объекта в конфигурацию-заглушку ---
|
||||
# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles
|
||||
# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а
|
||||
# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем
|
||||
# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим
|
||||
# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и
|
||||
# макеты, а формат может расти без правок здесь.
|
||||
#
|
||||
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
|
||||
# такой же текст был бы кодом, и трогать его нельзя.
|
||||
GUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')
|
||||
|
||||
|
||||
def add_source_object_to_config(source_xml, cfg_dir):
|
||||
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
|
||||
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
|
||||
# XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные
|
||||
# между её файлами (ссылки внутри объекта идут по идентификатору).
|
||||
guid_map = {}
|
||||
|
||||
def reissue(text):
|
||||
def sub(m):
|
||||
k = m.group(0).lower()
|
||||
if k not in guid_map:
|
||||
guid_map[k] = new_uuid()
|
||||
return guid_map[k]
|
||||
return GUID_RE.sub(sub, text)
|
||||
|
||||
with io.open(source_xml, encoding='utf-8-sig') as fh:
|
||||
text = fh.read()
|
||||
if re.search(r'<ExternalDataProcessor[\s>]', text):
|
||||
ext_tag, cfg_tag, folder = 'ExternalDataProcessor', 'DataProcessor', 'DataProcessors'
|
||||
elif re.search(r'<ExternalReport[\s>]', text):
|
||||
ext_tag, cfg_tag, folder = 'ExternalReport', 'Report', 'Reports'
|
||||
else:
|
||||
return None
|
||||
|
||||
m = re.search(r'<Name>([^<]+)</Name>', text)
|
||||
name = m.group(1) if m else os.path.splitext(os.path.basename(source_xml))[0]
|
||||
|
||||
conv = reissue(text)
|
||||
conv = conv.replace('<%s ' % ext_tag, '<%s ' % cfg_tag).replace('<%s>' % ext_tag, '<%s>' % cfg_tag)
|
||||
conv = conv.replace('</%s>' % ext_tag, '</%s>' % cfg_tag)
|
||||
conv = conv.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
|
||||
|
||||
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
|
||||
# без него платформа отвечает «отсутствует один или более типов объекта».
|
||||
mgr = ('\t\t\t<xr:GeneratedType name="%sManager.%s" category="Manager">\r\n' % (cfg_tag, name) +
|
||||
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
|
||||
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
|
||||
'\t\t\t</xr:GeneratedType>\r\n')
|
||||
if '</InternalInfo>' in conv:
|
||||
conv = re.sub(r'\s*</InternalInfo>', lambda _m: '\r\n' + mgr + '\t\t</InternalInfo>', conv, count=1)
|
||||
else:
|
||||
obj_type = ('\t\t\t<xr:GeneratedType name="%sObject.%s" category="Object">\r\n' % (cfg_tag, name) +
|
||||
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
|
||||
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
|
||||
'\t\t\t</xr:GeneratedType>\r\n')
|
||||
conv = re.sub('(<%s[^>]*>)' % cfg_tag,
|
||||
lambda m2: m2.group(1) + '\r\n\t\t<InternalInfo>\r\n' + obj_type + mgr + '\t\t</InternalInfo>',
|
||||
conv, count=1)
|
||||
|
||||
obj_dir = os.path.join(cfg_dir, folder)
|
||||
os.makedirs(obj_dir, exist_ok=True)
|
||||
write_bom(os.path.join(obj_dir, '%s.xml' % name), conv)
|
||||
|
||||
# Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт.
|
||||
src_content = os.path.join(os.path.dirname(source_xml), name)
|
||||
if os.path.isdir(src_content):
|
||||
dst_content = os.path.join(obj_dir, name)
|
||||
for root, _dirs, files in os.walk(src_content):
|
||||
for fname in files:
|
||||
full = os.path.join(root, fname)
|
||||
rel = os.path.relpath(full, src_content)
|
||||
dst = os.path.join(dst_content, rel)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
if os.path.splitext(fname)[1].lower() == '.xml':
|
||||
with io.open(full, encoding='utf-8-sig') as fh:
|
||||
t = fh.read()
|
||||
t = reissue(t)
|
||||
t = t.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
|
||||
write_bom(dst, t)
|
||||
else:
|
||||
shutil.copyfile(full, dst)
|
||||
|
||||
return {'tag': cfg_tag, 'name': name}
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
@@ -1091,6 +1216,9 @@ def main():
|
||||
parser.add_argument('-SourceDir', required=True)
|
||||
parser.add_argument('-V8Path', required=True)
|
||||
parser.add_argument('-TempBasePath', default='')
|
||||
# XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа
|
||||
# смогла проверить его штатными проверками. Без параметра стаб работает как раньше.
|
||||
parser.add_argument('-EmbedSourceFile', default='')
|
||||
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
|
||||
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
|
||||
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
|
||||
@@ -1102,10 +1230,13 @@ def main():
|
||||
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
|
||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
|
||||
args.EmbedSourceFile = clean_path(args.EmbedSourceFile, "-EmbedSourceFile")
|
||||
|
||||
type_map = scan_ref_types(args.SourceDir)
|
||||
register_columns = scan_register_columns(args.SourceDir)
|
||||
has_ref_types = len(type_map) > 0
|
||||
embed_requested = bool(args.EmbedSourceFile and args.EmbedSourceFile.strip())
|
||||
need_cfg = has_ref_types or embed_requested
|
||||
stub_format_version = detect_stub_format_version(args.SourceDir)
|
||||
stub_compat = stub_compatibility_mode(stub_format_version)
|
||||
ns_decl = f'{NS} version="{stub_format_version}"'
|
||||
@@ -1118,10 +1249,18 @@ def main():
|
||||
if needs_registrator:
|
||||
type_map.setdefault('Document', {})['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430'] = True # ЗаглушкаРегистратора
|
||||
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
cfg_dir = os.path.join(temp_base, 'cfg')
|
||||
os.makedirs(cfg_dir, exist_ok=True)
|
||||
|
||||
embedded = None
|
||||
if embed_requested:
|
||||
embedded = add_source_object_to_config(args.EmbedSourceFile, cfg_dir)
|
||||
if not embedded:
|
||||
print('Error: %s is neither ExternalDataProcessor nor ExternalReport' % args.EmbedSourceFile,
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Configuration.xml
|
||||
uuid_cfg = new_uuid()
|
||||
uuid_lang = new_uuid()
|
||||
@@ -1138,6 +1277,8 @@ def main():
|
||||
tag = META_INFO[meta_type][0]
|
||||
for name in names:
|
||||
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
|
||||
if embedded:
|
||||
child_xml += '\n\t\t\t<%s>%s</%s>' % (embedded['tag'], embedded['name'], embedded['tag'])
|
||||
|
||||
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject {ns_decl}>
|
||||
@@ -1368,7 +1509,7 @@ def main():
|
||||
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||
ib_data = tempfile.mkdtemp(prefix="stub_data_")
|
||||
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||
ib_args.append(f'--data={ib_data}')
|
||||
ib_args.extend(extra_args)
|
||||
@@ -1381,7 +1522,7 @@ def main():
|
||||
print(result.stderr, file=sys.stderr)
|
||||
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
import shutil
|
||||
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
|
||||
print(f'[OK] Stub database created: {temp_base}')
|
||||
@@ -1397,13 +1538,24 @@ def main():
|
||||
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
cfg_dir = os.path.join(temp_base, 'cfg')
|
||||
# LoadConfigFromFiles
|
||||
print('Loading configuration from files...')
|
||||
load_log = os.path.join(tempfile.gettempdir(), 'stub_load_log.txt')
|
||||
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
|
||||
'/Out', f'"{load_log}"',
|
||||
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
|
||||
if result.returncode != 0:
|
||||
# Причина отказа живёт только в /Out: в консоль пакетный 1cv8 не пишет ничего.
|
||||
if os.path.isfile(load_log):
|
||||
try:
|
||||
with io.open(load_log, encoding='utf-8-sig', errors='replace') as fh:
|
||||
text = fh.read().strip()
|
||||
if text:
|
||||
print(text)
|
||||
except Exception:
|
||||
pass
|
||||
print_platform_output(result)
|
||||
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -49,7 +49,7 @@
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
@@ -98,6 +98,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -148,6 +159,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
|
||||
v8path = max(candidates, key=_version_key)
|
||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||
else:
|
||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||
print("Error: 1C executable not found. Specify -V8Path")
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||
v8path = os.path.join(v8path, exe)
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||
print(f"Error: 1C executable not found at {v8path}")
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
|
||||
if not path:
|
||||
return
|
||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
|
||||
if len(v) > 3 and v[-1] in "\\/":
|
||||
v = v[:-1]
|
||||
if '"' in v:
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||
sys.exit(1)
|
||||
return v
|
||||
|
||||
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||
"""
|
||||
if warn_no_user and os.name == "nt" and not has_username:
|
||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||
sys.stderr.flush()
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
@@ -428,20 +463,20 @@ def main():
|
||||
|
||||
# --- Validate database connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef")
|
||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
if not args.InfoBasePath:
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||
sys.exit(1)
|
||||
if args.Format == "Plain":
|
||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate input file ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
print(f"Error: input file not found: {args.InputFile}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
@@ -473,9 +508,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output")
|
||||
else:
|
||||
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping external data processor/report (code: {exit_code})")
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Build arguments ---
|
||||
@@ -513,9 +548,9 @@ def main():
|
||||
if exit_code == 0:
|
||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
||||
elif out_missing:
|
||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output")
|
||||
else:
|
||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
||||
print(f"Error dumping (code: {exit_code})")
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
|
||||
@@ -12,6 +12,8 @@ allowed-tools:
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
|
||||
|
||||
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# epf-validate v1.5 — Validate 1C external data processor / report structure
|
||||
# epf-validate v1.6 — Validate 1C external data processor / report structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Parameter(Mandatory, Position=0)]
|
||||
[Alias('Path')]
|
||||
[string]$ObjectPath,
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-validate v1.5 — Validate 1C external data processor / report structure
|
||||
# epf-validate v1.6 — Validate 1C external data processor / report structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ allowed-tools:
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
|
||||
под проверку исходников, если она не отключена.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
@@ -57,11 +58,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
||||
| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` |
|
||||
| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Проверка перед сборкой
|
||||
|
||||
Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм.
|
||||
Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение
|
||||
платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off`
|
||||
или ключом `"externalCheck": false` в `.v8-project.json`.
|
||||
|
||||
Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -14,6 +14,8 @@ allowed-tools:
|
||||
|
||||
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
|
||||
|
||||
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: form-add
|
||||
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
|
||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
||||
argument-hint: <ObjectPath> <FormName> [-Purpose <Purpose>] [-Synonym <Synonym>] [-SetDefault]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -18,16 +18,16 @@ allowed-tools:
|
||||
## Usage
|
||||
|
||||
```
|
||||
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
|
||||
/form-add <ObjectPath> <FormName> [-Purpose <Purpose>] [-Synonym <Synonym>] [-SetDefault]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-------------|:------------:|--------------|----------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
|
||||
| FormName | да | — | Имя формы (ФормаДокумента) |
|
||||
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
|
||||
| Purpose | нет | основная форма вида | Назначение формы — см. таблицу ниже: у справочника это форма объекта, у регистра сведений — форма записи, у журнала — форма списка |
|
||||
| Synonym | нет | = FormName | Синоним формы |
|
||||
| --set-default | нет | авто | Установить как форму по умолчанию |
|
||||
| -SetDefault | нет | авто | Сделать основной. Без флага основной становится первая форма каждого назначения |
|
||||
|
||||
## Команда
|
||||
|
||||
@@ -37,30 +37,57 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -Obje
|
||||
|
||||
## Purpose — назначение формы
|
||||
|
||||
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|
||||
|---------|-------------------------|-------------------|---------------------|
|
||||
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
|
||||
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
|
||||
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
|
||||
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
|
||||
| Purpose | Какая форма | Становится основной |
|
||||
|---------|-------------|---------------------|
|
||||
| Object | форма объекта (элемента, документа, обработки) | да |
|
||||
| List | форма списка | да |
|
||||
| Choice | форма выбора | да |
|
||||
| Folder | форма группы | да |
|
||||
| FolderChoice | форма выбора группы | да |
|
||||
| Record | форма записи | да |
|
||||
| RecordSet | форма набора записей | нет — в платформе нет такого свойства |
|
||||
| Save | форма сохранения настроек | да |
|
||||
| Load | форма загрузки настроек | да |
|
||||
| Custom | произвольная форма, без привязки к объекту | нет |
|
||||
|
||||
### Что доступно типу объекта
|
||||
|
||||
| Тип объекта | Назначения |
|
||||
|-------------|------------|
|
||||
| Catalog, ChartOfCharacteristicTypes | Object, Folder, List, Choice, FolderChoice, Custom |
|
||||
| Document, ChartOfAccounts, ChartOfCalculationTypes, ExchangePlan, BusinessProcess, Task | Object, List, Choice, Custom |
|
||||
| DataProcessor, Report, ExternalDataProcessor, ExternalReport | Object, Custom |
|
||||
| InformationRegister | Record, List, RecordSet, Custom |
|
||||
| AccumulationRegister, AccountingRegister, CalculationRegister | List, RecordSet, Custom |
|
||||
| DocumentJournal, FilterCriterion | List, Custom |
|
||||
| Enum | List, Choice, Custom |
|
||||
| SettingsStorage | Save, Load, Custom |
|
||||
| Table (таблица внешнего источника) | `tableDataType=ObjectData` — Object, List, Choice, Custom; `NonobjectData` — Record, List, Choice, Custom |
|
||||
|
||||
Недопустимое сочетание отклоняется со списком доступных для этого типа. У константы собственных
|
||||
форм нет — для неё используется общая форма (`CommonForm`).
|
||||
|
||||
Таблица внешнего источника адресуется файлом таблицы:
|
||||
`ExternalDataSources/<Источник>/Tables/<Таблица>.xml`. Ссылки в такой форме трёхчастные;
|
||||
без `-Purpose` берётся форма объекта, а у таблицы с составным ключом — форма записи.
|
||||
|
||||
## Примеры
|
||||
|
||||
```
|
||||
# Форма документа
|
||||
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
|
||||
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента -Purpose Object
|
||||
|
||||
# Форма списка каталога
|
||||
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
|
||||
/form-add Catalogs/Контрагенты.xml ФормаСписка -Purpose List
|
||||
|
||||
# Форма записи регистра сведений
|
||||
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
|
||||
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи -Purpose Record
|
||||
|
||||
# Форма выбора с синонимом
|
||||
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
|
||||
/form-add Catalogs/Номенклатура.xml ФормаВыбора -Purpose Choice -Synonym "Выбор номенклатуры"
|
||||
|
||||
# Установить как форму по умолчанию
|
||||
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
|
||||
/form-add Documents/Заказ.xml ФормаДокументаНовая -Purpose Object -SetDefault
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# form-add v1.29 — Add managed form to 1C config object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ObjectPath,
|
||||
@@ -9,8 +10,15 @@ param(
|
||||
|
||||
[string]$Synonym = $FormName,
|
||||
|
||||
[string]$Purpose = "Object",
|
||||
# Пусто = основная форма вида (Primary в таблице): у справочника это форма объекта,
|
||||
# у регистра сведений — форма записи, у журнала — форма списка. Жёсткое "Object"
|
||||
# по умолчанию было бы неверным для видов, у которых формы объекта не бывает.
|
||||
[string]$Purpose = "",
|
||||
|
||||
# Алиас с дефисом внутри имени: вызов вида --set-default PowerShell разбирает как имя
|
||||
# параметра "set-default" и без алиаса отвечает отказом биндинга. Написания -SetDefault,
|
||||
# --SetDefault и --setdefault совпадают с именем параметра и так.
|
||||
[Alias('set-default')]
|
||||
[switch]$SetDefault
|
||||
)
|
||||
|
||||
@@ -241,26 +249,175 @@ if (-not $metaDataObject) {
|
||||
$metaDataObject = $xmlDoc.DocumentElement
|
||||
}
|
||||
|
||||
$supportedTypes = @(
|
||||
"Document", "Catalog", "DataProcessor", "Report",
|
||||
"ExternalDataProcessor", "ExternalReport",
|
||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
|
||||
)
|
||||
# --- Таблица видов: вид → допустимые назначения ---
|
||||
#
|
||||
# Одна запись на вид вместо разрозненных списков «поддерживаемые типы», «объектные типы»,
|
||||
# «обработко-подобные» и «карта типов реквизита». Раньше они расходились молча: DocumentJournal
|
||||
# был среди поддерживаемых, но не в карте типов, и в форму уходило `cfg:.Журнал` — платформа
|
||||
# такую выгрузку не принимает, а навык рапортовал успех.
|
||||
#
|
||||
# MainAttr — тип главного реквизита; `{0}` подставляется именем объекта:
|
||||
# "DynamicList" — динамический список (добавляется Settings/MainTable);
|
||||
# $null — произвольная форма, блока Attributes нет вовсе.
|
||||
# Slot — свойство объекта под «основную форму»; $null — такого свойства у вида нет.
|
||||
# Эталон таблицы — docs/1c-form-spec.md, сверяется гардом check-form-purposes.mjs.
|
||||
|
||||
$formKinds = @{
|
||||
"Catalog" = @{
|
||||
"Object" = @{ MainAttr = "CatalogObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"Folder" = @{ MainAttr = "CatalogObject.{1}"; AttrName = "Объект"; Slot = "DefaultFolderForm"; SavedData = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"FolderChoice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultFolderChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"ChartOfCharacteristicTypes" = @{
|
||||
"Object" = @{ MainAttr = "ChartOfCharacteristicTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"Folder" = @{ MainAttr = "ChartOfCharacteristicTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultFolderForm"; SavedData = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"FolderChoice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultFolderChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"Document" = @{
|
||||
"Object" = @{ MainAttr = "DocumentObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"ChartOfAccounts" = @{
|
||||
"Object" = @{ MainAttr = "ChartOfAccountsObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"ChartOfCalculationTypes" = @{
|
||||
"Object" = @{ MainAttr = "ChartOfCalculationTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"ExchangePlan" = @{
|
||||
"Object" = @{ MainAttr = "ExchangePlanObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"BusinessProcess" = @{
|
||||
"Object" = @{ MainAttr = "BusinessProcessObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"Task" = @{
|
||||
"Object" = @{ MainAttr = "TaskObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"DataProcessor" = @{
|
||||
"Object" = @{ MainAttr = "DataProcessorObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"Report" = @{
|
||||
"Object" = @{ MainAttr = "ReportObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"ExternalDataProcessor" = @{
|
||||
"Object" = @{ MainAttr = "ExternalDataProcessorObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"ExternalReport" = @{
|
||||
"Object" = @{ MainAttr = "ExternalReportObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"InformationRegister" = @{
|
||||
"Record" = @{ MainAttr = "InformationRegisterRecordManager.{1}"; AttrName = "Запись"; Slot = "DefaultRecordForm"; SavedData = $true; Primary = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"RecordSet" = @{ MainAttr = "InformationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"AccumulationRegister" = @{
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||
"RecordSet" = @{ MainAttr = "AccumulationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"AccountingRegister" = @{
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||
"RecordSet" = @{ MainAttr = "AccountingRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"CalculationRegister" = @{
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||
"RecordSet" = @{ MainAttr = "CalculationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"DocumentJournal" = @{
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultForm"; Primary = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"FilterCriterion" = @{
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultForm"; Primary = $true }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"Enum" = @{
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
"SettingsStorage" = @{
|
||||
"Save" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultSaveForm"; Primary = $true }
|
||||
"Load" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultLoadForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
# Таблица внешнего источника — единственный вид, чьё имя в ссылках трёхчастное
|
||||
# (Источник.Таблица): подставляется {2}, а не {1}.
|
||||
"Table" = @{
|
||||
"Object" = @{ MainAttr = "ExternalDataSourceTableObject.{2}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"Record" = @{ MainAttr = "ExternalDataSourceTableRecordManager.{2}"; AttrName = "Запись"; Slot = "DefaultRecordForm"; SavedData = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
}
|
||||
|
||||
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает — отказ с причиной,
|
||||
# а не «тип не поддерживается».
|
||||
$noOwnForms = @{
|
||||
"Constant" = "у константы нет собственных форм — используйте общую форму (CommonForm)"
|
||||
}
|
||||
|
||||
$supportedTypes = @($formKinds.Keys) + @($noOwnForms.Keys)
|
||||
|
||||
# Отдельный факт, не выводимый из таблицы назначений: у форм обработок и отчётов в метаданных
|
||||
# формы есть <ExtendedPresentation>.
|
||||
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
|
||||
|
||||
# Вид объекта — первый элемент-потомок MetaDataObject, а не первое совпавшее по всему документу
|
||||
# имя. Поиск по документу зависел от порядка перебора видов: у бизнес-процесса есть свойство
|
||||
# <Task>, и он определялся как задача, после чего имя объекта не находилось вовсе.
|
||||
$objectType = $null
|
||||
$objectNode = $null
|
||||
foreach ($t in $supportedTypes) {
|
||||
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
|
||||
if ($node) {
|
||||
$objectType = $t
|
||||
$objectNode = $node
|
||||
foreach ($child in $metaDataObject.ChildNodes) {
|
||||
if ($child.NodeType -eq [System.Xml.XmlNodeType]::Element) {
|
||||
$objectType = $child.LocalName
|
||||
$objectNode = $child
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($objectType -and -not ($formKinds.ContainsKey($objectType) -or $noOwnForms.ContainsKey($objectType))) {
|
||||
Write-Error "Тип объекта '$objectType' не поддерживается. Поддерживаемые типы: $(($formKinds.Keys | Sort-Object) -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $objectType) {
|
||||
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
|
||||
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $(($formKinds.Keys | Sort-Object) -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($noOwnForms.ContainsKey($objectType)) {
|
||||
Write-Error "$objectType не поддерживается: $($noOwnForms[$objectType])"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -274,48 +431,101 @@ if (-not $objectName) {
|
||||
Write-Host ""
|
||||
Write-Host "=== form-add ==="
|
||||
Write-Host ""
|
||||
# Ссылка на объект и имя для типов формы. У всех видов это "Вид.Имя", и только
|
||||
# у таблицы внешнего источника — "ExternalDataSource.<Источник>.Table.<Таблица>", а в именах
|
||||
# типов — "<Источник>.<Таблица>". Имя источника в самом файле таблицы не хранится —
|
||||
# единственное место, где навык смотрит на путь: ExternalDataSources/<Источник>/Tables/<Таблица>.xml
|
||||
$objectQualifiedName = $objectName
|
||||
$objectRef = "$objectType.$objectName"
|
||||
if ($objectType -eq "Table") {
|
||||
$tablesDir = Split-Path -Parent $objectXmlFull.Path
|
||||
$edsSource = Split-Path -Leaf (Split-Path -Parent $tablesDir)
|
||||
if (-not $edsSource -or (Split-Path -Leaf $tablesDir) -ne "Tables") {
|
||||
Write-Error "Таблица внешнего источника ожидается по пути ExternalDataSources/<Источник>/Tables/<Таблица>.xml, а не '$($objectXmlFull.Path)'"
|
||||
exit 1
|
||||
}
|
||||
$objectQualifiedName = "$edsSource.$objectName"
|
||||
$objectRef = "ExternalDataSource.$edsSource.Table.$objectName"
|
||||
$tdtNode = $xmlDoc.SelectSingleNode("//md:Table/md:Properties/md:TableDataType", $nsMgr)
|
||||
$tableDataType = if ($tdtNode) { $tdtNode.InnerText.Trim() } else { "ObjectData" }
|
||||
}
|
||||
|
||||
Write-Host "Object: $objectType.$objectName"
|
||||
|
||||
# --- Фаза 2: Валидация Purpose ---
|
||||
|
||||
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
|
||||
# Нормализация
|
||||
switch ($Purpose) {
|
||||
"Object" { }
|
||||
"List" { }
|
||||
"Choice" { }
|
||||
"Record" { }
|
||||
default {
|
||||
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
|
||||
# Назначение ищем в таблице регистронезависимо — как принимает PowerShell (в py-порту .lower()).
|
||||
$kindPurposes = $formKinds[$objectType]
|
||||
|
||||
# Обиходные написания назначения приводим к канону молча: русское название вида формы и
|
||||
# английское с суффиксом Form. Ключ нормализуем — регистр, пробелы и разделители не значимы.
|
||||
# Канон в документации один; здесь только приём ошибочного ввода, чтобы вызов не падал на форме
|
||||
# записи вместо назначения. Применимость назначения к виду объекта проверяется ниже как обычно.
|
||||
$purposeSynonyms = @{
|
||||
"формаобъекта"="Object"; "формаэлемента"="Object"; "формадокумента"="Object"
|
||||
"объект"="Object"; "элемент"="Object"; "документ"="Object"; "objectform"="Object"
|
||||
"формасписка"="List"; "список"="List"; "listform"="List"
|
||||
"формавыбора"="Choice"; "выбор"="Choice"; "choiceform"="Choice"
|
||||
"формагруппы"="Folder"; "группа"="Folder"; "folderform"="Folder"
|
||||
"формавыборагруппы"="FolderChoice"; "выборгруппы"="FolderChoice"; "folderchoiceform"="FolderChoice"
|
||||
"формазаписи"="Record"; "запись"="Record"; "recordform"="Record"
|
||||
"форманаборазаписей"="RecordSet"; "наборзаписей"="RecordSet"; "recordsetform"="RecordSet"
|
||||
"формасохранения"="Save"; "формасохранениянастроек"="Save"; "сохранение"="Save"; "saveform"="Save"
|
||||
"формазагрузки"="Load"; "формазагрузкинастроек"="Load"; "загрузка"="Load"; "loadform"="Load"
|
||||
"произвольная"="Custom"; "произвольнаяформа"="Custom"; "customform"="Custom"
|
||||
}
|
||||
if ($Purpose) {
|
||||
$purposeProbe = ($Purpose -replace '[\s_-]', '').ToLowerInvariant()
|
||||
$isKnownPurpose = $false
|
||||
foreach ($p in $kindPurposes.Keys) {
|
||||
if ($p.ToLowerInvariant() -eq $Purpose.ToLowerInvariant()) { $isKnownPurpose = $true; break }
|
||||
}
|
||||
if (-not $isKnownPurpose -and $purposeSynonyms.ContainsKey($purposeProbe)) {
|
||||
$Purpose = $purposeSynonyms[$purposeProbe]
|
||||
}
|
||||
}
|
||||
if (-not $Purpose) {
|
||||
if ($objectType -eq "Table" -and $tableDataType -eq "NonobjectData") {
|
||||
# Пометка Primary в таблице видов одна на вид, а у таблицы с составным ключом
|
||||
# формы объекта не бывает — основной становится форма записи.
|
||||
$Purpose = "Record"
|
||||
} else {
|
||||
foreach ($p in $kindPurposes.Keys) {
|
||||
if ($kindPurposes[$p].Primary) { $Purpose = $p; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
$purposeKey = $null
|
||||
foreach ($p in $kindPurposes.Keys) {
|
||||
if ($p.ToLowerInvariant() -eq $Purpose.ToLowerInvariant()) { $purposeKey = $p; break }
|
||||
}
|
||||
if (-not $purposeKey) {
|
||||
Write-Error "Назначение '$Purpose' недопустимо для $objectType. Допустимые: $(($kindPurposes.Keys | Sort-Object) -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
$Purpose = $purposeKey
|
||||
$purposeRule = $kindPurposes[$Purpose]
|
||||
|
||||
# У таблицы внешнего источника набор назначений зависит от вида данных (замерено на
|
||||
# 8.3.24.1691): ObjectData — Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
|
||||
# не отвергается схемой формы, а валит загрузку всей конфигурации «Исключением XDTO» без причины.
|
||||
if ($objectType -eq "Table") {
|
||||
if ($tableDataType -eq "NonobjectData" -and $Purpose -eq "Object") {
|
||||
Write-Error "Таблица '$objectName' с составным ключом (TableDataType=NonobjectData): формы объекта у неё нет — используйте -Purpose Record."
|
||||
exit 1
|
||||
}
|
||||
if ($tableDataType -ne "NonobjectData" -and $Purpose -eq "Record") {
|
||||
Write-Error "Таблица '$objectName' с ключом из одного поля (TableDataType=ObjectData): формы записи у неё нет — используйте -Purpose Object."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
|
||||
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
|
||||
|
||||
switch ($Purpose) {
|
||||
"Object" {
|
||||
# допустимо для всех типов
|
||||
}
|
||||
"List" {
|
||||
if ($objectType -eq "DataProcessor") {
|
||||
Write-Error "Purpose=List недопустим для DataProcessor"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
"Choice" {
|
||||
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
|
||||
Write-Error "Purpose=Choice недопустим для $objectType"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
"Record" {
|
||||
if ($objectType -ne "InformationRegister") {
|
||||
Write-Error "Purpose=Record допустим только для InformationRegister"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой MainAttr — это
|
||||
# произвольная форма (законное состояние), а вот наполовину заполненная запись означала бы, что
|
||||
# таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
|
||||
if ($purposeRule.MainAttr -and -not $purposeRule.AttrName) {
|
||||
Write-Error "Внутренняя ошибка таблицы видов: у $objectType/$Purpose задан MainAttr без AttrName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Фаза 3: Создание файлов ---
|
||||
@@ -395,102 +605,47 @@ Write-XmlFile $formMetaPath $formMetaXml $encBom
|
||||
|
||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||
|
||||
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
||||
# Динамический список
|
||||
# MainTable: тип.имя
|
||||
$mainTable = "$objectType.$objectName"
|
||||
# Одна ветка вместо трёх: что писать, решает запись таблицы видов. Раньше тип главного
|
||||
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
|
||||
$attributesBlock = ""
|
||||
if ($purposeRule.MainAttr) {
|
||||
$mainAttrType = $purposeRule.MainAttr -f $objectType, $objectName, $objectQualifiedName
|
||||
$mainAttrName = $purposeRule.AttrName
|
||||
|
||||
$formXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||
<Autofill>true</Autofill>
|
||||
</AutoCommandBar>
|
||||
<ChildItems/>
|
||||
<Attributes>
|
||||
<Attribute name="Список" id="1">
|
||||
<Type>
|
||||
<v8:Type>cfg:DynamicList</v8:Type>
|
||||
</Type>
|
||||
<MainAttribute>true</MainAttribute>
|
||||
<Settings xsi:type="DynamicList">
|
||||
<MainTable>$mainTable</MainTable>
|
||||
</Settings>
|
||||
</Attribute>
|
||||
</Attributes>
|
||||
</Form>
|
||||
"@
|
||||
} elseif ($Purpose -eq "Record") {
|
||||
# Запись регистра сведений
|
||||
$mainAttrName = "Запись"
|
||||
$mainAttrType = "InformationRegisterRecordManager.$objectName"
|
||||
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
|
||||
$tailLines = ""
|
||||
if ($mainAttrType -eq "DynamicList") {
|
||||
$mainTable = $objectRef
|
||||
$tailLines = "`n`t`t`t<Settings xsi:type=""DynamicList"">`n`t`t`t`t<MainTable>$mainTable</MainTable>`n`t`t`t</Settings>"
|
||||
} elseif ($purposeRule.SavedData) {
|
||||
$tailLines = "`n`t`t`t<SavedData>true</SavedData>"
|
||||
}
|
||||
|
||||
$attributesBlock = @"
|
||||
|
||||
$formXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||
<Autofill>true</Autofill>
|
||||
</AutoCommandBar>
|
||||
<ChildItems/>
|
||||
<Attributes>
|
||||
<Attribute name="$mainAttrName" id="1">
|
||||
<Type>
|
||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
||||
</Type>
|
||||
<MainAttribute>true</MainAttribute>
|
||||
<SavedData>true</SavedData>
|
||||
<MainAttribute>true</MainAttribute>$tailLines
|
||||
</Attribute>
|
||||
</Attributes>
|
||||
</Form>
|
||||
"@
|
||||
} else {
|
||||
# Object — форма объекта
|
||||
$mainAttrName = "Объект"
|
||||
|
||||
# Маппинг типа объекта на тип реквизита
|
||||
$attrTypeMap = @{
|
||||
"Document" = "DocumentObject"
|
||||
"Catalog" = "CatalogObject"
|
||||
"DataProcessor" = "DataProcessorObject"
|
||||
"Report" = "ReportObject"
|
||||
"ExternalDataProcessor" = "ExternalDataProcessorObject"
|
||||
"ExternalReport" = "ExternalReportObject"
|
||||
"ChartOfAccounts" = "ChartOfAccountsObject"
|
||||
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
|
||||
"ExchangePlan" = "ExchangePlanObject"
|
||||
"BusinessProcess" = "BusinessProcessObject"
|
||||
"Task" = "TaskObject"
|
||||
"InformationRegister" = "InformationRegisterRecordManager"
|
||||
"AccumulationRegister" = "AccumulationRegisterRecordSet"
|
||||
}
|
||||
|
||||
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
||||
|
||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
||||
$savedDataLine = ""
|
||||
if ($objectType -notin $processorLikeTypes) {
|
||||
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
|
||||
}
|
||||
|
||||
$formXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||
<Autofill>true</Autofill>
|
||||
</AutoCommandBar>
|
||||
<ChildItems/>
|
||||
<Attributes>
|
||||
<Attribute name="$mainAttrName" id="1">
|
||||
<Type>
|
||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
||||
</Type>
|
||||
<MainAttribute>true</MainAttribute>$savedDataLine
|
||||
</Attribute>
|
||||
</Attributes>
|
||||
</Form>
|
||||
"@
|
||||
}
|
||||
|
||||
# Произвольная форма (MainAttr = $null) — без блока Attributes вовсе. В типовых это самая
|
||||
# частая форма после объектной: 907 у справочников, 941 у документов, 3482 у отчётов.
|
||||
$formXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||
<Autofill>true</Autofill>
|
||||
</AutoCommandBar>
|
||||
<ChildItems/>$attributesBlock
|
||||
</Form>
|
||||
"@
|
||||
|
||||
if (Test-Path $formXmlPath) {
|
||||
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
||||
} else {
|
||||
@@ -606,26 +761,19 @@ if ($insertBefore) {
|
||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
||||
$isFirstFormForPurpose = $false
|
||||
$defaultPropName = $null
|
||||
$defaultValue = "$objectType.$objectName.Form.$FormName"
|
||||
$defaultValue = "$objectRef.Form.$FormName"
|
||||
|
||||
# Определяем имя свойства для DefaultForm
|
||||
switch ($Purpose) {
|
||||
"Object" {
|
||||
if ($objectType -in $processorLikeTypes) {
|
||||
$defaultPropName = "DefaultForm"
|
||||
} else {
|
||||
$defaultPropName = "DefaultObjectForm"
|
||||
}
|
||||
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному Purpose без учёта
|
||||
# вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не находился, навык
|
||||
# молча ничего не делал.
|
||||
$defaultPropName = $purposeRule.Slot
|
||||
|
||||
$defaultNode = $null
|
||||
if ($defaultPropName) {
|
||||
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
|
||||
if ($defaultNode) {
|
||||
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
|
||||
}
|
||||
"List" { $defaultPropName = "DefaultListForm" }
|
||||
"Choice" { $defaultPropName = "DefaultChoiceForm" }
|
||||
"Record" { $defaultPropName = "DefaultRecordForm" }
|
||||
}
|
||||
|
||||
# Проверяем, установлено ли уже значение
|
||||
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
|
||||
if ($defaultNode) {
|
||||
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
|
||||
}
|
||||
|
||||
$defaultUpdated = $false
|
||||
@@ -687,5 +835,9 @@ if ($alreadyRegistered) {
|
||||
}
|
||||
if ($defaultUpdated) {
|
||||
Write-Host "${defaultPropName}: $defaultValue"
|
||||
} elseif (-not $defaultPropName) {
|
||||
# Молчать здесь нельзя: пользователь ждёт, что форма станет основной, а свойства под неё
|
||||
# у платформы нет (форма набора записей, произвольная форма).
|
||||
Write-Host "Основной не назначена: у $objectType нет свойства для формы с назначением $Purpose"
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||
# form-add v1.29 — Add managed form to 1C config object (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -323,8 +323,13 @@ def main():
|
||||
parser.add_argument("-ObjectPath", required=True)
|
||||
parser.add_argument("-FormName", required=True)
|
||||
parser.add_argument("-Synonym", default=None)
|
||||
parser.add_argument("-Purpose", default="Object")
|
||||
parser.add_argument("-SetDefault", action="store_true")
|
||||
# Пусто = основная форма вида (primary в таблице): у справочника это форма объекта,
|
||||
# у регистра сведений — форма записи, у журнала — форма списка.
|
||||
parser.add_argument("-Purpose", default="")
|
||||
# Написания с дефисом внутри имени и с двойным дефисом: в PS-порте их принимает алиас
|
||||
# set-default, здесь — перечисление опций, чтобы порты принимали ровно одно и то же.
|
||||
parser.add_argument("-SetDefault", "--SetDefault", "--set-default", "-set-default",
|
||||
dest="SetDefault", action="store_true")
|
||||
args = ci_parse_args(parser)
|
||||
|
||||
object_path = args.ObjectPath
|
||||
@@ -415,24 +420,192 @@ def main():
|
||||
tree = etree.parse(object_xml_full, parser_xml)
|
||||
root = tree.getroot()
|
||||
|
||||
supported_types = [
|
||||
"Document", "Catalog", "DataProcessor", "Report",
|
||||
"ExternalDataProcessor", "ExternalReport",
|
||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
|
||||
]
|
||||
# --- Таблица видов: вид -> допустимые назначения ---
|
||||
#
|
||||
# Зеркало $formKinds из PS-порта. Одна запись на вид вместо разрозненных списков
|
||||
# «поддерживаемые типы», «объектные типы», «обработко-подобные» и «карта типов реквизита»:
|
||||
# раньше они расходились молча, и для DocumentJournal в форму уходило `cfg:.Журнал`.
|
||||
#
|
||||
# main_attr — тип главного реквизита, {0} = вид, {1} = имя объекта;
|
||||
# "DynamicList" — динамический список (добавляется Settings/MainTable);
|
||||
# None — произвольная форма, блока Attributes нет вовсе.
|
||||
# slot — свойство объекта под «основную форму»; None — такого свойства у вида нет.
|
||||
# Эталон таблицы — docs/1c-form-spec.md, сверяется гардом check-form-purposes.mjs.
|
||||
|
||||
form_kinds = {
|
||||
"Catalog": {
|
||||
"Object": {"main_attr": "CatalogObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"Folder": {"main_attr": "CatalogObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultFolderForm", "saved_data": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"FolderChoice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultFolderChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"ChartOfCharacteristicTypes": {
|
||||
"Object": {"main_attr": "ChartOfCharacteristicTypesObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"Folder": {"main_attr": "ChartOfCharacteristicTypesObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultFolderForm", "saved_data": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"FolderChoice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultFolderChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"Document": {
|
||||
"Object": {"main_attr": "DocumentObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"ChartOfAccounts": {
|
||||
"Object": {"main_attr": "ChartOfAccountsObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"ChartOfCalculationTypes": {
|
||||
"Object": {"main_attr": "ChartOfCalculationTypesObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"ExchangePlan": {
|
||||
"Object": {"main_attr": "ExchangePlanObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"BusinessProcess": {
|
||||
"Object": {"main_attr": "BusinessProcessObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"Task": {
|
||||
"Object": {"main_attr": "TaskObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"DataProcessor": {
|
||||
"Object": {"main_attr": "DataProcessorObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultForm", "primary": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"Report": {
|
||||
"Object": {"main_attr": "ReportObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultForm", "primary": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"ExternalDataProcessor": {
|
||||
"Object": {"main_attr": "ExternalDataProcessorObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultForm", "primary": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"ExternalReport": {
|
||||
"Object": {"main_attr": "ExternalReportObject.{1}", "attr_name": "Объект",
|
||||
"slot": "DefaultForm", "primary": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"InformationRegister": {
|
||||
"Record": {"main_attr": "InformationRegisterRecordManager.{1}", "attr_name": "Запись",
|
||||
"slot": "DefaultRecordForm", "saved_data": True, "primary": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"RecordSet": {"main_attr": "InformationRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||
"slot": None, "saved_data": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"AccumulationRegister": {
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||
"RecordSet": {"main_attr": "AccumulationRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||
"slot": None, "saved_data": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"AccountingRegister": {
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||
"RecordSet": {"main_attr": "AccountingRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||
"slot": None, "saved_data": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"CalculationRegister": {
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||
"RecordSet": {"main_attr": "CalculationRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||
"slot": None, "saved_data": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"DocumentJournal": {
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultForm", "primary": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"FilterCriterion": {
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultForm", "primary": True},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"Enum": {
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
"SettingsStorage": {
|
||||
"Save": {"main_attr": None, "attr_name": None, "slot": "DefaultSaveForm", "primary": True},
|
||||
"Load": {"main_attr": None, "attr_name": None, "slot": "DefaultLoadForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
# Таблица внешнего источника — единственный вид, чьё имя в ссылках трёхчастное
|
||||
# (Источник.Таблица): подставляется {2}, а не {1}.
|
||||
"Table": {
|
||||
"Object": {"main_attr": "ExternalDataSourceTableObject.{2}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"Record": {"main_attr": "ExternalDataSourceTableRecordManager.{2}", "attr_name": "Запись",
|
||||
"slot": "DefaultRecordForm", "saved_data": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
}
|
||||
|
||||
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает.
|
||||
no_own_forms = {
|
||||
"Constant": "у константы нет собственных форм — используйте общую форму (CommonForm)",
|
||||
}
|
||||
|
||||
supported_types = list(form_kinds) + list(no_own_forms)
|
||||
|
||||
# Отдельный факт, не выводимый из таблицы назначений: у форм обработок и отчётов в
|
||||
# метаданных формы есть <ExtendedPresentation>.
|
||||
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
|
||||
|
||||
# Вид объекта — первый элемент-потомок MetaDataObject, а не первое совпавшее по всему
|
||||
# документу имя. Поиск по документу зависел от порядка перебора видов: у бизнес-процесса
|
||||
# есть свойство <Task>, и он определялся как задача, после чего имя объекта не находилось.
|
||||
object_type = None
|
||||
object_node = None
|
||||
for t in supported_types:
|
||||
node = root.find(f".//md:{t}", NSMAP)
|
||||
if node is not None:
|
||||
object_type = t
|
||||
object_node = node
|
||||
for child in root:
|
||||
if isinstance(child.tag, str):
|
||||
object_type = etree.QName(child).localname
|
||||
object_node = child
|
||||
break
|
||||
|
||||
if object_type is not None and object_type not in form_kinds and object_type not in no_own_forms:
|
||||
print(f"Тип объекта '{object_type}' не поддерживается. "
|
||||
f"Поддерживаемые типы: {', '.join(sorted(form_kinds))}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if object_type is None:
|
||||
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
|
||||
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(sorted(form_kinds))}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if object_type in no_own_forms:
|
||||
print(f"{object_type} не поддерживается: {no_own_forms[object_type]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Object name from Properties/Name
|
||||
@@ -442,6 +615,26 @@ def main():
|
||||
sys.exit(1)
|
||||
object_name = name_node.text
|
||||
|
||||
# Ссылка на объект и имя для типов формы. У всех видов это "Вид.Имя", и только
|
||||
# у таблицы внешнего источника — "ExternalDataSource.<Источник>.Table.<Таблица>", а в именах
|
||||
# типов — "<Источник>.<Таблица>". Имя источника в самом файле таблицы не хранится —
|
||||
# единственное место, где навык смотрит на путь: ExternalDataSources/<Источник>/Tables/<Таблица>.xml
|
||||
object_qualified_name = object_name
|
||||
object_ref = f"{object_type}.{object_name}"
|
||||
table_data_type = "ObjectData"
|
||||
if object_type == "Table":
|
||||
tables_dir = os.path.dirname(object_xml_full)
|
||||
eds_source = os.path.basename(os.path.dirname(tables_dir))
|
||||
if not eds_source or os.path.basename(tables_dir) != "Tables":
|
||||
print("Таблица внешнего источника ожидается по пути "
|
||||
f"ExternalDataSources/<Источник>/Tables/<Таблица>.xml, а не '{object_xml_full}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
object_qualified_name = f"{eds_source}.{object_name}"
|
||||
object_ref = f"ExternalDataSource.{eds_source}.Table.{object_name}"
|
||||
tdt_node = root.find(".//md:Table/md:Properties/md:TableDataType", NSMAP)
|
||||
if tdt_node is not None and tdt_node.text:
|
||||
table_data_type = tdt_node.text.strip()
|
||||
|
||||
print()
|
||||
print("=== form-add ===")
|
||||
print()
|
||||
@@ -449,32 +642,77 @@ def main():
|
||||
|
||||
# --- Phase 2: Validate Purpose ---
|
||||
|
||||
# Normalize: capitalize first letter, lowercase rest
|
||||
purpose = purpose[0].upper() + purpose[1:].lower()
|
||||
# Назначение ищем в таблице регистронезависимо — как принимает PowerShell.
|
||||
kind_purposes = form_kinds[object_type]
|
||||
|
||||
valid_purposes = ["Object", "List", "Choice", "Record"]
|
||||
if purpose not in valid_purposes:
|
||||
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
|
||||
# Обиходные написания назначения приводим к канону молча: русское название вида формы и
|
||||
# английское с суффиксом Form. Ключ нормализуем — регистр, пробелы и разделители не значимы.
|
||||
# Канон в документации один; здесь только приём ошибочного ввода, чтобы вызов не падал на форме
|
||||
# записи вместо назначения. Применимость назначения к виду объекта проверяется ниже как обычно.
|
||||
purpose_synonyms = {
|
||||
"формаобъекта": "Object", "формаэлемента": "Object", "формадокумента": "Object",
|
||||
"объект": "Object", "элемент": "Object", "документ": "Object", "objectform": "Object",
|
||||
"формасписка": "List", "список": "List", "listform": "List",
|
||||
"формавыбора": "Choice", "выбор": "Choice", "choiceform": "Choice",
|
||||
"формагруппы": "Folder", "группа": "Folder", "folderform": "Folder",
|
||||
"формавыборагруппы": "FolderChoice", "выборгруппы": "FolderChoice",
|
||||
"folderchoiceform": "FolderChoice",
|
||||
"формазаписи": "Record", "запись": "Record", "recordform": "Record",
|
||||
"форманаборазаписей": "RecordSet", "наборзаписей": "RecordSet", "recordsetform": "RecordSet",
|
||||
"формасохранения": "Save", "формасохранениянастроек": "Save", "сохранение": "Save",
|
||||
"saveform": "Save",
|
||||
"формазагрузки": "Load", "формазагрузкинастроек": "Load", "загрузка": "Load",
|
||||
"loadform": "Load",
|
||||
"произвольная": "Custom", "произвольнаяформа": "Custom", "customform": "Custom",
|
||||
}
|
||||
if purpose:
|
||||
purpose_probe = re.sub(r"[\s_-]", "", purpose).lower()
|
||||
is_known_purpose = any(k.lower() == purpose.lower() for k in kind_purposes)
|
||||
if not is_known_purpose and purpose_probe in purpose_synonyms:
|
||||
purpose = purpose_synonyms[purpose_probe]
|
||||
|
||||
if not purpose:
|
||||
if object_type == "Table" and table_data_type == "NonobjectData":
|
||||
# Пометка primary в таблице видов одна на вид, а у таблицы с составным ключом
|
||||
# формы объекта не бывает — основной становится форма записи.
|
||||
purpose = "Record"
|
||||
else:
|
||||
for k, rule in kind_purposes.items():
|
||||
if rule.get("primary"):
|
||||
purpose = k
|
||||
break
|
||||
purpose_key = None
|
||||
for k in kind_purposes:
|
||||
if k.lower() == purpose.lower():
|
||||
purpose_key = k
|
||||
break
|
||||
if purpose_key is None:
|
||||
print(f"Назначение '{purpose}' недопустимо для {object_type}. "
|
||||
f"Допустимые: {', '.join(sorted(kind_purposes))}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
purpose = purpose_key
|
||||
purpose_rule = kind_purposes[purpose]
|
||||
|
||||
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||
"ExchangePlan", "BusinessProcess", "Task"]
|
||||
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
|
||||
|
||||
if purpose == "List":
|
||||
if object_type == "DataProcessor":
|
||||
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
|
||||
# У таблицы внешнего источника набор назначений зависит от вида данных (замерено на
|
||||
# 8.3.24.1691): ObjectData — Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
|
||||
# не отвергается схемой формы, а валит загрузку всей конфигурации «Исключением XDTO» без причины.
|
||||
if object_type == "Table":
|
||||
if table_data_type == "NonobjectData" and purpose == "Object":
|
||||
print(f"Таблица '{object_name}' с составным ключом (TableDataType=NonobjectData): "
|
||||
"формы объекта у неё нет — используйте -Purpose Record.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if table_data_type != "NonobjectData" and purpose == "Record":
|
||||
print(f"Таблица '{object_name}' с ключом из одного поля (TableDataType=ObjectData): "
|
||||
"формы записи у неё нет — используйте -Purpose Object.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif purpose == "Choice":
|
||||
if object_type in processor_like_types or object_type == "InformationRegister":
|
||||
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif purpose == "Record":
|
||||
if object_type != "InformationRegister":
|
||||
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой main_attr —
|
||||
# это произвольная форма (законное состояние), а наполовину заполненная запись означала бы,
|
||||
# что таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
|
||||
if purpose_rule.get("main_attr") and not purpose_rule.get("attr_name"):
|
||||
print(f"Внутренняя ошибка таблицы видов: у {object_type}/{purpose} задан main_attr без attr_name",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Phase 3: Create files ---
|
||||
|
||||
@@ -531,100 +769,47 @@ def main():
|
||||
|
||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
||||
|
||||
if purpose in ("List", "Choice"):
|
||||
# Dynamic list
|
||||
main_table = f"{object_type}.{object_name}"
|
||||
# Одна ветка вместо трёх: что писать, решает запись таблицы видов. Раньше тип главного
|
||||
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
|
||||
attributes_block = ''
|
||||
if purpose_rule.get("main_attr"):
|
||||
main_attr_type = purpose_rule["main_attr"].format(object_type, object_name, object_qualified_name)
|
||||
main_attr_name = purpose_rule["attr_name"]
|
||||
|
||||
form_xml = (
|
||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||
'\t\t<Autofill>true</Autofill>\n'
|
||||
'\t</AutoCommandBar>\n'
|
||||
'\t<ChildItems/>\n'
|
||||
'\t<Attributes>\n'
|
||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
||||
'\t\t\t<Type>\n'
|
||||
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
|
||||
'\t\t\t</Type>\n'
|
||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||
'\t\t\t<Settings xsi:type="DynamicList">\n'
|
||||
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
||||
'\t\t\t</Settings>\n'
|
||||
'\t\t</Attribute>\n'
|
||||
'\t</Attributes>\n'
|
||||
'</Form>'
|
||||
)
|
||||
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
|
||||
tail_lines = ''
|
||||
if main_attr_type == "DynamicList":
|
||||
main_table = object_ref
|
||||
tail_lines = ('\t\t\t<Settings xsi:type="DynamicList">\n'
|
||||
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
||||
'\t\t\t</Settings>\n')
|
||||
elif purpose_rule.get("saved_data"):
|
||||
tail_lines = '\t\t\t<SavedData>true</SavedData>\n'
|
||||
|
||||
elif purpose == "Record":
|
||||
# Information register record
|
||||
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
|
||||
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
|
||||
|
||||
form_xml = (
|
||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||
'\t\t<Autofill>true</Autofill>\n'
|
||||
'\t</AutoCommandBar>\n'
|
||||
'\t<ChildItems/>\n'
|
||||
attributes_block = (
|
||||
'\t<Attributes>\n'
|
||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||
'\t\t\t<Type>\n'
|
||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
||||
'\t\t\t</Type>\n'
|
||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||
'\t\t\t<SavedData>true</SavedData>\n'
|
||||
f'{tail_lines}'
|
||||
'\t\t</Attribute>\n'
|
||||
'\t</Attributes>\n'
|
||||
'</Form>'
|
||||
)
|
||||
|
||||
else:
|
||||
# Object — object form
|
||||
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
|
||||
|
||||
attr_type_map = {
|
||||
"Document": "DocumentObject",
|
||||
"Catalog": "CatalogObject",
|
||||
"DataProcessor": "DataProcessorObject",
|
||||
"Report": "ReportObject",
|
||||
"ExternalDataProcessor": "ExternalDataProcessorObject",
|
||||
"ExternalReport": "ExternalReportObject",
|
||||
"ChartOfAccounts": "ChartOfAccountsObject",
|
||||
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
|
||||
"ExchangePlan": "ExchangePlanObject",
|
||||
"BusinessProcess": "BusinessProcessObject",
|
||||
"Task": "TaskObject",
|
||||
"InformationRegister": "InformationRegisterRecordManager",
|
||||
"AccumulationRegister": "AccumulationRegisterRecordSet",
|
||||
}
|
||||
|
||||
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
||||
|
||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
||||
saved_data_line = ''
|
||||
if object_type not in processor_like_types:
|
||||
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
|
||||
|
||||
form_xml = (
|
||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||
'\t\t<Autofill>true</Autofill>\n'
|
||||
'\t</AutoCommandBar>\n'
|
||||
'\t<ChildItems/>\n'
|
||||
'\t<Attributes>\n'
|
||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||
'\t\t\t<Type>\n'
|
||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
||||
'\t\t\t</Type>\n'
|
||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||
f'{saved_data_line}'
|
||||
'\t\t</Attribute>\n'
|
||||
'\t</Attributes>\n'
|
||||
'</Form>'
|
||||
)
|
||||
# Произвольная форма (main_attr=None) — без блока Attributes вовсе. В типовых это самая
|
||||
# частая форма после объектной: 907 у справочников, 941 у документов, 3482 у отчётов.
|
||||
form_xml = (
|
||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||
'\t\t<Autofill>true</Autofill>\n'
|
||||
'\t</AutoCommandBar>\n'
|
||||
'\t<ChildItems/>\n'
|
||||
f'{attributes_block}'
|
||||
'</Form>'
|
||||
)
|
||||
|
||||
if os.path.exists(form_xml_path):
|
||||
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
||||
@@ -721,26 +906,18 @@ def main():
|
||||
# --- SetDefault ---
|
||||
|
||||
is_first_form_for_purpose = False
|
||||
default_prop_name = None
|
||||
default_value = f"{object_type}.{object_name}.Form.{form_name}"
|
||||
default_value = f"{object_ref}.Form.{form_name}"
|
||||
|
||||
# Determine property name for DefaultForm
|
||||
if purpose == "Object":
|
||||
if object_type in processor_like_types:
|
||||
default_prop_name = "DefaultForm"
|
||||
else:
|
||||
default_prop_name = "DefaultObjectForm"
|
||||
elif purpose == "List":
|
||||
default_prop_name = "DefaultListForm"
|
||||
elif purpose == "Choice":
|
||||
default_prop_name = "DefaultChoiceForm"
|
||||
elif purpose == "Record":
|
||||
default_prop_name = "DefaultRecordForm"
|
||||
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному purpose без
|
||||
# учёта вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не
|
||||
# находился, навык молча ничего не делал.
|
||||
default_prop_name = purpose_rule.get("slot")
|
||||
|
||||
# Check if value is already set
|
||||
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
|
||||
if default_node is not None:
|
||||
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
|
||||
default_node = None
|
||||
if default_prop_name:
|
||||
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
|
||||
if default_node is not None:
|
||||
is_first_form_for_purpose = not (default_node.text or "").strip()
|
||||
|
||||
default_updated = False
|
||||
if set_default or is_first_form_for_purpose:
|
||||
@@ -767,6 +944,10 @@ def main():
|
||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
||||
if default_updated:
|
||||
print(f"{default_prop_name}: {default_value}")
|
||||
elif not default_prop_name:
|
||||
# Молчать здесь нельзя: пользователь ждёт, что форма станет основной, а свойства под неё
|
||||
# у платформы нет (форма набора записей, произвольная форма).
|
||||
print(f"Основной не назначена: у {object_type} нет свойства для формы с назначением {purpose}")
|
||||
print()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
|
||||
# form-compile v1.196 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[string]$JsonPath,
|
||||
|
||||
@@ -14,6 +15,70 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Разбор пользовательского JSON ---
|
||||
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
|
||||
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
|
||||
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
|
||||
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
|
||||
# Возврат через -NoEnumerate: без него одноэлементный
|
||||
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
|
||||
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
|
||||
try {
|
||||
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
|
||||
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
|
||||
$parsed = $text | ConvertFrom-Json
|
||||
} catch {
|
||||
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
|
||||
if ($Inline) {
|
||||
$got = ($text -replace '\s+', ' ').Trim()
|
||||
$label = 'got'
|
||||
if (-not $got) { $got = '(empty)' }
|
||||
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
|
||||
$what = "${what}, ${label}: ${got}"
|
||||
}
|
||||
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
|
||||
exit 1
|
||||
}
|
||||
Write-Output -NoEnumerate $parsed
|
||||
}
|
||||
|
||||
# --- Чтение входного JSON-файла ---
|
||||
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
|
||||
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
|
||||
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
|
||||
# угаданное имя уйдёт в метаданные так же молча.
|
||||
function Read-JsonInputFile([string]$path) {
|
||||
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
|
||||
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
|
||||
# проверкой срабатывают раньше и сохраняют свой текст.
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
[Console]::Error.WriteLine("[ERROR] File not found: $path")
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
|
||||
exit 1
|
||||
}
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
||||
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
try {
|
||||
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
|
||||
} catch {
|
||||
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
|
||||
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -300,7 +365,7 @@ function Load-Preset([string]$PresetName, [string]$ScriptDir) {
|
||||
$presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets"
|
||||
$builtInPath = Join-Path $presetDir "$PresetName.json"
|
||||
if (Test-Path $builtInPath) {
|
||||
$presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json
|
||||
$presetJson = ConvertFrom-JsonInput (Read-JsonInputFile $builtInPath) $builtInPath
|
||||
# Convert PSCustomObject to hashtable recursively
|
||||
$toHash = {
|
||||
param($obj)
|
||||
@@ -327,7 +392,7 @@ function Load-Preset([string]$PresetName, [string]$ScriptDir) {
|
||||
while ($scanDir) {
|
||||
$projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json"
|
||||
if (Test-Path $projPreset) {
|
||||
$projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json
|
||||
$projJson = ConvertFrom-JsonInput (Read-JsonInputFile $projPreset) $projPreset
|
||||
$projHash = & $toHash $projJson
|
||||
foreach ($k in @($projHash.Keys)) {
|
||||
$defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k]
|
||||
@@ -1655,8 +1720,8 @@ if ($FromObject) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
||||
$def = $json | ConvertFrom-Json
|
||||
$json = Read-JsonInputFile $JsonPath
|
||||
$def = ConvertFrom-JsonInput $json $JsonPath
|
||||
}
|
||||
|
||||
# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
|
||||
# form-compile v1.196 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import copy
|
||||
@@ -15,6 +15,68 @@ from lxml import etree
|
||||
|
||||
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||
# регистр не различают, в argparse совпадение точное.
|
||||
|
||||
def parse_json_input(text, source, expected=None, inline=False):
|
||||
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
|
||||
|
||||
expected заполняем только для полиморфного входа: у файла подсказка
|
||||
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
|
||||
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
|
||||
|
||||
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
|
||||
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
|
||||
"""
|
||||
import json as _pj
|
||||
import sys as _psys
|
||||
try:
|
||||
if not str(text).strip():
|
||||
raise ValueError("input is empty")
|
||||
return _pj.loads(text)
|
||||
except ValueError as exc:
|
||||
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
|
||||
if inline:
|
||||
got = " ".join(str(text).split())
|
||||
label = "got"
|
||||
if not got:
|
||||
got = "(empty)"
|
||||
elif len(got) > 60:
|
||||
label = "got (first 60 chars)"
|
||||
got = got[:60]
|
||||
what = "%s, %s: %s" % (what, label, got)
|
||||
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
def read_json_file(path):
|
||||
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
|
||||
|
||||
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
|
||||
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
|
||||
"""
|
||||
import os as _pos
|
||||
import sys as _psys
|
||||
if not _pos.path.exists(path):
|
||||
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
if _pos.path.isdir(path):
|
||||
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
with open(path, "rb") as _fh:
|
||||
data = _fh.read()
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
return data[3:].decode("utf-8")
|
||||
if data[:2] == b"\xff\xfe":
|
||||
return data[2:].decode("utf-16-le")
|
||||
if data[:2] == b"\xfe\xff":
|
||||
return data[2:].decode("utf-16-be")
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
|
||||
% (path, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
class CIDict(dict):
|
||||
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||
@@ -542,8 +604,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
|
||||
preset_dir = os.path.join(os.path.dirname(script_dir), 'presets')
|
||||
built_in_path = os.path.join(preset_dir, f'{preset_name}.json')
|
||||
if os.path.isfile(built_in_path):
|
||||
with open(built_in_path, 'r', encoding='utf-8-sig') as f:
|
||||
preset_data = ci_json(json.load(f))
|
||||
preset_data = ci_json(parse_json_input(read_json_file(built_in_path), built_in_path))
|
||||
for k in list(preset_data.keys()):
|
||||
defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
|
||||
|
||||
@@ -552,8 +613,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
|
||||
while scan_dir:
|
||||
proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json')
|
||||
if os.path.isfile(proj_preset):
|
||||
with open(proj_preset, 'r', encoding='utf-8-sig') as f:
|
||||
proj_data = json.load(f)
|
||||
proj_data = parse_json_input(read_json_file(proj_preset), proj_preset)
|
||||
for k in list(proj_data.keys()):
|
||||
defaults[k] = _deep_merge(defaults.get(k), proj_data[k])
|
||||
break
|
||||
@@ -6455,8 +6515,7 @@ def main():
|
||||
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 = ci_json(json.load(f))
|
||||
defn = ci_json(parse_json_input(read_json_file(json_path), json_path))
|
||||
global QUERY_BASE_DIR
|
||||
QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||
# form-decompile v0.150 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias('Path')]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||
# form-decompile v0.150 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||
#
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||
# form-edit v1.18 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias('Path')]
|
||||
@@ -10,6 +11,70 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Разбор пользовательского JSON ---
|
||||
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
|
||||
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
|
||||
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
|
||||
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
|
||||
# Возврат через -NoEnumerate: без него одноэлементный
|
||||
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
|
||||
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
|
||||
try {
|
||||
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
|
||||
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
|
||||
$parsed = $text | ConvertFrom-Json
|
||||
} catch {
|
||||
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
|
||||
if ($Inline) {
|
||||
$got = ($text -replace '\s+', ' ').Trim()
|
||||
$label = 'got'
|
||||
if (-not $got) { $got = '(empty)' }
|
||||
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
|
||||
$what = "${what}, ${label}: ${got}"
|
||||
}
|
||||
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
|
||||
exit 1
|
||||
}
|
||||
Write-Output -NoEnumerate $parsed
|
||||
}
|
||||
|
||||
# --- Чтение входного JSON-файла ---
|
||||
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
|
||||
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
|
||||
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
|
||||
# угаданное имя уйдёт в метаданные так же молча.
|
||||
function Read-JsonInputFile([string]$path) {
|
||||
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
|
||||
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
|
||||
# проверкой срабатывают раньше и сохраняют свой текст.
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
[Console]::Error.WriteLine("[ERROR] File not found: $path")
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
|
||||
exit 1
|
||||
}
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
||||
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
try {
|
||||
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
|
||||
} catch {
|
||||
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
|
||||
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
@@ -175,7 +240,7 @@ $root = $xmlDoc.DocumentElement
|
||||
|
||||
# === 2. Load JSON ===
|
||||
|
||||
$def = Get-Content -Raw -Encoding UTF8 $JsonPath | ConvertFrom-Json
|
||||
$def = ConvertFrom-JsonInput (Read-JsonInputFile $JsonPath) $JsonPath
|
||||
|
||||
# === 3. Form name + header ===
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-edit v1.14 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||
# form-edit v1.18 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -13,6 +13,68 @@ sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||
# регистр не различают, в argparse совпадение точное.
|
||||
|
||||
def parse_json_input(text, source, expected=None, inline=False):
|
||||
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
|
||||
|
||||
expected заполняем только для полиморфного входа: у файла подсказка
|
||||
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
|
||||
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
|
||||
|
||||
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
|
||||
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
|
||||
"""
|
||||
import json as _pj
|
||||
import sys as _psys
|
||||
try:
|
||||
if not str(text).strip():
|
||||
raise ValueError("input is empty")
|
||||
return _pj.loads(text)
|
||||
except ValueError as exc:
|
||||
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
|
||||
if inline:
|
||||
got = " ".join(str(text).split())
|
||||
label = "got"
|
||||
if not got:
|
||||
got = "(empty)"
|
||||
elif len(got) > 60:
|
||||
label = "got (first 60 chars)"
|
||||
got = got[:60]
|
||||
what = "%s, %s: %s" % (what, label, got)
|
||||
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
def read_json_file(path):
|
||||
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
|
||||
|
||||
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
|
||||
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
|
||||
"""
|
||||
import os as _pos
|
||||
import sys as _psys
|
||||
if not _pos.path.exists(path):
|
||||
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
if _pos.path.isdir(path):
|
||||
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
with open(path, "rb") as _fh:
|
||||
data = _fh.read()
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
return data[3:].decode("utf-8")
|
||||
if data[:2] == b"\xff\xfe":
|
||||
return data[2:].decode("utf-16-le")
|
||||
if data[:2] == b"\xfe\xff":
|
||||
return data[2:].decode("utf-16-be")
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
|
||||
% (path, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
class CIDict(dict):
|
||||
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||
@@ -317,8 +379,7 @@ root = tree.getroot()
|
||||
|
||||
# ── 2. Load JSON ────────────────────────────────────────────
|
||||
|
||||
with open(json_path, "r", encoding="utf-8-sig") as f:
|
||||
defn = ci_json(json.load(f))
|
||||
defn = ci_json(parse_json_input(read_json_file(json_path), json_path))
|
||||
|
||||
# ── 3. Form name + header ───────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
|
||||
# form-info v1.8 — Analyze 1C managed form structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
[Alias('Path')]
|
||||
[string]$FormPath,
|
||||
[int]$Limit = 150,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
|
||||
# form-info v1.8 — Analyze 1C managed form structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -27,11 +27,12 @@ allowed-tools:
|
||||
| ObjectName | да | — | Имя объекта |
|
||||
| FormName | да | — | Имя формы для удаления |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
| Force | нет | — | Удалить, даже если на форму ссылаются, и очистить ссылки |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"] [-Force]
|
||||
```
|
||||
|
||||
## Что удаляется
|
||||
@@ -44,4 +45,4 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -O
|
||||
## Что модифицируется
|
||||
|
||||
- `<SrcDir>/<ObjectName>.xml` — убирается `<Form>` из `ChildObjects`
|
||||
- Если удаляемая форма была DefaultForm — очищается значение DefaultForm
|
||||
- Свойства объекта, указывавшие на удалённую форму — очищаются
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-remove v1.9 — Remove form from 1C object
|
||||
# form-remove v1.10 — Remove form from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -8,7 +8,9 @@ param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FormName,
|
||||
|
||||
[string]$SrcDir = "src"
|
||||
[string]$SrcDir = "src",
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -33,6 +35,180 @@ if (-not (Test-Path $formMetaPath)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Загрузка корневого XML: вид и имя объекта ---
|
||||
|
||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($rootXmlFull.Path)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$typeNode = $null
|
||||
foreach ($c in $xmlDoc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) { $typeNode = $c; break }
|
||||
}
|
||||
if (-not $typeNode) {
|
||||
Write-Error "Не удалось определить вид объекта в $rootXmlPath"
|
||||
exit 1
|
||||
}
|
||||
$mdType = $typeNode.LocalName
|
||||
$nameNode = $typeNode.SelectSingleNode("md:Properties/md:Name", $nsMgr)
|
||||
$objMetaName = if ($nameNode -and $nameNode.InnerText.Trim()) { $nameNode.InnerText.Trim() } else { [System.IO.Path]::GetFileNameWithoutExtension($rootXmlPath) }
|
||||
|
||||
# Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при
|
||||
# удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка.
|
||||
$formRef = "$mdType.$objMetaName.Form.$FormName"
|
||||
|
||||
# --- Чистка ссылок и сохранение в стиле файла-источника ---
|
||||
|
||||
# Каноничное «не задано» зависит от файла: в корневом XML объекта и в Configuration.xml
|
||||
# пустой слот штатен (164 508 пустых на корпус), а внутри Ext/Form.xml пустых <ChoiceForm/>
|
||||
# и <SettingsStorage/> нет ни одного — там свойство просто отсутствует.
|
||||
function Clear-FormRefs {
|
||||
param([System.Xml.XmlDocument]$doc, [string]$ref)
|
||||
|
||||
$isFormFile = $doc.DocumentElement -and $doc.DocumentElement.LocalName -eq "Form"
|
||||
$touched = @()
|
||||
foreach ($node in @($doc.SelectNodes("//*"))) {
|
||||
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
if ($node.SelectNodes("*").Count -gt 0) { continue } # только листья
|
||||
# Сравнение регистронезависимое — как у платформы (в py-порту .lower()).
|
||||
if ($node.InnerText.Trim() -ne $ref) { continue }
|
||||
|
||||
$ln = $node.LocalName
|
||||
$parent = $node.ParentNode
|
||||
if ($ln -eq "Form" -and $parent -and $parent.LocalName -eq "Item") {
|
||||
$touched += "$($parent.LocalName)/$ln"
|
||||
Remove-NodeWithIndent $parent
|
||||
} elseif ($isFormFile) {
|
||||
$touched += $ln
|
||||
Remove-NodeWithIndent $node
|
||||
} else {
|
||||
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
|
||||
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||
$touched += $ln
|
||||
$node.IsEmpty = $true
|
||||
}
|
||||
}
|
||||
return $touched
|
||||
}
|
||||
|
||||
function Remove-NodeWithIndent {
|
||||
param([System.Xml.XmlNode]$node)
|
||||
$parent = $node.ParentNode
|
||||
if (-not $parent) { return }
|
||||
$prev = $node.PreviousSibling
|
||||
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$parent.RemoveChild($prev) | Out-Null
|
||||
}
|
||||
$parent.RemoveChild($node) | Out-Null
|
||||
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
|
||||
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
|
||||
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
|
||||
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
|
||||
}
|
||||
|
||||
function Save-XmlPreservingStyle {
|
||||
param([System.Xml.XmlDocument]$doc, [string]$path)
|
||||
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = $encBom
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$doc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $path) -and ([System.IO.File]::ReadAllText($path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($path, $xmlText, $encBom)
|
||||
}
|
||||
|
||||
# --- Поиск ссылок на форму по всей конфигурации ---
|
||||
|
||||
# Get-Item, а не Resolve-Path: последний оставляет короткое имя 8.3 (NSHIRO~1), а
|
||||
# Get-ChildItem отдаёт длинное (nshirokov) — сравнение путей молча не совпадало.
|
||||
function Get-LongPath {
|
||||
param([string]$path)
|
||||
if (-not (Test-Path -LiteralPath $path)) { return "" }
|
||||
return (Get-Item -LiteralPath $path -Force).FullName
|
||||
}
|
||||
|
||||
# Корень конфигурации: обычно это сам SrcDir, но объект могут передать и из глубины.
|
||||
$configDir = $null
|
||||
$probe = Get-LongPath $SrcDir
|
||||
for ($depth = 0; $depth -lt 4; $depth++) {
|
||||
if (-not $probe) { break }
|
||||
if (Test-Path (Join-Path $probe "Configuration.xml")) { $configDir = $probe; break }
|
||||
$probe = Split-Path $probe
|
||||
}
|
||||
|
||||
$rootXmlLong = Get-LongPath $rootXmlFull.Path
|
||||
$formMetaFull = Get-LongPath $formMetaPath
|
||||
$formDirFull = Get-LongPath $formDir
|
||||
|
||||
$references = @()
|
||||
if ($configDir) {
|
||||
# Полный обход, как в meta-remove: ссылки лежат и внутри Ext/Form.xml (ChoiceForm,
|
||||
# SettingsStorage), узкий скан по корневым XML их не видит.
|
||||
# EnumerateFiles, а не Get-ChildItem -Recurse: на ERP (73 904 XML) обход обёртками
|
||||
# занимает 180 с против 47 с — чтение файлов не узкое место, узкое место перечисление.
|
||||
$refPattern = '<([A-Za-z0-9_.]+)>' + [regex]::Escape($formRef) + '</'
|
||||
$scanSw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$scanned = 0
|
||||
foreach ($fp in [System.IO.Directory]::EnumerateFiles($configDir, "*.xml", [System.IO.SearchOption]::AllDirectories)) {
|
||||
if ($fp -eq $rootXmlLong) { continue } # свой файл чистится всегда
|
||||
if ($fp -eq $formMetaFull) { continue } # файлы удаляемой формы
|
||||
if ($formDirFull -and $fp.StartsWith($formDirFull)) { continue }
|
||||
$scanned++
|
||||
$content = [System.IO.File]::ReadAllText($fp, [System.Text.Encoding]::UTF8)
|
||||
if (-not $content.Contains($formRef)) { continue }
|
||||
foreach ($m in [regex]::Matches($content, $refPattern)) {
|
||||
$references += @{ Path = $fp; Rel = $fp.Substring($configDir.Length + 1); Tag = $m.Groups[1].Value }
|
||||
}
|
||||
}
|
||||
$scanSw.Stop()
|
||||
if ($scanSw.Elapsed.TotalSeconds -ge 5) {
|
||||
Write-Host "[INFO] Проверено ссылок в $scanned файлах за $([math]::Round($scanSw.Elapsed.TotalSeconds, 1)) c"
|
||||
}
|
||||
}
|
||||
|
||||
if ($references.Count -gt 0) {
|
||||
Write-Host "[WARN] На форму $formRef ссылаются $($references.Count) раз(а):"
|
||||
foreach ($grp in ($references | Group-Object { "$($_.Rel)|$($_.Tag)" } | Sort-Object Name)) {
|
||||
$parts = $grp.Name.Split("|")
|
||||
$suffix = if ($grp.Count -gt 1) { " x$($grp.Count)" } else { "" }
|
||||
Write-Host " $($parts[0]) — <$($parts[1])>$suffix"
|
||||
}
|
||||
Write-Host ""
|
||||
if (-not $Force) {
|
||||
Write-Host "[ERROR] Удаление остановлено: форма используется."
|
||||
Write-Host " Решает пользователь: убрать ссылки, отказаться от удаления или"
|
||||
Write-Host " повторить с -Force — тогда ссылки будут очищены."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[WARN] -Force: ссылки будут очищены"
|
||||
Write-Host ""
|
||||
} elseif (-not $configDir) {
|
||||
Write-Host "[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены"
|
||||
}
|
||||
|
||||
# --- Удаление файлов ---
|
||||
|
||||
if (Test-Path $formDir) {
|
||||
@@ -45,70 +221,35 @@ Write-Host "[OK] Удалён файл: $formMetaPath"
|
||||
|
||||
# --- Модификация корневого XML ---
|
||||
|
||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($rootXmlFull.Path)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
# Удалить <Form>FormName</Form> из ChildObjects
|
||||
$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr)
|
||||
foreach ($node in $formNodes) {
|
||||
if ($node.InnerText -eq $FormName) {
|
||||
$parent = $node.ParentNode
|
||||
# Удалить предшествующий whitespace
|
||||
$prev = $node.PreviousSibling
|
||||
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$parent.RemoveChild($prev) | Out-Null
|
||||
}
|
||||
$parent.RemoveChild($node) | Out-Null
|
||||
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
|
||||
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
|
||||
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
|
||||
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
|
||||
Remove-NodeWithIndent $node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
|
||||
# (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
|
||||
# DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
|
||||
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
||||
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
||||
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
||||
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
|
||||
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||
$node.IsEmpty = $true
|
||||
}
|
||||
}
|
||||
# Очистить слоты своего объекта, указывавшие на удалённую форму: Default*/Auxiliary*Form
|
||||
# (form-add пишет свойство по назначению) и ChoiceForm у реквизитов.
|
||||
Clear-FormRefs $xmlDoc $formRef | Out-Null
|
||||
|
||||
# Сохранить с BOM
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = $encBom
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
|
||||
Save-XmlPreservingStyle $xmlDoc $rootXmlFull.Path
|
||||
|
||||
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
||||
|
||||
# --- Чистка ссылок в других файлах (только с -Force) ---
|
||||
|
||||
if ($references.Count -gt 0) {
|
||||
foreach ($grp in ($references | Group-Object { $_.Path } | Sort-Object Name)) {
|
||||
$path = $grp.Name
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $true
|
||||
$doc.Load($path)
|
||||
$touched = @(Clear-FormRefs $doc $formRef)
|
||||
if ($touched.Count -eq 0) { continue }
|
||||
Save-XmlPreservingStyle $doc $path
|
||||
$rel = $path.Substring($configDir.Length + 1)
|
||||
Write-Host "[OK] Очищена ссылка в $rel — $(($touched | Sort-Object -Unique) -join ', ')"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-remove v1.9 — Remove form from 1C object
|
||||
# form-remove v1.10 — Remove form from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -83,6 +83,69 @@ def save_xml_with_bom(tree, path):
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def long_path(path):
|
||||
"""Полный путь в длинной форме. Зеркало Get-LongPath в PS: там Resolve-Path оставляет
|
||||
короткое имя 8.3 (NSHIRO~1), а перечисление отдаёт длинное — сравнение молча не совпадало."""
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
return os.path.realpath(path)
|
||||
|
||||
|
||||
def remove_node_with_indent(node):
|
||||
"""Удалить элемент вместе с предшествующим whitespace; опустевший контейнер сделать
|
||||
самозакрывающимся. Зеркало Remove-NodeWithIndent в PS."""
|
||||
parent = node.getparent()
|
||||
if parent is None:
|
||||
return
|
||||
# В DOM (PS) whitespace — отдельные узлы: удаляются предшествующий и сам элемент, а
|
||||
# whitespace ПОСЛЕ элемента остаётся. В lxml он лежит в node.tail и ушёл бы вместе с
|
||||
# узлом, поэтому его надо передать предшественнику — иначе `</Attributes></Form>`.
|
||||
prev = node.getprevious()
|
||||
if prev is not None:
|
||||
prev.tail = node.tail
|
||||
else:
|
||||
parent.text = node.tail
|
||||
parent.remove(node)
|
||||
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
|
||||
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
|
||||
if len(parent) == 0 and not (parent.text or "").strip():
|
||||
parent.text = None
|
||||
|
||||
|
||||
def clear_form_refs(tree, ref):
|
||||
"""Очистить ссылки на форму. Каноничное «не задано» зависит от файла: в корневом XML
|
||||
объекта и в Configuration.xml пустой слот штатен (164 508 пустых на корпус), а внутри
|
||||
Ext/Form.xml пустых <ChoiceForm/> и <SettingsStorage/> нет ни одного — там свойство
|
||||
просто отсутствует. Зеркало Clear-FormRefs в PS."""
|
||||
root = tree.getroot()
|
||||
is_form_file = etree.QName(root).localname == "Form"
|
||||
touched = []
|
||||
ref_lc = ref.lower()
|
||||
for el in list(root.iter()):
|
||||
if not isinstance(el.tag, str):
|
||||
continue
|
||||
if len(el) > 0: # только листья
|
||||
continue
|
||||
# Сравнение регистронезависимое — как у платформы (в PS -eq регистр не различает).
|
||||
if (el.text or "").strip().lower() != ref_lc:
|
||||
continue
|
||||
|
||||
ln = etree.QName(el).localname
|
||||
parent = el.getparent()
|
||||
if ln == "Form" and parent is not None and etree.QName(parent).localname == "Item":
|
||||
touched.append(f"{etree.QName(parent).localname}/{ln}")
|
||||
remove_node_with_indent(parent)
|
||||
elif is_form_file:
|
||||
touched.append(ln)
|
||||
remove_node_with_indent(el)
|
||||
else:
|
||||
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
|
||||
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||
touched.append(ln)
|
||||
el.text = None
|
||||
return touched
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -90,11 +153,13 @@ def main():
|
||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
||||
parser.add_argument("-FormName", required=True)
|
||||
parser.add_argument("-SrcDir", default="src")
|
||||
parser.add_argument("-Force", action="store_true")
|
||||
args = ci_parse_args(parser)
|
||||
|
||||
object_name = args.ObjectName
|
||||
form_name = args.FormName
|
||||
src_dir = args.SrcDir
|
||||
force = args.Force
|
||||
|
||||
# --- Checks ---
|
||||
|
||||
@@ -112,6 +177,91 @@ def main():
|
||||
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Load root XML: kind and object name ---
|
||||
|
||||
root_xml_full = long_path(root_xml_path) or os.path.abspath(root_xml_path)
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(root_xml_full, parser_xml)
|
||||
root = tree.getroot()
|
||||
|
||||
type_node = None
|
||||
for c in root:
|
||||
if isinstance(c.tag, str):
|
||||
type_node = c
|
||||
break
|
||||
if type_node is None:
|
||||
print(f"Не удалось определить вид объекта в {root_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
md_type = etree.QName(type_node).localname
|
||||
name_node = type_node.find("md:Properties/md:Name", NSMAP)
|
||||
obj_meta_name = (name_node.text or "").strip() if name_node is not None else ""
|
||||
if not obj_meta_name:
|
||||
obj_meta_name = os.path.splitext(os.path.basename(root_xml_path))[0]
|
||||
|
||||
# Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при
|
||||
# удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка.
|
||||
form_ref = f"{md_type}.{obj_meta_name}.Form.{form_name}"
|
||||
|
||||
# --- Find references across the configuration ---
|
||||
|
||||
config_dir = None
|
||||
probe = long_path(src_dir) or os.path.abspath(src_dir)
|
||||
for _ in range(4):
|
||||
if not probe:
|
||||
break
|
||||
if os.path.exists(os.path.join(probe, "Configuration.xml")):
|
||||
config_dir = probe
|
||||
break
|
||||
parent_probe = os.path.dirname(probe)
|
||||
if parent_probe == probe:
|
||||
break
|
||||
probe = parent_probe
|
||||
|
||||
form_meta_full = long_path(form_meta_path)
|
||||
form_dir_full = long_path(form_dir)
|
||||
|
||||
references = []
|
||||
if config_dir:
|
||||
ref_pattern = re.compile(r"<([A-Za-z0-9_.]+)>" + re.escape(form_ref) + r"</")
|
||||
for dirpath, _dirnames, filenames in os.walk(config_dir):
|
||||
for fn in filenames:
|
||||
if not fn.lower().endswith(".xml"):
|
||||
continue
|
||||
fp = os.path.join(dirpath, fn)
|
||||
if fp == root_xml_full or fp == form_meta_full:
|
||||
continue # свой файл и файлы удаляемой формы
|
||||
if form_dir_full and fp.startswith(form_dir_full):
|
||||
continue
|
||||
try:
|
||||
with open(fp, "r", encoding="utf-8-sig") as f:
|
||||
content = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if form_ref not in content:
|
||||
continue
|
||||
for m in ref_pattern.finditer(content):
|
||||
references.append({"path": fp, "rel": os.path.relpath(fp, config_dir),
|
||||
"tag": m.group(1)})
|
||||
|
||||
if references:
|
||||
print(f"[WARN] На форму {form_ref} ссылаются {len(references)} раз(а):")
|
||||
grouped = {}
|
||||
for r in references:
|
||||
grouped[(r["rel"], r["tag"])] = grouped.get((r["rel"], r["tag"]), 0) + 1
|
||||
for (rel, tag) in sorted(grouped):
|
||||
suffix = f" x{grouped[(rel, tag)]}" if grouped[(rel, tag)] > 1 else ""
|
||||
print(f" {rel} — <{tag}>{suffix}")
|
||||
print()
|
||||
if not force:
|
||||
print("[ERROR] Удаление остановлено: форма используется.")
|
||||
print(" Решает пользователь: убрать ссылки, отказаться от удаления или")
|
||||
print(" повторить с -Force — тогда ссылки будут очищены.")
|
||||
sys.exit(1)
|
||||
print("[WARN] -Force: ссылки будут очищены")
|
||||
print()
|
||||
elif not config_dir:
|
||||
print("[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены")
|
||||
|
||||
# --- Delete files ---
|
||||
|
||||
if os.path.isdir(form_dir):
|
||||
@@ -123,48 +273,31 @@ def main():
|
||||
|
||||
# --- Modify root XML ---
|
||||
|
||||
root_xml_full = os.path.abspath(root_xml_path)
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(root_xml_full, parser_xml)
|
||||
root = tree.getroot()
|
||||
|
||||
# Remove <Form>FormName</Form> from ChildObjects
|
||||
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
|
||||
if node.text and node.text.strip() == form_name:
|
||||
parent = node.getparent()
|
||||
prev = node.getprevious()
|
||||
if prev is not None:
|
||||
# Whitespace is in prev.tail
|
||||
if prev.tail and prev.tail.strip() == "":
|
||||
prev.tail = ""
|
||||
else:
|
||||
# First child — whitespace is in parent.text
|
||||
if parent.text and parent.text.strip() == "":
|
||||
parent.text = ""
|
||||
parent.remove(node)
|
||||
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
|
||||
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
|
||||
if len(parent) == 0 and not (parent.text or "").strip():
|
||||
parent.text = None
|
||||
remove_node_with_indent(node)
|
||||
break
|
||||
|
||||
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
||||
# (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
|
||||
# DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
|
||||
ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
|
||||
for el in root.iter():
|
||||
if not isinstance(el.tag, str):
|
||||
continue
|
||||
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
||||
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
|
||||
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||
el.text = None
|
||||
# Очистить слоты своего объекта: Default*/Auxiliary*Form и ChoiceForm у реквизитов.
|
||||
clear_form_refs(tree, form_ref)
|
||||
|
||||
# Save with BOM
|
||||
save_xml_with_bom(tree, root_xml_full)
|
||||
|
||||
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
|
||||
|
||||
# --- Clean references in other files (only with -Force) ---
|
||||
|
||||
for fp in sorted({r["path"] for r in references}):
|
||||
other_tree = etree.parse(fp, parser_xml)
|
||||
touched = clear_form_refs(other_tree, form_ref)
|
||||
if not touched:
|
||||
continue
|
||||
save_xml_with_bom(other_tree, fp)
|
||||
rel = os.path.relpath(fp, config_dir)
|
||||
print(f"[OK] Очищена ссылка в {rel} — {', '.join(sorted(set(touched)))}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# form-validate v1.17 — Validate 1C managed form
|
||||
# form-validate v1.19 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Parameter(Mandatory, Position=0)]
|
||||
[Alias('Path')]
|
||||
[string]$FormPath,
|
||||
|
||||
@@ -927,6 +928,8 @@ $validCfgPrefixes = @(
|
||||
"ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef"
|
||||
"DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef"
|
||||
"ExternalDataProcessorObject","ExternalReportObject"
|
||||
"ExternalDataSourceTableObject","ExternalDataSourceTableRecordManager"
|
||||
"ExternalDataSourceTableRef"
|
||||
"InformationRegisterRecordManager","InformationRegisterRecordSet"
|
||||
"ReportObject","TaskObject","TaskRef"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-validate v1.17 — Validate 1C managed form
|
||||
# form-validate v1.19 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -66,6 +66,8 @@ VALID_CFG_PREFIXES = {
|
||||
'ConstantsSet', 'DataProcessorObject', 'DocumentObject', 'DocumentRef',
|
||||
'DynamicList', 'EnumRef', 'ExchangePlanObject', 'ExchangePlanRef',
|
||||
'ExternalDataProcessorObject', 'ExternalReportObject',
|
||||
'ExternalDataSourceTableObject', 'ExternalDataSourceTableRecordManager',
|
||||
'ExternalDataSourceTableRef',
|
||||
'InformationRegisterRecordManager', 'InformationRegisterRecordSet',
|
||||
'ReportObject', 'TaskObject', 'TaskRef',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||
# interface-edit v1.22 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||
[string]$DefinitionFile,
|
||||
@@ -17,6 +18,70 @@ $ErrorActionPreference = "Stop"
|
||||
if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 }
|
||||
if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 }
|
||||
|
||||
# --- Разбор пользовательского JSON ---
|
||||
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
|
||||
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
|
||||
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
|
||||
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
|
||||
# Возврат через -NoEnumerate: без него одноэлементный
|
||||
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
|
||||
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
|
||||
try {
|
||||
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
|
||||
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
|
||||
$parsed = $text | ConvertFrom-Json
|
||||
} catch {
|
||||
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
|
||||
if ($Inline) {
|
||||
$got = ($text -replace '\s+', ' ').Trim()
|
||||
$label = 'got'
|
||||
if (-not $got) { $got = '(empty)' }
|
||||
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
|
||||
$what = "${what}, ${label}: ${got}"
|
||||
}
|
||||
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
|
||||
exit 1
|
||||
}
|
||||
Write-Output -NoEnumerate $parsed
|
||||
}
|
||||
|
||||
# --- Чтение входного JSON-файла ---
|
||||
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
|
||||
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
|
||||
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
|
||||
# угаданное имя уйдёт в метаданные так же молча.
|
||||
function Read-JsonInputFile([string]$path) {
|
||||
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
|
||||
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
|
||||
# проверкой срабатывают раньше и сохраняют свой текст.
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
[Console]::Error.WriteLine("[ERROR] File not found: $path")
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
|
||||
exit 1
|
||||
}
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
||||
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
try {
|
||||
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
|
||||
} catch {
|
||||
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
|
||||
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
|
||||
$CIPath = Join-Path (Get-Location).Path $CIPath
|
||||
@@ -351,10 +416,10 @@ function Ensure-Section([string]$sectionName) {
|
||||
}
|
||||
|
||||
# --- Parse value: string or JSON array ---
|
||||
function Parse-ValueList([string]$val) {
|
||||
function Parse-ValueList([string]$val, [string]$opName) {
|
||||
$val = $val.Trim()
|
||||
if ($val.StartsWith("[")) {
|
||||
$arr = $val | ConvertFrom-Json
|
||||
$arr = ConvertFrom-JsonInput $val "-Value for operation '$opName'" "a JSON array of command names" -Inline
|
||||
$result = @(); foreach ($item in $arr) { $result += "$item" }
|
||||
return ,$result
|
||||
}
|
||||
@@ -519,7 +584,7 @@ function Do-Show([string[]]$commands) {
|
||||
}
|
||||
|
||||
function Do-Place([string]$jsonVal) {
|
||||
$def = $jsonVal | ConvertFrom-Json
|
||||
$def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'place'" "a JSON object {command, group}" -Inline
|
||||
$cmdName = Normalize-CmdName "$($def.command)"
|
||||
$groupName = "$($def.group)"
|
||||
if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 }
|
||||
@@ -552,7 +617,7 @@ function Do-Place([string]$jsonVal) {
|
||||
}
|
||||
|
||||
function Do-Order([string]$jsonVal) {
|
||||
$def = $jsonVal | ConvertFrom-Json
|
||||
$def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'order'" "a JSON object {group, commands:[...]}" -Inline
|
||||
$groupName = "$($def.group)"
|
||||
$commands = @($def.commands | ForEach-Object { Normalize-CmdName "$_" })
|
||||
if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 }
|
||||
@@ -590,7 +655,7 @@ function Do-Order([string]$jsonVal) {
|
||||
}
|
||||
|
||||
function Do-SubsystemOrder([string]$jsonVal) {
|
||||
$parsed = $jsonVal | ConvertFrom-Json
|
||||
$parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'subsystem-order'" "a JSON array of subsystem paths" -Inline
|
||||
$subsystems = @(); foreach ($s in $parsed) { $subsystems += "$s" }
|
||||
if ($subsystems.Count -eq 0) { Write-Error "subsystem-order requires array of subsystem paths"; exit 1 }
|
||||
|
||||
@@ -618,7 +683,7 @@ function Do-SubsystemOrder([string]$jsonVal) {
|
||||
}
|
||||
|
||||
function Do-GroupOrder([string]$jsonVal) {
|
||||
$parsed = $jsonVal | ConvertFrom-Json
|
||||
$parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'group-order'" "a JSON array of group names" -Inline
|
||||
$groups = @(); foreach ($g in $parsed) { $groups += "$g" }
|
||||
if ($groups.Count -eq 0) { Write-Error "group-order requires array of group names"; exit 1 }
|
||||
|
||||
@@ -651,8 +716,8 @@ if ($DefinitionFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
||||
}
|
||||
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
||||
$ops = $jsonText | ConvertFrom-Json
|
||||
$jsonText = Read-JsonInputFile $DefinitionFile
|
||||
$ops = ConvertFrom-JsonInput $jsonText $DefinitionFile
|
||||
if ($ops -is [System.Array]) {
|
||||
foreach ($op in $ops) { $operations += $op }
|
||||
} else {
|
||||
@@ -669,8 +734,8 @@ foreach ($op in $operations) {
|
||||
$opValue = if ($opValueRaw -is [string]) { $opValueRaw } else { $opValueRaw | ConvertTo-Json -Compress }
|
||||
|
||||
switch ($opName) {
|
||||
"hide" { Do-Hide (Parse-ValueList $opValue) }
|
||||
"show" { Do-Show (Parse-ValueList $opValue) }
|
||||
"hide" { Do-Hide (Parse-ValueList $opValue $opName) }
|
||||
"show" { Do-Show (Parse-ValueList $opValue $opName) }
|
||||
"place" { Do-Place $opValue }
|
||||
"order" { Do-Order $opValue }
|
||||
"subsystem-order" { Do-SubsystemOrder $opValue }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||
# interface-edit v1.22 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -348,10 +348,71 @@ def import_ci_fragment(xml_string):
|
||||
return nodes
|
||||
|
||||
|
||||
def parse_value_list(val):
|
||||
def parse_json_input(text, source, expected=None, inline=False):
|
||||
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
|
||||
|
||||
expected заполняем только для полиморфного входа: у файла подсказка
|
||||
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
|
||||
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
|
||||
|
||||
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
|
||||
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
|
||||
"""
|
||||
import json as _pj
|
||||
import sys as _psys
|
||||
try:
|
||||
if not str(text).strip():
|
||||
raise ValueError("input is empty")
|
||||
return _pj.loads(text)
|
||||
except ValueError as exc:
|
||||
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
|
||||
if inline:
|
||||
got = " ".join(str(text).split())
|
||||
label = "got"
|
||||
if not got:
|
||||
got = "(empty)"
|
||||
elif len(got) > 60:
|
||||
label = "got (first 60 chars)"
|
||||
got = got[:60]
|
||||
what = "%s, %s: %s" % (what, label, got)
|
||||
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
def read_json_file(path):
|
||||
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
|
||||
|
||||
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
|
||||
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
|
||||
"""
|
||||
import os as _pos
|
||||
import sys as _psys
|
||||
if not _pos.path.exists(path):
|
||||
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
if _pos.path.isdir(path):
|
||||
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
with open(path, "rb") as _fh:
|
||||
data = _fh.read()
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
return data[3:].decode("utf-8")
|
||||
if data[:2] == b"\xff\xfe":
|
||||
return data[2:].decode("utf-16-le")
|
||||
if data[:2] == b"\xfe\xff":
|
||||
return data[2:].decode("utf-16-be")
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
|
||||
% (path, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
def parse_value_list(val, op_name):
|
||||
val = val.strip()
|
||||
if val.startswith("["):
|
||||
arr = ci_json(json.loads(val))
|
||||
arr = ci_json(parse_json_input(val, "-Value for operation '%s'" % op_name, "a JSON array of command names", inline=True))
|
||||
return [str(item) for item in arr]
|
||||
return [val]
|
||||
|
||||
@@ -647,7 +708,8 @@ def main():
|
||||
|
||||
def do_place(json_val):
|
||||
nonlocal add_count, modify_count
|
||||
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
|
||||
defn = ci_json(json_val if isinstance(json_val, dict) else parse_json_input(
|
||||
json_val, "-Value for operation 'place'", "a JSON object {command, group}", inline=True))
|
||||
cmd_name = normalize_cmd_name(str(defn["command"]))
|
||||
group_name = str(defn["group"])
|
||||
if not cmd_name or not group_name:
|
||||
@@ -675,7 +737,8 @@ def main():
|
||||
|
||||
def do_order(json_val):
|
||||
nonlocal add_count, remove_count
|
||||
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
|
||||
defn = ci_json(json_val if isinstance(json_val, dict) else parse_json_input(
|
||||
json_val, "-Value for operation 'order'", "a JSON object {group, commands:[...]}", inline=True))
|
||||
group_name = str(defn["group"])
|
||||
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
|
||||
if not group_name or not commands:
|
||||
@@ -709,7 +772,8 @@ def main():
|
||||
|
||||
def do_subsystem_order(json_val):
|
||||
nonlocal add_count, remove_count
|
||||
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
|
||||
parsed = ci_json(json_val if isinstance(json_val, list) else parse_json_input(
|
||||
json_val, "-Value for operation 'subsystem-order'", "a JSON array of subsystem paths", inline=True))
|
||||
subsystems = [str(s) for s in parsed]
|
||||
if not subsystems:
|
||||
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
|
||||
@@ -734,7 +798,8 @@ def main():
|
||||
|
||||
def do_group_order(json_val):
|
||||
nonlocal add_count, remove_count
|
||||
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
|
||||
parsed = ci_json(json_val if isinstance(json_val, list) else parse_json_input(
|
||||
json_val, "-Value for operation 'group-order'", "a JSON array of group names", inline=True))
|
||||
groups = [str(g) for g in parsed]
|
||||
if not groups:
|
||||
print("group-order requires array of group names", file=sys.stderr)
|
||||
@@ -763,8 +828,7 @@ def main():
|
||||
def_file = args.DefinitionFile
|
||||
if not os.path.isabs(def_file):
|
||||
def_file = os.path.join(os.getcwd(), def_file)
|
||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||
ops = ci_json(json.loads(fh.read()))
|
||||
ops = ci_json(parse_json_input(read_json_file(def_file), def_file))
|
||||
if isinstance(ops, list):
|
||||
operations = ops
|
||||
else:
|
||||
@@ -779,9 +843,9 @@ def main():
|
||||
op_value = op.get("value", args.Value or "")
|
||||
|
||||
if op_key == "hide":
|
||||
do_hide(parse_value_list(op_value))
|
||||
do_hide(parse_value_list(op_value, op_name))
|
||||
elif op_key == "show":
|
||||
do_show(parse_value_list(op_value))
|
||||
do_show(parse_value_list(op_value, op_name))
|
||||
elif op_key == "place":
|
||||
do_place(op_value)
|
||||
elif op_key == "order":
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
|
||||
# interface-validate v1.4 — Validate 1C CommandInterface.xml structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||
[Parameter(Mandatory, Position=0)][Alias('Path')][string]$CIPath,
|
||||
[switch]$Detailed,
|
||||
[int]$MaxErrors = 30,
|
||||
[string]$OutFile
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
|
||||
# interface-validate v1.4 — Validate 1C CommandInterface.xml structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
|
||||
import sys, os, argparse, re
|
||||
|
||||
@@ -98,6 +98,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
|
||||
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
|
||||
| HTTPService, WebService | `reference/web.md` |
|
||||
| Enum, Constant, DefinedType | `reference/simple.md` |
|
||||
| ExternalDataSource (внешний источник данных) | `reference/external-data-source.md` |
|
||||
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
|
||||
|
||||
Кросс-типовые детали:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# ExternalDataSource (внешний источник данных)
|
||||
|
||||
Источник описывается **одним JSON целиком**: сам источник, его таблицы с полями и функции.
|
||||
Результат — `ExternalDataSources/<Имя>.xml` плюс по файлу на таблицу в `<Имя>/Tables/`.
|
||||
|
||||
Строку соединения, пользователя, пароль и тип СУБД задавать не нужно: в конфигурации их нет,
|
||||
они настраиваются в режиме «Предприятие» и хранятся в базе.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ExternalDataSource",
|
||||
"name": "PG",
|
||||
"tables": {
|
||||
"prices": ["product_id: Number(10,0)", "period: Date", "price: Number(15,2)"],
|
||||
"products": {
|
||||
"nameInDataSource": "eds.public.products",
|
||||
"tableDataType": "ObjectData",
|
||||
"keyFields": ["id"],
|
||||
"presentationField": "name",
|
||||
"fields": [
|
||||
"id: Number(10,0)",
|
||||
"name: String(150)",
|
||||
"article: String(50) | nullable",
|
||||
"parent_id: ExternalDataSourceTableRef.PG.products | nullable"
|
||||
]
|
||||
}
|
||||
},
|
||||
"functions": {
|
||||
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Свойства источника
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
|
||||
| `tables` | `{}` | таблицы (см. ниже) |
|
||||
| `functions` | `{}` | функции (см. ниже) |
|
||||
|
||||
При `AutomaticAndManaged` режим блокировок решает каждая таблица сама; при конкретном значении
|
||||
одноимённое свойство таблицы игнорируется платформой.
|
||||
|
||||
## Таблицы
|
||||
|
||||
Ключ — имя таблицы в конфигурации. Значение — **массив полей** либо **объект** со свойствами
|
||||
и ключом `fields` (та же двойственность, что у `tabularSections` справочника).
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `nameInDataSource` | = имя таблицы | имя физической таблицы; для реляционной СУБД обычно `<база>.<схема>.<таблица>` |
|
||||
| `tableType` | `Table` | `Table` — таблица или представление; `Expression` — табличная функция |
|
||||
| `readOnly` | `false` | запрет записи; ставь `true` для представлений и таблиц вида `Expression` — писать в них нельзя |
|
||||
| `expressionInDataSource` | пусто | выражение для `Expression`, напр. `public.f_by_parent(&1)`; имя базы не указывается |
|
||||
| `tableDataType` | `NonobjectData` | `ObjectData` (запись определяется одним полем) / `NonobjectData` |
|
||||
| `keyFields` | `[]` | имена ключевых полей; без них таблица собирается, но недоступны форма записи и набор записей |
|
||||
| `presentationField` | пусто | имя поля представления (только `ObjectData`) |
|
||||
| `parentField` | пусто | имя поля родителя; его тип должен быть ссылкой на эту же таблицу |
|
||||
| `inputByString` | `[]` | имена полей ввода по строке; ключа нет → берётся `presentationField` |
|
||||
| `dataVersionField` | пусто | имя поля версии данных |
|
||||
| `dataLockFields` | `[]` | имена полей блокировки |
|
||||
| `transactionsIsolationLevel` | `Auto` | `Auto` / `ReadUncommitted` / `ReadCommitted` / `RepeatableRead` / `Serializable` |
|
||||
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
|
||||
| `basedOn` | `[]` | ввод на основании, ссылки вида `Catalog.Контрагенты` |
|
||||
| `useStandardCommands` | `true` | bool |
|
||||
| `quickChoice` | `false` | bool |
|
||||
| `editType` | `InDialog` | `InDialog` / `InList` |
|
||||
| `fields` | `[]` | поля (см. ниже); синоним — `columns` |
|
||||
|
||||
Ссылки на поля (`keyFields`, `presentationField`, `parentField`, `dataVersionField`,
|
||||
`inputByString`, `dataLockFields`) задаются **короткими именами полей** этой же таблицы.
|
||||
|
||||
Прочие свойства — представления (`objectPresentation`, `listPresentation`, …), формы по умолчанию
|
||||
(`defaultObjectForm`, `defaultListForm`, …), `characteristics`, `explanation`,
|
||||
`includeHelpInContents` — как у справочника.
|
||||
|
||||
**Значение незаполненного родителя загрузкой XML не задаётся** — платформа сбрасывает его в пустое
|
||||
при любой загрузке, включая загрузку собственной выгрузки. Ставится только в Конфигураторе вручную.
|
||||
|
||||
## Поля
|
||||
|
||||
Строковая и объектная форма — те же, что у реквизитов (см. `attributes.md`). Своих ключа три:
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `nameInDataSource` | = имя поля | имя колонки; в одинарных кавычках уходит в SQL как есть |
|
||||
| `readOnly` | `false` | поле не записывается (вычисляемые, автоинкрементные) |
|
||||
| `allowNull` | `false` | допускает `NULL` |
|
||||
|
||||
Флаги строковой формы: `readonly`, `nullable`.
|
||||
|
||||
```json
|
||||
"fields": [
|
||||
"id: Number(10,0) | readonly",
|
||||
{ "name": "article", "type": "String(50)", "nameInDataSource": "art_code", "allowNull": true }
|
||||
]
|
||||
```
|
||||
|
||||
Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу
|
||||
внешнего источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`.
|
||||
|
||||
Двоичные данные: `BinaryData` — безлимит (так их пишет платформа при импорте из СУБД),
|
||||
`BinaryData(N)` — переменной длины, `BinaryData(N,fixed)` — фиксированной.
|
||||
|
||||
**Составной тип у поля недопустим** — платформа такую конфигурацию не загружает.
|
||||
|
||||
## Функции
|
||||
|
||||
Ключ — имя функции. Значение — строка (интерпретируется как `expression`) либо объект.
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `expression` | — | выражение в источнике, обязательный |
|
||||
| `returns` | `String` | тип возвращаемого значения |
|
||||
| `returnValue` | `true` | `false` — процедура, тип не пишется |
|
||||
|
||||
Параметры описываются прямо в выражении как `&1`, `&2` — отдельного ключа для них нет.
|
||||
Необязательные — в фигурных скобках `f(&1{, &2})`, переменное число — `&n[]` (только последним).
|
||||
|
||||
```json
|
||||
"functions": {
|
||||
"nextKey": "NEXT VALUE FOR dbo.SimpleSequence",
|
||||
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }
|
||||
}
|
||||
```
|
||||
|
||||
## Добавить в существующий источник
|
||||
|
||||
`meta-compile` описывает источник **целиком**: повторный запуск заменяет его файл и выдаёт новый
|
||||
uuid, а таблицы, которых нет в описании, останутся на диске сиротами. Чтобы дописать таблицу или
|
||||
функцию в уже существующий источник, есть `meta-edit`:
|
||||
|
||||
```json
|
||||
{ "add": {
|
||||
"tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } },
|
||||
"functions": { "nextKey": "NEXT VALUE FOR public.seq_key" }
|
||||
} }
|
||||
```
|
||||
|
||||
Удалить таблицу — `meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`.
|
||||
|
||||
## Не поддерживается
|
||||
|
||||
- **Кубы OLAP** (`Cube`, `DimensionTable`, `Dimension`, `Resource`).
|
||||
- **Формы и модули** таблиц — форму добавляет навык `form-add`, содержимое собирает `form-compile`.
|
||||
Набор назначений зависит от `tableDataType`: `ObjectData` — Object/List/Choice, `NonobjectData` — Record/List/Choice.
|
||||
@@ -1,5 +1,6 @@
|
||||
# meta-compile v1.94 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.109 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$JsonPath,
|
||||
@@ -9,6 +10,70 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Разбор пользовательского JSON ---
|
||||
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
|
||||
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
|
||||
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
|
||||
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
|
||||
# Возврат через -NoEnumerate: без него одноэлементный
|
||||
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
|
||||
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
|
||||
try {
|
||||
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
|
||||
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
|
||||
$parsed = $text | ConvertFrom-Json
|
||||
} catch {
|
||||
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
|
||||
if ($Inline) {
|
||||
$got = ($text -replace '\s+', ' ').Trim()
|
||||
$label = 'got'
|
||||
if (-not $got) { $got = '(empty)' }
|
||||
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
|
||||
$what = "${what}, ${label}: ${got}"
|
||||
}
|
||||
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
|
||||
exit 1
|
||||
}
|
||||
Write-Output -NoEnumerate $parsed
|
||||
}
|
||||
|
||||
# --- Чтение входного JSON-файла ---
|
||||
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
|
||||
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
|
||||
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
|
||||
# угаданное имя уйдёт в метаданные так же молча.
|
||||
function Read-JsonInputFile([string]$path) {
|
||||
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
|
||||
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
|
||||
# проверкой срабатывают раньше и сохраняют свой текст.
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
[Console]::Error.WriteLine("[ERROR] File not found: $path")
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
|
||||
exit 1
|
||||
}
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
||||
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||
}
|
||||
try {
|
||||
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
|
||||
} catch {
|
||||
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
|
||||
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- 1. Load and validate JSON ---
|
||||
@@ -18,8 +83,8 @@ if (-not (Test-Path $JsonPath)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
||||
$def = $json | ConvertFrom-Json
|
||||
$json = Read-JsonInputFile $JsonPath
|
||||
$def = ConvertFrom-JsonInput $json $JsonPath
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
@@ -214,6 +279,7 @@ $script:objectTypeSynonyms = @{
|
||||
"ВебСервис" = "WebService"
|
||||
"ОпределяемыйТип" = "DefinedType"
|
||||
"ФункциональнаяОпция" = "FunctionalOption"
|
||||
"ВнешнийИсточникДанных" = "ExternalDataSource"
|
||||
}
|
||||
|
||||
# Enum property value synonyms — model often gets these slightly wrong
|
||||
@@ -255,7 +321,9 @@ $script:validEnumValues = @{
|
||||
"WriteMode" = @("Independent","RecorderSubordinate")
|
||||
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
|
||||
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
|
||||
"DataLockControlMode" = @("Automatic","Managed")
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц: там режим может
|
||||
# решаться на уровне таблицы, у прочих объектов такого значения нет.
|
||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||
"FullTextSearch" = @("Use","DontUse")
|
||||
"DataHistory" = @("Use","DontUse")
|
||||
"DefaultPresentation" = @("AsDescription","AsCode")
|
||||
@@ -407,7 +475,7 @@ $validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","Ac
|
||||
"HTTPService","WebService","DefinedType","FunctionalOption",
|
||||
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
|
||||
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
|
||||
"CommonPicture","CommonTemplate")
|
||||
"CommonPicture","CommonTemplate","ExternalDataSource")
|
||||
# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в
|
||||
# Configuration.xml как есть — выгрузка получалась с <catalog>, которую платформа не принимает.
|
||||
# Прощаем регистр, но приводим к канону списка.
|
||||
@@ -536,6 +604,10 @@ $script:typeSynonyms["bool"] = "Boolean"
|
||||
# ValueStorage / UUID — прощающий ввод (модель может написать base64Binary / рус. форму → канон).
|
||||
$script:typeSynonyms["valuestorage"] = "ValueStorage"
|
||||
$script:typeSynonyms["base64binary"] = "ValueStorage"
|
||||
# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
|
||||
# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
|
||||
$script:typeSynonyms["binarydata"] = "BinaryData"
|
||||
$script:typeSynonyms["двоичныеданные"] = "BinaryData"
|
||||
$script:typeSynonyms["хранилищезначений"] = "ValueStorage"
|
||||
$script:typeSynonyms["хранилищезначения"] = "ValueStorage"
|
||||
$script:typeSynonyms["uuid"] = "UUID"
|
||||
@@ -573,6 +645,7 @@ $script:typeSynonyms["планвидовхарактеристикссылка"]
|
||||
$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef"
|
||||
$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef"
|
||||
$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef"
|
||||
$script:typeSynonyms["внешнийисточникданныхтаблицассылка"] = "ExternalDataSourceTableRef"
|
||||
$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef"
|
||||
$script:typeSynonyms["задачассылка"] = "TaskRef"
|
||||
$script:typeSynonyms["определяемыйтип"] = "DefinedType"
|
||||
@@ -738,6 +811,25 @@ function Emit-TypeContent {
|
||||
}
|
||||
|
||||
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба).
|
||||
# ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
|
||||
if ($typeStr -match '^BinaryData(\(|$)') {
|
||||
# BinaryData — безлимит (так платформа пишет поле внешнего источника: 4294967292/Fixed).
|
||||
# BinaryData(N) — переменной длины, BinaryData(N,fixed) — фиксированной.
|
||||
$bm = [regex]::Match($typeStr, '^BinaryData(\((\d+)(,\s*(fixed|variable))?\))?$', 'IgnoreCase')
|
||||
if (-not $bm.Success) {
|
||||
Write-Error "Неверный тип '$typeStr': ждётся BinaryData, BinaryData(Длина) или BinaryData(Длина,fixed|variable)."
|
||||
exit 1
|
||||
}
|
||||
$blen = if ($bm.Groups[2].Success) { $bm.Groups[2].Value } else { "4294967292" }
|
||||
$ballowed = if ($bm.Groups[4].Success) { if ($bm.Groups[4].Value.ToLowerInvariant() -eq "fixed") { "Fixed" } else { "Variable" } }
|
||||
elseif ($bm.Groups[2].Success) { "Variable" } else { "Fixed" }
|
||||
X "$indent<v8:Type>xs:base64Binary</v8:Type>"
|
||||
X "$indent<v8:BinaryDataQualifiers>"
|
||||
X "$indent`t<v8:Length>$blen</v8:Length>"
|
||||
X "$indent`t<v8:AllowedLength>$ballowed</v8:AllowedLength>"
|
||||
X "$indent</v8:BinaryDataQualifiers>"
|
||||
return
|
||||
}
|
||||
if ($typeStr -eq "ValueStorage") {
|
||||
X "$indent<v8:Type>v8:ValueStorage</v8:Type>"
|
||||
return
|
||||
@@ -791,7 +883,9 @@ function Emit-TypeContent {
|
||||
# $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет»
|
||||
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
|
||||
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
|
||||
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
|
||||
# ExternalDataSourceTableRef — единственный ссылочный тип с ДВУМЯ частями после префикса
|
||||
# (Источник.Таблица), поэтому `(.+)$` здесь существенно.
|
||||
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef|ExternalDataSourceTableRef)\.(.+)$') {
|
||||
if ($script:cfgPrefix) {
|
||||
X "$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>"
|
||||
} else {
|
||||
@@ -1015,8 +1109,10 @@ function Emit-FillValue {
|
||||
# --- 5. Attribute shorthand parser ---
|
||||
|
||||
function Build-TypeStr {
|
||||
param($obj)
|
||||
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" }
|
||||
# ValueTypeOnly — для корневого определения объекта: там ключ type означает ВИД объекта
|
||||
# (Constant, Catalog, …), а не тип значения, и подхватывать его нельзя.
|
||||
param($obj, [switch]$ValueTypeOnly)
|
||||
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif (-not $ValueTypeOnly -and $obj.type) { "$($obj.type)" } else { "" }
|
||||
if ($t -and -not $t.Contains('(')) {
|
||||
if ($t -eq "String" -and $obj.length) {
|
||||
$t = "String($($obj.length))"
|
||||
@@ -1119,6 +1215,10 @@ function Parse-AttributeShorthand {
|
||||
# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
|
||||
# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
|
||||
typeReductionMode = $val.typeReductionMode
|
||||
# Поле внешнего источника данных (контекст eds-field).
|
||||
nameInDataSource = if ($val.nameInDataSource) { "$($val.nameInDataSource)" } else { "" }
|
||||
readOnly = if ($val.readOnly -eq $true) { $true } else { $false }
|
||||
allowNull = if ($val.allowNull -eq $true) { $true } else { $false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1259,6 +1359,11 @@ $script:generatedTypes = @{
|
||||
"DefinedType" = @(
|
||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||
)
|
||||
"ExternalDataSource" = @(
|
||||
@{ prefix = "ExternalDataSourceManager"; category = "Manager" }
|
||||
@{ prefix = "ExternalDataSourceTablesManager"; category = "TablesManager" }
|
||||
@{ prefix = "ExternalDataSourceCubesManager"; category = "CubesManager" }
|
||||
)
|
||||
"DocumentJournal" = @(
|
||||
@{ prefix = "DocumentJournalSelection"; category = "Selection" }
|
||||
@{ prefix = "DocumentJournalList"; category = "List" }
|
||||
@@ -1984,7 +2089,7 @@ function Emit-Attribute {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} elseif ($context -notin @("tabular", "processor-tabular") -and
|
||||
} elseif ($context -notin @("tabular", "processor-tabular", "eds-field") -and
|
||||
($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) {
|
||||
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
|
||||
}
|
||||
@@ -2048,18 +2153,34 @@ function Emit-Attribute {
|
||||
if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking }
|
||||
X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"
|
||||
|
||||
X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
|
||||
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
|
||||
# у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы.
|
||||
if ($context -ne "eds-field") {
|
||||
X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
|
||||
}
|
||||
Emit-ChoiceParameterLinks "$indent`t`t" $parsed.choiceParameterLinks
|
||||
Emit-ChoiceParameters "$indent`t`t" $parsed.choiceParameters
|
||||
$qc = if ($parsed.quickChoice) { $parsed.quickChoice } else { "Auto" }
|
||||
X "$indent`t`t<QuickChoice>$qc</QuickChoice>"
|
||||
$coi = if ($parsed.createOnInput) { $parsed.createOnInput } else { "Auto" }
|
||||
X "$indent`t`t<CreateOnInput>$coi</CreateOnInput>"
|
||||
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
|
||||
Emit-LinkByType "$indent`t`t" $parsed.linkByType
|
||||
if ($context -ne "eds-field") {
|
||||
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
|
||||
Emit-LinkByType "$indent`t`t" $parsed.linkByType
|
||||
}
|
||||
$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
|
||||
X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"
|
||||
|
||||
if ($context -eq "eds-field") {
|
||||
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
|
||||
$nids = if ($parsed.nameInDataSource) { "$($parsed.nameInDataSource)" } else { $parsed.name }
|
||||
X "$indent`t`t<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>"
|
||||
$ro = if ($parsed.readOnly -eq $true -or $parsed.flags -contains "readonly") { "true" } else { "false" }
|
||||
X "$indent`t`t<ReadOnly>$ro</ReadOnly>"
|
||||
$an = if ($parsed.allowNull -eq $true -or $parsed.flags -contains "nullable") { "true" } else { "false" }
|
||||
X "$indent`t`t<AllowNull>$an</AllowNull>"
|
||||
}
|
||||
|
||||
# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
|
||||
if ($elemTag -eq "Dimension" -and $context -eq "register-info") {
|
||||
$master = if ($parsed.master -eq $true -or $parsed.flags -contains "master") { "true" } else { "false" }
|
||||
@@ -2113,7 +2234,8 @@ function Emit-Attribute {
|
||||
}
|
||||
|
||||
# Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular)
|
||||
if ($context -notin @("processor", "processor-tabular")) {
|
||||
# и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет.
|
||||
if ($context -notin @("processor", "processor-tabular", "eds-field")) {
|
||||
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
|
||||
if ($context -ne "account-flag") {
|
||||
# Ресурс регистра накопления/расчёта/бухгалтерии НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
|
||||
@@ -2706,8 +2828,8 @@ function Emit-ConstantProperties {
|
||||
if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
|
||||
|
||||
# Type — valueType (пустой явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
|
||||
$valueType = Build-TypeStr $def
|
||||
$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '') -or ($null -ne $def.type -and "$($def.type)".Trim() -eq '')
|
||||
$valueType = Build-TypeStr $def -ValueTypeOnly
|
||||
$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '')
|
||||
if ($typeEmpty) { X "$i<Type/>" }
|
||||
else { if (-not $valueType) { $valueType = "String" }; Emit-ValueType $i $valueType }
|
||||
|
||||
@@ -4249,6 +4371,222 @@ function Emit-AddressingAttribute {
|
||||
Emit-Attribute $indent $parsed "task-addressing" "AddressingAttribute"
|
||||
}
|
||||
|
||||
# --- 13h. Внешние источники данных ---
|
||||
# Источник пишется в один файл, каждая его таблица — в свой. Функции живут ВНУТРИ файла источника
|
||||
# полными узлами, наравне со списком имён таблиц: так их выгружает платформа.
|
||||
|
||||
# Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns.
|
||||
# Нормализуем в [ordered]@{ имя = @{ props; fields } } — форма зеркальна tabularSections.
|
||||
function Get-EdsTables {
|
||||
param($val)
|
||||
$tables = [ordered]@{}
|
||||
if (-not $val) { return $tables }
|
||||
function New-EdsTableEntry { param($v)
|
||||
if ($v -is [array] -or $v.GetType().Name -eq 'Object[]') {
|
||||
return @{ props = $null; fields = @($v) }
|
||||
}
|
||||
$f = if ($null -ne $v.fields) { @($v.fields) } elseif ($null -ne $v.columns) { @($v.columns) } else { @() }
|
||||
return @{ props = $v; fields = $f }
|
||||
}
|
||||
if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
|
||||
foreach ($t in $val) { $tables["$($t.name)"] = New-EdsTableEntry $t }
|
||||
} else {
|
||||
$val.PSObject.Properties | ForEach-Object { $tables[$_.Name] = New-EdsTableEntry $_.Value }
|
||||
}
|
||||
return $tables
|
||||
}
|
||||
|
||||
# Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь.
|
||||
function Get-EdsFieldRef {
|
||||
param([string]$srcName, [string]$tableName, [string]$fieldName)
|
||||
if (-not $fieldName) { return "" }
|
||||
if ($fieldName -like "ExternalDataSource.*") { return $fieldName }
|
||||
return "ExternalDataSource.$srcName.Table.$tableName.Field.$fieldName"
|
||||
}
|
||||
|
||||
function Emit-EdsFieldRefList {
|
||||
param([string]$indent, [string]$tag, $names, [string]$srcName, [string]$tableName)
|
||||
$list = @($names | Where-Object { $_ })
|
||||
if ($list.Count -eq 0) { X "$indent<$tag/>"; return }
|
||||
X "$indent<$tag>"
|
||||
foreach ($n in $list) {
|
||||
X "$indent`t<xr:Field>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$n"))</xr:Field>"
|
||||
}
|
||||
X "$indent</$tag>"
|
||||
}
|
||||
|
||||
function Emit-EdsFieldRefScalar {
|
||||
param([string]$indent, [string]$tag, $name, [string]$srcName, [string]$tableName)
|
||||
if (-not $name) { X "$indent<$tag/>"; return }
|
||||
X "$indent<$tag>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$name"))</$tag>"
|
||||
}
|
||||
|
||||
function Emit-ExternalDataSourceProperties {
|
||||
param([string]$indent)
|
||||
$i = $indent
|
||||
X "$i<Name>$(Esc-XmlText $objName)</Name>"
|
||||
Emit-MLText $i "Synonym" $synonym
|
||||
if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
|
||||
$dlcm = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
|
||||
X "$i<DataLockControlMode>$dlcm</DataLockControlMode>"
|
||||
}
|
||||
|
||||
# Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
|
||||
# в выражении как &1, &2 (см. reference/external-data-source.md).
|
||||
function Emit-EdsFunction {
|
||||
# $typeXml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
|
||||
# своим эмиттером типов. Так тело функции не зависит от того, какой это навык.
|
||||
param([string]$indent, [string]$fnName, $val, [string]$typeXml)
|
||||
$expr = ""
|
||||
$returns = ""
|
||||
$returnValue = $true
|
||||
$fnSynonym = $null
|
||||
$fnComment = ""
|
||||
if ($val -is [string]) {
|
||||
$expr = "$val"
|
||||
} else {
|
||||
$expr = if ($val.expression) { "$($val.expression)" } elseif ($val.expressionInDataSource) { "$($val.expressionInDataSource)" } else { "" }
|
||||
$returns = if ($val.returns) { "$($val.returns)" } elseif ($val.returnType) { "$($val.returnType)" } else { "" }
|
||||
if ($null -ne $val.returnValue) { $returnValue = ($val.returnValue -eq $true) }
|
||||
$fnSynonym = $val.synonym
|
||||
$fnComment = if ($val.comment) { "$($val.comment)" } else { "" }
|
||||
}
|
||||
if (-not $expr) {
|
||||
Write-Error "Функция '$fnName' внешнего источника данных: не задано выражение (ключ expression)."
|
||||
exit 1
|
||||
}
|
||||
$uuid = New-Guid-String
|
||||
X "$indent<Function uuid=`"$uuid`">"
|
||||
X "$indent`t<Properties>"
|
||||
X "$indent`t`t<Name>$(Esc-XmlText $fnName)</Name>"
|
||||
Emit-MLText "$indent`t`t" "Synonym" $fnSynonym
|
||||
if ($fnComment) { X "$indent`t`t<Comment>$(Esc-XmlText $fnComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
|
||||
X "$indent`t`t<ReturnValue>$(if ($returnValue) { 'true' } else { 'false' })</ReturnValue>"
|
||||
if ($returnValue -and $typeXml) {
|
||||
X $typeXml.TrimEnd("`r", "`n")
|
||||
} else {
|
||||
X "$indent`t`t<Type/>"
|
||||
}
|
||||
X "$indent`t`t<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>"
|
||||
X "$indent`t</Properties>"
|
||||
X "$indent</Function>"
|
||||
}
|
||||
|
||||
# Свойства таблицы: 38 узлов в порядке выгрузки платформы.
|
||||
function Emit-EdsTableProperties {
|
||||
# $charXml и $defaultFormsXml — уже собранные блоки <Characteristics> и четыре слота
|
||||
# <Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
|
||||
# от хелперов конкретного навыка и годится для копирования (check-inline-drift).
|
||||
param([string]$indent, [string]$srcName, [string]$tableName, $t, [string]$charXml, [string]$defaultFormsXml)
|
||||
$i = $indent
|
||||
$tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName }
|
||||
X "$i<Name>$(Esc-XmlText $tableName)</Name>"
|
||||
Emit-MLText $i "Synonym" $tblSynonym
|
||||
if ($t -and $t.comment) { X "$i<Comment>$(Esc-XmlText "$($t.comment)")</Comment>" } else { X "$i<Comment/>" }
|
||||
|
||||
$tableType = if ($t -and $t.tableType) { "$($t.tableType)" } else { "Table" }
|
||||
X "$i<TableType>$tableType</TableType>"
|
||||
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
|
||||
$nids = if ($t -and $t.nameInDataSource) { "$($t.nameInDataSource)" } elseif ($tableType -eq "Expression") { "" } else { $tableName }
|
||||
if ($nids) { X "$i<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>" } else { X "$i<NameInDataSource/>" }
|
||||
$expr = if ($t -and $t.expressionInDataSource) { "$($t.expressionInDataSource)" } elseif ($t -and $t.expression) { "$($t.expression)" } else { "" }
|
||||
if ($expr) { X "$i<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>" } else { X "$i<ExpressionInDataSource/>" }
|
||||
$dataType = if ($t -and $t.tableDataType) { "$($t.tableDataType)" } else { "NonobjectData" }
|
||||
X "$i<TableDataType>$dataType</TableDataType>"
|
||||
|
||||
Emit-EdsFieldRefList $i "KeyFields" $(if ($t) { $t.keyFields } else { $null }) $srcName $tableName
|
||||
Emit-EdsFieldRefScalar $i "PresentationField" $(if ($t) { $t.presentationField } else { $null }) $srcName $tableName
|
||||
Emit-EdsFieldRefScalar $i "ParentField" $(if ($t) { $t.parentField } else { $null }) $srcName $tableName
|
||||
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
|
||||
# различаются формой самого значения (xsi:nil против типизированного).
|
||||
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
|
||||
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
|
||||
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
|
||||
if ($t -and $t.parentField) { X "$i<UnfilledParentValue xsi:type=`"xs:string`"/>" }
|
||||
else { X "$i<UnfilledParentValue xsi:nil=`"true`"/>" }
|
||||
if ($charXml) { X $charXml.TrimEnd("`r", "`n") } else { X "$i<Characteristics/>" }
|
||||
|
||||
X "$i<UseStandardCommands>$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })</UseStandardCommands>"
|
||||
X "$i<QuickChoice>$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })</QuickChoice>"
|
||||
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
|
||||
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
|
||||
$ibsGiven = ($t -and $t.PSObject -and $t.PSObject.Properties -and ($t.PSObject.Properties.Name -contains 'inputByString'))
|
||||
$ibs = if ($ibsGiven) { $t.inputByString } elseif ($t -and $t.presentationField) { @($t.presentationField) } else { $null }
|
||||
Emit-EdsFieldRefList $i "InputByString" $ibs $srcName $tableName
|
||||
X "$i<CreateOnInput>$(if ($t -and $t.createOnInput) { "$($t.createOnInput)" } else { 'Auto' })</CreateOnInput>"
|
||||
X "$i<SearchStringModeOnInputByString>$(if ($t -and $t.searchStringModeOnInputByString) { "$($t.searchStringModeOnInputByString)" } else { 'Begin' })</SearchStringModeOnInputByString>"
|
||||
X "$i<ChoiceDataGetModeOnInputByString>$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })</ChoiceDataGetModeOnInputByString>"
|
||||
X "$i<ChoiceHistoryOnInput>$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })</ChoiceHistoryOnInput>"
|
||||
|
||||
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
|
||||
if ($defaultFormsXml) { X $defaultFormsXml.TrimEnd("`r", "`n") }
|
||||
else { foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { X "$i<$formTag/>" } }
|
||||
foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation",
|
||||
"ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) {
|
||||
$key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1)
|
||||
Emit-MLText $i $presTag $(if ($t) { $t.$key } else { $null })
|
||||
}
|
||||
X "$i<IncludeHelpInContents>$(if ($t -and $t.includeHelpInContents -eq $true) { 'true' } else { 'false' })</IncludeHelpInContents>"
|
||||
X "$i<ReadOnly>$(if ($t -and $t.readOnly -eq $true) { 'true' } else { 'false' })</ReadOnly>"
|
||||
X "$i<TransactionsIsolationLevel>$(if ($t -and $t.transactionsIsolationLevel) { "$($t.transactionsIsolationLevel)" } else { 'Auto' })</TransactionsIsolationLevel>"
|
||||
Emit-EdsFieldRefScalar $i "DataVersionField" $(if ($t) { $t.dataVersionField } else { $null }) $srcName $tableName
|
||||
X "$i<EditType>$(if ($t -and $t.editType) { "$($t.editType)" } else { 'InDialog' })</EditType>"
|
||||
Emit-MDRefList $i "BasedOn" $(if ($t) { $t.basedOn } else { $null })
|
||||
Emit-EdsFieldRefList $i "DataLockFields" $(if ($t) { $t.dataLockFields } else { $null }) $srcName $tableName
|
||||
X "$i<DataLockControlMode>$(if ($t -and $t.dataLockControlMode) { "$($t.dataLockControlMode)" } else { 'Automatic' })</DataLockControlMode>"
|
||||
}
|
||||
|
||||
# Отдельный XML-документ таблицы. Возвращает строку: X пишет в общий StringBuilder,
|
||||
# поэтому «перехват» — запомнить длину, отдать эмиттерам, вырезать добавленное
|
||||
# (тот же приём, что у составного типа).
|
||||
function Build-EdsTableXml {
|
||||
# $fieldsXml, $charXml, $defaultFormsXml — уже собранные узлы: их рендерит вызывающий навык
|
||||
# своими эмиттерами. Так тело функции не зависит от того, какой это навык.
|
||||
param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml, [string]$charXml, [string]$defaultFormsXml)
|
||||
$before = $script:xml.Length
|
||||
|
||||
$tableUuid = New-Guid-String
|
||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
|
||||
X "`t<Table uuid=`"$tableUuid`">"
|
||||
# InternalInfo у таблицы эмитится здесь, а не через $script:generatedTypes: имя элемента
|
||||
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
|
||||
X "`t`t<InternalInfo>"
|
||||
foreach ($pair in @(
|
||||
@("ExternalDataSourceTableManager", "Manager"),
|
||||
@("ExternalDataSourceTableObject", "Object"),
|
||||
@("ExternalDataSourceTableRef", "Ref"),
|
||||
@("ExternalDataSourceTableList", "List"),
|
||||
@("ExternalDataSourceTableRecord", "Record"),
|
||||
@("ExternalDataSourceTableRecordSet", "RecordSet"),
|
||||
@("ExternalDataSourceTableRecordKey", "RecordKey"),
|
||||
@("ExternalDataSourceTableRecordManager", "RecordManager"))) {
|
||||
X "`t`t`t<xr:GeneratedType name=`"$($pair[0]).$srcName.$tableName`" category=`"$($pair[1])`">"
|
||||
X "`t`t`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
|
||||
X "`t`t`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
|
||||
X "`t`t`t</xr:GeneratedType>"
|
||||
}
|
||||
X "`t`t</InternalInfo>"
|
||||
|
||||
X "`t`t<Properties>"
|
||||
Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props $charXml $defaultFormsXml
|
||||
X "`t`t</Properties>"
|
||||
|
||||
if ($fieldsXml) {
|
||||
X "`t`t<ChildObjects>"
|
||||
X $fieldsXml.TrimEnd("`r", "`n")
|
||||
X "`t`t</ChildObjects>"
|
||||
} else {
|
||||
X "`t`t<ChildObjects/>"
|
||||
}
|
||||
X "`t</Table>"
|
||||
X "</MetaDataObject>"
|
||||
|
||||
$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
|
||||
[void]$script:xml.Remove($before, $script:xml.Length - $before)
|
||||
return $chunk
|
||||
}
|
||||
|
||||
# --- 14. Namespaces ---
|
||||
|
||||
$script:xmlnsDecl = '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"'
|
||||
@@ -4392,6 +4730,7 @@ switch ($objType) {
|
||||
"Task" { Emit-TaskProperties "`t`t`t" }
|
||||
"HTTPService" { Emit-HTTPServiceProperties "`t`t`t" }
|
||||
"WebService" { Emit-WebServiceProperties "`t`t`t" }
|
||||
"ExternalDataSource" { Emit-ExternalDataSourceProperties "`t`t`t" }
|
||||
}
|
||||
|
||||
X "`t`t</Properties>"
|
||||
@@ -4689,6 +5028,41 @@ if ($objType -eq "WebService") {
|
||||
}
|
||||
}
|
||||
|
||||
# --- ExternalDataSource: Tables (именами) + Functions (полными узлами) ---
|
||||
$script:edsTables = [ordered]@{}
|
||||
if ($objType -eq "ExternalDataSource") {
|
||||
$script:edsTables = Get-EdsTables $def.tables
|
||||
$functions = [ordered]@{}
|
||||
if ($def.functions) {
|
||||
$def.functions.PSObject.Properties | ForEach-Object { $functions[$_.Name] = $_.Value }
|
||||
}
|
||||
if ($script:edsTables.Count -gt 0 -or $functions.Count -gt 0) {
|
||||
$hasChildren = $true
|
||||
X "`t`t<ChildObjects>"
|
||||
foreach ($tblName in $script:edsTables.Keys) {
|
||||
X "`t`t`t<Table>$(Esc-XmlText $tblName)</Table>"
|
||||
}
|
||||
foreach ($fnName in $functions.Keys) {
|
||||
$fnVal = $functions[$fnName]
|
||||
$fnReturns = if ($fnVal -is [string]) { "String" }
|
||||
elseif ($fnVal.returns) { "$($fnVal.returns)" }
|
||||
elseif ($fnVal.returnType) { "$($fnVal.returnType)" } else { "String" }
|
||||
$fnNoValue = (-not ($fnVal -is [string])) -and ($null -ne $fnVal.returnValue) -and ($fnVal.returnValue -ne $true)
|
||||
$fnTypeXml = ""
|
||||
if (-not $fnNoValue) {
|
||||
$typeBefore = $script:xml.Length
|
||||
Emit-ValueType "`t`t`t`t`t" $fnReturns
|
||||
$fnTypeXml = $script:xml.ToString($typeBefore, $script:xml.Length - $typeBefore)
|
||||
[void]$script:xml.Remove($typeBefore, $script:xml.Length - $typeBefore)
|
||||
}
|
||||
Emit-EdsFunction "`t`t`t" $fnName $fnVal $fnTypeXml
|
||||
}
|
||||
X "`t`t</ChildObjects>"
|
||||
} else {
|
||||
X "`t`t<ChildObjects/>"
|
||||
}
|
||||
}
|
||||
|
||||
# --- CommonModule: no ChildObjects ---
|
||||
|
||||
X "`t</$objType>"
|
||||
@@ -4737,6 +5111,7 @@ $script:typePluralMap = @{
|
||||
"WSReference" = "WSReferences"
|
||||
"CommonPicture" = "CommonPictures"
|
||||
"CommonTemplate" = "CommonTemplates"
|
||||
"ExternalDataSource" = "ExternalDataSources"
|
||||
}
|
||||
|
||||
$typePlural = $script:typePluralMap[$objType]
|
||||
@@ -4969,8 +5344,54 @@ function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) {
|
||||
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding)
|
||||
}
|
||||
|
||||
# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid —
|
||||
# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими.
|
||||
# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя.
|
||||
if (Test-Path $mainXmlPath) {
|
||||
Write-Warning "$objType '$objName' уже существует ($typePlural/$objName.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit."
|
||||
}
|
||||
|
||||
Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc
|
||||
|
||||
# Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/.
|
||||
# Единственный вид, у которого объект складывается более чем из одного XML.
|
||||
$edsTablesCreated = @()
|
||||
if ($objType -eq "ExternalDataSource" -and $script:edsTables.Count -gt 0) {
|
||||
$tablesDir = Join-Path $objSubDir "Tables"
|
||||
if (-not (Test-Path $tablesDir)) { New-Item -ItemType Directory -Path $tablesDir -Force | Out-Null }
|
||||
foreach ($tblName in $script:edsTables.Keys) {
|
||||
$entry = $script:edsTables[$tblName]
|
||||
$fieldsBefore = $script:xml.Length
|
||||
foreach ($f in @($entry.fields)) {
|
||||
Emit-Attribute "`t`t`t" (Parse-AttributeShorthand $f) "eds-field" "Field"
|
||||
}
|
||||
$fieldsXml = $script:xml.ToString($fieldsBefore, $script:xml.Length - $fieldsBefore)
|
||||
[void]$script:xml.Remove($fieldsBefore, $script:xml.Length - $fieldsBefore)
|
||||
$tp = $entry.props
|
||||
$charBefore = $script:xml.Length
|
||||
Emit-Characteristics "`t`t`t" $(if ($tp) { $tp.characteristics } else { $null })
|
||||
$charXml = $script:xml.ToString($charBefore, $script:xml.Length - $charBefore)
|
||||
[void]$script:xml.Remove($charBefore, $script:xml.Length - $charBefore)
|
||||
|
||||
# Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника —
|
||||
# голое имя платформа отвергает («Неизвестный объект метаданных»).
|
||||
$formsBefore = $script:xml.Length
|
||||
foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) {
|
||||
$key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1)
|
||||
$formVal = if ($tp) { $tp.$key } else { $null }
|
||||
if ($formVal -and "$formVal" -notmatch '\.') { $formVal = "ExternalDataSource.$objName.Table.$tblName.Form.$formVal" }
|
||||
Emit-FormRef "`t`t`t" $formTag $formVal
|
||||
}
|
||||
$defaultFormsXml = $script:xml.ToString($formsBefore, $script:xml.Length - $formsBefore)
|
||||
[void]$script:xml.Remove($formsBefore, $script:xml.Length - $formsBefore)
|
||||
|
||||
$tableXml = Build-EdsTableXml $objName $tblName $entry $fieldsXml $charXml $defaultFormsXml
|
||||
$tablePath = Join-Path $tablesDir "$tblName.xml"
|
||||
Write-XmlFileKeepEol $tablePath $tableXml $enc
|
||||
$edsTablesCreated += $tablePath
|
||||
}
|
||||
}
|
||||
|
||||
# Module files
|
||||
$modulesCreated = @()
|
||||
|
||||
@@ -5169,91 +5590,195 @@ if ($commands -and $commands.Count -gt 0) {
|
||||
|
||||
# --- 17. Register in Configuration.xml ---
|
||||
|
||||
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
|
||||
$regResult = $null
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
|
||||
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# Возвращает исход: added | already | no-childobj | no-config.
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
|
||||
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $true
|
||||
$doc.Load($ParentXmlPath)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $nsMgr)
|
||||
if (-not $childObjects) { return "no-childobj" }
|
||||
|
||||
$existing = $childObjects.SelectNodes("md:$ChildTag", $nsMgr)
|
||||
foreach ($e in $existing) {
|
||||
if ($e.InnerText -eq $ChildName) { return "already" }
|
||||
}
|
||||
|
||||
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
|
||||
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
|
||||
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
|
||||
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
|
||||
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||||
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
|
||||
if (-not $block.Success) {
|
||||
# Самозакрытый <ChildObjects/> раскрываем первой записью
|
||||
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
|
||||
if (-not $empty.Success) { return "no-childobj" }
|
||||
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
|
||||
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. Test-OrderSensitiveType.
|
||||
if (-not (Test-OrderSensitiveType $ChildTag) -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
|
||||
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
|
||||
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($m.Success) {
|
||||
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
|
||||
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
$m = $m.NextMatch()
|
||||
}
|
||||
}
|
||||
|
||||
$closeSame = "</$ChildTag>"
|
||||
$blockEnd = $block.Index + $block.Length
|
||||
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
if ($lastSame -ge 0) {
|
||||
# После последнего объекта того же вида (группы по видам сохраняются)
|
||||
$insertAt = $lastSame + $closeSame.Length
|
||||
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
|
||||
} else {
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по $childObjectTypes. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
$ownIdx = $childObjectTypes.IndexOf($ChildTag)
|
||||
$anchor = $null
|
||||
if ($ownIdx -ge 0) {
|
||||
$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
|
||||
$tm = $typeRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($tm.Success) {
|
||||
$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
|
||||
if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
|
||||
$tm = $tm.NextMatch()
|
||||
}
|
||||
}
|
||||
if ($anchor) {
|
||||
$newContent = $configContent.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $configContent.Substring($anchor.Index)
|
||||
} else {
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется
|
||||
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
|
||||
}
|
||||
}
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
# XML tag name for Configuration.xml ChildObjects
|
||||
$childTag = $objType
|
||||
|
||||
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:$childTag", $nsMgr)
|
||||
$alreadyExists = $false
|
||||
foreach ($e in $existing) {
|
||||
if ($e.InnerText -eq $objName) {
|
||||
$alreadyExists = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($alreadyExists) {
|
||||
$regResult = "already"
|
||||
} else {
|
||||
$newElem = $configDoc.CreateElement($childTag, "http://v8.1c.ru/8.3/MDClasses")
|
||||
$newElem.InnerText = $objName
|
||||
|
||||
if ($existing.Count -gt 0) {
|
||||
# Insert after last existing element of same type
|
||||
$lastElem = $existing[$existing.Count - 1]
|
||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
|
||||
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
|
||||
} else {
|
||||
# No existing elements of this type — 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($newElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($newElem) | Out-Null
|
||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
|
||||
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
|
||||
# `encoding="UTF-8"` и `<a/>`.
|
||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgSettings.Indent = $false
|
||||
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
|
||||
$configDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
|
||||
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
|
||||
|
||||
$regResult = "added"
|
||||
}
|
||||
} else {
|
||||
$regResult = "no-childobj"
|
||||
}
|
||||
} else {
|
||||
$regResult = "no-config"
|
||||
}
|
||||
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
|
||||
$regResult = Register-InChildObjects $configXmlPath "Configuration" $childTag $objName
|
||||
|
||||
# --- 18. Summary ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-compile v1.94 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.109 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -23,6 +23,68 @@ sys.stderr.reconfigure(encoding="utf-8")
|
||||
# молча терял свойства DSL, написанные в другом регистре. Обёртки ниже выравнивают поведение.
|
||||
# ============================================================
|
||||
|
||||
|
||||
def parse_json_input(text, source, expected=None, inline=False):
|
||||
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
|
||||
|
||||
expected заполняем только для полиморфного входа: у файла подсказка
|
||||
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
|
||||
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
|
||||
|
||||
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
|
||||
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
|
||||
"""
|
||||
import json as _pj
|
||||
import sys as _psys
|
||||
try:
|
||||
if not str(text).strip():
|
||||
raise ValueError("input is empty")
|
||||
return _pj.loads(text)
|
||||
except ValueError as exc:
|
||||
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
|
||||
if inline:
|
||||
got = " ".join(str(text).split())
|
||||
label = "got"
|
||||
if not got:
|
||||
got = "(empty)"
|
||||
elif len(got) > 60:
|
||||
label = "got (first 60 chars)"
|
||||
got = got[:60]
|
||||
what = "%s, %s: %s" % (what, label, got)
|
||||
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
def read_json_file(path):
|
||||
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
|
||||
|
||||
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
|
||||
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
|
||||
"""
|
||||
import os as _pos
|
||||
import sys as _psys
|
||||
if not _pos.path.exists(path):
|
||||
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
if _pos.path.isdir(path):
|
||||
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
with open(path, "rb") as _fh:
|
||||
data = _fh.read()
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
return data[3:].decode("utf-8")
|
||||
if data[:2] == b"\xff\xfe":
|
||||
return data[2:].decode("utf-16-le")
|
||||
if data[:2] == b"\xfe\xff":
|
||||
return data[2:].decode("utf-16-be")
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
|
||||
% (path, exc), file=_psys.stderr)
|
||||
_psys.exit(1)
|
||||
|
||||
|
||||
class CIDict(dict):
|
||||
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||
@@ -374,10 +436,9 @@ if not os.path.isfile(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:
|
||||
json_text = f.read()
|
||||
json_text = read_json_file(json_path)
|
||||
|
||||
defn = ci_json(json.loads(json_text))
|
||||
defn = ci_json(parse_json_input(json_text, json_path))
|
||||
|
||||
assert_edit_allowed(output_dir, "editable")
|
||||
|
||||
@@ -436,6 +497,7 @@ object_type_synonyms = {
|
||||
'ВебСервис': 'WebService',
|
||||
'ОпределяемыйТип': 'DefinedType',
|
||||
'ФункциональнаяОпция': 'FunctionalOption',
|
||||
'ВнешнийИсточникДанных': 'ExternalDataSource',
|
||||
}
|
||||
|
||||
# Enum property value synonyms — model often gets these slightly wrong
|
||||
@@ -481,7 +543,9 @@ valid_enum_values = {
|
||||
'WriteMode': ['Independent', 'RecorderSubordinate'],
|
||||
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
|
||||
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
|
||||
'DataLockControlMode': ['Automatic', 'Managed'],
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц: там режим может
|
||||
# решаться на уровне таблицы, у прочих объектов такого значения нет.
|
||||
'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'],
|
||||
'FullTextSearch': ['Use', 'DontUse'],
|
||||
'DataHistory': ['Use', 'DontUse'],
|
||||
'DefaultPresentation': ['AsDescription', 'AsCode'],
|
||||
@@ -631,7 +695,7 @@ valid_types = [
|
||||
'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage',
|
||||
'CommonForm',
|
||||
'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference',
|
||||
'CommonPicture', 'CommonTemplate',
|
||||
'CommonPicture', 'CommonTemplate', 'ExternalDataSource',
|
||||
]
|
||||
# Регистр имени вида — как в PS (-contains регистронезависим): приводим к канону списка
|
||||
obj_type = next((t for t in valid_types if t.lower() == obj_type.lower()), obj_type)
|
||||
@@ -671,6 +735,10 @@ type_synonyms = {
|
||||
# ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон).
|
||||
'valuestorage': 'ValueStorage',
|
||||
'base64binary': 'ValueStorage',
|
||||
# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
|
||||
# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
|
||||
'binarydata': 'BinaryData',
|
||||
'двоичныеданные': 'BinaryData',
|
||||
'хранилищезначений': 'ValueStorage',
|
||||
'хранилищезначения': 'ValueStorage',
|
||||
'uuid': 'UUID',
|
||||
@@ -684,6 +752,7 @@ type_synonyms = {
|
||||
'планвидоврасчётассылка': 'ChartOfCalculationTypesRef',
|
||||
'планвидоврасчетассылка': 'ChartOfCalculationTypesRef',
|
||||
'планобменассылка': 'ExchangePlanRef',
|
||||
'внешнийисточникданныхтаблицассылка': 'ExternalDataSourceTableRef',
|
||||
'бизнеспроцессссылка': 'BusinessProcessRef',
|
||||
'задачассылка': 'TaskRef',
|
||||
'определяемыйтип': 'DefinedType',
|
||||
@@ -862,6 +931,25 @@ def emit_type_content(indent, type_str):
|
||||
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$', type_str):
|
||||
X(f'{indent}<v8:TypeSet>cfg:{type_str}</v8:TypeSet>')
|
||||
return
|
||||
# ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
|
||||
if re.match(r'^BinaryData(\(|$)', type_str, re.I):
|
||||
# BinaryData — безлимит (так платформа пишет поле внешнего источника: 4294967292/Fixed).
|
||||
# BinaryData(N) — переменной длины, BinaryData(N,fixed) — фиксированной.
|
||||
m_bin = re.match(r'^BinaryData(?:\((\d+)(?:,\s*(fixed|variable))?\))?$', type_str, re.I)
|
||||
if not m_bin:
|
||||
print(f"Неверный тип '{type_str}': ждётся BinaryData, BinaryData(Длина) или BinaryData(Длина,fixed|variable).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
blen = m_bin.group(1) or '4294967292'
|
||||
if m_bin.group(2):
|
||||
ballowed = 'Fixed' if m_bin.group(2).lower() == 'fixed' else 'Variable'
|
||||
else:
|
||||
ballowed = 'Variable' if m_bin.group(1) else 'Fixed'
|
||||
X(f'{indent}<v8:Type>xs:base64Binary</v8:Type>')
|
||||
X(f'{indent}<v8:BinaryDataQualifiers>')
|
||||
X(f'{indent}\t<v8:Length>{blen}</v8:Length>')
|
||||
X(f'{indent}\t<v8:AllowedLength>{ballowed}</v8:AllowedLength>')
|
||||
X(f'{indent}</v8:BinaryDataQualifiers>')
|
||||
return
|
||||
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary).
|
||||
if type_str == 'ValueStorage':
|
||||
X(f'{indent}<v8:Type>v8:ValueStorage</v8:Type>')
|
||||
@@ -908,7 +996,9 @@ def emit_type_content(indent, type_str):
|
||||
# cfg_prefix = None означает «пишем файл, корень которого cfg НЕ объявляет»
|
||||
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
|
||||
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
|
||||
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
|
||||
# ExternalDataSourceTableRef — единственный ссылочный тип с ДВУМЯ частями после префикса
|
||||
# (Источник.Таблица), поэтому `(.+)$` здесь существенно.
|
||||
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef|ExternalDataSourceTableRef)\.(.+)$', type_str)
|
||||
if m:
|
||||
if cfg_prefix:
|
||||
X(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
|
||||
@@ -1133,8 +1223,10 @@ def emit_fill_value(indent, type_str, spec, has_spec, type_empty=False):
|
||||
# 5. Attribute shorthand parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_type_str(obj):
|
||||
t = str(obj.get('valueType') or obj.get('type') or '')
|
||||
def build_type_str(obj, value_type_only=False):
|
||||
# value_type_only — для корневого определения объекта: там ключ type означает ВИД объекта
|
||||
# (Constant, Catalog, …), а не тип значения, и подхватывать его нельзя.
|
||||
t = str(obj.get('valueType') or ('' if value_type_only else obj.get('type')) or '')
|
||||
if t and '(' not in t:
|
||||
if t == 'String' and obj.get('length'):
|
||||
t = f"String({obj['length']})"
|
||||
@@ -1230,6 +1322,10 @@ def parse_attribute_shorthand(val):
|
||||
# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
|
||||
# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
|
||||
'typeReductionMode': val.get('typeReductionMode'),
|
||||
# Поле внешнего источника данных (контекст eds-field).
|
||||
'nameInDataSource': str(val['nameInDataSource']) if val.get('nameInDataSource') else '',
|
||||
'readOnly': val.get('readOnly') is True,
|
||||
'allowNull': val.get('allowNull') is True,
|
||||
}
|
||||
|
||||
def parse_enum_value_shorthand(val):
|
||||
@@ -1366,6 +1462,11 @@ generated_types = {
|
||||
'DefinedType': [
|
||||
{'prefix': 'DefinedType', 'category': 'DefinedType'},
|
||||
],
|
||||
'ExternalDataSource': [
|
||||
{'prefix': 'ExternalDataSourceManager', 'category': 'Manager'},
|
||||
{'prefix': 'ExternalDataSourceTablesManager', 'category': 'TablesManager'},
|
||||
{'prefix': 'ExternalDataSourceCubesManager', 'category': 'CubesManager'},
|
||||
],
|
||||
'DocumentJournal': [
|
||||
{'prefix': 'DocumentJournalSelection', 'category': 'Selection'},
|
||||
{'prefix': 'DocumentJournalList', 'category': 'List'},
|
||||
@@ -2217,15 +2318,28 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
||||
if parsed.get('fillChecking'):
|
||||
fill_checking = parsed['fillChecking']
|
||||
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
|
||||
X(f'{indent}\t\t<ChoiceFoldersAndItems>{parsed.get("choiceFoldersAndItems") or "Items"}</ChoiceFoldersAndItems>')
|
||||
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
|
||||
# у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы.
|
||||
if context != 'eds-field':
|
||||
X(f'{indent}\t\t<ChoiceFoldersAndItems>{parsed.get("choiceFoldersAndItems") or "Items"}</ChoiceFoldersAndItems>')
|
||||
emit_choice_parameter_links(f'{indent}\t\t', parsed.get('choiceParameterLinks'))
|
||||
emit_choice_parameters(f'{indent}\t\t', parsed.get('choiceParameters'))
|
||||
X(f'{indent}\t\t<QuickChoice>{parsed.get("quickChoice") or "Auto"}</QuickChoice>')
|
||||
X(f'{indent}\t\t<CreateOnInput>{parsed.get("createOnInput") or "Auto"}</CreateOnInput>')
|
||||
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
|
||||
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
|
||||
if context != 'eds-field':
|
||||
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
|
||||
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
|
||||
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
|
||||
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
|
||||
|
||||
if context == 'eds-field':
|
||||
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
|
||||
nids = parsed.get('nameInDataSource') or parsed['name']
|
||||
X(f'{indent}\t\t<NameInDataSource>{esc_xml_text(str(nids))}</NameInDataSource>')
|
||||
ro = 'true' if (parsed.get('readOnly') is True or 'readonly' in parsed.get('flags', [])) else 'false'
|
||||
X(f'{indent}\t\t<ReadOnly>{ro}</ReadOnly>')
|
||||
an = 'true' if (parsed.get('allowNull') is True or 'nullable' in parsed.get('flags', [])) else 'false'
|
||||
X(f'{indent}\t\t<AllowNull>{an}</AllowNull>')
|
||||
# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
|
||||
if elem_tag == 'Dimension' and context == 'register-info':
|
||||
master = 'true' if (parsed.get('master') is True or 'master' in parsed.get('flags', [])) else 'false'
|
||||
@@ -2273,7 +2387,8 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
||||
use_value = parsed.get("use") or "ForItem"
|
||||
if context == 'catalog':
|
||||
X(f'{indent}\t\t<Use>{use_value}</Use>')
|
||||
if context not in ('processor', 'processor-tabular'):
|
||||
# и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет.
|
||||
if context not in ('processor', 'processor-tabular', 'eds-field'):
|
||||
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
|
||||
if context != 'account-flag':
|
||||
# Ресурс регистра накопления НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
|
||||
@@ -2808,9 +2923,8 @@ def emit_constant_properties(indent):
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
# Type — valueType (явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
|
||||
value_type = build_type_str(defn)
|
||||
type_empty = (defn.get('valueType') is not None and str(defn.get('valueType')).strip() == '') or \
|
||||
(defn.get('type') is not None and str(defn.get('type')).strip() == '')
|
||||
value_type = build_type_str(defn, value_type_only=True)
|
||||
type_empty = defn.get('valueType') is not None and str(defn.get('valueType')).strip() == ''
|
||||
if type_empty:
|
||||
X(f'{i}<Type/>')
|
||||
else:
|
||||
@@ -4246,6 +4360,248 @@ def emit_addressing_attribute(indent, addr_def):
|
||||
parsed = parse_attribute_shorthand(addr_def)
|
||||
emit_attribute(indent, parsed, 'task-addressing', 'AddressingAttribute')
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 13h. Внешние источники данных
|
||||
# ---------------------------------------------------------------------------
|
||||
# Источник пишется в один файл, каждая его таблица — в свой. Функции живут ВНУТРИ файла источника
|
||||
# полными узлами, наравне со списком имён таблиц: так их выгружает платформа.
|
||||
|
||||
def get_eds_tables(val):
|
||||
"""Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns."""
|
||||
tables = {}
|
||||
if not val:
|
||||
return tables
|
||||
|
||||
def entry(v):
|
||||
if isinstance(v, list):
|
||||
return {'props': None, 'fields': list(v)}
|
||||
f = v.get('fields') if v.get('fields') is not None else v.get('columns')
|
||||
return {'props': v, 'fields': list(f) if f else []}
|
||||
|
||||
if isinstance(val, list):
|
||||
for t in val:
|
||||
tables[str(t.get('name'))] = entry(t)
|
||||
else:
|
||||
for k, v in val.items():
|
||||
tables[k] = entry(v)
|
||||
return tables
|
||||
|
||||
|
||||
def get_eds_field_ref(src_name, table_name, field_name):
|
||||
"""Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь."""
|
||||
if not field_name:
|
||||
return ''
|
||||
if str(field_name).startswith('ExternalDataSource.'):
|
||||
return str(field_name)
|
||||
return f'ExternalDataSource.{src_name}.Table.{table_name}.Field.{field_name}'
|
||||
|
||||
|
||||
def emit_eds_field_ref_list(indent, tag, names, src_name, table_name):
|
||||
items = [n for n in (names or []) if n]
|
||||
if not items:
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>')
|
||||
for n in items:
|
||||
X(f'{indent}\t<xr:Field>{esc_xml_text(get_eds_field_ref(src_name, table_name, n))}</xr:Field>')
|
||||
X(f'{indent}</{tag}>')
|
||||
|
||||
|
||||
def emit_eds_field_ref_scalar(indent, tag, name, src_name, table_name):
|
||||
if not name:
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>{esc_xml_text(get_eds_field_ref(src_name, table_name, name))}</{tag}>')
|
||||
|
||||
|
||||
def emit_external_data_source_properties(indent):
|
||||
i = indent
|
||||
X(f'{i}<Name>{esc_xml_text(obj_name)}</Name>')
|
||||
emit_mltext(i, 'Synonym', synonym)
|
||||
if defn.get('comment'):
|
||||
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
dlcm = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
|
||||
X(f'{i}<DataLockControlMode>{dlcm}</DataLockControlMode>')
|
||||
|
||||
|
||||
def emit_eds_function(indent, fn_name, val, type_xml):
|
||||
"""Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
|
||||
в выражении как &1, &2 (см. reference/external-data-source.md).
|
||||
|
||||
type_xml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
|
||||
своим эмиттером типов. Так тело функции не зависит от того, какой это навык."""
|
||||
fn_synonym = None
|
||||
fn_comment = ''
|
||||
returns = ''
|
||||
return_value = True
|
||||
if isinstance(val, str):
|
||||
expr = val
|
||||
else:
|
||||
expr = str(val.get('expression') or val.get('expressionInDataSource') or '')
|
||||
returns = str(val.get('returns') or val.get('returnType') or '')
|
||||
if val.get('returnValue') is not None:
|
||||
return_value = val.get('returnValue') is True
|
||||
fn_synonym = val.get('synonym')
|
||||
fn_comment = str(val['comment']) if val.get('comment') else ''
|
||||
if not expr:
|
||||
print(f"ERROR: Функция '{fn_name}' внешнего источника данных: не задано выражение (ключ expression).",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
X(f'{indent}<Function uuid="{new_uuid()}">')
|
||||
X(f'{indent}\t<Properties>')
|
||||
X(f'{indent}\t\t<Name>{esc_xml_text(fn_name)}</Name>')
|
||||
emit_mltext(f'{indent}\t\t', 'Synonym', fn_synonym)
|
||||
if fn_comment:
|
||||
X(f'{indent}\t\t<Comment>{esc_xml_text(fn_comment)}</Comment>')
|
||||
else:
|
||||
X(f'{indent}\t\t<Comment/>')
|
||||
X(f'{indent}\t\t<ReturnValue>{"true" if return_value else "false"}</ReturnValue>')
|
||||
if return_value and type_xml:
|
||||
X(type_xml.rstrip('\r\n'))
|
||||
else:
|
||||
X(f'{indent}\t\t<Type/>')
|
||||
X(f'{indent}\t\t<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>')
|
||||
X(f'{indent}\t</Properties>')
|
||||
X(f'{indent}</Function>')
|
||||
|
||||
|
||||
def emit_eds_table_properties(indent, src_name, table_name, t, char_xml, default_forms_xml):
|
||||
"""Свойства таблицы: 38 узлов в порядке выгрузки платформы.
|
||||
|
||||
char_xml и default_forms_xml — уже собранные блоки <Characteristics> и четыре слота
|
||||
<Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
|
||||
от хелперов конкретного навыка и годится для копирования (check-inline-drift)."""
|
||||
i = indent
|
||||
t = t or {}
|
||||
tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name)
|
||||
X(f'{i}<Name>{esc_xml_text(table_name)}</Name>')
|
||||
emit_mltext(i, 'Synonym', tbl_synonym)
|
||||
if t.get('comment'):
|
||||
X(f'{i}<Comment>{esc_xml_text(str(t["comment"]))}</Comment>')
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
|
||||
table_type = str(t.get('tableType') or 'Table')
|
||||
X(f'{i}<TableType>{table_type}</TableType>')
|
||||
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
|
||||
if t.get('nameInDataSource'):
|
||||
nids = str(t['nameInDataSource'])
|
||||
elif table_type == 'Expression':
|
||||
nids = ''
|
||||
else:
|
||||
nids = table_name
|
||||
X(f'{i}<NameInDataSource>{esc_xml_text(nids)}</NameInDataSource>' if nids else f'{i}<NameInDataSource/>')
|
||||
expr = str(t.get('expressionInDataSource') or t.get('expression') or '')
|
||||
X(f'{i}<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>' if expr else f'{i}<ExpressionInDataSource/>')
|
||||
X(f'{i}<TableDataType>{t.get("tableDataType") or "NonobjectData"}</TableDataType>')
|
||||
|
||||
emit_eds_field_ref_list(i, 'KeyFields', t.get('keyFields'), src_name, table_name)
|
||||
emit_eds_field_ref_scalar(i, 'PresentationField', t.get('presentationField'), src_name, table_name)
|
||||
emit_eds_field_ref_scalar(i, 'ParentField', t.get('parentField'), src_name, table_name)
|
||||
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
|
||||
# различаются формой самого значения (xsi:nil против типизированного).
|
||||
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
|
||||
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
|
||||
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
|
||||
if t.get('parentField'):
|
||||
X(f'{i}<UnfilledParentValue xsi:type="xs:string"/>')
|
||||
else:
|
||||
X(f'{i}<UnfilledParentValue xsi:nil="true"/>')
|
||||
if char_xml:
|
||||
X(char_xml.rstrip('\r\n'))
|
||||
else:
|
||||
X(f'{i}<Characteristics/>')
|
||||
|
||||
X(f'{i}<UseStandardCommands>{"false" if t.get("useStandardCommands") is False else "true"}</UseStandardCommands>')
|
||||
X(f'{i}<QuickChoice>{"true" if t.get("quickChoice") is True else "false"}</QuickChoice>')
|
||||
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
|
||||
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
|
||||
if 'inputByString' in t:
|
||||
ibs = t.get('inputByString')
|
||||
elif t.get('presentationField'):
|
||||
ibs = [t['presentationField']]
|
||||
else:
|
||||
ibs = None
|
||||
emit_eds_field_ref_list(i, 'InputByString', ibs, src_name, table_name)
|
||||
X(f'{i}<CreateOnInput>{t.get("createOnInput") or "Auto"}</CreateOnInput>')
|
||||
X(f'{i}<SearchStringModeOnInputByString>{t.get("searchStringModeOnInputByString") or "Begin"}</SearchStringModeOnInputByString>')
|
||||
X(f'{i}<ChoiceDataGetModeOnInputByString>{t.get("choiceDataGetModeOnInputByString") or "Directly"}</ChoiceDataGetModeOnInputByString>')
|
||||
X(f'{i}<ChoiceHistoryOnInput>{t.get("choiceHistoryOnInput") or "Auto"}</ChoiceHistoryOnInput>')
|
||||
|
||||
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
|
||||
if default_forms_xml:
|
||||
X(default_forms_xml.rstrip('\r\n'))
|
||||
else:
|
||||
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
|
||||
X(f'{i}<{form_tag}/>')
|
||||
for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation',
|
||||
'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'):
|
||||
key = pres_tag[0].lower() + pres_tag[1:]
|
||||
emit_mltext(i, pres_tag, t.get(key))
|
||||
X(f'{i}<IncludeHelpInContents>{"true" if t.get("includeHelpInContents") is True else "false"}</IncludeHelpInContents>')
|
||||
X(f'{i}<ReadOnly>{"true" if t.get("readOnly") is True else "false"}</ReadOnly>')
|
||||
X(f'{i}<TransactionsIsolationLevel>{t.get("transactionsIsolationLevel") or "Auto"}</TransactionsIsolationLevel>')
|
||||
emit_eds_field_ref_scalar(i, 'DataVersionField', t.get('dataVersionField'), src_name, table_name)
|
||||
X(f'{i}<EditType>{t.get("editType") or "InDialog"}</EditType>')
|
||||
emit_md_ref_list(i, 'BasedOn', t.get('basedOn'))
|
||||
emit_eds_field_ref_list(i, 'DataLockFields', t.get('dataLockFields'), src_name, table_name)
|
||||
X(f'{i}<DataLockControlMode>{t.get("dataLockControlMode") or "Automatic"}</DataLockControlMode>')
|
||||
|
||||
|
||||
EDS_TABLE_GENERATED_TYPES = (
|
||||
('ExternalDataSourceTableManager', 'Manager'),
|
||||
('ExternalDataSourceTableObject', 'Object'),
|
||||
('ExternalDataSourceTableRef', 'Ref'),
|
||||
('ExternalDataSourceTableList', 'List'),
|
||||
('ExternalDataSourceTableRecord', 'Record'),
|
||||
('ExternalDataSourceTableRecordSet', 'RecordSet'),
|
||||
('ExternalDataSourceTableRecordKey', 'RecordKey'),
|
||||
('ExternalDataSourceTableRecordManager', 'RecordManager'),
|
||||
)
|
||||
|
||||
|
||||
def build_eds_table_xml(src_name, table_name, entry, fields_xml, char_xml, default_forms_xml):
|
||||
"""Отдельный XML-документ таблицы. fields_xml, char_xml, default_forms_xml — уже собранные
|
||||
узлы: их рендерит вызывающий навык своими эмиттерами, поэтому тело не зависит от того,
|
||||
какой это навык.
|
||||
Возвращает строку: X пишет в общий список строк,
|
||||
поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное
|
||||
(в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики)."""
|
||||
before = len(lines)
|
||||
|
||||
X('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
|
||||
X(f'\t<Table uuid="{new_uuid()}">')
|
||||
# InternalInfo у таблицы эмитится здесь, а не через generated_types: имя элемента
|
||||
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
|
||||
X('\t\t<InternalInfo>')
|
||||
for prefix, category in EDS_TABLE_GENERATED_TYPES:
|
||||
X(f'\t\t\t<xr:GeneratedType name="{prefix}.{src_name}.{table_name}" category="{category}">')
|
||||
X(f'\t\t\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
|
||||
X(f'\t\t\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
|
||||
X('\t\t\t</xr:GeneratedType>')
|
||||
X('\t\t</InternalInfo>')
|
||||
|
||||
X('\t\t<Properties>')
|
||||
emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props'], char_xml, default_forms_xml)
|
||||
X('\t\t</Properties>')
|
||||
|
||||
if fields_xml:
|
||||
X('\t\t<ChildObjects>')
|
||||
X(fields_xml.rstrip('\r\n'))
|
||||
X('\t\t</ChildObjects>')
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
X('\t</Table>')
|
||||
X('</MetaDataObject>')
|
||||
|
||||
chunk = '\r\n'.join(lines[before:])
|
||||
del lines[before:]
|
||||
return chunk
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 14. Namespaces
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -4386,6 +4742,7 @@ property_emitters = {
|
||||
'Task': emit_task_properties,
|
||||
'HTTPService': emit_http_service_properties,
|
||||
'WebService': emit_web_service_properties,
|
||||
'ExternalDataSource': emit_external_data_source_properties,
|
||||
}
|
||||
|
||||
property_emitters[obj_type]('\t\t\t')
|
||||
@@ -4659,6 +5016,36 @@ if obj_type == 'WebService':
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
|
||||
# --- ExternalDataSource: Tables (именами) + Functions (полными узлами) ---
|
||||
eds_tables = {}
|
||||
if obj_type == 'ExternalDataSource':
|
||||
eds_tables = get_eds_tables(defn.get('tables'))
|
||||
functions = {}
|
||||
if defn.get('functions'):
|
||||
for k, v in defn['functions'].items():
|
||||
functions[k] = v
|
||||
if eds_tables or functions:
|
||||
has_children = True
|
||||
X('\t\t<ChildObjects>')
|
||||
for tbl_name in eds_tables:
|
||||
X(f'\t\t\t<Table>{esc_xml_text(tbl_name)}</Table>')
|
||||
for fn_name, fn_val in functions.items():
|
||||
if isinstance(fn_val, str):
|
||||
fn_returns, fn_no_value = 'String', False
|
||||
else:
|
||||
fn_returns = str(fn_val.get('returns') or fn_val.get('returnType') or 'String')
|
||||
fn_no_value = fn_val.get('returnValue') is not None and fn_val.get('returnValue') is not True
|
||||
fn_type_xml = ''
|
||||
if not fn_no_value:
|
||||
type_before = len(lines)
|
||||
emit_value_type('\t\t\t\t\t', fn_returns)
|
||||
fn_type_xml = '\r\n'.join(lines[type_before:])
|
||||
del lines[type_before:]
|
||||
emit_eds_function('\t\t\t', fn_name, fn_val, fn_type_xml)
|
||||
X('\t\t</ChildObjects>')
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
|
||||
# --- CommonModule: no ChildObjects ---
|
||||
|
||||
X(f'\t</{obj_type}>')
|
||||
@@ -4708,6 +5095,7 @@ type_plural_map = {
|
||||
'WSReference': 'WSReferences',
|
||||
'CommonPicture': 'CommonPictures',
|
||||
'CommonTemplate': 'CommonTemplates',
|
||||
'ExternalDataSource': 'ExternalDataSources',
|
||||
}
|
||||
|
||||
type_plural = type_plural_map[obj_type]
|
||||
@@ -4726,8 +5114,50 @@ os.makedirs(type_dir, exist_ok=True)
|
||||
if obj_type not in types_no_sub_dir:
|
||||
os.makedirs(obj_sub_dir, exist_ok=True)
|
||||
|
||||
# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid —
|
||||
# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими.
|
||||
# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя.
|
||||
if os.path.exists(main_xml_path):
|
||||
print(f"WARNING: {obj_type} '{obj_name}' уже существует ({type_plural}/{obj_name}.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit.", file=sys.stderr)
|
||||
|
||||
write_xml_file_keep_eol(main_xml_path, metadata_xml)
|
||||
|
||||
# Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/.
|
||||
# Единственный вид, у которого объект складывается более чем из одного XML.
|
||||
eds_tables_created = []
|
||||
if obj_type == 'ExternalDataSource' and eds_tables:
|
||||
tables_dir = os.path.join(obj_sub_dir, 'Tables')
|
||||
os.makedirs(tables_dir, exist_ok=True)
|
||||
for tbl_name in eds_tables:
|
||||
entry = eds_tables[tbl_name]
|
||||
fields_before = len(lines)
|
||||
for f in entry['fields']:
|
||||
emit_attribute('\t\t\t', parse_attribute_shorthand(f), 'eds-field', 'Field')
|
||||
fields_xml = '\r\n'.join(lines[fields_before:])
|
||||
del lines[fields_before:]
|
||||
tp = entry['props'] or {}
|
||||
char_before = len(lines)
|
||||
emit_characteristics('\t\t\t', tp.get('characteristics'))
|
||||
char_xml = '\r\n'.join(lines[char_before:])
|
||||
del lines[char_before:]
|
||||
|
||||
# Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника —
|
||||
# голое имя платформа отвергает («Неизвестный объект метаданных»).
|
||||
forms_before = len(lines)
|
||||
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
|
||||
key = form_tag[0].lower() + form_tag[1:]
|
||||
form_val = tp.get(key)
|
||||
if form_val and '.' not in str(form_val):
|
||||
form_val = f'ExternalDataSource.{obj_name}.Table.{tbl_name}.Form.{form_val}'
|
||||
emit_form_ref('\t\t\t', form_tag, form_val)
|
||||
default_forms_xml = '\r\n'.join(lines[forms_before:])
|
||||
del lines[forms_before:]
|
||||
|
||||
table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, char_xml, default_forms_xml)
|
||||
table_path = os.path.join(tables_dir, f'{tbl_name}.xml')
|
||||
write_xml_file_keep_eol(table_path, table_xml)
|
||||
eds_tables_created.append(table_path)
|
||||
|
||||
# Module files
|
||||
modules_created = []
|
||||
|
||||
@@ -5148,78 +5578,208 @@ if commands:
|
||||
# 17. Register in Configuration.xml
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
config_xml_path = os.path.join(output_dir, 'Configuration.xml')
|
||||
reg_result = None
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
child_tag = obj_type
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
if os.path.isfile(config_xml_path):
|
||||
# Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation).
|
||||
with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
|
||||
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
|
||||
"""Регистрация объекта в <ChildObjects> родительского XML.
|
||||
|
||||
Общая реализация: эталон — meta-compile, копии — role-compile, xdto-compile.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
Возвращает исход: added | already | no-childobj | no-config.
|
||||
"""
|
||||
if not os.path.isfile(parent_xml_path):
|
||||
return 'no-config'
|
||||
|
||||
# Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation)
|
||||
with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
|
||||
config_content = f.read()
|
||||
|
||||
ns = 'http://v8.1c.ru/8.3/MDClasses'
|
||||
# ET is used ONLY read-only here: to locate ChildObjects and detect a duplicate.
|
||||
# We deliberately do NOT re-serialize Configuration.xml with ElementTree.write():
|
||||
# it drops every xmlns declaration used only inside attribute VALUES (e.g.
|
||||
# xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees those
|
||||
# xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees such
|
||||
# prefixes in element/attribute names. The dropped declaration makes XDTO read the
|
||||
# value as anyType and Designer refuses to load the file (issue #38). Registration is
|
||||
# therefore done by raw-text insertion, preserving BOM, EOL and all namespaces
|
||||
# byte-for-byte (same approach as subsystem-compile).
|
||||
tree = ET.parse(config_xml_path)
|
||||
tree = ET.parse(parent_xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
child_objects = root.find(f'{{{ns}}}Configuration/{{{ns}}}ChildObjects')
|
||||
child_objects = root.find(f'{{{ns}}}{parent_tag}/{{{ns}}}ChildObjects')
|
||||
if child_objects is None:
|
||||
# Try direct path
|
||||
config_elem = root.find(f'{{{ns}}}Configuration')
|
||||
if config_elem is not None:
|
||||
child_objects = config_elem.find(f'{{{ns}}}ChildObjects')
|
||||
parent_elem = root.find(f'{{{ns}}}{parent_tag}')
|
||||
if parent_elem is not None:
|
||||
child_objects = parent_elem.find(f'{{{ns}}}ChildObjects')
|
||||
|
||||
if child_objects is None:
|
||||
reg_result = 'no-childobj'
|
||||
return 'no-childobj'
|
||||
|
||||
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
|
||||
if any((e.text or '').strip() == child_name for e in existing):
|
||||
return 'already'
|
||||
|
||||
eol = '\r\n' if '\r\n' in config_content else '\n'
|
||||
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
|
||||
|
||||
block = re.search(r'<ChildObjects\s*>.*?</ChildObjects>', config_content, re.S)
|
||||
if block is None:
|
||||
# Empty self-closing <ChildObjects/> => open it with the first entry.
|
||||
empty = re.search(r'<ChildObjects\s*/>', config_content)
|
||||
if empty is None:
|
||||
return 'no-childobj'
|
||||
replacement = f'<ChildObjects>{eol}\t\t\t{entry}{eol}\t\t</ChildObjects>'
|
||||
new_content = config_content[:empty.start()] + replacement + config_content[empty.end():]
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. is_order_sensitive_type.
|
||||
if (not is_order_sensitive_type(child_tag)
|
||||
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
|
||||
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
|
||||
for m in line_rx.finditer(config_content, block.start(), block.end()):
|
||||
if compare_metadata_names(m.group(2), child_name) > 0:
|
||||
new_content = (config_content[:m.start()]
|
||||
+ f'{m.group(1)}{entry}{eol}'
|
||||
+ config_content[m.start():])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
close_same = f'</{child_tag}>'
|
||||
last_same = config_content.rfind(close_same, block.start(), block.end())
|
||||
if last_same != -1:
|
||||
# After the last element of the same type (keeps them grouped).
|
||||
insert_at = last_same + len(close_same)
|
||||
new_content = (config_content[:insert_at]
|
||||
+ f'{eol}\t\t\t{entry}'
|
||||
+ config_content[insert_at:])
|
||||
else:
|
||||
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
|
||||
already_exists = any((e.text or '').strip() == obj_name for e in existing)
|
||||
|
||||
if already_exists:
|
||||
reg_result = 'already'
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по CHILD_OBJECT_TYPES. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
anchor = None
|
||||
if child_tag in CHILD_OBJECT_TYPES:
|
||||
own_idx = CHILD_OBJECT_TYPES.index(child_tag)
|
||||
type_rx = re.compile(r'(?m)^([ \t]*)<(\w+)>[^<]*</\2>')
|
||||
for m in type_rx.finditer(config_content, block.start(), block.end()):
|
||||
other = m.group(2)
|
||||
if other in CHILD_OBJECT_TYPES and CHILD_OBJECT_TYPES.index(other) > own_idx:
|
||||
anchor = m
|
||||
break
|
||||
if anchor is not None:
|
||||
new_content = (config_content[:anchor.start()]
|
||||
+ f'{anchor.group(1)}{entry}{eol}'
|
||||
+ config_content[anchor.start():])
|
||||
else:
|
||||
eol = '\r\n' if '\r\n' in config_content else '\n'
|
||||
entry = f'<{child_tag}>{esc_xml_text(obj_name)}</{child_tag}>'
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
block = re.search(r'<ChildObjects\s*>.*?</ChildObjects>', config_content, re.S)
|
||||
if block is None:
|
||||
# Empty self-closing <ChildObjects/> => open it with the first entry.
|
||||
empty = re.search(r'<ChildObjects\s*/>', config_content)
|
||||
if empty is None:
|
||||
reg_result = 'no-childobj'
|
||||
else:
|
||||
replacement = f'<ChildObjects>{eol}\t\t\t{entry}{eol}\t\t</ChildObjects>'
|
||||
new_content = config_content[:empty.start()] + replacement + config_content[empty.end():]
|
||||
write_utf8_bom(config_xml_path, new_content)
|
||||
reg_result = 'added'
|
||||
else:
|
||||
close_same = f'</{child_tag}>'
|
||||
last_same = config_content.rfind(close_same, block.start(), block.end())
|
||||
if last_same != -1:
|
||||
# After the last element of the same type (keeps them grouped).
|
||||
insert_at = last_same + len(close_same)
|
||||
new_content = (config_content[:insert_at]
|
||||
+ f'{eol}\t\t\t{entry}'
|
||||
+ config_content[insert_at:])
|
||||
else:
|
||||
# No element of this type yet: new line before </ChildObjects>,
|
||||
# reusing the block's existing closing indent for </ChildObjects>.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
write_utf8_bom(config_xml_path, new_content)
|
||||
reg_result = 'added'
|
||||
else:
|
||||
reg_result = 'no-config'
|
||||
|
||||
child_tag = obj_type
|
||||
config_xml_path = os.path.join(output_dir, 'Configuration.xml')
|
||||
reg_result = register_in_childobjects(config_xml_path, 'Configuration', child_tag, obj_name)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 18. Summary
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# meta-decompile v0.69 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
#
|
||||
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
||||
# InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, BusinessProcess, Task, Enum. Инверс meta-compile (omit-on-default: ключ эмитим только
|
||||
# когда значение в XML отличается от умолчания компилятора). Неподдерживаемый тип / не-MetaDataObject
|
||||
# root → exit 3 (ring3, как form-decompile).
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias('Path')]
|
||||
@@ -92,7 +93,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode =
|
||||
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
|
||||
$objType = $objNode.LocalName
|
||||
|
||||
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService')) {
|
||||
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService', 'ExternalDataSource')) {
|
||||
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
|
||||
}
|
||||
|
||||
@@ -205,7 +206,24 @@ function Get-TypeShorthand {
|
||||
if ($dq) { $dn = $dq.SelectSingleNode('v8:DateFractions', $nsm); if ($dn) { $fr = $dn.InnerText } }
|
||||
$parts += $fr; break # Date | DateTime
|
||||
}
|
||||
'(^|:)base64Binary$' { $parts += 'ValueStorage'; break }
|
||||
'(^|:)base64Binary$' {
|
||||
# xs:base64Binary — всегда ДвоичныеДанные (ХранилищеЗначения — это v8:ValueStorage).
|
||||
# Узел без квалификаторов встречается только в рукописном XML: замерено на 8.3.24.1691 —
|
||||
# платформа читает его как безлимит (Length 0, Variable) и так же выгружает обратно.
|
||||
$bq = $typeNode.SelectSingleNode('v8:BinaryDataQualifiers', $nsm)
|
||||
if ($bq) {
|
||||
$blen = $bq.SelectSingleNode('v8:Length', $nsm)
|
||||
$bal = $bq.SelectSingleNode('v8:AllowedLength', $nsm)
|
||||
$blenVal = if ($blen) { $blen.InnerText.Trim() } else { "" }
|
||||
$balVal = if ($bal) { $bal.InnerText.Trim() } else { "" }
|
||||
# Голым BinaryData сворачиваем ТОЛЬКО точный дефолт компилятора
|
||||
# (4294967292/Fixed), иначе фиксированная длина терялась на раундтрипе.
|
||||
if ($balVal -eq 'Variable' -and $blenVal) { $parts += "BinaryData($blenVal)" }
|
||||
elseif ($blenVal -and $blenVal -ne '4294967292') { $parts += "BinaryData($blenVal,fixed)" }
|
||||
else { $parts += 'BinaryData' }
|
||||
} else { $parts += 'BinaryData(0)' }
|
||||
break
|
||||
}
|
||||
default { $parts += (Strip-NsPrefix $raw) } # cfg:CatalogRef.X → CatalogRef.X
|
||||
}
|
||||
} elseif ($ln -eq 'TypeSet') {
|
||||
@@ -287,12 +305,19 @@ function Attr-ToDsl {
|
||||
param($attrNode)
|
||||
$ap = $attrNode.SelectSingleNode('md:Properties', $nsm)
|
||||
$nm = ($ap.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||
# Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля,
|
||||
# поэтому в DSL попадает только отличающееся.
|
||||
$edsNids = $ap.SelectSingleNode('md:NameInDataSource', $nsm)
|
||||
$edsRo = $ap.SelectSingleNode('md:ReadOnly', $nsm)
|
||||
$edsNull = $ap.SelectSingleNode('md:AllowNull', $nsm)
|
||||
$ts = Get-TypeShorthand ($ap.SelectSingleNode('md:Type', $nsm))
|
||||
$flags = @()
|
||||
$fc = $ap.SelectSingleNode('md:FillChecking', $nsm); if ($fc -and $fc.InnerText -eq 'ShowError') { $flags += 'req' }
|
||||
$ix = $ap.SelectSingleNode('md:Indexing', $nsm)
|
||||
if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } }
|
||||
$ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' }
|
||||
if ($edsRo -and $edsRo.InnerText -eq 'true') { $flags += 'readonly' }
|
||||
if ($edsNull -and $edsNull.InnerText -eq 'true') { $flags += 'nullable' }
|
||||
|
||||
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
|
||||
$synNode = $ap.SelectSingleNode('md:Synonym', $nsm)
|
||||
@@ -406,6 +431,7 @@ function Attr-ToDsl {
|
||||
# Пустой <Type/> (реквизит без типа / произвольный) → $ts=''. Отличаем от «дефолтного» отсутствия:
|
||||
# заставляем объектную форму с явным type:'' (компилятор без маркера подставил бы xs:string).
|
||||
$typeEmpty = ($ts -eq '')
|
||||
if ($edsNids -and $edsNids.InnerText -and $edsNids.InnerText -cne $nm) { $extra['nameInDataSource'] = $edsNids.InnerText }
|
||||
if ($synCustom -or $synEmpty -or ($null -ne $ttVal) -or $extra.Count -gt 0 -or $typeEmpty) {
|
||||
$o = [ordered]@{ name = $nm }
|
||||
if ($ts) { $o['type'] = $ts } elseif ($typeEmpty) { $o['type'] = '' }
|
||||
@@ -1657,6 +1683,122 @@ if ($objType -eq 'ExchangePlan') {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) ---
|
||||
if ($objType -eq 'ExternalDataSource') {
|
||||
$dlcmVal = P 'DataLockControlMode'
|
||||
if ($dlcmVal -and $dlcmVal -cne 'Automatic') { $dsl['dataLockControlMode'] = $dlcmVal }
|
||||
|
||||
# Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П
|
||||
function Short-FieldRef { param([string]$ref) if ($ref) { return ($ref -split '\.')[-1] } else { return $null } }
|
||||
function Field-RefList { param($parent, [string]$tag)
|
||||
$out = [System.Collections.ArrayList]@()
|
||||
foreach ($f in @($parent.SelectNodes("md:$tag/xr:Field", $nsm))) { [void]$out.Add((Short-FieldRef $f.InnerText)) }
|
||||
# Запятая обязательна: return разворачивает коллекцию из одного элемента в скаляр,
|
||||
# и список ключевых полей из одного поля уехал бы в JSON строкой вместо массива.
|
||||
return ,$out
|
||||
}
|
||||
|
||||
$srcDir = Join-Path (Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path) $objName
|
||||
$childObjsEds = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
|
||||
if ($childObjsEds) {
|
||||
$tablesMap = [ordered]@{}
|
||||
foreach ($tNode in @($childObjsEds.SelectNodes('md:Table', $nsm))) {
|
||||
$tblName = $tNode.InnerText.Trim()
|
||||
$tblPath = Join-Path (Join-Path $srcDir 'Tables') "$tblName.xml"
|
||||
if (-not (Test-Path -LiteralPath $tblPath)) {
|
||||
[Console]::Error.WriteLine("meta-decompile: файл таблицы не найден: $tblPath")
|
||||
continue
|
||||
}
|
||||
$tdoc = New-Object System.Xml.XmlDocument
|
||||
$tdoc.PreserveWhitespace = $true
|
||||
$tdoc.Load($tblPath)
|
||||
$tObjNode = $null
|
||||
foreach ($c in $tdoc.DocumentElement.ChildNodes) { if ($c.NodeType -eq 'Element') { $tObjNode = $c; break } }
|
||||
$tp = $tObjNode.SelectSingleNode('md:Properties', $nsm)
|
||||
function TP { param([string]$tag) $n = $tp.SelectSingleNode("md:$tag", $nsm); if ($n) { return $n.InnerText } else { return $null } }
|
||||
|
||||
$tbl = [ordered]@{}
|
||||
$tSynNode = $tp.SelectSingleNode('md:Synonym', $nsm)
|
||||
$tSyn = Get-MLValue $tSynNode
|
||||
if ($tSyn -is [string]) { if ($tSyn -cne (Split-CamelWords $tblName)) { $tbl['synonym'] = $tSyn } }
|
||||
elseif ($null -ne $tSyn) { $tbl['synonym'] = $tSyn }
|
||||
# Пустой <Synonym/> ≠ авто-синоним из имени: без явного '' компилятор до-генерит его из имени.
|
||||
elseif ($tSynNode) { $tbl['synonym'] = '' }
|
||||
$tCmt = TP 'Comment'; if ($tCmt) { $tbl['comment'] = $tCmt }
|
||||
$tType = TP 'TableType'; if ($tType -and $tType -cne 'Table') { $tbl['tableType'] = $tType }
|
||||
$nids = TP 'NameInDataSource'; if ($nids -and $nids -cne $tblName) { $tbl['nameInDataSource'] = $nids }
|
||||
$expr = TP 'ExpressionInDataSource'; if ($expr) { $tbl['expressionInDataSource'] = $expr }
|
||||
$tdt = TP 'TableDataType'; if ($tdt -and $tdt -cne 'NonobjectData') { $tbl['tableDataType'] = $tdt }
|
||||
$keys = Field-RefList $tp 'KeyFields'; if ($keys.Count -gt 0) { $tbl['keyFields'] = $keys }
|
||||
foreach ($pair in @(@('PresentationField','presentationField'), @('ParentField','parentField'), @('DataVersionField','dataVersionField'))) {
|
||||
$v = Short-FieldRef (TP $pair[0]); if ($v) { $tbl[$pair[1]] = $v }
|
||||
}
|
||||
$ibs = Field-RefList $tp 'InputByString'
|
||||
# Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем.
|
||||
$ibsAuto = if ($tbl['presentationField']) { @($tbl['presentationField']) } else { @() }
|
||||
if (($ibs -join ',') -cne ($ibsAuto -join ',')) { $tbl['inputByString'] = $ibs }
|
||||
$dlf = Field-RefList $tp 'DataLockFields'; if ($dlf.Count -gt 0) { $tbl['dataLockFields'] = $dlf }
|
||||
if ((TP 'ReadOnly') -eq 'true') { $tbl['readOnly'] = $true }
|
||||
$til = TP 'TransactionsIsolationLevel'; if ($til -and $til -cne 'Auto') { $tbl['transactionsIsolationLevel'] = $til }
|
||||
$tdlcm = TP 'DataLockControlMode'; if ($tdlcm -and $tdlcm -cne 'Automatic') { $tbl['dataLockControlMode'] = $tdlcm }
|
||||
if ((TP 'UseStandardCommands') -eq 'false') { $tbl['useStandardCommands'] = $false }
|
||||
if ((TP 'QuickChoice') -eq 'true') { $tbl['quickChoice'] = $true }
|
||||
$tet = TP 'EditType'; if ($tet -and $tet -cne 'InDialog') { $tbl['editType'] = $tet }
|
||||
# Слоты форм — такая же часть свойств таблицы, как у прочих объектов (сами формы
|
||||
# вне скоупа раундтрипа: это отдельные файлы, их делает навык form-add).
|
||||
foreach ($fp in @(@('DefaultObjectForm','defaultObjectForm'), @('DefaultRecordForm','defaultRecordForm'),
|
||||
@('DefaultListForm','defaultListForm'), @('DefaultChoiceForm','defaultChoiceForm'))) {
|
||||
$fv = TP $fp[0]; if ($fv) { $tbl[$fp[1]] = $fv }
|
||||
}
|
||||
$basedOn = [System.Collections.ArrayList]@()
|
||||
foreach ($it in @($tp.SelectNodes('md:BasedOn/xr:Item', $nsm))) { [void]$basedOn.Add($it.InnerText) }
|
||||
if ($basedOn.Count -gt 0) { $tbl['basedOn'] = $basedOn }
|
||||
|
||||
$fieldsArr = [System.Collections.ArrayList]@()
|
||||
$tChild = $tObjNode.SelectSingleNode('md:ChildObjects', $nsm)
|
||||
if ($tChild) {
|
||||
foreach ($f in @($tChild.SelectNodes('md:Field', $nsm))) { [void]$fieldsArr.Add((Attr-ToDsl $f)) }
|
||||
}
|
||||
# Таблица без собственных свойств — короткая форма: просто массив полей.
|
||||
if ($tbl.Count -eq 0) { $tablesMap[$tblName] = $fieldsArr }
|
||||
else { $tbl['fields'] = $fieldsArr; $tablesMap[$tblName] = $tbl }
|
||||
}
|
||||
if ($tablesMap.Count -gt 0) { $dsl['tables'] = $tablesMap }
|
||||
|
||||
$fnMap = [ordered]@{}
|
||||
foreach ($fnNode in @($childObjsEds.SelectNodes('md:Function', $nsm))) {
|
||||
$fp = $fnNode.SelectSingleNode('md:Properties', $nsm)
|
||||
$fnName = ($fp.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||
$fnExprNode = $fp.SelectSingleNode('md:ExpressionInDataSource', $nsm)
|
||||
$fnExpr = if ($fnExprNode) { $fnExprNode.InnerText } else { '' }
|
||||
$fnRetNode = $fp.SelectSingleNode('md:ReturnValue', $nsm)
|
||||
$fnReturns = Get-TypeShorthand ($fp.SelectSingleNode('md:Type', $nsm))
|
||||
$fnSyn = Get-MLValue ($fp.SelectSingleNode('md:Synonym', $nsm))
|
||||
$fnCmtNode = $fp.SelectSingleNode('md:Comment', $nsm)
|
||||
$fnCmt = if ($fnCmtNode) { $fnCmtNode.InnerText } else { '' }
|
||||
$fnNoValue = ($fnRetNode -and $fnRetNode.InnerText -eq 'false')
|
||||
$synCustomFn = ($fnSyn -isnot [string]) -and ($null -ne $fnSyn)
|
||||
if ($fnSyn -is [string]) { $synCustomFn = ($fnSyn -cne (Split-CamelWords $fnName)) -and ($fnSyn -ne '') }
|
||||
# Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем,
|
||||
# иначе короткая форма (одна строка выражения) не срабатывала бы никогда.
|
||||
if (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn -and ($fnReturns -cne 'String(10)')) {
|
||||
$fo = [ordered]@{ expression = $fnExpr; returns = $fnReturns }
|
||||
$fnMap[$fnName] = $fo
|
||||
} elseif (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn) {
|
||||
# Тип по умолчанию String — короткая форма: одна строка выражения.
|
||||
$fnMap[$fnName] = $fnExpr
|
||||
} else {
|
||||
$fo = [ordered]@{ expression = $fnExpr }
|
||||
if ($fnNoValue) { $fo['returnValue'] = $false } elseif ($fnReturns) { $fo['returns'] = $fnReturns }
|
||||
if ($synCustomFn) { $fo['synonym'] = $fnSyn }
|
||||
if ($fnCmt) { $fo['comment'] = $fnCmt }
|
||||
$fnMap[$fnName] = $fo
|
||||
}
|
||||
}
|
||||
if ($fnMap.Count -gt 0) { $dsl['functions'] = $fnMap }
|
||||
}
|
||||
}
|
||||
|
||||
# === Вывод ===
|
||||
$json = ConvertTo-CompactJson $dsl 0
|
||||
if ($OutputPath) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# meta-decompile v0.69 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
#
|
||||
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
||||
@@ -311,7 +311,25 @@ def get_type_shorthand(type_node):
|
||||
fr = _text(dn)
|
||||
parts.append(fr) # Date | DateTime
|
||||
elif re.search(r'(^|:)base64Binary$', raw, re.I):
|
||||
parts.append('ValueStorage')
|
||||
# xs:base64Binary — всегда ДвоичныеДанные (ХранилищеЗначения — это v8:ValueStorage).
|
||||
# Узел без квалификаторов встречается только в рукописном XML: замерено на 8.3.24.1691 —
|
||||
# платформа читает его как безлимит (Length 0, Variable) и так же выгружает обратно.
|
||||
bq = type_node.find('v8:BinaryDataQualifiers', NS)
|
||||
if bq is not None:
|
||||
blen = bq.find('v8:Length', NS)
|
||||
bal = bq.find('v8:AllowedLength', NS)
|
||||
blen_val = _text(blen).strip() if blen is not None else ''
|
||||
bal_val = _text(bal).strip() if bal is not None else ''
|
||||
# Голым BinaryData сворачиваем ТОЛЬКО точный дефолт компилятора
|
||||
# (4294967292/Fixed), иначе фиксированная длина терялась на раундтрипе.
|
||||
if bal_val.lower() == 'variable' and blen_val:
|
||||
parts.append(f'BinaryData({blen_val})')
|
||||
elif blen_val and blen_val != '4294967292':
|
||||
parts.append(f'BinaryData({blen_val},fixed)')
|
||||
else:
|
||||
parts.append('BinaryData')
|
||||
else:
|
||||
parts.append('BinaryData(0)')
|
||||
else:
|
||||
parts.append(strip_ns_prefix(raw)) # cfg:CatalogRef.X → CatalogRef.X
|
||||
elif ln == 'TypeSet':
|
||||
@@ -420,6 +438,11 @@ def parse_choice_parameters(parent, tag):
|
||||
def attr_to_dsl(attr_node):
|
||||
ap = _single(attr_node, 'md:Properties')
|
||||
nm = _text(_single(ap, 'md:Name'))
|
||||
# Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля,
|
||||
# поэтому в DSL попадает только отличающееся.
|
||||
eds_nids = _single(ap, 'md:NameInDataSource')
|
||||
eds_ro = _single(ap, 'md:ReadOnly')
|
||||
eds_null = _single(ap, 'md:AllowNull')
|
||||
ts = get_type_shorthand(_single(ap, 'md:Type'))
|
||||
flags = []
|
||||
fc = _single(ap, 'md:FillChecking')
|
||||
@@ -435,6 +458,10 @@ def attr_to_dsl(attr_node):
|
||||
ml = _single(ap, 'md:MultiLine')
|
||||
if ml is not None and _text(ml) == 'true':
|
||||
flags.append('multiline')
|
||||
if eds_ro is not None and _text(eds_ro) == 'true':
|
||||
flags.append('readonly')
|
||||
if eds_null is not None and _text(eds_null) == 'true':
|
||||
flags.append('nullable')
|
||||
|
||||
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
|
||||
syn_node = _single(ap, 'md:Synonym')
|
||||
@@ -618,6 +645,8 @@ def attr_to_dsl(attr_node):
|
||||
|
||||
# Пустой <Type/> (реквизит без типа) → ts=''. Отличаем от «дефолтного» отсутствия: явный type:''.
|
||||
type_empty = (ts == '')
|
||||
if eds_nids is not None and _text(eds_nids) and _text(eds_nids) != nm:
|
||||
extra['nameInDataSource'] = _text(eds_nids)
|
||||
if syn_custom or syn_empty or (tt_val is not None) or len(extra) > 0 or type_empty:
|
||||
o = {'name': nm}
|
||||
if ts:
|
||||
@@ -2112,6 +2141,7 @@ SUPPORTED_TYPES = (
|
||||
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
|
||||
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
|
||||
'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
|
||||
'ExternalDataSource',
|
||||
)
|
||||
|
||||
|
||||
@@ -2154,6 +2184,159 @@ def main():
|
||||
|
||||
build_dsl()
|
||||
|
||||
# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) ---
|
||||
if obj_type == 'ExternalDataSource':
|
||||
dlcm_val = P('DataLockControlMode')
|
||||
if dlcm_val and dlcm_val != 'Automatic':
|
||||
dsl['dataLockControlMode'] = dlcm_val
|
||||
|
||||
def short_field_ref(ref):
|
||||
"""Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П"""
|
||||
return ref.split('.')[-1] if ref else None
|
||||
|
||||
def field_ref_list(parent, tag):
|
||||
return [short_field_ref(_text(f)) for f in parent.findall('md:%s/xr:Field' % tag, NS)]
|
||||
|
||||
src_dir = os.path.join(os.path.dirname(os.path.abspath(args.ObjectPath)), obj_name)
|
||||
child_objs_eds = _single(obj_node, 'md:ChildObjects')
|
||||
if child_objs_eds is not None:
|
||||
tables_map = {}
|
||||
for t_node in child_objs_eds.findall('md:Table', NS):
|
||||
tbl_name = (_text(t_node) or '').strip()
|
||||
tbl_path = os.path.join(src_dir, 'Tables', tbl_name + '.xml')
|
||||
if not os.path.isfile(tbl_path):
|
||||
sys.stderr.write("meta-decompile: файл таблицы не найден: %s\n" % tbl_path)
|
||||
continue
|
||||
t_root = etree.parse(tbl_path).getroot()
|
||||
t_obj_node = next((c for c in t_root if isinstance(c.tag, str)), None)
|
||||
tp = _single(t_obj_node, 'md:Properties')
|
||||
|
||||
def TP(tag, _tp=None):
|
||||
n = _single(_tp if _tp is not None else tp, 'md:%s' % tag)
|
||||
return _text(n) if n is not None else None
|
||||
|
||||
tbl = {}
|
||||
t_syn_node = _single(tp, 'md:Synonym')
|
||||
t_syn = get_ml_value(t_syn_node)
|
||||
if isinstance(t_syn, str):
|
||||
if t_syn != split_camel_words(tbl_name):
|
||||
tbl['synonym'] = t_syn
|
||||
elif t_syn is not None:
|
||||
tbl['synonym'] = t_syn
|
||||
elif t_syn_node is not None:
|
||||
# Пустой <Synonym/> != авто-синоним из имени: без явного '' компилятор до-генерит его.
|
||||
tbl['synonym'] = ''
|
||||
t_cmt = TP('Comment')
|
||||
if t_cmt:
|
||||
tbl['comment'] = t_cmt
|
||||
t_type = TP('TableType')
|
||||
if t_type and t_type != 'Table':
|
||||
tbl['tableType'] = t_type
|
||||
nids = TP('NameInDataSource')
|
||||
if nids and nids != tbl_name:
|
||||
tbl['nameInDataSource'] = nids
|
||||
expr = TP('ExpressionInDataSource')
|
||||
if expr:
|
||||
tbl['expressionInDataSource'] = expr
|
||||
tdt = TP('TableDataType')
|
||||
if tdt and tdt != 'NonobjectData':
|
||||
tbl['tableDataType'] = tdt
|
||||
keys = field_ref_list(tp, 'KeyFields')
|
||||
if keys:
|
||||
tbl['keyFields'] = keys
|
||||
for tag, key in (('PresentationField', 'presentationField'), ('ParentField', 'parentField'),
|
||||
('DataVersionField', 'dataVersionField')):
|
||||
v = short_field_ref(TP(tag))
|
||||
if v:
|
||||
tbl[key] = v
|
||||
ibs = field_ref_list(tp, 'InputByString')
|
||||
# Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем.
|
||||
ibs_auto = [tbl['presentationField']] if tbl.get('presentationField') else []
|
||||
if ibs != ibs_auto:
|
||||
tbl['inputByString'] = ibs
|
||||
dlf = field_ref_list(tp, 'DataLockFields')
|
||||
if dlf:
|
||||
tbl['dataLockFields'] = dlf
|
||||
if TP('ReadOnly') == 'true':
|
||||
tbl['readOnly'] = True
|
||||
til = TP('TransactionsIsolationLevel')
|
||||
if til and til != 'Auto':
|
||||
tbl['transactionsIsolationLevel'] = til
|
||||
tdlcm = TP('DataLockControlMode')
|
||||
if tdlcm and tdlcm != 'Automatic':
|
||||
tbl['dataLockControlMode'] = tdlcm
|
||||
if TP('UseStandardCommands') == 'false':
|
||||
tbl['useStandardCommands'] = False
|
||||
if TP('QuickChoice') == 'true':
|
||||
tbl['quickChoice'] = True
|
||||
t_et = TP('EditType')
|
||||
if t_et and t_et != 'InDialog':
|
||||
tbl['editType'] = t_et
|
||||
# Слоты форм — такая же часть свойств таблицы, как у прочих объектов (сами формы
|
||||
# вне скоупа раундтрипа: это отдельные файлы, их делает навык form-add).
|
||||
for xml_tag, dsl_key in (('DefaultObjectForm', 'defaultObjectForm'),
|
||||
('DefaultRecordForm', 'defaultRecordForm'),
|
||||
('DefaultListForm', 'defaultListForm'),
|
||||
('DefaultChoiceForm', 'defaultChoiceForm')):
|
||||
fv = TP(xml_tag)
|
||||
if fv:
|
||||
tbl[dsl_key] = fv
|
||||
based_on = [_text(it) for it in tp.findall('md:BasedOn/xr:Item', NS)]
|
||||
if based_on:
|
||||
tbl['basedOn'] = based_on
|
||||
|
||||
fields_arr = []
|
||||
t_child = _single(t_obj_node, 'md:ChildObjects')
|
||||
if t_child is not None:
|
||||
for f in t_child.findall('md:Field', NS):
|
||||
fields_arr.append(attr_to_dsl(f))
|
||||
# Таблица без собственных свойств — короткая форма: просто массив полей.
|
||||
if not tbl:
|
||||
tables_map[tbl_name] = fields_arr
|
||||
else:
|
||||
tbl['fields'] = fields_arr
|
||||
tables_map[tbl_name] = tbl
|
||||
if tables_map:
|
||||
dsl['tables'] = tables_map
|
||||
|
||||
fn_map = {}
|
||||
for fn_node in child_objs_eds.findall('md:Function', NS):
|
||||
fp = _single(fn_node, 'md:Properties')
|
||||
fn_name = _text(_single(fp, 'md:Name'))
|
||||
fn_expr_node = _single(fp, 'md:ExpressionInDataSource')
|
||||
fn_expr = _text(fn_expr_node) if fn_expr_node is not None else ''
|
||||
fn_ret_node = _single(fp, 'md:ReturnValue')
|
||||
fn_returns = get_type_shorthand(_single(fp, 'md:Type'))
|
||||
fn_syn = get_ml_value(_single(fp, 'md:Synonym'))
|
||||
fn_cmt_node = _single(fp, 'md:Comment')
|
||||
fn_cmt = _text(fn_cmt_node) if fn_cmt_node is not None else ''
|
||||
fn_no_value = fn_ret_node is not None and _text(fn_ret_node) == 'false'
|
||||
if isinstance(fn_syn, str):
|
||||
syn_custom_fn = fn_syn != split_camel_words(fn_name) and fn_syn != ''
|
||||
else:
|
||||
syn_custom_fn = fn_syn is not None
|
||||
# Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем,
|
||||
# иначе короткая форма (одна строка выражения) не срабатывала бы никогда.
|
||||
if not fn_no_value and not fn_cmt and not syn_custom_fn and fn_returns != 'String(10)':
|
||||
fn_map[fn_name] = {'expression': fn_expr, 'returns': fn_returns}
|
||||
elif not fn_no_value and not fn_cmt and not syn_custom_fn:
|
||||
# Тип по умолчанию String — короткая форма: одна строка выражения.
|
||||
fn_map[fn_name] = fn_expr
|
||||
else:
|
||||
fo = {'expression': fn_expr}
|
||||
if fn_no_value:
|
||||
fo['returnValue'] = False
|
||||
elif fn_returns:
|
||||
fo['returns'] = fn_returns
|
||||
if syn_custom_fn:
|
||||
fo['synonym'] = fn_syn
|
||||
if fn_cmt:
|
||||
fo['comment'] = fn_cmt
|
||||
fn_map[fn_name] = fo
|
||||
if fn_map:
|
||||
dsl['functions'] = fn_map
|
||||
|
||||
|
||||
# === Вывод ===
|
||||
json_str = convert_to_compact_json(dsl, 0)
|
||||
if args.OutputPath:
|
||||
|
||||
@@ -35,48 +35,28 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -Def
|
||||
| DefinitionFile | JSON-файл с операциями (альтернатива Operation) |
|
||||
| NoValidate | Не запускать meta-validate после правки |
|
||||
|
||||
## Операции — сводная таблица
|
||||
## Частые операции
|
||||
|
||||
Batch через `;;` во всех операциях. Подробный синтаксис — в файлах по ссылкам.
|
||||
|
||||
### Дочерние элементы — [child-operations.md](child-operations.md)
|
||||
Batch через `;;` во всех операциях.
|
||||
|
||||
| Операция | Формат Value | Пример |
|
||||
|----------|-------------|--------|
|
||||
| `add-attribute` | `Имя: Тип \| флаги` | `"Сумма: Число(15,2) \| req, index"` |
|
||||
| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` |
|
||||
| `add-dimension` | `Имя: Тип \| флаги` | `"Организация: CatalogRef.Организации \| master"` |
|
||||
| `add-resource` | `Имя: Тип` | `"Сумма: Число(15,2)"` |
|
||||
| `add-enumValue` | `Имя` | `"Значение1 ;; Значение2"` |
|
||||
| `add-column` | `Имя: Тип` | `"Тип: EnumRef.ТипыДокументов"` |
|
||||
| `add-form` / `add-template` / `add-command` | `Имя` | `"ФормаЭлемента"` |
|
||||
| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` |
|
||||
| `add-ts-attribute` | `ТЧ.Имя: Тип` | `"Товары.Скидка: Число(15,2)"` |
|
||||
| `remove-*` | `Имя` | `"СтарыйРеквизит ;; ЕщёОдин"` |
|
||||
| `remove-ts-attribute` | `ТЧ.Имя` | `"Товары.УстаревшийРекв"` |
|
||||
| `modify-attribute` | `Имя: ключ=значение` | `"СтароеИмя: name=НовоеИмя, type=Строка(500)"` |
|
||||
| `modify-ts-attribute` | `ТЧ.Имя: ключ=значение` | `"Товары.Рекв: name=НовоеИмя"` |
|
||||
| `modify-ts` | `ТЧ: ключ=значение` | `"Товары: synonym=Товарный состав"` |
|
||||
| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` |
|
||||
|
||||
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
|
||||
|
||||
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
|
||||
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
|
||||
|
||||
### Свойства объекта — [properties-reference.md](properties-reference.md)
|
||||
|
||||
| Операция | Формат Value | Пример |
|
||||
|----------|-------------|--------|
|
||||
| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` |
|
||||
| `add-owner` | `MetaType.Name` | `"Catalog.Контрагенты ;; Catalog.Организации"` |
|
||||
| `add-registerRecord` | `MetaType.Name` | `"AccumulationRegister.ОстаткиТоваров"` |
|
||||
| `add-basedOn` | `MetaType.Name` | `"Document.ЗаказКлиента"` |
|
||||
| `add-inputByString` | `Путь поля` | `"StandardAttribute.Description"` |
|
||||
| `set-owners` / `set-registerRecords` / `set-basedOn` / `set-inputByString` | Замена всего списка | `"Catalog.Орг ;; Catalog.Контр"` |
|
||||
| `remove-owner` / `remove-registerRecord` / ... | Удаление из списка | `"Catalog.Контрагенты"` |
|
||||
|
||||
### JSON DSL — [json-dsl.md](json-dsl.md)
|
||||
|
||||
Для комбинированных операций (add + remove + modify в одном файле), синонимы ключей/типов, таблица поддерживаемых объектов.
|
||||
Составной тип — через `+`: `"Значение: Строка + Число(15,2) + Дата"`.
|
||||
|
||||
## Быстрые примеры
|
||||
|
||||
@@ -84,9 +64,6 @@ Batch через `;;` во всех операциях. Подробный си
|
||||
# Добавить реквизиты
|
||||
-Operation add-attribute -Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index"
|
||||
|
||||
# Составной тип (несколько типов через +)
|
||||
-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"
|
||||
|
||||
# Добавить ТЧ с реквизитами
|
||||
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2)"
|
||||
|
||||
@@ -103,6 +80,21 @@ Batch через `;;` во всех операциях. Подробный си
|
||||
-Operation set-owners -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
||||
```
|
||||
|
||||
## Индекс: что правишь → файл
|
||||
|
||||
| Что нужно | Файл |
|
||||
|-----------|------|
|
||||
| Реквизит, измерение, ресурс, графа: флаги, составные типы, структурные свойства (`Format`, `ChoiceParameters`, `FillValue`, …) | `reference/attributes.md` |
|
||||
| Табличная часть и её реквизиты | `reference/tabular-sections.md` |
|
||||
| Свойства самого объекта, списочные свойства (владельцы, движения, основание, ввод по строке) | `reference/properties.md` |
|
||||
| Предопределённые элементы | `reference/predefined.md` |
|
||||
| Значения перечисления, команды | `reference/other-children.md` |
|
||||
| Внешний источник данных: поля, таблицы, функции | `reference/external-data-source.md` |
|
||||
| Комбинированные операции в одном JSON, синонимы ключей и типов, какие дети допустимы у типа объекта | `reference/json-dsl.md` |
|
||||
|
||||
Форму добавляет и удаляет навык `form-add` / `form-remove`, макет — навык `template-add` /
|
||||
`template-remove`: кроме записи в `ChildObjects` у них есть собственные файлы, и `meta-edit` их не трогает.
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
|
||||
+8
-80
@@ -1,6 +1,7 @@
|
||||
# Inline-операции над дочерними элементами
|
||||
# Реквизиты, измерения, ресурсы, графы
|
||||
|
||||
Подробный справочник операций `add-*` / `remove-*` / `modify-*` для дочерних элементов объекта метаданных.
|
||||
Операции над «плоскими» дочерними элементами объекта: реквизит (`attribute`), измерение (`dimension`),
|
||||
ресурс (`resource`), графа журнала (`column`). Синтаксис у всех один.
|
||||
|
||||
## Общие правила
|
||||
|
||||
@@ -9,9 +10,10 @@
|
||||
-Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index"
|
||||
```
|
||||
|
||||
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
|
||||
**Shorthand-формат**: `ИмяРеквизита: Тип | флаги`
|
||||
|
||||
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только dimensions); `mainFilter` — основной отбор (только dimensions).
|
||||
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только
|
||||
измерения); `mainFilter` — основной отбор (только измерения).
|
||||
|
||||
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
|
||||
```powershell
|
||||
@@ -20,11 +22,10 @@
|
||||
|
||||
## Составные типы
|
||||
|
||||
Для реквизитов с несколькими допустимыми типами — разделитель `+`:
|
||||
Разделитель `+`:
|
||||
```powershell
|
||||
-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"
|
||||
-Operation add-attribute -Value "Значение: Строка + Число(15,2) | req"
|
||||
-Operation modify-ts-attribute -Value "Данные.Значение: type=Строка + Число(15,2) + Дата"
|
||||
```
|
||||
|
||||
В JSON DSL — массив в `type`:
|
||||
@@ -43,84 +44,11 @@
|
||||
-Operation add-column -Value "Тип: EnumRef.ТипыДокументов"
|
||||
```
|
||||
|
||||
## add-ts
|
||||
|
||||
Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...`
|
||||
|
||||
```powershell
|
||||
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)"
|
||||
```
|
||||
|
||||
## add-ts-attribute / remove-ts-attribute / modify-ts-attribute
|
||||
|
||||
Операции над реквизитами **внутри существующей ТЧ**. Формат: `ИмяТЧ.ОпределениеРеквизита` (dot-нотация).
|
||||
|
||||
```powershell
|
||||
# Добавить реквизит в ТЧ
|
||||
-Operation add-ts-attribute -Value "Товары.СтавкаНДС: EnumRef.СтавкиНДС"
|
||||
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) ;; Товары.Бонус: Число(15,2)"
|
||||
|
||||
# Позиционная вставка в ТЧ
|
||||
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) >> after Цена"
|
||||
|
||||
# Удалить реквизит из ТЧ
|
||||
-Operation remove-ts-attribute -Value "Товары.УстаревшийРекв"
|
||||
-Operation remove-ts-attribute -Value "Товары.Рекв1 ;; Товары.Рекв2"
|
||||
|
||||
# Изменить реквизит в ТЧ (rename, type change и т.д.)
|
||||
-Operation modify-ts-attribute -Value "Товары.СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
||||
```
|
||||
|
||||
Batch через `;;` — можно указать разные ТЧ: `"Товары.А: Строка(50) ;; Услуги.Б: Число(10)"`.
|
||||
|
||||
## modify-ts
|
||||
|
||||
Изменение свойств **самой табличной части** (Synonym, FillChecking, Use и др.):
|
||||
|
||||
```powershell
|
||||
-Operation modify-ts -Value "Товары: synonym=Товарный состав"
|
||||
-Operation modify-ts -Value "Товары: fillChecking=ShowError"
|
||||
```
|
||||
|
||||
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
|
||||
|
||||
## add-predefined
|
||||
|
||||
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
|
||||
идентификаторы сохраняются, новые получают свежий id.
|
||||
|
||||
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
|
||||
```powershell
|
||||
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
|
||||
```
|
||||
|
||||
JSON — строки и/или объекты (для групп с вложенными):
|
||||
```json
|
||||
{ "add": { "predefined": [
|
||||
"(001) Основной",
|
||||
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
|
||||
] } }
|
||||
```
|
||||
|
||||
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
|
||||
берётся из объекта автоматически.
|
||||
|
||||
## add-enumValue / add-form / add-template / add-command
|
||||
|
||||
Просто имена (batch через `;;`):
|
||||
```powershell
|
||||
-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3"
|
||||
-Operation add-form -Value "ФормаЭлемента ;; ФормаСписка"
|
||||
-Operation add-template -Value "ПечатнаяФорма"
|
||||
-Operation add-command -Value "Команда1"
|
||||
```
|
||||
|
||||
## remove-*
|
||||
|
||||
Имя элемента (или несколько через `;;`):
|
||||
```powershell
|
||||
-Operation remove-attribute -Value "СтарыйРеквизит ;; ЕщёОдин"
|
||||
-Operation remove-ts -Value "УстаревшаяТЧ"
|
||||
-Operation remove-enumValue -Value "НеиспользуемоеЗначение"
|
||||
```
|
||||
|
||||
@@ -140,7 +68,7 @@ JSON — строки и/или объекты (для групп с вложе
|
||||
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
|
||||
```
|
||||
|
||||
### Структурные свойства реквизита
|
||||
## Структурные свойства реквизита
|
||||
|
||||
Свойства со сложным значением задавайте через JSON DSL (`{ "modify": { "attributes": { "Имя": { ... } } } }`):
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Внешние источники данных
|
||||
|
||||
Точечные правки внутри уже созданного источника: поле в таблицу, таблица и функция в источник.
|
||||
Источник целиком собирает навык `meta-compile` — его повторный запуск заменяет файл источника,
|
||||
выдаёт новый uuid и оставляет сиротами таблицы, которых нет в описании.
|
||||
|
||||
## add-field — поле таблицы
|
||||
|
||||
Правится файл таблицы: `ExternalDataSources/<Источник>/Tables/<Таблица>.xml`.
|
||||
|
||||
Поле задаётся как обычный реквизит, плюс три своих ключа: `nameInDataSource` (умолчание — имя поля),
|
||||
`readOnly`, `allowNull`. Флаги строковой формы — `readonly`, `nullable`.
|
||||
|
||||
```json
|
||||
{ "add": { "fields": [
|
||||
"barcode: String(20) | nullable",
|
||||
{ "name": "cost", "type": "Number(15,2)", "nameInDataSource": "cost_net", "readOnly": true }
|
||||
] } }
|
||||
```
|
||||
|
||||
Типы поля: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу того же
|
||||
источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`. Составной тип платформа запрещает.
|
||||
|
||||
## add-tables / add-functions — в файле источника
|
||||
|
||||
Правится файл источника: `ExternalDataSources/<Источник>.xml`. Таблица — единственная операция
|
||||
навыка, создающая **файл**: `<Источник>/Tables/<Имя>.xml` плюс имя в `ChildObjects` источника.
|
||||
Синтаксис таблицы и функции тот же, что у навыка `meta-compile` (`reference/external-data-source.md` там же).
|
||||
|
||||
```json
|
||||
{ "add": {
|
||||
"tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } },
|
||||
"functions": { "nextKey": "NEXT VALUE FOR public.seq_key" }
|
||||
} }
|
||||
```
|
||||
|
||||
При добавлении таблицы не поддержаны ключи `characteristics` и `default*Form` — навык отвергает их,
|
||||
а не проглатывает молча. Характеристики задаются при сборке источника навыком `meta-compile`,
|
||||
форму назначает навык `form-add`.
|
||||
|
||||
## Удаление
|
||||
|
||||
Таблицу удаляет навык `meta-remove`: `ExternalDataSource.<Источник>.Table.<Таблица>` — вместе с файлом
|
||||
и записью в `ChildObjects`. Весь источник — `ExternalDataSource.<Источник>`.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user