diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1
index 80339fa0..150cbece 100644
--- a/.claude/skills/form-compile/scripts/form-compile.ps1
+++ b/.claude/skills/form-compile/scripts/form-compile.ps1
@@ -1,4 +1,4 @@
-# form-compile v1.38 — Compile 1C managed form from JSON or object metadata
+# form-compile v1.39 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -1496,6 +1496,22 @@ if ($FromObject) {
$def = $json | ConvertFrom-Json
}
+# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile)
+$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path }
+function Resolve-QueryValue {
+ param([string]$val, [string]$baseDir)
+ if (-not $val.StartsWith("@")) { return $val }
+ $filePath = $val.Substring(1)
+ if ([System.IO.Path]::IsPathRooted($filePath)) {
+ $candidates = @($filePath)
+ } else {
+ $candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath))
+ }
+ foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } }
+ Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))"
+ exit 1
+}
+
# --- 2. ID allocator ---
$script:nextId = 1
@@ -1547,6 +1563,347 @@ function Emit-MLText {
X "$indent$tag>"
}
+# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
+# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип
+# (harness нормализует GUID для хвоста с иными идентификаторами).
+$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
+$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
+$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
+$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Настройки компоновщика ListSettings: filter/order/conditionalAppearance.
+# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны).
+# ─────────────────────────────────────────────────────────────────────────────
+function New-Guid-String { return [System.Guid]::NewGuid().ToString() }
+
+$script:comparisonTypes = @{
+ "=" = "Equal"; "<>" = "NotEqual"
+ ">" = "Greater"; ">=" = "GreaterOrEqual"
+ "<" = "Less"; "<=" = "LessOrEqual"
+ "in" = "InList"; "notIn" = "NotInList"
+ "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy"
+ "contains" = "Contains"; "notContains" = "NotContains"
+ "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith"
+ "filled" = "Filled"; "notFilled" = "NotFilled"
+}
+
+function Parse-FilterShorthand {
+ param([string]$s)
+ $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null }
+ if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' }
+ if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' }
+ if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' }
+ if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' }
+ if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible"; $s = $s -replace '\s*@inaccessible', '' }
+ $s = $s.Trim()
+ $opPatterns = @('<>', '>=', '<=', '=', '>', '<',
+ 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b',
+ 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b',
+ 'notFilled\b', 'filled\b')
+ $opJoined = $opPatterns -join '|'
+ if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") {
+ $result.field = $Matches[1].Trim()
+ $result.op = $Matches[2].Trim()
+ $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" }
+ if ($valPart -and $valPart -ne "_") {
+ if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true"); $result["valueType"] = "xs:boolean" }
+ elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart; $result["valueType"] = "xs:dateTime" }
+ elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart; $result["valueType"] = "xs:decimal" }
+ elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart; $result["valueType"] = "dcscor:DesignTimeValue" }
+ else { $result.value = $valPart; $result["valueType"] = "xs:string" }
+ }
+ } else { $result.field = $s }
+ return $result
+}
+
+function Emit-FilterItem {
+ param($item, [string]$indent)
+ if ($item.group) {
+ $groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } }
+ X "$indent"
+ X "$indent`t$groupType"
+ if ($item.items) {
+ foreach ($sub in $item.items) {
+ if ($sub -is [string]) {
+ $parsed = Parse-FilterShorthand $sub
+ $obj = @{ field = $parsed.field; op = $parsed.op }
+ if ($parsed.use -eq $false) { $obj.use = $false }
+ if ($null -ne $parsed.value) { $obj.value = $parsed.value }
+ if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] }
+ if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID }
+ if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode }
+ $sub = [pscustomobject]$obj
+ }
+ Emit-FilterItem -item $sub -indent "$indent`t"
+ }
+ }
+ if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" }
+ if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" }
+ if ($item.userSettingID) {
+ $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
+ X "$indent`t$(Esc-Xml $guid)"
+ }
+ if ($item.userSettingPresentation) { Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" }
+ X "$indent"
+ return
+ }
+ X "$indent"
+ if ($item.use -eq $false) { X "$indent`tfalse" }
+ X "$indent`t$(Esc-Xml "$($item.field)")"
+ $compType = $script:comparisonTypes["$($item.op)"]
+ if (-not $compType) { $compType = "$($item.op)" }
+ X "$indent`t$(Esc-Xml $compType)"
+ $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
+ if ($valIsArray) {
+ if (@($item.value).Count -eq 0) {
+ X "$indent`t"
+ X "$indent`t`t"
+ X "$indent`t`t-1"
+ X "$indent`t"
+ } else {
+ foreach ($v in $item.value) {
+ $vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
+ if (-not $vt) {
+ if ($v -is [bool]) { $vt = 'xs:boolean' }
+ elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' }
+ elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' }
+ elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' }
+ elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
+ else { $vt = 'xs:string' }
+ }
+ $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" }
+ X "$indent`t$vStr"
+ }
+ }
+ } elseif ($null -ne $item.value) {
+ $vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
+ if (-not $vt) {
+ $v = $item.value
+ if ($v -is [bool]) { $vt = "xs:boolean" }
+ elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = "xs:decimal" }
+ elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = "xs:dateTime" }
+ elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" }
+ elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
+ else { $vt = "xs:string" }
+ }
+ $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
+ X "$indent`t$vStr"
+ }
+ if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" }
+ if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" }
+ if ($item.userSettingID) {
+ $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
+ X "$indent`t$(Esc-Xml $uid)"
+ }
+ if ($item.userSettingPresentation) { Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" }
+ X "$indent"
+}
+
+function Emit-Filter {
+ param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null)
+ $hasItems = $items -and $items.Count -gt 0
+ $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID)
+ if (-not $hasItems -and -not $hasBlockMeta) { return }
+ X "$indent"
+ foreach ($item in $items) {
+ if ($item -is [string]) {
+ $parsed = Parse-FilterShorthand $item
+ $obj = @{ field = $parsed.field; op = $parsed.op }
+ if ($parsed.use -eq $false) { $obj.use = $false }
+ if ($null -ne $parsed.value) { $obj.value = $parsed.value }
+ if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] }
+ if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID }
+ if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode }
+ Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
+ } else { Emit-FilterItem -item $item -indent "$indent`t" }
+ }
+ if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" }
+ if ($null -ne $blockUserSettingID) {
+ $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
+ X "$indent`t$(Esc-Xml $uid)"
+ }
+ X "$indent"
+}
+
+function Emit-Order {
+ param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null)
+ $hasItems = $items -and $items.Count -gt 0
+ $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID)
+ if (-not $hasItems -and -not $hasBlockMeta) { return }
+ X "$indent"
+ foreach ($item in $items) {
+ if ($item -is [string]) {
+ if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t" } }
+ else {
+ $parts = $item -split '\s+'
+ $field = $parts[0]
+ $dir = "Asc"
+ if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
+ elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml $field)"
+ X "$indent`t`t$dir"
+ X "$indent`t"
+ }
+ } else {
+ if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t" }; continue }
+ $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" }
+ if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
+ X "$indent`t"
+ if ($item.use -eq $false) { X "$indent`t`tfalse" }
+ X "$indent`t`t$(Esc-Xml "$($item.field)")"
+ X "$indent`t`t$dir"
+ if ($item.viewMode) { X "$indent`t`t$(Esc-Xml "$($item.viewMode)")" }
+ X "$indent`t"
+ }
+ }
+ if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" }
+ if ($null -ne $blockUserSettingID) {
+ $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
+ X "$indent`t$(Esc-Xml $uid)"
+ }
+ X "$indent"
+}
+
+function Emit-AppearanceValue {
+ param([string]$key, $val, [string]$indent)
+ X "$indent"
+ function _HasKey { param($o, [string]$k)
+ if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] }
+ if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) }
+ return $false
+ }
+ function _Get { param($o, [string]$k)
+ if ($o -is [PSCustomObject]) { return $o.$k }
+ if ($o -is [System.Collections.IDictionary]) { return $o[$k] }
+ return $null
+ }
+ $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line')
+ $useWrapper = $false
+ $innerVal = $val
+ $nestedItems = $null
+ if ($isTopLevelLine) {
+ if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true }
+ if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
+ } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) {
+ $innerVal = (_Get $val 'value')
+ if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true }
+ if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
+ }
+ if ($useWrapper) { X "$indent`tfalse" }
+ X "$indent`t$(Esc-Xml $key)"
+ $isFontDict = $false
+ if ($innerVal -is [PSCustomObject]) {
+ $tProp = $innerVal.PSObject.Properties['@type']
+ if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true }
+ } elseif ($innerVal -is [System.Collections.IDictionary]) {
+ if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true }
+ }
+ $isLineDict = $false
+ if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') }
+ $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject])
+ if ($isLineDict) {
+ $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 }
+ $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
+ $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
+ X "$indent`t"
+ X "$indent`t`t$(Esc-Xml $ls)"
+ X "$indent`t"
+ } elseif ($isFontDict) {
+ $attrParts = @()
+ foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
+ $av = $null
+ if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } }
+ else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } }
+ if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" }
+ }
+ X "$indent`t"
+ } elseif ($isDict) {
+ Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t"
+ } else {
+ $actualVal = "$innerVal"
+ $keyTypeMap = @{
+ 'Размещение' = 'dcscor:DataCompositionTextPlacementType'
+ 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign'
+ 'ВертикальноеПоложение' = 'v8ui:VerticalAlign'
+ 'ОриентацияТекста' = 'xs:decimal'
+ 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement'
+ 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
+ }
+ $keyType = $keyTypeMap[$key]
+ if ($keyType) { X "$indent`t$(Esc-Xml $actualVal)" }
+ elseif ($actualVal -match '^(style|web|win):') { X "$indent`t$(Esc-Xml $actualVal)" }
+ elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t$actualVal" }
+ elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" }
+ elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t$actualVal" }
+ elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t$(Esc-Xml $actualVal)" }
+ else { X "$indent`t$(Esc-Xml $actualVal)" }
+ }
+ if ($nestedItems) {
+ $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
+ if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } }
+ elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } }
+ }
+ X "$indent"
+}
+
+function Emit-ConditionalAppearance {
+ param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null)
+ $hasItems = $items -and $items.Count -gt 0
+ $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID)
+ if (-not $hasItems -and -not $hasBlockMeta) { return }
+ X "$indent"
+ foreach ($ca in $items) {
+ X "$indent`t"
+ if ($ca.use -eq $false) { X "$indent`t`tfalse" }
+ if ($ca.selection -and $ca.selection.Count -gt 0) {
+ X "$indent`t`t"
+ foreach ($sel in $ca.selection) {
+ X "$indent`t`t`t"
+ X "$indent`t`t`t`t$(Esc-Xml "$sel")"
+ X "$indent`t`t`t"
+ }
+ X "$indent`t`t"
+ } else { X "$indent`t`t" }
+ if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" }
+ else { X "$indent`t`t" }
+ if ($ca.appearance) {
+ X "$indent`t`t"
+ foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" }
+ X "$indent`t`t"
+ }
+ if ($ca.presentation) {
+ if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { Emit-MLText -tag "dcsset:presentation" -text $ca.presentation -indent "$indent`t`t" }
+ else { X "$indent`t`t$(Esc-Xml "$($ca.presentation)")" }
+ }
+ if ($ca.viewMode) { X "$indent`t`t$(Esc-Xml "$($ca.viewMode)")" }
+ if ($ca.userSettingID) {
+ $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
+ X "$indent`t`t$(Esc-Xml $uid)"
+ }
+ if ($ca.userSettingPresentation) { Emit-MLText -tag "dcsset:userSettingPresentation" -text $ca.userSettingPresentation -indent "$indent`t`t" }
+ if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
+ $useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader')
+ $set = @{}
+ foreach ($n in $ca.useInDontUse) { $set["$n"] = $true }
+ foreach ($n in $useInOrder) {
+ if ($set.ContainsKey($n)) {
+ $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1))
+ X "$indent`t`tDontUse"
+ }
+ }
+ }
+ X "$indent`t"
+ }
+ if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" }
+ if ($null -ne $blockUserSettingID) {
+ $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
+ X "$indent`t$(Esc-Xml $uid)"
+ }
+ X "$indent"
+}
+
# --- 5. Type emitter ---
$script:formTypeSynonyms = New-Object System.Collections.Hashtable
@@ -2327,9 +2684,25 @@ $script:refRootSynonyms = @{
"РегистрБухгалтерии" = "AccountingRegister"
"РегистрРасчета" = "CalculationRegister"
"РегистрРасчёта" = "CalculationRegister"
+ "ЖурналДокументов" = "DocumentJournal"
+ "КритерийОтбора" = "FilterCriterion"
}
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")
+# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты".
+# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений.
+function Normalize-MetaTypeRef {
+ param([string]$ref)
+ if ([string]::IsNullOrEmpty($ref)) { return $ref }
+ $dot = $ref.IndexOf('.')
+ if ($dot -lt 1) { return $ref }
+ $root = $ref.Substring(0, $dot)
+ if ($script:refRootSynonyms.ContainsKey($root)) {
+ return $script:refRootSynonyms[$root] + $ref.Substring($dot)
+ }
+ return $ref
+}
+
# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." }
function Normalize-ChoiceValue {
param($value)
@@ -3056,15 +3429,48 @@ function Emit-Attributes {
X "$inner"
}
- # Settings (for DynamicList)
+ # Settings (динамический список)
if ($attr.settings) {
+ $st = $attr.settings
X "$inner"
$si = "$inner`t"
- if ($attr.settings.mainTable) { X "$si$($attr.settings.mainTable)" }
- $mq = if ($attr.settings.manualQuery -eq $true) { "true" } else { "false" }
+ # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
+ $hasQuery = $st.query -and "$($st.query)".Trim()
+ $mq = if ($hasQuery -or $st.manualQuery -eq $true) { "true" } else { "false" }
X "$si$mq"
- $ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" }
+ # DynamicDataRead: дефолт true; false только при явном отключении
+ $ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" }
X "$si$ddr"
+ if ($hasQuery) {
+ $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
+ X "$si$(Esc-Xml $qtext)"
+ }
+ # Явные поля набора (редко): override title/dataPath
+ if ($st.fields) {
+ foreach ($fld in $st.fields) {
+ X "$si"
+ $dp = if ($fld.dataPath) { $fld.dataPath } else { $fld.field }
+ X "$si`t$(Esc-Xml "$dp")"
+ X "$si`t$(Esc-Xml "$($fld.field)")"
+ if ($fld.title) {
+ X "$si`t"
+ Emit-MLItems -val $fld.title -indent "$si`t`t"
+ X "$si`t"
+ }
+ X "$si"
+ }
+ }
+ if ($st.mainTable) { X "$si$(Normalize-MetaTypeRef "$($st.mainTable)")" }
+ # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID.
+ # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
+ $lsi = "$si`t"
+ X "$si"
+ Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID
+ Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID
+ Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID
+ X "$lsiNormal"
+ X "$lsi$($script:CANON_ITEMS_ID)"
+ X "$si"
X "$inner"
}
diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py
index a5ab5c1a..be8064d1 100644
--- a/.claude/skills/form-compile/scripts/form-compile.py
+++ b/.claude/skills/form-compile/scripts/form-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# form-compile v1.38 — Compile 1C managed form from JSON or object metadata
+# form-compile v1.39 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -1256,6 +1256,26 @@ def esc_xml(s):
return s.replace('&', '&').replace('<', '<').replace('>', '>')
+# Базовая директория для @file-ссылок в query динсписка (устанавливается в main)
+QUERY_BASE_DIR = None
+
+
+def resolve_query_value(val, base_dir):
+ if not val.startswith('@'):
+ return val
+ file_path = val[1:]
+ if os.path.isabs(file_path):
+ candidates = [file_path]
+ else:
+ candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)]
+ for c in candidates:
+ if os.path.exists(c):
+ with open(c, 'r', encoding='utf-8-sig') as f:
+ return f.read().rstrip()
+ print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
+ sys.exit(1)
+
+
def emit_ml_items(lines, indent, val):
# строка → один ru-элемент; объект {lang: text} → по элементу на язык
if isinstance(val, dict):
@@ -1280,10 +1300,395 @@ def emit_mltext(lines, indent, tag, text):
lines.append(f"{indent}{tag}>")
+# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
+CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
+CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
+CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
+CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
+
+
def new_uuid():
return str(uuid.uuid4())
+# ─────────────────────────────────────────────────────────────────────────────
+# Настройки компоновщика ListSettings: filter/order/conditionalAppearance.
+# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны).
+# ─────────────────────────────────────────────────────────────────────────────
+COMPARISON_TYPES = {
+ '=': 'Equal', '<>': 'NotEqual',
+ '>': 'Greater', '>=': 'GreaterOrEqual',
+ '<': 'Less', '<=': 'LessOrEqual',
+ 'in': 'InList', 'notIn': 'NotInList',
+ 'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy',
+ 'contains': 'Contains', 'notContains': 'NotContains',
+ 'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith',
+ 'filled': 'Filled', 'notFilled': 'NotFilled',
+}
+
+_REF_TYPE_RE = re.compile(
+ r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|'
+ r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|'
+ r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|'
+ r'InformationRegister|ExchangePlan)\.')
+
+
+def parse_filter_shorthand(s):
+ result = {'field': '', 'op': 'Equal', 'value': None, 'use': True,
+ 'userSettingID': None, 'viewMode': None, 'presentation': None}
+ if re.search(r'@user', s):
+ result['userSettingID'] = 'auto'
+ s = re.sub(r'\s*@user', '', s)
+ if re.search(r'@off', s):
+ result['use'] = False
+ s = re.sub(r'\s*@off', '', s)
+ if re.search(r'@quickAccess', s):
+ result['viewMode'] = 'QuickAccess'
+ s = re.sub(r'\s*@quickAccess', '', s)
+ if re.search(r'@normal', s):
+ result['viewMode'] = 'Normal'
+ s = re.sub(r'\s*@normal', '', s)
+ if re.search(r'@inaccessible', s):
+ result['viewMode'] = 'Inaccessible'
+ s = re.sub(r'\s*@inaccessible', '', s)
+ s = s.strip()
+ op_patterns = ['<>', '>=', '<=', '=', '>', '<',
+ r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b',
+ r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b',
+ r'notFilled\b', r'filled\b']
+ op_joined = '|'.join(op_patterns)
+ m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s)
+ if m:
+ result['field'] = m.group(1).strip()
+ result['op'] = m.group(2).strip()
+ val_part = m.group(3).strip() if m.group(3) else ''
+ if val_part and val_part != '_':
+ if val_part == 'true' or val_part == 'false':
+ result['value'] = (val_part == 'true')
+ result['valueType'] = 'xs:boolean'
+ elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part):
+ result['value'] = val_part
+ result['valueType'] = 'xs:dateTime'
+ elif re.match(r'^\d+(\.\d+)?$', val_part):
+ result['value'] = val_part
+ result['valueType'] = 'xs:decimal'
+ elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
+ result['value'] = val_part
+ result['valueType'] = 'dcscor:DesignTimeValue'
+ else:
+ result['value'] = val_part
+ result['valueType'] = 'xs:string'
+ else:
+ result['field'] = s
+ return result
+
+
+def _value_type_for(v, explicit=None):
+ if explicit:
+ return explicit
+ if isinstance(v, bool):
+ return 'xs:boolean'
+ if isinstance(v, (int, float)):
+ return 'xs:decimal'
+ vs = str(v)
+ if re.match(r'^\d{4}-\d{2}-\d{2}T', vs):
+ return 'xs:dateTime'
+ if re.match(r'^-?\d+(\.\d+)?$', vs):
+ return 'xs:decimal'
+ if _REF_TYPE_RE.match(vs):
+ return 'dcscor:DesignTimeValue'
+ return 'xs:string'
+
+
+def emit_filter_item(lines, item, indent):
+ if item.get('group'):
+ g = str(item['group'])
+ group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group')
+ lines.append(f'{indent}')
+ lines.append(f'{indent}\t{group_type}')
+ if item.get('items'):
+ for sub in item['items']:
+ if isinstance(sub, str):
+ parsed = parse_filter_shorthand(sub)
+ obj = {'field': parsed['field'], 'op': parsed['op']}
+ if parsed['use'] is False:
+ obj['use'] = False
+ if parsed['value'] is not None:
+ obj['value'] = parsed['value']
+ if parsed.get('valueType'):
+ obj['valueType'] = parsed['valueType']
+ if parsed.get('userSettingID'):
+ obj['userSettingID'] = parsed['userSettingID']
+ if parsed.get('viewMode'):
+ obj['viewMode'] = parsed['viewMode']
+ sub = obj
+ emit_filter_item(lines, sub, f'{indent}\t')
+ if item.get('presentation'):
+ emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation'])
+ if item.get('viewMode'):
+ lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}')
+ if item.get('userSettingID'):
+ guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
+ lines.append(f'{indent}\t{esc_xml(guid)}')
+ if item.get('userSettingPresentation'):
+ emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
+ lines.append(f'{indent}')
+ return
+
+ lines.append(f'{indent}')
+ if item.get('use') is False:
+ lines.append(f'{indent}\tfalse')
+ lines.append(f'{indent}\t{esc_xml(str(item.get("field", "")))}')
+ comp_type = COMPARISON_TYPES.get(str(item.get('op')))
+ if not comp_type:
+ comp_type = str(item.get('op'))
+ lines.append(f'{indent}\t{esc_xml(comp_type)}')
+ val = item.get('value')
+ if isinstance(val, list):
+ if len(val) == 0:
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}\t\t')
+ lines.append(f'{indent}\t\t-1')
+ lines.append(f'{indent}\t')
+ else:
+ for v in val:
+ vt = _value_type_for(v, item.get('valueType'))
+ v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v))
+ lines.append(f'{indent}\t{v_str}')
+ elif val is not None:
+ vt = _value_type_for(val, item.get('valueType'))
+ v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val))
+ lines.append(f'{indent}\t{v_str}')
+ if item.get('presentation'):
+ emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation'])
+ if item.get('viewMode'):
+ lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}')
+ if item.get('userSettingID'):
+ uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ if item.get('userSettingPresentation'):
+ emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
+ lines.append(f'{indent}')
+
+
+def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None):
+ has_items = bool(items) and len(items) > 0
+ has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None)
+ if not has_items and not has_block_meta:
+ return
+ lines.append(f'{indent}')
+ for item in (items or []):
+ if isinstance(item, str):
+ parsed = parse_filter_shorthand(item)
+ obj = {'field': parsed['field'], 'op': parsed['op']}
+ if parsed['use'] is False:
+ obj['use'] = False
+ if parsed['value'] is not None:
+ obj['value'] = parsed['value']
+ if parsed.get('valueType'):
+ obj['valueType'] = parsed['valueType']
+ if parsed.get('userSettingID'):
+ obj['userSettingID'] = parsed['userSettingID']
+ if parsed.get('viewMode'):
+ obj['viewMode'] = parsed['viewMode']
+ emit_filter_item(lines, obj, f'{indent}\t')
+ else:
+ emit_filter_item(lines, item, f'{indent}\t')
+ if block_view_mode is not None:
+ lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}')
+ if block_user_setting_id is not None:
+ uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ lines.append(f'{indent}')
+
+
+def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None):
+ has_items = bool(items) and len(items) > 0
+ has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None)
+ if not has_items and not has_block_meta:
+ return
+ lines.append(f'{indent}')
+ for item in (items or []):
+ if isinstance(item, str):
+ if item == 'Auto':
+ if not skip_auto:
+ lines.append(f'{indent}\t')
+ else:
+ parts = re.split(r'\s+', item)
+ field = parts[0]
+ direction = 'Asc'
+ if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]):
+ direction = 'Desc'
+ elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]):
+ direction = 'Asc'
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}\t\t{esc_xml(field)}')
+ lines.append(f'{indent}\t\t{direction}')
+ lines.append(f'{indent}\t')
+ else:
+ if item.get('field') == 'Auto' or item.get('type') == 'auto':
+ if not skip_auto:
+ lines.append(f'{indent}\t')
+ continue
+ direction = str(item['direction']) if item.get('direction') else 'Asc'
+ if re.match(r'(?i)^(desc|убыв)', direction):
+ direction = 'Desc'
+ elif re.match(r'(?i)^(asc|возр)', direction):
+ direction = 'Asc'
+ lines.append(f'{indent}\t')
+ if item.get('use') is False:
+ lines.append(f'{indent}\t\tfalse')
+ lines.append(f'{indent}\t\t{esc_xml(str(item.get("field", "")))}')
+ lines.append(f'{indent}\t\t{direction}')
+ if item.get('viewMode'):
+ lines.append(f'{indent}\t\t{esc_xml(str(item["viewMode"]))}')
+ lines.append(f'{indent}\t')
+ if block_view_mode is not None:
+ lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}')
+ if block_user_setting_id is not None:
+ uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ lines.append(f'{indent}')
+
+
+def emit_appearance_value(lines, key, val, indent):
+ lines.append(f'{indent}')
+
+ def _has_key(o, k):
+ return isinstance(o, dict) and (k in o)
+
+ def _get(o, k):
+ return o.get(k) if isinstance(o, dict) else None
+
+ is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line')
+ use_wrapper = False
+ inner_val = val
+ nested_items = None
+ if is_top_level_line:
+ if _has_key(val, 'use') and (_get(val, 'use') is False):
+ use_wrapper = True
+ if _has_key(val, 'items'):
+ nested_items = _get(val, 'items')
+ elif _has_key(val, 'value') and isinstance(val, dict):
+ inner_val = _get(val, 'value')
+ if _has_key(val, 'use') and (_get(val, 'use') is False):
+ use_wrapper = True
+ if _has_key(val, 'items'):
+ nested_items = _get(val, 'items')
+ if use_wrapper:
+ lines.append(f'{indent}\tfalse')
+ lines.append(f'{indent}\t{esc_xml(key)}')
+
+ is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font'
+ is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line')
+ is_dict = isinstance(inner_val, dict)
+ if is_line_dict:
+ lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0
+ lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false'
+ ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None'
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}\t\t{esc_xml(ls)}')
+ lines.append(f'{indent}\t')
+ elif is_font_dict:
+ attr_parts = []
+ for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'):
+ if attr_name in inner_val:
+ av = inner_val[attr_name]
+ if av is not None:
+ attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"')
+ lines.append(f'{indent}\t')
+ elif is_dict:
+ emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val)
+ else:
+ actual_val = str(inner_val)
+ key_type_map = {
+ 'Размещение': 'dcscor:DataCompositionTextPlacementType',
+ 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign',
+ 'ВертикальноеПоложение': 'v8ui:VerticalAlign',
+ 'ОриентацияТекста': 'xs:decimal',
+ 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement',
+ 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType',
+ }
+ key_type = key_type_map.get(key)
+ if key_type:
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ elif re.match(r'^(style|web|win):', actual_val):
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ elif actual_val == 'true' or actual_val == 'false':
+ lines.append(f'{indent}\t{actual_val}')
+ elif key == 'Текст' or key == 'Заголовок' or key == 'Формат':
+ emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val)
+ elif re.match(r'^-?\d+(\.\d+)?$', actual_val):
+ lines.append(f'{indent}\t{actual_val}')
+ elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы':
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ else:
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ if nested_items:
+ if isinstance(nested_items, dict):
+ for nk, nv in nested_items.items():
+ emit_appearance_value(lines, nk, nv, f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None):
+ has_items = bool(items) and len(items) > 0
+ has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None)
+ if not has_items and not has_block_meta:
+ return
+ lines.append(f'{indent}')
+ for ca in (items or []):
+ lines.append(f'{indent}\t')
+ if ca.get('use') is False:
+ lines.append(f'{indent}\t\tfalse')
+ if ca.get('selection') and len(ca['selection']) > 0:
+ lines.append(f'{indent}\t\t')
+ for sel in ca['selection']:
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t\t\t{esc_xml(str(sel))}')
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t')
+ else:
+ lines.append(f'{indent}\t\t')
+ if ca.get('filter') and len(ca['filter']) > 0:
+ emit_filter(lines, ca['filter'], f'{indent}\t\t')
+ else:
+ lines.append(f'{indent}\t\t')
+ if ca.get('appearance'):
+ lines.append(f'{indent}\t\t')
+ for k, v in ca['appearance'].items():
+ emit_appearance_value(lines, k, v, f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t')
+ if ca.get('presentation'):
+ if isinstance(ca['presentation'], dict):
+ emit_mltext(lines, f'{indent}\t\t', 'dcsset:presentation', ca['presentation'])
+ else:
+ lines.append(f'{indent}\t\t{esc_xml(str(ca["presentation"]))}')
+ if ca.get('viewMode'):
+ lines.append(f'{indent}\t\t{esc_xml(str(ca["viewMode"]))}')
+ if ca.get('userSettingID'):
+ uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID'])
+ lines.append(f'{indent}\t\t{esc_xml(uid)}')
+ if ca.get('userSettingPresentation'):
+ emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation'])
+ if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0:
+ use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header',
+ 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader',
+ 'overallResourceFieldsHeader']
+ sset = {str(n): True for n in ca['useInDontUse']}
+ for n in use_in_order:
+ if n in sset:
+ tag = 'useIn' + n[0].upper() + n[1:]
+ lines.append(f'{indent}\t\tDontUse')
+ lines.append(f'{indent}\t')
+ if block_view_mode is not None:
+ lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}')
+ if block_user_setting_id is not None:
+ uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ lines.append(f'{indent}')
+
+
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
@@ -1440,10 +1845,25 @@ REF_ROOT_SYNONYMS = {
"РегистрБухгалтерии": "AccountingRegister",
"РегистрРасчета": "CalculationRegister",
"РегистрРасчёта": "CalculationRegister",
+ "ЖурналДокументов": "DocumentJournal",
+ "КритерийОтбора": "FilterCriterion",
}
ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"}
+def normalize_meta_type_ref(ref):
+ # "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений
+ if not ref:
+ return ref
+ dot = ref.find('.')
+ if dot < 1:
+ return ref
+ root = ref[:dot]
+ if root in REF_ROOT_SYNONYMS:
+ return REF_ROOT_SYNONYMS[root] + ref[dot:]
+ return ref
+
+
def normalize_choice_value(value):
"""Returns dict {xsi_type, text} for a choiceList item value."""
if isinstance(value, bool):
@@ -2669,17 +3089,45 @@ def emit_attributes(lines, attrs, indent):
lines.append(f'{inner}\t')
lines.append(f'{inner}')
- # Settings (for DynamicList)
+ # Settings (динамический список)
if attr.get('settings'):
s = attr['settings']
lines.append(f'{inner}')
si = f'{inner}\t'
- if s.get('mainTable'):
- lines.append(f'{si}{s["mainTable"]}')
- mq = 'true' if s.get('manualQuery') else 'false'
+ # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
+ has_query = bool(s.get('query') and str(s['query']).strip())
+ mq = 'true' if (has_query or s.get('manualQuery')) else 'false'
lines.append(f'{si}{mq}')
- ddr = 'true' if s.get('dynamicDataRead') else 'false'
+ # DynamicDataRead: дефолт true; false только при явном отключении
+ ddr = 'false' if s.get('dynamicDataRead') is False else 'true'
lines.append(f'{si}{ddr}')
+ if has_query:
+ qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR)
+ lines.append(f'{si}{esc_xml(qtext)}')
+ # Явные поля набора (редко): override title/dataPath
+ if s.get('fields'):
+ for fld in s['fields']:
+ lines.append(f'{si}')
+ dp = fld.get('dataPath') or fld.get('field')
+ lines.append(f'{si}\t{esc_xml(str(dp))}')
+ lines.append(f'{si}\t{esc_xml(str(fld.get("field", "")))}')
+ if fld.get('title'):
+ lines.append(f'{si}\t')
+ emit_ml_items(lines, f'{si}\t\t', fld['title'])
+ lines.append(f'{si}\t')
+ lines.append(f'{si}')
+ if s.get('mainTable'):
+ lines.append(f'{si}{normalize_meta_type_ref(str(s["mainTable"]))}')
+ # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID.
+ # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
+ lsi = f'{si}\t'
+ lines.append(f'{si}')
+ emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID)
+ emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID)
+ emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID)
+ lines.append(f'{lsi}Normal')
+ lines.append(f'{lsi}{CANON_ITEMS_ID}')
+ lines.append(f'{si}')
lines.append(f'{inner}')
lines.append(f'{indent}\t')
@@ -3047,6 +3495,8 @@ def main():
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
+ global QUERY_BASE_DIR
+ QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path))
# --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
def _normalize_synonyms(el):
diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1
index 475eb44f..4db284d9 100644
--- a/.claude/skills/form-decompile/scripts/form-decompile.ps1
+++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1
@@ -1,4 +1,4 @@
-# form-decompile v0.18 — Decompile 1C managed Form.xml to JSON DSL (draft)
+# form-decompile v0.21 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
param(
@@ -36,11 +36,80 @@ $NS_V8 = "http://v8.1c.ru/8.1/data/core"
$NS_XR = "http://v8.1c.ru/8.3/xcf/readable"
$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
+$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings"
+$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema"
+$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core"
+$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
+
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("lf", $NS_LF)
$ns.AddNamespace("v8", $NS_V8)
$ns.AddNamespace("xr", $NS_XR)
$ns.AddNamespace("xsi", $NS_XSI)
+$ns.AddNamespace("dcsset", $NS_DCSSET)
+$ns.AddNamespace("dcssch", $NS_DCSSCH)
+$ns.AddNamespace("dcscor", $NS_DCSCOR)
+$ns.AddNamespace("v8ui", $NS_V8UI)
+
+# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
+# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе,
+# компилятор регенерит тот же скелет → чистый раундтрип.
+$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
+$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
+$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
+$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
+
+# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) ---
+$script:outputDir = $null
+$script:outputBasename = $null
+if ($OutputPath) {
+ $od = Split-Path -Parent $OutputPath
+ if (-not $od) { $od = (Get-Location).Path }
+ $script:outputDir = $od
+ $script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath)
+}
+$script:queryFilesAccumulator = @()
+$script:queryFileNamesUsed = @{}
+
+# Запрос ≥3 строк + есть outputDir → вынести в `-.sql`, вернуть "@file".
+function Maybe-ExternalizeQuery {
+ param([string]$queryText, [string]$listName)
+ if (-not $queryText) { return $queryText }
+ if (-not $script:outputDir) { return $queryText }
+ $lineCount = ([regex]::Matches($queryText, "`n")).Count + 1
+ if ($lineCount -lt 3) { return $queryText }
+ $safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' }
+ $prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' }
+ $fileName = "$prefix$safe.sql"
+ $suffix = 1
+ while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" }
+ $script:queryFileNamesUsed[$fileName] = $true
+ $script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText }
+ return "@$fileName"
+}
+function Save-QueryFiles {
+ if ($script:queryFilesAccumulator.Count -eq 0) { return }
+ if (-not $script:outputDir) { return }
+ $enc = New-Object System.Text.UTF8Encoding($false)
+ foreach ($qf in $script:queryFilesAccumulator) {
+ [System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc)
+ }
+ [Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)")
+}
+
+# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/
+# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false:
+# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness
+# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже).
+function Test-ListSettingsHasContent {
+ param($lsNode)
+ if (-not $lsNode) { return $false }
+ foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) {
+ $cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns)
+ if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true }
+ }
+ return $false
+}
# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) ---
function Fail-Ring3 {
@@ -184,6 +253,464 @@ function Convert-TypedValue {
}
}
+# =====================================================================
+# Захват настроек компоновщика динамического списка (ListSettings):
+# filter / order / conditionalAppearance. Логика портирована из навыка
+# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance
+# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd
+# заменён на запись в stderr + пропуск элемента (form-decompile — draft,
+# скрипт не падает на непокрытых конструкциях).
+# =====================================================================
+
+# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text.
+function Get-Text {
+ param($node, [string]$xpath)
+ if (-not $node) { return $null }
+ if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText }
+ $n = $node.SelectSingleNode($xpath, $ns)
+ if ($n) { return $n.InnerText } else { return $null }
+}
+
+# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash.
+# Алиас на уже существующий Get-LangText (тот же контракт).
+function Get-MLText { param($node) return (Get-LangText $node) }
+
+# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string.
+# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText.
+function Get-PresText {
+ param($node)
+ if (-not $node) { return $null }
+ $ml = Get-MLText $node
+ if ($null -ne $ml) { return $ml }
+ if ($node.InnerText) { return $node.InnerText }
+ return $null
+}
+
+# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo")
+function Get-LocalXsiType {
+ param($node)
+ if (-not $node) { return $null }
+ $t = $node.GetAttribute("type", $NS_XSI)
+ if ($t -match ':(.+)$') { return $matches[1] }
+ return $t
+}
+
+# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile).
+function Get-FontValue {
+ param($valNode)
+ $f = [ordered]@{ '@type' = 'Font' }
+ foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
+ $a = $valNode.Attributes[$attrName]
+ if ($null -ne $a) { $f[$attrName] = $a.Value }
+ }
+ return $f
+}
+
+# Линия (граница) оформления → объект {@type:Line, width, gap, style}.
+function Get-LineValue {
+ param($valNode)
+ $obj = [ordered]@{ '@type' = 'Line' }
+ $w = $valNode.GetAttribute("width")
+ $g = $valNode.GetAttribute("gap")
+ if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } }
+ if ($g -ne '') { $obj['gap'] = ($g -eq 'true') }
+ $styleNode = $valNode.SelectSingleNode("v8ui:style", $ns)
+ if ($styleNode) { $obj['style'] = $styleNode.InnerText }
+ return $obj
+}
+
+# Прочитать в JSON-значение: Font/Line/multilang/raw text.
+function Read-AppearanceValueNode {
+ param($valNode)
+ if (-not $valNode) { return $null }
+ $vt = Get-LocalXsiType $valNode
+ if ($vt -eq 'LocalStringType') { return (Get-MLText $valNode) }
+ if ($vt -eq 'Font') { return (Get-FontValue $valNode) }
+ if ($vt -eq 'Line') { return (Get-LineValue $valNode) }
+ return $valNode.InnerText
+}
+
+# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd).
+$script:filterOpMap = @{
+ 'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>=';
+ 'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn';
+ 'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy';
+ 'Contains'='contains'; 'NotContains'='notContains';
+ 'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith';
+ 'Filled'='filled'; 'NotFilled'='notFilled'
+}
+
+# Render filter value node → shorthand-acceptable scalar string
+function Get-FilterValue {
+ param($valNode)
+ if (-not $valNode) { return '_' }
+ $nil = $valNode.GetAttribute("nil", $NS_XSI)
+ if ($nil -eq 'true') { return '_' }
+ $vType = Get-LocalXsiType $valNode
+ if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText }
+ if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) }
+ $txt = $valNode.InnerText
+ if (-not $txt) { return '_' }
+ return $txt
+}
+
+# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field).
+function Get-FilterValueWithType {
+ param($valNode)
+ if (-not $valNode) { return @{ value = '_'; type = $null } }
+ $rawType = $valNode.GetAttribute("type", $NS_XSI)
+ $nil = $valNode.GetAttribute("nil", $NS_XSI)
+ if ($nil -eq 'true') { return @{ value = '_'; type = $null } }
+ $vType = Get-LocalXsiType $valNode
+ if ($vType -eq 'LocalStringType') {
+ return @{ value = (Get-MLText $valNode); type = $rawType }
+ }
+ $txt = $valNode.InnerText
+ if (-not $txt) { return @{ value = '_'; type = $rawType } }
+ if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } }
+ if ($vType -eq 'decimal') {
+ if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } }
+ return @{ value = [double]$txt; type = $rawType }
+ }
+ return @{ value = $txt; type = $rawType }
+}
+
+# Convert filter item node → shorthand string или object form (рекурсивно для групп).
+function Build-FilterItem {
+ param($itemNode, [string]$loc)
+ $xtype = Get-LocalXsiType $itemNode
+ if ($xtype -eq 'FilterItemGroup') {
+ $gt = Get-Text $itemNode "dcsset:groupType"
+ $groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } }
+ $items = @()
+ foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) {
+ $bi = (Build-FilterItem -itemNode $c -loc "$loc/item")
+ if ($null -ne $bi) { $items += $bi }
+ }
+ $gObj = [ordered]@{ group = $groupName; items = $items }
+ $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
+ if ($gPresNode) {
+ $gPres = Get-MLText $gPresNode
+ if (-not $gPres) { $gPres = $gPresNode.InnerText }
+ if ($gPres) { $gObj['presentation'] = $gPres }
+ }
+ $gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
+ if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText }
+ $gUSID = Get-Text $itemNode "dcsset:userSettingID"
+ if ($gUSID) { $gObj['userSettingID'] = 'auto' }
+ $gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
+ if ($gUSPN) {
+ $gUSP = Get-PresText $gUSPN
+ if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP }
+ }
+ return $gObj
+ }
+ if ($xtype -ne 'FilterItemComparison') {
+ [Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)")
+ return $null
+ }
+ $leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns)
+ $field = if ($leftNode) { $leftNode.InnerText } else { $null }
+ $ct = Get-Text $itemNode "dcsset:comparisonType"
+ $op = $script:filterOpMap[$ct]
+ if (-not $op) { $op = $ct }
+
+ $rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns))
+ $value = $null
+ $valueIsArrayFlag = $false
+ $valueTypeAttr = $null
+ if ($rightNodes.Count -eq 1) {
+ $rn = $rightNodes[0]
+ if ((Get-LocalXsiType $rn) -eq 'ValueListType') {
+ $value = @()
+ $valueIsArrayFlag = $true
+ } else {
+ $vt = Get-FilterValueWithType $rn
+ $value = $vt.value
+ $autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and `
+ ("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.')
+ if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) {
+ $valueTypeAttr = $vt.type
+ }
+ }
+ } elseif ($rightNodes.Count -gt 1) {
+ $arr = @()
+ $rawTypes = @()
+ foreach ($rn in $rightNodes) {
+ $arr += (Get-FilterValue $rn)
+ $rawTypes += $rn.GetAttribute("type", $NS_XSI)
+ }
+ $value = $arr
+ $valueIsArrayFlag = $true
+ $uniqTypes = @($rawTypes | Sort-Object -Unique)
+ if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) {
+ $autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and `
+ ($arr.Count -gt 0) -and `
+ (@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0)
+ if (-not $autoDetectsDTV) {
+ $valueTypeAttr = $uniqTypes[0]
+ }
+ }
+ }
+
+ $use = Get-Text $itemNode "dcsset:use"
+ $userId = Get-Text $itemNode "dcsset:userSettingID"
+ $vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
+ $viewMode = if ($vmNode) { $vmNode.InnerText } else { $null }
+ $userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
+ $fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
+ $fiPres = $null
+ if ($fiPresNode) {
+ $fiPres = Get-MLText $fiPresNode
+ if (-not $fiPres) { $fiPres = $fiPresNode.InnerText }
+ }
+
+ $flags = @()
+ if ($use -eq 'false') { $flags += '@off' }
+ if ($userId) { $flags += '@user' }
+ if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' }
+ elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' }
+ elseif ($viewMode -eq 'Normal') { $flags += '@normal' }
+
+ $noValueOps = @('filled','notFilled')
+
+ if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres) {
+ $obj = [ordered]@{ field = $field; op = $op }
+ if ($op -notin $noValueOps -and $null -ne $value) {
+ if ($valueIsArrayFlag) {
+ $arrAsList = New-Object System.Collections.ArrayList
+ foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) }
+ $obj['value'] = $arrAsList
+ } else {
+ $obj['value'] = $value
+ }
+ }
+ if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr }
+ if ($use -eq 'false') { $obj['use'] = $false }
+ if ($userId) { $obj['userSettingID'] = 'auto' }
+ if ($fiPres) { $obj['presentation'] = $fiPres }
+ if ($viewMode) { $obj['viewMode'] = $viewMode }
+ if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode }
+ return $obj
+ }
+
+ $s = $field
+ if ($op -in $noValueOps) {
+ $s += " $op"
+ } else {
+ $vDisplay = '_'
+ if ($null -ne $value) {
+ if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } }
+ elseif ("$value" -ne '') { $vDisplay = "$value" }
+ }
+ $s += " $op $vDisplay"
+ }
+ if ($flags) { $s += ' ' + ($flags -join ' ') }
+ return $s
+}
+
+# Рекурсивный хелпер одного элемента selection (для conditionalAppearance).
+function Build-SelectionItem {
+ param($item, [string]$loc)
+ $xt = Get-LocalXsiType $item
+ if (-not $xt) {
+ $fName = Get-Text $item "dcsset:field"
+ if ($fName) { return $fName }
+ $fieldEl = $item.SelectSingleNode("dcsset:field", $ns)
+ if ($fieldEl) { return 'Auto' }
+ }
+ switch ($xt) {
+ 'SelectedItemAuto' {
+ $useV = Get-Text $item "dcsset:use"
+ if ($useV -eq 'false') {
+ return [ordered]@{ auto = $true; use = $false }
+ }
+ return 'Auto'
+ }
+ 'SelectedItemField' {
+ $fName = Get-Text $item "dcsset:field"
+ $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
+ $title = Get-MLText $titleNode
+ $vmN = $item.SelectSingleNode("dcsset:viewMode", $ns)
+ $useV = Get-Text $item "dcsset:use"
+ $useFalse = ($useV -eq 'false')
+ if ($title -or $vmN -or $useFalse) {
+ $obj = [ordered]@{ field = $fName }
+ if ($useFalse) { $obj['use'] = $false }
+ if ($title) { $obj['title'] = $title }
+ if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
+ return $obj
+ }
+ return $fName
+ }
+ 'SelectedItemFolder' {
+ $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
+ $folderTitle = Get-MLText $titleNode
+ $inner = @()
+ foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) {
+ $bi = (Build-SelectionItem -item $sub -loc "$loc/folder")
+ if ($null -ne $bi) { $inner += $bi }
+ }
+ $entry = [ordered]@{ folder = $folderTitle; items = $inner }
+ $folderField = Get-Text $item "dcsset:field"
+ if ($folderField) { $entry['field'] = $folderField }
+ $plN = $item.SelectSingleNode("dcsset:placement", $ns)
+ if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') {
+ $entry['placement'] = $plN.InnerText
+ }
+ return $entry
+ }
+ default {
+ [Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)")
+ return $null
+ }
+ }
+}
+
+# Build selection items array (для conditionalAppearance).
+function Build-Selection {
+ param($selNode, [string]$loc)
+ if (-not $selNode) { return @() }
+ $out = @()
+ foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) {
+ $bi = (Build-SelectionItem -item $it -loc $loc)
+ if ($null -ne $bi) { $out += $bi }
+ }
+ return ,$out
+}
+
+# Build order items array.
+function Build-Order {
+ param($ordNode, [string]$loc)
+ if (-not $ordNode) { return @() }
+ $out = @()
+ foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) {
+ $xt = Get-LocalXsiType $it
+ switch ($xt) {
+ 'OrderItemAuto' { $out += 'Auto' }
+ 'OrderItemField' {
+ $fn = Get-Text $it "dcsset:field"
+ $ot = Get-Text $it "dcsset:orderType"
+ $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
+ $useV = Get-Text $it "dcsset:use"
+ $useFalse = ($useV -eq 'false')
+ if ($vmN -or $useFalse) {
+ $obj = [ordered]@{ field = $fn }
+ if ($useFalse) { $obj['use'] = $false }
+ if ($ot -eq 'Desc') { $obj['direction'] = 'desc' }
+ if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
+ $out += $obj
+ } else {
+ if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
+ }
+ }
+ default {
+ [Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)")
+ }
+ }
+ }
+ return ,$out
+}
+
+# Build appearance dict из (Line/Font/multilang/nested items).
+function Get-SettingsAppearance {
+ param($appNode)
+ if (-not $appNode) { return $null }
+ $dict = [ordered]@{}
+ foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
+ $pName = Get-Text $it "dcscor:parameter"
+ $val = $it.SelectSingleNode("dcscor:value", $ns)
+ if (-not $pName -or -not $val) { continue }
+ $rawVal = Read-AppearanceValueNode $val
+ $useV = Get-Text $it "dcscor:use"
+ $nestedItems = [ordered]@{}
+ foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
+ $subName = Get-Text $sub "dcscor:parameter"
+ $subVal = $sub.SelectSingleNode("dcscor:value", $ns)
+ if (-not $subName) { continue }
+ $subRaw = Read-AppearanceValueNode $subVal
+ $subUse = Get-Text $sub "dcscor:use"
+ $subEntry = [ordered]@{ value = $subRaw }
+ if ($subUse -eq 'false') { $subEntry['use'] = $false }
+ $nestedItems[$subName] = $subEntry
+ }
+ $valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line')
+ if ($valIsLine) {
+ if ($useV -eq 'false') { $rawVal['use'] = $false }
+ if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems }
+ $dict[$pName] = $rawVal
+ } elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) {
+ $wrap = [ordered]@{ value = $rawVal }
+ if ($useV -eq 'false') { $wrap['use'] = $false }
+ if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems }
+ $dict[$pName] = $wrap
+ } else {
+ $dict[$pName] = $rawVal
+ }
+ }
+ return $dict
+}
+
+# Build conditionalAppearance array.
+function Build-ConditionalAppearance {
+ param($caNode, [string]$loc)
+ if (-not $caNode) { return @() }
+ $out = @()
+ $i = 0
+ foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) {
+ $entry = [ordered]@{}
+ $scopeNode = $it.SelectSingleNode("dcsset:scope", $ns)
+ if ($scopeNode -and $scopeNode.HasChildNodes) {
+ [Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)")
+ }
+ $selNode = $it.SelectSingleNode("dcsset:selection", $ns)
+ if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
+ $entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection"
+ }
+ $filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
+ if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
+ $f = @()
+ foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) {
+ $bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter")
+ if ($null -ne $bi) { $f += $bi }
+ }
+ $entry['filter'] = $f
+ }
+ $appNode = $it.SelectSingleNode("dcsset:appearance", $ns)
+ $ap = Get-SettingsAppearance $appNode
+ if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap }
+ $presNode = $it.SelectSingleNode("dcsset:presentation", $ns)
+ if ($presNode) {
+ $pres = Get-MLText $presNode
+ if (-not $pres) { $pres = $presNode.InnerText }
+ if ($pres) { $entry['presentation'] = $pres }
+ }
+ $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
+ if ($vmN) { $entry['viewMode'] = $vmN.InnerText }
+ $usid = Get-Text $it "dcsset:userSettingID"
+ if ($usid) { $entry['userSettingID'] = 'auto' }
+ $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
+ if ($uspN) {
+ $usp = Get-PresText $uspN
+ if ($usp) { $entry['userSettingPresentation'] = $usp }
+ }
+ $useV = Get-Text $it "dcsset:use"
+ if ($useV -eq 'false') { $entry['use'] = $false }
+ $useInDontUse = @()
+ foreach ($ch in $it.ChildNodes) {
+ if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue }
+ if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') {
+ $shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1)
+ $useInDontUse += $shortName
+ }
+ }
+ if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse }
+ $out += $entry
+ $i++
+ }
+ return ,$out
+}
+
# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора).
# Вызывается один раз для любого элемента. Height тут — пиксельная высота
# (); Table хранит высоту в строках () и ловит её сам.
@@ -614,6 +1141,58 @@ if ($attrsNode) {
}
if ($cols.Count -gt 0) { $ao['columns'] = @($cols) }
}
+ # Settings динамического списка
+ $setNode = $a.SelectSingleNode("lf:Settings", $ns)
+ if ($setNode) {
+ $so = [ordered]@{}
+ $mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt }
+ $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns)
+ if ($qtNode -and $qtNode.InnerText) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" }
+ # DynamicDataRead: дефолт true → эмитим только false
+ if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false }
+ # Явные поля набора (редко, ~4.5%) — захват только при наличии Field
+ $fieldNodes = @($setNode.SelectNodes("lf:Field", $ns))
+ if ($fieldNodes.Count -gt 0) {
+ $fields = New-Object System.Collections.ArrayList
+ foreach ($fn in $fieldNodes) {
+ $fo = [ordered]@{}
+ $fld = Get-Child $fn 'field'
+ $dp = Get-Child $fn 'dataPath'
+ if ($fld) { $fo['field'] = $fld }
+ if ($dp -and $dp -ne $fld) { $fo['dataPath'] = $dp }
+ $ftn = $fn.SelectSingleNode("dcssch:title", $ns)
+ if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } }
+ [void]$fields.Add($fo)
+ }
+ $so['fields'] = @($fields)
+ }
+ # ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор
+ # регенерит каноничный скелет. Захватываем только контейнеры с реальными
+ # dcsset:item (filter/order/conditionalAppearance) в формат компилятора.
+ $lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns)
+ if ($lsNode) {
+ $fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns)
+ if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) {
+ $flt = @()
+ foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) {
+ $bi = (Build-FilterItem -itemNode $fc -loc "settings/filter")
+ if ($null -ne $bi) { $flt += $bi }
+ }
+ if ($flt.Count -gt 0) { $so['filter'] = @($flt) }
+ }
+ $oNode = $lsNode.SelectSingleNode("dcsset:order", $ns)
+ if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) {
+ $ord = Build-Order -ordNode $oNode -loc "settings/order"
+ if (@($ord).Count -gt 0) { $so['order'] = @($ord) }
+ }
+ $caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns)
+ if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) {
+ $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance"
+ if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) }
+ }
+ }
+ if ($so.Count -gt 0) { $ao['settings'] = $so }
+ }
[void]$attrs.Add($ao)
}
if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) }
@@ -654,6 +1233,7 @@ if ($cmdsNode) {
$json = ConvertTo-CompactJson -obj $dsl
if ($OutputPath) {
[System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false)))
+ Save-QueryFiles
Write-Host "form-decompile: $OutputPath"
} else {
Write-Output $json
diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md
index 5edd8de4..899a46a5 100644
--- a/docs/form-dsl-spec.md
+++ b/docs/form-dsl-spec.md
@@ -500,6 +500,56 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs
| `savedData` | bool | Сохраняемые данные |
| `fillChecking` | string | `Show`, `DontShow` |
| `columns` | array | Колонки для ValueTable/ValueTree |
+| `settings` | object | Настройки динамического списка (только `type: "DynamicList"`) |
+
+### settings — динамический список
+
+Для реквизита `type: "DynamicList"` объект `settings` описывает источник данных и настройки компоновщика (`ListSettings`).
+
+```json
+{ "name": "Список", "type": "DynamicList", "main": true,
+ "settings": {
+ "mainTable": "Catalog.Контрагенты",
+ "query": "@Список.sql",
+ "dynamicDataRead": false,
+ "fields": [ { "field": "Отложен", "title": "Отложен" } ]
+ } }
+```
+
+| Ключ | Тип | Описание |
+|------|-----|----------|
+| `mainTable` | string | Основная таблица. Принимает рус-имена метаданных (`Справочник.X` → `Catalog.X`) |
+| `query` | string | Текст запроса (`ManualQuery=true`). Поддерживает `@file.sql` (путь относительно JSON) |
+| `dynamicDataRead` | bool | Динамическое считывание. **Умолчание `true`** — указывать только для отключения (`false`) |
+| `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title? }` — для переопределения заголовка. Обычно поля выводятся из запроса автоматически |
+| `order` | array | Сортировка списка (см. ниже) |
+| `filter` | array | Отбор списка (грамматика как в СКД) |
+| `conditionalAppearance` | array | Условное оформление списка (грамматика как в СКД) |
+
+`ManualQuery` выводится из наличия `query` — отдельным ключом не задаётся.
+
+Пустой блок настроек компоновщика (`ListSettings`) генерируется автоматически (каноничный скелет платформы); указывать ничего не нужно.
+
+#### order / filter / conditionalAppearance
+
+Грамматика этих ключей идентична настройкам СКД — см. [skd-dsl-spec.md](skd-dsl-spec.md) (разделы filter / order / conditionalAppearance). Кратко:
+
+```json
+"settings": {
+ "mainTable": "Catalog.Контрагенты",
+ "order": [ "Дата desc", "Наименование", "Auto" ],
+ "filter": [ "Организация = _ @off @user", "Сумма > 1000" ],
+ "conditionalAppearance": [
+ { "filter": ["Просрочено = true"], "appearance": { "ЦветТекста": "web:Red" } }
+ ]
+}
+```
+
+- **order** — строка `"Поле"` (asc) / `"Поле desc"` (синонимы `убыв`/`desc`, `возр`/`asc`) / `"Auto"`, либо объект `{ field, direction?, use?, viewMode? }`.
+- **filter** — shorthand `"Поле оператор значение @флаги"` (`@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`; `_` = пусто) или объект `{ field, op, value?, use?, userSettingID? }` или группа `{ group: "And"|"Or"|"Not", items: [...] }`.
+- **conditionalAppearance** — объект `{ selection?, filter?, appearance?, presentation?, viewMode?, userSettingID?, use? }`. `appearance` — словарь «параметр: значение» платформы (`ЦветТекста`, `ЦветФона`, `Шрифт` и т.п.).
+
+`userSettingID: "auto"` → платформа сгенерирует идентификатор пользовательской настройки. Пустые контейнеры (без правил) эмитируются автоматически.
---
diff --git a/tests/skills/cases/form-compile-from-object/snapshots/accumreg-list-simple/AccumulationRegisters/ДенежныеСредства/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile-from-object/snapshots/accumreg-list-simple/AccumulationRegisters/ДенежныеСредства/Forms/ФормаСписка/Ext/Form.xml
index ffdc59aa..1d09406c 100644
--- a/tests/skills/cases/form-compile-from-object/snapshots/accumreg-list-simple/AccumulationRegisters/ДенежныеСредства/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile-from-object/snapshots/accumreg-list-simple/AccumulationRegisters/ДенежныеСредства/Forms/ФормаСписка/Ext/Form.xml
@@ -92,9 +92,25 @@
true
- AccumulationRegister.ДенежныеСредства
false
true
+ AccumulationRegister.ДенежныеСредства
+
+
+ Normal
+ UUID-001
+
+
+ Normal
+ UUID-002
+
+
+ Normal
+ UUID-003
+
+ Normal
+ UUID-004
+
diff --git a/tests/skills/cases/form-compile-from-object/snapshots/catalog-list-simple/Catalogs/Валюты/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile-from-object/snapshots/catalog-list-simple/Catalogs/Валюты/Forms/ФормаСписка/Ext/Form.xml
index 65f871bb..890e2bae 100644
--- a/tests/skills/cases/form-compile-from-object/snapshots/catalog-list-simple/Catalogs/Валюты/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile-from-object/snapshots/catalog-list-simple/Catalogs/Валюты/Forms/ФормаСписка/Ext/Form.xml
@@ -105,9 +105,25 @@
true
- Catalog.Валюты
false
true
+ Catalog.Валюты
+
+
+ Normal
+ UUID-001
+
+
+ Normal
+ UUID-002
+
+
+ Normal
+ UUID-003
+
+ Normal
+ UUID-004
+
diff --git a/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-list-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-list-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСписка/Ext/Form.xml
index 8845e471..a3f78aed 100644
--- a/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-list-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile-from-object/snapshots/chartofaccounts-list-simple/ChartsOfAccounts/Хозрасчетный/Forms/ФормаСписка/Ext/Form.xml
@@ -73,9 +73,25 @@
true
- ChartOfAccounts.Хозрасчетный
false
true
+ ChartOfAccounts.Хозрасчетный
+
+
+ Normal
+ UUID-001
+
+
+ Normal
+ UUID-002
+
+
+ Normal
+ UUID-003
+
+ Normal
+ UUID-004
+
diff --git a/tests/skills/cases/form-compile-from-object/snapshots/document-list-medium/Documents/АктВыполненныхВнутреннихРабот/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile-from-object/snapshots/document-list-medium/Documents/АктВыполненныхВнутреннихРабот/Forms/ФормаСписка/Ext/Form.xml
index 1622e9fe..85965504 100644
--- a/tests/skills/cases/form-compile-from-object/snapshots/document-list-medium/Documents/АктВыполненныхВнутреннихРабот/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile-from-object/snapshots/document-list-medium/Documents/АктВыполненныхВнутреннихРабот/Forms/ФормаСписка/Ext/Form.xml
@@ -125,9 +125,25 @@
true
- Document.АктВыполненныхВнутреннихРабот
false
true
+ Document.АктВыполненныхВнутреннихРабот
+
+
+ Normal
+ UUID-001
+
+
+ Normal
+ UUID-002
+
+
+ Normal
+ UUID-003
+
+ Normal
+ UUID-004
+
diff --git a/tests/skills/cases/form-compile-from-object/snapshots/inforeg-list-periodic/InformationRegisters/ЦеныНоменклатуры/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile-from-object/snapshots/inforeg-list-periodic/InformationRegisters/ЦеныНоменклатуры/Forms/ФормаСписка/Ext/Form.xml
index cbf8a2ce..0f2dac91 100644
--- a/tests/skills/cases/form-compile-from-object/snapshots/inforeg-list-periodic/InformationRegisters/ЦеныНоменклатуры/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile-from-object/snapshots/inforeg-list-periodic/InformationRegisters/ЦеныНоменклатуры/Forms/ФормаСписка/Ext/Form.xml
@@ -82,9 +82,25 @@
true
- InformationRegister.ЦеныНоменклатуры
false
true
+ InformationRegister.ЦеныНоменклатуры
+
+
+ Normal
+ UUID-001
+
+
+ Normal
+ UUID-002
+
+
+ Normal
+ UUID-003
+
+ Normal
+ UUID-004
+
diff --git a/tests/skills/cases/form-compile/dynamic-list-form.json b/tests/skills/cases/form-compile/dynamic-list-form.json
index 7ada47e1..5d80f9e1 100644
--- a/tests/skills/cases/form-compile/dynamic-list-form.json
+++ b/tests/skills/cases/form-compile/dynamic-list-form.json
@@ -16,7 +16,12 @@
"input": {
"title": "Товары",
"attributes": [
- { "name": "Список", "type": "DynamicList", "settings": { "mainTable": "Catalog.Товары", "dynamicDataRead": true } }
+ { "name": "Список", "type": "DynamicList", "settings": {
+ "mainTable": "Catalog.Товары", "dynamicDataRead": true,
+ "order": [ "Description", "Code desc" ],
+ "filter": [ "Артикул = _ @off @user" ],
+ "conditionalAppearance": [ { "filter": ["Артикул = _"], "appearance": { "ЦветТекста": "web:Red" } } ]
+ } }
],
"elements": [
{ "table": "Список", "path": "Список", "columns": [
diff --git a/tests/skills/cases/form-compile/snapshots/auto-cmd-bar/Catalogs/Бригады/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/auto-cmd-bar/Catalogs/Бригады/Forms/ФормаСписка/Ext/Form.xml
index 45ab3419..beec2dda 100644
--- a/tests/skills/cases/form-compile/snapshots/auto-cmd-bar/Catalogs/Бригады/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile/snapshots/auto-cmd-bar/Catalogs/Бригады/Forms/ФормаСписка/Ext/Form.xml
@@ -66,9 +66,25 @@
true
- Catalog.Бригады
false
true
+ Catalog.Бригады
+
+
+ Normal
+ UUID-001
+
+
+ Normal
+ UUID-002
+
+
+ Normal
+ UUID-003
+
+ Normal
+ UUID-004
+
diff --git a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml
index 9979f860..1f7522a6 100644
--- a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml
+++ b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml
@@ -62,9 +62,54 @@
true
- Catalog.Товары
false
true
+ Catalog.Товары
+
+
+
+ false
+ Артикул
+ Equal
+ UUID-001
+
+ Normal
+ UUID-002
+
+
+
+ Description
+ Asc
+
+
+ Code
+ Desc
+
+ Normal
+ UUID-003
+
+
+
+
+
+
+ Артикул
+ Equal
+
+
+
+
+ ЦветТекста
+ web:Red
+
+
+
+ Normal
+ UUID-004
+
+ Normal
+ UUID-005
+