diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 44f1c497..219dfaba 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,3427 +1,3448 @@ -# form-compile v1.24 — Compile 1C managed form from JSON or object metadata -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [string]$JsonPath, - - [Parameter(Mandatory)] - [string]$OutputPath, - - [switch]$FromObject, - [string]$ObjectPath, - [string]$Purpose, - [string]$Preset = "erp-standard", - [string]$EmitDsl -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# ═══════════════════════════════════════════════════════════════════════════ -# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation -# ═══════════════════════════════════════════════════════════════════════════ - -function Parse-ObjectMeta([string]$ObjectPath) { - $doc = New-Object System.Xml.XmlDocument - $doc.PreserveWhitespace = $false - $doc.Load($ObjectPath) - - $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) - $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") - $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") - - # Detect object type from root child - $metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns) - if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 } - $typeNode = $metaRoot.FirstChild - $objType = $typeNode.LocalName # "Document", "Catalog", etc. - - $propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) - $childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) - - # Name - $objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText - - # Synonym (Russian) - $synonym = $objName - $synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - if ($synNode) { $synonym = $synNode.InnerText } - - # Helper: extract type string from md:Type - $extractType = { - param($typeParent) - if (-not $typeParent) { return "string" } - $types = @() - foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) { - $types += $t.InnerText - } - if ($types.Count -eq 0) { return "string" } - return ($types -join " | ") - } - - # Helper: check if type is a reference - $isRefType = { - param([string]$t) - return ($t -match 'Ref\.' -or $t -match 'ссылка\.') - } - - # Helper: extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag) - $extractFields = { - param($parentNode, [string]$tagName) - $result = @() - if (-not $parentNode) { return $result } - foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) { - $fp = $fieldNode.SelectSingleNode("md:Properties", $ns) - $fName = $fp.SelectSingleNode("md:Name", $ns).InnerText - $fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - $fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName } - $fTypeNode = $fp.SelectSingleNode("md:Type", $ns) - $fType = & $extractType $fTypeNode - $result += @{ - Name = $fName - Synonym = $fSyn - Type = $fType - IsRef = (& $isRefType $fType) - } - } - return $result - } - - # Attributes - $attributes = @(& $extractFields $childObjs "Attribute") - - # Tabular sections - $tabularSections = @() - if ($childObjs) { - foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) { - $tsp = $tsNode.SelectSingleNode("md:Properties", $ns) - $tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText - $tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - $tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName } - $tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns) - $tsCols = @(& $extractFields $tsCo "Attribute") - $tabularSections += @{ - Name = $tsName - Synonym = $tsSyn - Columns = $tsCols - } - } - } - - $meta = @{ - Type = $objType - Name = $objName - Synonym = $synonym - Attributes = $attributes - TabularSections = $tabularSections - } - - # Type-specific properties - switch ($objType) { - "Document" { - $ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns) - $meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" } - } - "Catalog" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) - $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - # Owners - $owners = @() - foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { - $owners += $ow.InnerText - } - $meta.Owners = $owners - } - "InformationRegister" { - $meta.Dimensions = @(& $extractFields $childObjs "Dimension") - $meta.Resources = @(& $extractFields $childObjs "Resource") - $prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) - $meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" } - $wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns) - $meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" } - } - "AccumulationRegister" { - $meta.Dimensions = @(& $extractFields $childObjs "Dimension") - $meta.Resources = @(& $extractFields $childObjs "Resource") - $rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns) - $meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" } - } - "ChartOfCharacteristicTypes" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) - $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - $owners = @() - foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { - $owners += $ow.InnerText - } - $meta.Owners = $owners - $meta.HasValueType = $true - } - "ExchangePlan" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $meta.Hierarchical = $false - $meta.HierarchyType = $null - $meta.Owners = @() - } - "ChartOfAccounts" { - $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) - $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } - $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) - $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } - $meta.Hierarchical = $true - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - $meta.Owners = @() - $maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) - $meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 } - $meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag") - $meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag") - } - } - - return $meta -} - -function Load-Preset([string]$PresetName, [string]$ScriptDir) { - # Hardcoded defaults (ERP-oriented) - $defaults = @{ - "document.item" = @{ - header = @{ position = "insidePage"; layout = "2col"; distribute = "even"; dateTitle = "от" } - footer = @{ fields = @("Комментарий"); position = "insidePage" } - tabularSections = @{ container = "pages"; exclude = @("ДополнительныеРеквизиты"); lineNumber = $true } - additional = @{ position = "page"; layout = "2col"; bspGroup = $true } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - commandBar = "auto" - properties = @{ autoTitle = $false } - } - "document.list" = @{ - columns = "all"; columnType = "labelField"; hiddenRef = $true - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "document.choice" = @{ - basedOn = "document.list" - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "catalog.item" = @{ - header = @{ layout = "1col"; distribute = "left" } - codeDescription = @{ layout = "horizontal"; order = "descriptionFirst" } - parent = @{ title = "Входит в группу"; position = "afterCodeDescription" } - owner = @{ readOnly = $true; position = "first" } - tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); lineNumber = $true } - footer = @{ fields = @(); position = "none" } - additional = @{ position = "none"; bspGroup = $true } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - commandBar = "auto" - properties = @{} - } - "catalog.folder" = @{ - parent = @{ title = "Входит в группу" } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "catalog.list" = @{ - columns = "all"; columnType = "labelField"; hiddenRef = $true - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "catalog.choice" = @{ - basedOn = "catalog.list"; choiceMode = $true - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - # ─── Register defaults ─── - "informationRegister.record" = @{ - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "informationRegister.list" = @{ - columns = "all"; columnType = "labelField" - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "accumulationRegister.list" = @{ - columns = "all"; columnType = "labelField" - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - # ─── Catalog-like type defaults ─── - "chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" } - "chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" } - "chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" } - "chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" } - "exchangePlan.item" = @{ basedOn = "catalog.item" } - "exchangePlan.list" = @{ basedOn = "catalog.list" } - "exchangePlan.choice" = @{ basedOn = "catalog.choice" } - # ─── ChartOfAccounts defaults ─── - "chartOfAccounts.item" = @{ - parent = @{ title = "Подчинен счету" } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - properties = @{} - } - "chartOfAccounts.folder" = @{ - parent = @{ title = "Подчинен счету" } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "chartOfAccounts.list" = @{ basedOn = "catalog.list" } - "chartOfAccounts.choice" = @{ basedOn = "catalog.choice" } - } - - # Deep merge helper - $deepMerge = { - param($base, $overlay) - if (-not $overlay) { return $base } - if (-not $base) { return $overlay } - $result = @{} - foreach ($k in $base.Keys) { $result[$k] = $base[$k] } - foreach ($k in $overlay.Keys) { - if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { - $result[$k] = & $deepMerge $result[$k] $overlay[$k] - } else { - $result[$k] = $overlay[$k] - } - } - return $result - } - - # Try built-in preset - $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 - # Convert PSCustomObject to hashtable recursively - $toHash = { - param($obj) - if ($obj -is [System.Management.Automation.PSCustomObject]) { - $h = @{} - foreach ($p in $obj.PSObject.Properties) { - $h[$p.Name] = & $toHash $p.Value - } - return $h - } - if ($obj -is [System.Object[]]) { - return @($obj | ForEach-Object { & $toHash $_ }) - } - return $obj - } - $presetHash = & $toHash $presetJson - foreach ($k in @($presetHash.Keys)) { - $defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k] - } - } - - # Try project-level preset (scan up from output path) - $scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved) - 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 - $projHash = & $toHash $projJson - foreach ($k in @($projHash.Keys)) { - $defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k] - } - break - } - $parentDir = Split-Path $scanDir -Parent - if ($parentDir -eq $scanDir) { break } - $scanDir = $parentDir - } - - # Resolve basedOn references - foreach ($k in @($defaults.Keys)) { - $sect = $defaults[$k] - if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) { - $baseName = $sect["basedOn"] - if ($defaults.ContainsKey($baseName)) { - $merged = & $deepMerge $defaults[$baseName] $sect - $merged.Remove("basedOn") - $defaults[$k] = $merged - } - } - } - - return $defaults -} - -# --- Helper: build a field element DSL entry --- -# Non-displayable types — cannot be bound to form elements -$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения') - -function Test-DisplayableType([string]$typeStr) { - foreach ($nd in $script:nonDisplayableTypes) { - if ($typeStr -match [regex]::Escape($nd)) { return $false } - } - return $true -} - -function New-FieldElement { - param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps) - - $isRef = ($attrType -match 'Ref\.') - $isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean') - - # Determine element type - $elType = "input" - if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") { - $elType = "check" - } - - $el = [ordered]@{ $elType = $attrName; path = $dataPath } - - # Apply ref defaults - if ($isRef -and $fieldDefaults -and $fieldDefaults.ref) { - if ($fieldDefaults.ref.choiceButton -eq $true) { $el["choiceButton"] = $true } - } - - # Extra props - if ($extraProps) { - foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] } - } - - return $el -} - -# --- Catalog DSL generators --- -function Generate-CatalogDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - - $purposeKey = "catalog.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } - $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } - - switch ($purpose) { - "Folder" { return Generate-CatalogFolderDSL $meta $p } - "List" { return Generate-CatalogListDSL $meta $p } - "Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData } - "Item" { return Generate-CatalogItemDSL $meta $p $fd } - } -} - -function Generate-CatalogFolderDSL($meta, [hashtable]$p) { - $elements = @() - # Code (if CodeLength > 0) - if ($meta.CodeLength -gt 0) { - $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - # Description - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - # Parent - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } - $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } - if ($parentTitle) { $parentEl["title"] = $parentTitle } - $elements += $parentEl - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - $formProps = [ordered]@{ useForFoldersAndItems = "Folders" } - foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = $elements - attributes = @( - [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } - ) - } -} - -function Generate-CatalogListDSL($meta, [hashtable]$p) { - # Columns - $columns = @() - # Description always first - $columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" } - # Code if present - if ($meta.CodeLength -gt 0) { - $columns += [ordered]@{ labelField = "Код"; path = "Список.Code" } - } - # Custom attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } - } - # Hidden ref - if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { - $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - # Hierarchical properties - if ($meta.Hierarchical) { - $tableEl["initialTreeView"] = "ExpandTopLevel" - $tableEl["enableStartDrag"] = $true - $tableEl["enableDrag"] = $true - } - - $formProps = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = @($tableEl) - attributes = @( - [ordered]@{ - name = "Список"; type = "DynamicList"; main = $true - settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true } - } - ) - } -} - -function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { - # Start from list - $listKey = "catalog.list" - $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } - $dsl = Generate-CatalogListDSL $meta $lp - - # Add choice-specific properties - $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" - if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } - - # Set ChoiceMode on table - $dsl.elements[0]["choiceMode"] = $true - - return $dsl -} - -function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) { - $headerChildren = @() - - # Owner (if subordinate) - if ($meta.Owners -and $meta.Owners.Count -gt 0) { - $ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true } - $headerChildren += $ownerEl - } - - # Code + Description - $cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" } - $cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" } - $hasCode = ($meta.CodeLength -gt 0) - - if ($cdLayout -eq "horizontal" -and $hasCode) { - $cdChildren = @() - $descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" } - $codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" } - if ($cdOrder -eq "descriptionFirst") { - $cdChildren = @($descEl, $codeEl) - } else { - $cdChildren = @($codeEl, $descEl) - } - $headerChildren += [ordered]@{ - group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false - representation = "none"; children = $cdChildren - } - } else { - # Vertical or no code - $headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - if ($hasCode) { - $headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - } - - # Parent (for hierarchical catalogs) - $parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" } - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } - if ($meta.Hierarchical) { - $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } - if ($parentTitle) { $parentEl["title"] = $parentTitle } - if ($parentPos -eq "beforeCodeDescription") { - # Insert before Code/Description (after Owner if present) - $insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 } - $newChildren = @() - for ($i = 0; $i -lt $headerChildren.Count; $i++) { - if ($i -eq $insertIdx) { $newChildren += $parentEl } - $newChildren += $headerChildren[$i] - } - $headerChildren = $newChildren - } else { - # afterCodeDescription (default) - $headerChildren += $parentEl - } - } - - # Custom attributes → header - $footerFieldNames = @() - if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) } - - foreach ($attr in $meta.Attributes) { - if ($footerFieldNames -contains $attr.Name) { continue } - if (-not (Test-DisplayableType $attr.Type)) { continue } - $headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) - } - - # Build root elements - $rootElements = @() - - # ГруппаШапка - $rootElements += [ordered]@{ - group = "vertical"; name = "ГруппаШапка"; showTitle = $false - representation = "none"; children = $headerChildren - } - - # Tabular sections - $tsExclude = @("ДополнительныеРеквизиты", "Представления") - if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } - $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } - $tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" } - - $visibleTS = @() - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $visibleTS += $ts - } - - foreach ($ts in $visibleTS) { - $tsCols = @() - if ($tsLineNumber) { - $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } - } - foreach ($col in $ts.Columns) { - $colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{} - $tsCols += $colEl - } - $tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - $rootElements += $tableEl - } - - # Footer fields - foreach ($fn in $footerFieldNames) { - $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } - if ($fAttr) { - $rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - - # BSP group - $bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } - if ($bspGroup) { - $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } - } - - # Properties - $formProps = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - # UseForFoldersAndItems - if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") { - $formProps["useForFoldersAndItems"] = "Items" - } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = $rootElements - attributes = @( - [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } - ) - } -} - -# --- Document DSL generators --- -function Generate-DocumentDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - - $purposeKey = "document.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } - $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } - - switch ($purpose) { - "List" { return Generate-DocumentListDSL $meta $p } - "Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData } - "Item" { return Generate-DocumentItemDSL $meta $p $fd } - } -} - -function Generate-DocumentListDSL($meta, [hashtable]$p) { - $columns = @() - # Standard columns: Number + Date - $columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" } - $columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" } - # All custom attributes as labelField - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } - } - # Hidden ref - if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { - $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $formProps = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = @($tableEl) - attributes = @( - [ordered]@{ - name = "Список"; type = "DynamicList"; main = $true - settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true } - } - ) - } -} - -function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { - $listKey = "document.list" - $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } - $dsl = Generate-DocumentListDSL $meta $lp - - $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" - if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } - - return $dsl -} - -function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) { - $headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" } - $headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" } - $headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" } - $dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" } - - $footerFields = @() - if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) } - $footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" } - - $addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" } - $addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" } - $addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } - $addLeft = @(); $addRight = @() - if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) } - if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) } - - $headerRight = @() - if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) } - - $tsExclude = @("ДополнительныеРеквизиты") - if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } - $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } - - # Classify attributes - $claimed = @{} - foreach ($fn in $footerFields) { $claimed[$fn] = "footer" } - foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" } - foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" } - foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" } - - $unclaimed = @() - foreach ($attr in $meta.Attributes) { - if (-not $claimed.ContainsKey($attr.Name) -and (Test-DisplayableType $attr.Type)) { $unclaimed += $attr } - } - - # Distribute unclaimed - $leftAttrs = @(); $rightExtraAttrs = @() - switch ($headerDistribute) { - "left" { $leftAttrs = $unclaimed } - "right" { $rightExtraAttrs = $unclaimed } - default { # "even" - $half = [Math]::Ceiling($unclaimed.Count / 2) - for ($i = 0; $i -lt $unclaimed.Count; $i++) { - if ($i -lt $half) { $leftAttrs += $unclaimed[$i] } - else { $rightExtraAttrs += $unclaimed[$i] } - } - } - } - - # Build ГруппаНомерДата - $numDateChildren = @( - [ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 } - [ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle } - ) - $numDateGroup = [ordered]@{ - group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren - } - - # Build left column - $leftChildren = @($numDateGroup) - foreach ($attr in $leftAttrs) { - $leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) - } - - # Build right column - $rightChildren = @() - foreach ($rn in $headerRight) { - $rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn } - if ($rAttr) { - $rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - foreach ($attr in $rightExtraAttrs) { - $rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) - } - - # Header group - $headerGroup = $null - if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) { - $headerGroup = [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren } - [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren } - ) - } - } else { - # 1col or no right items - $allHeaderFields = $leftChildren + $rightChildren - $headerGroup = [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields } - ) - } - } - - # Footer elements - $footerElements = @() - foreach ($fn in $footerFields) { - $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } - if ($fAttr -and (Test-DisplayableType $fAttr.Type)) { - $footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - - # Visible tabular sections - $visibleTS = @() - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $visibleTS += $ts - } - - # Additional page content - $additionalPage = $null - if ($addPos -eq "page") { - $addLeftEls = @(); $addRightEls = @() - foreach ($aln in $addLeft) { - $alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln } - if ($alAttr) { - $addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - foreach ($arn in $addRight) { - $arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn } - if ($arAttr) { - $addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{}) - } - } - $addPageChildren = @() - if ($addLayout -eq "2col") { - $addPageChildren += [ordered]@{ - group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls } - [ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls } - ) - } - } else { - $addPageChildren += @($addLeftEls + $addRightEls) - } - if ($addBspGroup) { - $addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } - } - $additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren } - } - - # Build TS page elements - $tsPages = @() - foreach ($ts in $visibleTS) { - $tsCols = @() - if ($tsLineNumber) { - $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } - } - foreach ($col in $ts.Columns) { - $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}) - } - $tsPages += [ordered]@{ - page = "Группа$($ts.Name)"; title = $ts.Synonym - children = @( - [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - ) - } - } - - # Assemble root elements - $rootElements = @() - - if ($visibleTS.Count -eq 0) { - # Simple form — no Pages - $rootElements += $headerGroup - if ($footerElements.Count -gt 0) { $rootElements += $footerElements } - if ($addBspGroup -and $addPos -ne "none") { - $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } - } - } else { - # Pages form - if ($headerPos -eq "abovePages") { - $rootElements += $headerGroup - $pagesChildren = @() - $pagesChildren += $tsPages - if ($additionalPage) { $pagesChildren += $additionalPage } - $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } - } else { - # insidePage (default) - $osnovnoeChildren = @($headerGroup) - if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) { - $osnovnoeChildren += $footerElements - } - $pagesChildren = @() - $pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren } - $pagesChildren += $tsPages - if ($additionalPage) { $pagesChildren += $additionalPage } - $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } - } - - # Footer below pages - if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) { - $rootElements += $footerElements - } - } - - # Properties - $formProps = [ordered]@{ autoTitle = $false } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $formProps - elements = $rootElements - attributes = @( - [ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true } - ) - } -} - -# ─── InformationRegister ────────────────────────────────────────────────── - -function Generate-InformationRegisterDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "informationRegister.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } - switch ($purpose) { - "Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd } - "List" { return Generate-InformationRegisterListDSL $meta $p } - } -} - -function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) { - $elements = @() - $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" - - # Period first (if periodic) - if ($isPeriodic) { - $elements += [ordered]@{ input = "Период"; path = "Запись.Period" } - } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd) - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd) - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) - } - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = $elements - attributes = @( - @{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-InformationRegisterListDSL($meta, [hashtable]$p) { - $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" - $isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate" - - $columns = @() - # Period - if ($isPeriodic) { - $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } - } - # Recorder/LineNumber for subordinate registers - if ($isRecorderSubordinate) { - $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } - $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } - } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elKey = "labelField" - if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elKey = "labelField" - if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = @($tableEl) - attributes = @( - @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } } - ) - } -} - -# ─── AccumulationRegister ───────────────────────────────────────────────── - -function Generate-AccumulationRegisterDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "accumulationRegister.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - switch ($purpose) { - "List" { return Generate-AccumulationRegisterListDSL $meta $p } - } -} - -function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) { - $columns = @() - # AccumulationRegisters always have Period, Recorder, LineNumber - $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } - $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } - $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elKey = "labelField" - if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elKey = "labelField" - if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = @($tableEl) - attributes = @( - @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } } - ) - } -} - -# ─── ChartOfCharacteristicTypes (delegates to Catalog) ──────────────────── - -function Generate-ChartOfCharacteristicTypesDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - # Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc. - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose - - # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types - $catObjType = "CatalogObject.$($meta.Name)" - $ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)" - $catListType = "Catalog.$($meta.Name)" - $ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)" - - foreach ($a in $dsl.attributes) { - if ($a.type -eq $catObjType) { $a.type = $ccoctObjType } - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { - $a.settings.mainTable = $ccoctListType - } - } - - # For Item forms: inject ValueType field after Code/Description - if ($purpose -eq "Item" -and $dsl.elements) { - $vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" } - $newElements = @() - $inserted = $false - foreach ($el in $dsl.elements) { - $newElements += $el - if (-not $inserted) { - $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } - if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { - $newElements += $vtEl - $inserted = $true - } - } - } - if (-not $inserted) { $newElements += $vtEl } - $dsl.elements = $newElements - } - - return $dsl -} - -# ─── ExchangePlan (delegates to Catalog) ────────────────────────────────── - -function Generate-ExchangePlanDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - # ExchangePlans are not hierarchical and have no Folder form - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose - - # Post-patch: replace Catalog types with ExchangePlan types - $catObjType = "CatalogObject.$($meta.Name)" - $epObjType = "ExchangePlanObject.$($meta.Name)" - $catListType = "Catalog.$($meta.Name)" - $epListType = "ExchangePlan.$($meta.Name)" - - foreach ($a in $dsl.attributes) { - if ($a.type -eq $catObjType) { $a.type = $epObjType } - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { - $a.settings.mainTable = $epListType - } - } - - # For Item forms: inject SentNo, ReceivedNo after Code/Description - if ($purpose -eq "Item" -and $dsl.elements) { - $sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true } - $recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true } - $newElements = @() - $inserted = $false - foreach ($el in $dsl.elements) { - $newElements += $el - if (-not $inserted) { - $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } - if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { - $newElements += $sentEl - $newElements += $recvEl - $inserted = $true - } - } - } - if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl } - $dsl.elements = $newElements - } - - return $dsl -} - -# ─── ChartOfAccounts ────────────────────────────────────────────────────── - -function Generate-ChartOfAccountsDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "chartOfAccounts.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } - switch ($purpose) { - "Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData } - "Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p } - "List" { return Generate-ChartOfAccountsListDSL $meta $presetData } - "Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData } - } -} - -function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) { - $elements = @() - - # Header: Code + Parent - $headerLeftChildren = @() - if ($meta.CodeLength -gt 0) { - $headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - $headerRightChildren = @() - if ($meta.Hierarchical) { - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } - $headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } - } - - if ($headerRightChildren.Count -gt 0) { - $elements += [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren } - [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren } - ) - } - } elseif ($headerLeftChildren.Count -gt 0) { - $elements += $headerLeftChildren - } - - # Description - if ($meta.DescriptionLength -gt 0) { - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - } - - # OffBalance - $elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" } - - # AccountingFlags as checkboxes - if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) { - $flagChildren = @() - foreach ($flag in $meta.AccountingFlags) { - $flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" } - } - $elements += [ordered]@{ - group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета" - children = $flagChildren - } - } - - # ExtDimensionTypes table - if ($meta.MaxExtDimensionCount -gt 0) { - $edCols = @() - $edCols += [ordered]@{ input = "ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } - $edCols += [ordered]@{ check = "ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } - if ($meta.ExtDimensionAccountingFlags) { - foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { - $edCols += [ordered]@{ check = $edFlag.Name; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } - } - } - $elements += [ordered]@{ - table = "ВидыСубконто" - path = "Объект.ExtDimensionTypes" - columns = $edCols - } - } - - # Custom attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) - } - - # Tabular sections - $tsExclude = @("ДополнительныеРеквизиты","Представления") - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $tsCols = @() - foreach ($col in $ts.Columns) { - if (-not (Test-DisplayableType $col.Type)) { continue } - $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd) - } - $elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = $elements - attributes = @( - @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) { - $elements = @() - if ($meta.CodeLength -gt 0) { - $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - if ($meta.DescriptionLength -gt 0) { - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - } - if ($meta.Hierarchical) { - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } - $elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } - } - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - useForFoldersAndItems = "Folders" - properties = $props - elements = $elements - attributes = @( - @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) { - # Delegate to Catalog List and patch types - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List" - foreach ($a in $dsl.attributes) { - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { - $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" - } - } - return $dsl -} - -function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) { - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice" - foreach ($a in $dsl.attributes) { - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { - $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" - } - } - return $dsl -} - -# ═══════════════════════════════════════════════════════════════════════════ -# END OF FROM-OBJECT MODE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════ - -# --- Detect XML format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } -$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved)) - -# --- 0. Path normalization and mode dispatch --- - -# Form name → purpose mapping -$script:formNameToPurpose = @{ - "ФормаДокумента" = "Item" - "ФормаЭлемента" = "Item" - "ФормаСписка" = "List" - "ФормаВыбора" = "Choice" - "ФормаГруппы" = "Folder" - "ФормаЗаписи" = "Record" - "ФормаСчета" = "Item" - "ФормаУзла" = "Item" -} - -if ($FromObject -and $JsonPath) { - Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode." - exit 1 -} -if (-not $FromObject -and -not $JsonPath) { - Write-Error "Either -JsonPath or -FromObject is required." - exit 1 -} - -if ($FromObject) { - # Normalize OutputPath: append /Ext/Form.xml if missing - $outNorm = $OutputPath -replace '[\\/]$', '' - if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') { - if ($outNorm -match '[/\\]Ext$') { - $OutputPath = "$outNorm/Form.xml" - } else { - $OutputPath = "$outNorm/Ext/Form.xml" - } - Write-Host "[resolved] OutputPath -> $OutputPath" - } - - # Resolve object path and purpose from OutputPath convention: - # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml - $outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } - $pathParts = $outAbs -split '[/\\]' - # Find "Forms" segment - $formsIdx = -1 - for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { - if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break } - } - - $resolvedObjectPath = $null - $resolvedPurpose = $null - - if ($formsIdx -ge 2) { - $formName = $pathParts[$formsIdx + 1] - $objectName = $pathParts[$formsIdx - 1] - $typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar - - # Derive purpose from form name - if ($script:formNameToPurpose.ContainsKey($formName)) { - $resolvedPurpose = $script:formNameToPurpose[$formName] - } - - # Derive object XML path - $candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml" - if (Test-Path $candidateObjPath) { - $resolvedObjectPath = $candidateObjPath - } - } - - # Apply: explicit -ObjectPath / -Purpose override resolved values - $fromObjPath = $null - if ($ObjectPath) { - $fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath } - # Append .xml if missing - if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" } - } elseif ($resolvedObjectPath) { - $fromObjPath = $resolvedObjectPath - Write-Host "[resolved] ObjectPath -> $fromObjPath" - } else { - Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly." - exit 1 - } - - if (-not (Test-Path $fromObjPath)) { - Write-Error "Object file not found: $fromObjPath" - exit 1 - } - - $effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" } - if ($resolvedPurpose -and -not $Purpose) { - Write-Host "[resolved] Purpose -> $effectivePurpose" - } - - $meta = Parse-ObjectMeta $fromObjPath - Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)" - - $presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot - - $supportedPurposes = switch ($meta.Type) { - "Document" { @("Item","List","Choice") } - "Catalog" { @("Item","Folder","List","Choice") } - "InformationRegister" { @("Record","List") } - "AccumulationRegister" { @("List") } - "ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") } - "ExchangePlan" { @("Item","List","Choice") } - "ChartOfAccounts" { @("Item","Folder","List","Choice") } - default { @() } - } - if ($supportedPurposes.Count -eq 0) { - Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts." - exit 1 - } - if ($supportedPurposes -notcontains $effectivePurpose) { - Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')" - exit 1 - } - - # Generate DSL - $dsl = switch ($meta.Type) { - "Document" { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "Catalog" { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ChartOfAccounts" { Generate-ChartOfAccountsDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - } - - # Emit DSL if requested - if ($EmitDsl) { - $dslJson = $dsl | ConvertTo-Json -Depth 20 - $dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl } - $enc = New-Object System.Text.UTF8Encoding($true) - [System.IO.File]::WriteAllText($dslPath, $dslJson, $enc) - Write-Host "[from-object] DSL saved: $dslPath" - } - - # Feed DSL into existing compiler - $dslJson = $dsl | ConvertTo-Json -Depth 20 - $def = $dslJson | ConvertFrom-Json -} else { - # --- 1. Load and validate JSON (original mode) --- - - if (-not (Test-Path $JsonPath)) { - Write-Error "File not found: $JsonPath" - exit 1 - } - - $json = Get-Content -Raw -Encoding UTF8 $JsonPath - $def = $json | ConvertFrom-Json -} - -# --- 2. ID allocator --- - -$script:nextId = 1 -function New-Id { - $id = $script:nextId - $script:nextId++ - return $id -} - -# --- 3. XML helper --- - -$script:xml = New-Object System.Text.StringBuilder 8192 - -function X { - param([string]$text) - $script:xml.AppendLine($text) | Out-Null -} - -function Esc-Xml { - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') -} - -# --- 4. Multilang helper --- - -function Emit-MLText { - param([string]$tag, [string]$text, [string]$indent) - X "$indent<$tag>" - X "$indent`t" - X "$indent`t`tru" - X "$indent`t`t$(Esc-Xml $text)" - X "$indent`t" - X "$indent" -} - -# --- 5. Type emitter --- - -$script:formTypeSynonyms = New-Object System.Collections.Hashtable -$script:formTypeSynonyms["строка"] = "string" -$script:formTypeSynonyms["число"] = "decimal" -$script:formTypeSynonyms["булево"] = "boolean" -$script:formTypeSynonyms["дата"] = "date" -$script:formTypeSynonyms["датавремя"]= "dateTime" -$script:formTypeSynonyms["number"] = "decimal" -$script:formTypeSynonyms["bool"] = "boolean" -$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" -$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" -$script:formTypeSynonyms["документссылка"] = "DocumentRef" -$script:formTypeSynonyms["документобъект"] = "DocumentObject" -$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" -$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" -$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" -$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" -$script:formTypeSynonyms["задачассылка"] = "TaskRef" -$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" - -# Known invalid types (runtime/UI types that don't exist in XDTO schema) -$script:knownInvalidTypes = @{ - "FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)" - "FormDataCollection" = "Runtime type. Use ValueTable" - "FormDataTree" = "Runtime type. Use ValueTree" - "FormDataTreeItem" = "Runtime type, not valid in XML" - "FormDataCollectionItem"= "Runtime type, not valid in XML" - "FormGroup" = "UI element type, not a data type" - "FormField" = "UI element type, not a data type" - "FormButton" = "UI element type, not a data type" - "FormDecoration" = "UI element type, not a data type" - "FormTable" = "UI element type, not a data type" -} - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) - if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $base = $Matches[1].Trim(); $params = $Matches[2] - $r = $script:formTypeSynonyms[$base.ToLower()] - if ($r) { return "$r($params)" } - return $typeStr - } - if ($typeStr.Contains('.')) { - $i = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) - $r = $script:formTypeSynonyms[$prefix.ToLower()] - if ($r) { return "$r$suffix" } - return $typeStr - } - $r = $script:formTypeSynonyms[$typeStr.ToLower()] - if ($r) { return $r } - return $typeStr -} - -function Emit-Type { - param($typeStr, [string]$indent) - - if (-not $typeStr) { - X "$indent" - return - } - - $typeString = "$typeStr" - - # Composite type: "Type1 | Type2" or "Type1 + Type2" - $parts = $typeString -split '\s*[|+]\s*' - - X "$indent" - foreach ($part in $parts) { - $part = $part.Trim() - Emit-SingleType -typeStr $part -indent "$indent`t" - } - X "$indent" -} - -function Emit-SingleType { - param([string]$typeStr, [string]$indent) - - $typeStr = Resolve-TypeStr $typeStr - - # boolean - if ($typeStr -eq "boolean") { - X "$indentxs:boolean" - return - } - - # string or string(N) - if ($typeStr -match '^string(\((\d+)\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "0" } - X "$indentxs:string" - X "$indent" - X "$indent`t$len" - X "$indent`tVariable" - X "$indent" - return - } - - # decimal(D,F) or decimal(D,F,nonneg) - if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1] - $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } - X "$indentxs:decimal" - X "$indent" - X "$indent`t$digits" - X "$indent`t$fraction" - X "$indent`t$sign" - X "$indent" - return - } - - # date / dateTime / time - if ($typeStr -match '^(date|dateTime|time)$') { - $fractions = switch ($typeStr) { - "date" { "Date" } - "dateTime" { "DateTime" } - "time" { "Time" } - } - X "$indentxs:dateTime" - X "$indent" - X "$indent`t$fractions" - X "$indent" - return - } - - # ValueTable, ValueTree, ValueList, etc. - $v8Types = @{ - "ValueTable" = "v8:ValueTable" - "ValueTree" = "v8:ValueTree" - "ValueList" = "v8:ValueListType" - "TypeDescription" = "v8:TypeDescription" - "Universal" = "v8:Universal" - "FixedArray" = "v8:FixedArray" - "FixedStructure" = "v8:FixedStructure" - } - if ($v8Types.ContainsKey($typeStr)) { - X "$indent$($v8Types[$typeStr])" - return - } - - # UI types - $uiTypes = @{ - "FormattedString" = "v8ui:FormattedString" - "Picture" = "v8ui:Picture" - "Color" = "v8ui:Color" - "Font" = "v8ui:Font" - } - if ($uiTypes.ContainsKey($typeStr)) { - X "$indent$($uiTypes[$typeStr])" - return - } - - # DCS types - if ($typeStr -match '^DataComposition') { - $dcsMap = @{ - "DataCompositionSettings" = "dcsset:DataCompositionSettings" - "DataCompositionSchema" = "dcssch:DataCompositionSchema" - "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" - } - if ($dcsMap.ContainsKey($typeStr)) { - X "$indent$($dcsMap[$typeStr])" - return - } - } - - # DynamicList - if ($typeStr -eq "DynamicList") { - X "$indentcfg:DynamicList" - return - } - - # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) - if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') { - X "$indentcfg:$typeStr" - return - } - - # Fallback with validation - if ($script:knownInvalidTypes.ContainsKey($typeStr)) { - throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" - } - if ($typeStr.Contains('.')) { - X "$indentcfg:$typeStr" - } else { - Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" - X "$indent$typeStr" - } -} - -# --- 6. Event handler name generator --- - -$script:eventSuffixMap = @{ - "OnChange" = "ПриИзменении" - "StartChoice" = "НачалоВыбора" - "ChoiceProcessing" = "ОбработкаВыбора" - "AutoComplete" = "АвтоПодбор" - "Clearing" = "Очистка" - "Opening" = "Открытие" - "Click" = "Нажатие" - "OnActivateRow" = "ПриАктивизацииСтроки" - "BeforeAddRow" = "ПередНачаломДобавления" - "BeforeDeleteRow" = "ПередУдалением" - "BeforeRowChange" = "ПередНачаломИзменения" - "OnStartEdit" = "ПриНачалеРедактирования" - "OnEndEdit" = "ПриОкончанииРедактирования" - "Selection" = "ВыборСтроки" - "OnCurrentPageChange" = "ПриСменеСтраницы" - "TextEditEnd" = "ОкончаниеВводаТекста" - "URLProcessing" = "ОбработкаНавигационнойСсылки" - "DragStart" = "НачалоПеретаскивания" - "Drag" = "Перетаскивание" - "DragCheck" = "ПроверкаПеретаскивания" - "Drop" = "Помещение" - "AfterDeleteRow" = "ПослеУдаления" -} - -function Get-HandlerName { - param([string]$elementName, [string]$eventName) - $suffix = $script:eventSuffixMap[$eventName] - if ($suffix) { - return "$elementName$suffix" - } - return "$elementName$eventName" -} - -# --- 7. Element emitters --- - -function Get-ElementName { - param($el, [string]$typeKey) - if ($el.name) { return "$($el.name)" } - return "$($el.$typeKey)" -} - -$script:knownEvents = @{ - "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") - "check" = @("OnChange") - "radio" = @("OnChange") - "label" = @("Click","URLProcessing") - "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") - "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") - "pages" = @("OnCurrentPageChange") - "page" = @("OnCurrentPageChange") - "button" = @("Click") - "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") - "calendar" = @("OnChange","OnActivate") - "picture" = @("Click") - "cmdBar" = @() - "popup" = @() - "group" = @() -} -$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") - -function Emit-Events { - param($el, [string]$elementName, [string]$indent, [string]$typeKey) - - if (-not $el.on) { return } - - # Validate event names - if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { - $allowed = $script:knownEvents[$typeKey] - foreach ($evt in $el.on) { - if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") { - Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')" - } - } - } - - X "$indent" - foreach ($evt in $el.on) { - $evtName = "$evt" - $handler = if ($el.handlers -and $el.handlers.$evtName) { - "$($el.handlers.$evtName)" - } else { - Get-HandlerName -elementName $elementName -eventName $evtName - } - X "$indent`t$handler" - } - X "$indent" -} - -function Emit-Companion { - param([string]$tag, [string]$name, [string]$indent) - $id = New-Id - X "$indent<$tag name=`"$name`" id=`"$id`"/>" -} - -function Emit-Element { - param($el, [string]$indent, [bool]$inCmdBar = $false) - - # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). - # Maps any synonym to canonical short DSL key. - $synonyms = @{ - "commandBar" = "cmdBar" - "autoCommandBar" = "autoCmdBar" - "КоманднаяПанель" = "cmdBar" - "InputField" = "input" - "ПолеВвода" = "input" - "CheckBoxField" = "check" - "ПолеФлажка" = "check" - "RadioButtonField" = "radio" - "ПолеПереключателя" = "radio" - "radioButton" = "radio" - "PictureField" = "picField" - "ПолеКартинки" = "picField" - "LabelField" = "labelField" - "ПолеНадписи" = "labelField" - "CalendarField" = "calendar" - "ПолеКалендаря" = "calendar" - "LabelDecoration" = "label" - "Надпись" = "label" - "PictureDecoration" = "picture" - "Картинка" = "picture" - "UsualGroup" = "group" - "Группа" = "group" - "ОбычнаяГруппа" = "group" - "ColumnGroup" = "columnGroup" - "ГруппаКолонок" = "columnGroup" - "Pages" = "pages" - "ГруппаСтраниц" = "pages" - "Page" = "page" - "Страница" = "page" - "Table" = "table" - "Таблица" = "table" - "Button" = "button" - "Кнопка" = "button" - "Popup" = "popup" - "ВсплывающееМеню" = "popup" - } - foreach ($pair in $synonyms.GetEnumerator()) { - if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { - $val = $el.($pair.Key) - $el.PSObject.Properties.Remove($pair.Key) | Out-Null - $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force - } - } - - # Determine element type from key - $typeKey = $null - $xmlTag = $null - - foreach ($key in @("columnGroup","buttonGroup","group","input","check","radio","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { - if ($el.$key -ne $null) { - $typeKey = $key - break - } - } - - if (-not $typeKey) { - Write-Warning "Unknown element type, skipping" - return - } - - # Validate known keys — warn about typos and unknown properties - $knownKeys = @{ - # type keys - "group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 - "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 - # columnGroup-specific - "showInHeader"=1 - # radio-specific - "radioButtonType"=1;"choiceList"=1;"columnsCount"=1 - # naming & binding - "name"=1;"path"=1;"title"=1 - # visibility & state - "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 - # events - "on"=1;"handlers"=1 - # layout - "titleLocation"=1;"representation"=1;"width"=1;"height"=1 - "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 - "maxWidth"=1;"maxHeight"=1 - # input-specific - "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 - "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 - "textEdit"=1 - # label/hyperlink - "hyperlink"=1 - # group-specific - "showTitle"=1;"united"=1;"collapsed"=1 - # hierarchy - "children"=1;"columns"=1 - # table-specific - "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 - "commandBarLocation"=1;"searchStringLocation"=1 - "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 - "rowPictureDataPath"=1;"tableAutofill"=1 - # pages-specific - "pagesRepresentation"=1 - # button-specific - "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 - # picture/decoration - "src"=1;"valuesPicture"=1;"loadTransparent"=1 - # cmdBar-specific - "autofill"=1 - } - foreach ($p in $el.PSObject.Properties) { - if (-not $knownKeys.ContainsKey($p.Name)) { - Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." - } - } - - $name = Get-ElementName -el $el -typeKey $typeKey - $id = New-Id - - switch ($typeKey) { - "group" { Emit-Group -el $el -name $name -id $id -indent $indent } - "columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent } - "buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent } - "input" { Emit-Input -el $el -name $name -id $id -indent $indent } - "check" { Emit-Check -el $el -name $name -id $id -indent $indent } - "radio" { Emit-Radio -el $el -name $name -id $id -indent $indent } - "label" { Emit-Label -el $el -name $name -id $id -indent $indent } - "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } - "table" { Emit-Table -el $el -name $name -id $id -indent $indent } - "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } - "page" { Emit-Page -el $el -name $name -id $id -indent $indent } - "button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar } - "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } - "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } - "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } - "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } - "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } - } -} - -function Emit-CommonFlags { - param($el, [string]$indent) - if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } - if ($el.userVisible -eq $false) { - X "$indent" - X "$indent`tfalse" - X "$indent" - } - if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } - if ($el.readOnly -eq $true) { X "$indenttrue" } -} - -function Title-FromName { - param([string]$name) - if (-not $name) { return '' } - $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') - $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') - $parts = $s -split ' ' - if ($parts.Count -eq 0) { return $s } - $out = New-Object System.Collections.ArrayList - [void]$out.Add($parts[0]) - for ($i = 1; $i -lt $parts.Count; $i++) { - $p = $parts[$i] - if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { - [void]$out.Add($p) - } else { - [void]$out.Add($p.ToLower()) - } - } - return ($out -join ' ') -} - -function Emit-Title { - param($el, [string]$name, [string]$indent, [switch]$auto) - $title = $el.title - if (-not $title -and $auto -and $name) { - $title = Title-FromName -name $name - } - if ($title) { - Emit-MLText -tag "Title" -text "$title" -indent $indent - } -} - -function Emit-Group { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - # Group orientation - $groupVal = "$($el.group)" - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "alwaysHorizontal" { "AlwaysHorizontal" } - "alwaysVertical" { "AlwaysVertical" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - - # Behavior - if ($groupVal -eq "collapsible") { - X "$innerVertical" - X "$innerCollapsible" - if ($el.collapsed -eq $true) { X "$innertrue" } - } - - # Representation - if ($el.representation) { - $repr = switch ("$($el.representation)") { - "none" { "None" } - "normal" { "NormalSeparation" } - "weak" { "WeakSeparation" } - "strong" { "StrongSeparation" } - default { "$($el.representation)" } - } - X "$inner$repr" - } - - # ShowTitle - if ($el.showTitle -eq $false) { X "$innerfalse" } - - # United - if ($el.united -eq $false) { X "$innerfalse" } - - Emit-CommonFlags -el $el -indent $inner - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-ColumnGroup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - # Group orientation (horizontal / vertical / inCell — последнее только здесь) - $groupVal = "$($el.columnGroup)" - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "inCell" { "InCell" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - - if ($el.showTitle -eq $false) { X "$innerfalse" } - if ($null -ne $el.showInHeader) { - $shVal = if ($el.showInHeader) { "true" } else { "false" } - X "$inner$shVal" - } - if ($el.width) { X "$inner$($el.width)" } - - Emit-CommonFlags -el $el -indent $inner - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Input { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { - $loc = switch ("$($el.titleLocation)") { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - default { "$($el.titleLocation)" } - } - X "$inner$loc" - } - - if ($el.multiLine -eq $true) { X "$innertrue" } - if ($el.passwordMode -eq $true) { X "$innertrue" } - if ($el.choiceButton -eq $false) { X "$innerfalse" } - elseif ($el.choiceButton -eq $true -and ($el.on -contains 'StartChoice')) { X "$innertrue" } - if ($el.clearButton -eq $true) { X "$innertrue" } - if ($el.spinButton -eq $true) { X "$innertrue" } - if ($el.dropListButton -eq $true) { X "$innertrue" } - if ($el.markIncomplete -eq $true) { X "$innertrue" } - if ($el.textEdit -eq $false) { X "$innerfalse" } - if ($el.skipOnInput -eq $true) { X "$innertrue" } - $hasAmw = $el.PSObject.Properties.Name -contains 'autoMaxWidth' - if ($hasAmw) { - if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } - } elseif ($el.multiLine -eq $true) { - X "$innerfalse" - } - if ($null -ne $el.maxWidth) { X "$inner$($el.maxWidth)" } - if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } - if ($null -ne $el.maxHeight) { X "$inner$($el.maxHeight)" } - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - if ($el.horizontalStretch -eq $true) { X "$innertrue" } - if ($el.verticalStretch -eq $true) { X "$innertrue" } - - if ($el.inputHint) { - Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner - } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" - - X "$indent" -} - -function Emit-Check { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - $tl = if ($el.titleLocation) { "$($el.titleLocation)" } else { "Right" } - X "$inner$tl" - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" - - X "$indent" -} - -# Maps Russian/English root of a typed reference path to canonical English root. -# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y". -$script:refRootSynonyms = @{ - "Перечисление" = "Enum" - "Справочник" = "Catalog" - "Документ" = "Document" - "ПланСчетов" = "ChartOfAccounts" - "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" - "ПланВидовРасчета" = "ChartOfCalculationTypes" - "ПланВидовРасчёта" = "ChartOfCalculationTypes" - "ПланОбмена" = "ExchangePlan" - "БизнесПроцесс" = "BusinessProcess" - "Задача" = "Task" - "РегистрСведений" = "InformationRegister" - "РегистрНакопления" = "AccumulationRegister" - "РегистрБухгалтерии" = "AccountingRegister" - "РегистрРасчета" = "CalculationRegister" - "РегистрРасчёта" = "CalculationRegister" -} -$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления") - -# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." } -function Normalize-ChoiceValue { - param($value) - - # Booleans - if ($value -is [bool]) { - return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } } - } - # Numbers (int / decimal / double) - if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) { - return @{ XsiType = "xs:decimal"; Text = "$value" } - } - - $s = "$value" - if ([string]::IsNullOrEmpty($s)) { - return @{ XsiType = "xs:string"; Text = "" } - } - - # Try to detect typed reference path: ".[..]" - $parts = $s -split '\.' - if ($parts.Count -ge 2) { - $root = $parts[0] - $canonRoot = $null - if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] } - elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root } - - if ($canonRoot) { - $typeName = $parts[1] - $normalized = $null - - if ($canonRoot -eq "Enum") { - if ($parts.Count -eq 2) { - # "Enum.X" alone — not a value, treat as string - } elseif ($parts.Count -eq 3) { - # "Enum.X.Y" — insert .EnumValue. - $normalized = "Enum.$typeName.EnumValue.$($parts[2])" - } else { - # "Enum.X..Y..." — replace member with EnumValue (handles ЗначениеПеречисления too) - $member = $parts[2] - if ($script:enumValueSynonyms -contains $member) { - $rest = $parts[3..($parts.Count-1)] -join '.' - $normalized = "Enum.$typeName.EnumValue.$rest" - } else { - $rest = $parts[2..($parts.Count-1)] -join '.' - $normalized = "Enum.$typeName.EnumValue.$rest" - } - } - } else { - # Other ref roots: just translate root, keep tail as-is - if ($parts.Count -ge 3) { - $tail = $parts[1..($parts.Count-1)] -join '.' - $normalized = "$canonRoot.$tail" - } - } - - if ($normalized) { - return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized } - } - } - } - - return @{ XsiType = "xs:string"; Text = $s } -} - -# Emit Presentation block for a choiceList item. -# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}. -# Empty/null → emits empty . -function Emit-ChoicePresentation { - param($pres, [string]$indent) - if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) { - X "$indent" - return - } - - $pairs = @() - if ($pres -is [string]) { - $pairs += ,@("ru", $pres) - } elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) { - foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") } - } elseif ($pres.PSObject -and $pres.PSObject.Properties) { - foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") } - } else { - $pairs += ,@("ru", "$pres") - } - - X "$indent" - foreach ($pair in $pairs) { - X "$indent`t" - X "$indent`t`t$($pair[0])" - X "$indent`t`t$(Esc-Xml $pair[1])" - X "$indent`t" - } - X "$indent" -} - -function Emit-Radio { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - # TitleLocation default is None for radio (matches typical configurator behavior) - $tl = if ($el.titleLocation) { - switch ("$($el.titleLocation)") { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - default { "$($el.titleLocation)" } - } - } else { "None" } - X "$inner$tl" - - # RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms. - $rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" } - $rbt = switch -Regex ($rbtRaw.ToLower()) { - '^(auto|авто)$' { "Auto"; break } - '^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break } - '^(tumbler|тумблер)$' { "Tumbler"; break } - default { $rbtRaw } - } - X "$inner$rbt" - - if ($null -ne $el.columnsCount) { - X "$inner$($el.columnsCount)" - } - - # ChoiceList - if ($el.choiceList -and $el.choiceList.Count -gt 0) { - X "$inner" - $itemIndent = "$inner`t" - foreach ($item in $el.choiceList) { - # Pull value (and tolerate Russian synonym "значение") - $valRaw = $null - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("value")) { $valRaw = $item["value"] } - elseif ($item.Contains("значение")) { $valRaw = $item["значение"] } - } else { - if ($item.PSObject.Properties["value"]) { $valRaw = $item.value } - elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" } - } - - # Pull presentation (presentation OR title synonym) - $presRaw = $null - $hasPres = $false - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true } - elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true } - elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true } - } else { - if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true } - elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true } - elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true } - } - - $norm = Normalize-ChoiceValue -value $valRaw - - # Auto-derive presentation if missing - if (-not $hasPres) { - if ($norm.XsiType -eq "xr:DesignTimeRef") { - $tail = ($norm.Text -split '\.')[-1] - $presRaw = Title-FromName -name $tail - } elseif ($norm.XsiType -eq "xs:string") { - $presRaw = $norm.Text - } else { - $presRaw = $norm.Text - } - } - - X "$itemIndent" - $valIndent = "$itemIndent`t" - X "$valIndent" - X "$valIndent0" - X "$valIndent" - Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t" - X "$valIndent`t$(Esc-Xml $norm.Text)" - X "$valIndent" - X "$itemIndent" - } - X "$inner" - } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio" - - X "$indent" -} - -function Emit-Label { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - $labelTitle = if ($el.title) { "$($el.title)" } else { Title-FromName -name $name } - if ($labelTitle) { - $formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" } - X "$inner" - X "$inner`t<v8:item>" - X "$inner`t`t<v8:lang>ru</v8:lang>" - X "$inner`t`t<v8:content>$(Esc-Xml "$labelTitle")</v8:content>" - X "$inner`t</v8:item>" - X "$inner" - } - - Emit-CommonFlags -el $el -indent $inner - - if ($el.hyperlink -eq $true) { X "$innertrue" } - if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } - if ($null -ne $el.maxWidth) { X "$inner$($el.maxWidth)" } - if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } - if ($null -ne $el.maxHeight) { X "$inner$($el.maxHeight)" } - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" - - X "$indent" -} - -function Emit-LabelField { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.hyperlink -eq $true) { X "$innertrue" } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" - - X "$indent" -} - -function Emit-Table { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.representation) { - X "$inner$($el.representation)" - } - if ($el.changeRowSet -eq $true) { X "$innertrue" } - if ($el.changeRowOrder -eq $true) { X "$innertrue" } - if ($el.height) { X "$inner$($el.height)" } - if ($el.header -eq $false) { X "$inner
false
" } - if ($el.footer -eq $true) { X "$inner
true
" } - - if ($el.commandBarLocation) { - X "$inner$($el.commandBarLocation)" - } - if ($el.searchStringLocation) { - X "$inner$($el.searchStringLocation)" - } - if ($el.choiceMode -eq $true) { X "$innertrue" } - if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } - if ($el.enableStartDrag -eq $true) { X "$innertrue" } - if ($el.enableDrag -eq $true) { X "$innertrue" } - if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - # AutoCommandBar — with optional Autofill control - if ($null -ne $el.tableAutofill) { - $acbId = New-Id - X "$inner" - $afVal = if ($el.tableAutofill) { "true" } else { "false" } - X "$inner`t$afVal" - X "$inner" - } else { - Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner - } - Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner - Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner - Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner - - # Columns - if ($el.columns -and $el.columns.Count -gt 0) { - X "$inner" - foreach ($col in $el.columns) { - Emit-Element -el $col -indent "$inner`t" - } - X "$inner" - } - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" - - X "$indent
" -} - -function Emit-Pages { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.pagesRepresentation) { - X "$inner$($el.pagesRepresentation)" - } - - Emit-CommonFlags -el $el -indent $inner - - # Companion - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" - - # Children (pages) - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Page { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner -auto - Emit-CommonFlags -el $el -indent $inner - - if ($el.group) { - $orientation = switch ("$($el.group)") { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "alwaysHorizontal" { "AlwaysHorizontal" } - "alwaysVertical" { "AlwaysVertical" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - } - - # Companion - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Button { - param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false) - - X "$indent" -} - -function Emit-PictureDecoration { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - - if ($el.picture -or $el.src) { - $ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" } - X "$inner" - X "$inner`t$ref" - X "$inner`ttrue" - X "$inner" - } - - if ($el.hyperlink -eq $true) { X "$innertrue" } - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" - - X "$indent" -} - -function Emit-PictureField { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - - # ValuesPicture — picture (collection) used to render the field's value. - # Required for a Boolean-bound PictureField to actually show an icon. - # loadTransparent emitted only when true (1С default is false). - if ($el.valuesPicture) { - X "$inner" - X "$inner`t$($el.valuesPicture)" - if ($el.loadTransparent) { X "$inner`ttrue" } - X "$inner" - } - - if ($el.width) { X "$inner$($el.width)" } - if ($el.height) { X "$inner$($el.height)" } - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" - - X "$indent" -} - -function Emit-Calendar { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - # Companions - Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" - - X "$indent" -} - -function Emit-CommandBar { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.autofill -eq $true) { X "$innertrue" } - - Emit-CommonFlags -el $el -indent $inner - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -function Emit-ButtonGroup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.representation) { - X "$inner$($el.representation)" - } - - Emit-CommonFlags -el $el -indent $inner - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - - # Children (кнопки в контексте командной панели) - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -function Emit-Popup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner -auto - Emit-CommonFlags -el $el -indent $inner - - if ($el.picture) { - X "$inner" - X "$inner`t$($el.picture)" - X "$inner`ttrue" - X "$inner" - } - - if ($el.representation) { - X "$inner$($el.representation)" - } - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -# --- 8. Attribute emitter --- - -function Emit-Attributes { - param($attrs, [string]$indent) - - if (-not $attrs -or $attrs.Count -eq 0) { return } - - X "$indent" - foreach ($attr in $attrs) { - $attrId = New-Id - $attrName = "$($attr.name)" - - X "$indent`t" - $inner = "$indent`t`t" - - $attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' } - if ($attrTitle) { - Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner - } - - # Type - if ($attr.type) { - Emit-Type -typeStr "$($attr.type)" -indent $inner - } else { - X "$inner" - } - - if ($attr.main -eq $true) { - X "$innertrue" - } - $mainSaved = $false - if ($attr.main -eq $true -and $attr.type) { - $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' - } - if ($attr.savedData -eq $true -or $mainSaved) { - X "$innertrue" - } - if ($attr.fillChecking) { - X "$inner$($attr.fillChecking)" - } - - # Columns (for ValueTable/ValueTree) - if ($attr.columns -and $attr.columns.Count -gt 0) { - X "$inner" - foreach ($col in $attr.columns) { - $colId = New-Id - X "$inner`t" - if ($col.title) { - Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" - } - Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" - X "$inner`t" - } - X "$inner" - } - - # Settings (for DynamicList) - if ($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" } - X "$si$mq" - $ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" } - X "$si$ddr" - X "$inner" - } - - X "$indent`t" - } - X "$indent" -} - -# --- 9. Parameter emitter --- - -function Emit-Parameters { - param($params, [string]$indent) - - if (-not $params -or $params.Count -eq 0) { return } - - X "$indent" - foreach ($param in $params) { - X "$indent`t" - $inner = "$indent`t`t" - - Emit-Type -typeStr "$($param.type)" -indent $inner - - if ($param.key -eq $true) { - X "$innertrue" - } - - X "$indent`t" - } - X "$indent" -} - -# --- 10. Command emitter --- - -function Emit-Commands { - param($cmds, [string]$indent) - - if (-not $cmds -or $cmds.Count -eq 0) { return } - - X "$indent" - foreach ($cmd in $cmds) { - $cmdId = New-Id - X "$indent`t" - $inner = "$indent`t`t" - - $cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" } - if ($cmdTitle) { - Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner - } - - if ($cmd.tooltip) { - Emit-MLText -tag "ToolTip" -text "$($cmd.tooltip)" -indent $inner - } - - if ($cmd.action) { - X "$inner$($cmd.action)" - } - - if ($cmd.currentRowUse) { - X "$inner$($cmd.currentRowUse)" - } - - if ($cmd.shortcut) { - X "$inner$($cmd.shortcut)" - } - - if ($cmd.picture) { - X "$inner" - X "$inner`t$($cmd.picture)" - X "$inner`ttrue" - X "$inner" - } - - if ($cmd.representation) { - X "$inner$($cmd.representation)" - } - - X "$indent`t" - } - X "$indent" -} - -# --- 11. Properties emitter --- - -function Emit-Properties { - param($props, [string]$indent) - - if (-not $props) { return } - - # camelCase -> PascalCase mapping for known properties - $propMap = @{ - "autoTitle" = "AutoTitle" - "windowOpeningMode" = "WindowOpeningMode" - "commandBarLocation" = "CommandBarLocation" - "saveDataInSettings" = "SaveDataInSettings" - "autoSaveDataInSettings" = "AutoSaveDataInSettings" - "autoTime" = "AutoTime" - "usePostingMode" = "UsePostingMode" - "repostOnWrite" = "RepostOnWrite" - "autoURL" = "AutoURL" - "autoFillCheck" = "AutoFillCheck" - "customizable" = "Customizable" - "enterKeyBehavior" = "EnterKeyBehavior" - "verticalScroll" = "VerticalScroll" - "scalingMode" = "ScalingMode" - "useForFoldersAndItems" = "UseForFoldersAndItems" - "reportResult" = "ReportResult" - "detailsData" = "DetailsData" - "reportFormType" = "ReportFormType" - "autoShowState" = "AutoShowState" - "width" = "Width" - "height" = "Height" - "group" = "Group" - } - - foreach ($p in $props.PSObject.Properties) { - $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { - # Auto PascalCase: first letter uppercase - $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) - } - # Convert boolean to lowercase string (PS renders as True/False) - $val = $p.Value - if ($val -is [bool]) { - $val = if ($val) { "true" } else { "false" } - } - X "$indent<$xmlName>$val" - } -} - -# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- - -function Normalize-ElementSynonyms { - param($el) - if ($null -eq $el) { return } - $synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" } - foreach ($pair in $synonyms.GetEnumerator()) { - if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { - $val = $el.($pair.Key) - $el.PSObject.Properties.Remove($pair.Key) | Out-Null - $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force - } - } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { Normalize-ElementSynonyms $child } - } - if ($el.PSObject.Properties["columns"] -and $el.columns) { - foreach ($child in $el.columns) { Normalize-ElementSynonyms $child } - } -} - -function HasCmdBarRecursive { - param($el) - if ($null -eq $el) { return $false } - if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } } - } - if ($el.PSObject.Properties["columns"] -and $el.columns) { - foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } } - } - return $false -} - -function ApplyDynamicListTableHeuristic { - param($el, [string]$listName, [bool]$hasMainTable) - if ($null -eq $el) { return } - if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) { - if ($null -eq $el.PSObject.Properties["tableAutofill"]) { - $el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force - } - if ($null -eq $el.PSObject.Properties["commandBarLocation"]) { - $el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force - } - # DefaultPicture доступен только если у DynamicList есть основная таблица - if ($hasMainTable -and ($null -eq $el.PSObject.Properties["rowPictureDataPath"] -or [string]::IsNullOrEmpty("$($el.rowPictureDataPath)"))) { - $el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force - } - } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable } - } -} - -function Test-IsObjectLikeType { - param([string]$type) - if ([string]::IsNullOrEmpty($type)) { return $false } - if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true } - $objectSuffixes = @( - "CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject", - "ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject", - "TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject", - "ChartOfCalculationTypesObject", "ExchangePlanObject" - ) - $recordSetPrefixes = @( - "InformationRegisterRecordSet", "AccumulationRegisterRecordSet", - "AccountingRegisterRecordSet", "CalculationRegisterRecordSet", - "InformationRegisterRecordManager" - ) - foreach ($suffix in $objectSuffixes) { - if ($type -like "$suffix.*") { return $true } - } - foreach ($prefix in $recordSetPrefixes) { - if ($type -like "$prefix.*") { return $true } - } - return $false -} - -# 11b.1: Normalize synonyms recursively -if ($def.elements) { - foreach ($el in $def.elements) { Normalize-ElementSynonyms $el } -} - -# 11b.2: Extract autoCmdBar element from def.elements -$script:mainAcbDef = $null -if ($def.elements) { - $autoBars = @() - $rest = @() - foreach ($el in $def.elements) { - if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) { - $autoBars += $el - } else { - $rest += $el - } - } - if ($autoBars.Count -gt 1) { - Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed." - exit 1 - } - if ($autoBars.Count -eq 1) { - $script:mainAcbDef = $autoBars[0] - # Replace def.elements with the filtered list - $def.PSObject.Properties.Remove("elements") | Out-Null - $def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force - } -} - -# 11b.3: Infer main attribute (only if no attribute has main:true) -if ($def.attributes) { - $hasExplicitMain = $false - foreach ($attr in $def.attributes) { - if ($attr.main -eq $true) { $hasExplicitMain = $true; break } - } - if (-not $hasExplicitMain) { - $candidates = @() - foreach ($attr in $def.attributes) { - # Skip if user explicitly opted out via main:false - if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue } - if (Test-IsObjectLikeType "$($attr.type)") { - $candidates += $attr - } - } - if ($candidates.Count -eq 1) { - $candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force - Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))" - } elseif ($candidates.Count -gt 1) { - $names = ($candidates | ForEach-Object { $_.name }) -join ", " - Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly" - } - } -} - -# 11b.4: DynamicList → table heuristic -if ($def.attributes -and $def.elements) { - $mainAttr = $null - foreach ($attr in $def.attributes) { - if ($attr.main -eq $true) { $mainAttr = $attr; break } - } - if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") { - $mt = $null - if ($mainAttr.PSObject.Properties["settings"] -and $null -ne $mainAttr.settings) { - if ($mainAttr.settings -is [hashtable]) { - if ($mainAttr.settings.ContainsKey("mainTable")) { $mt = $mainAttr.settings["mainTable"] } - } elseif ($mainAttr.settings.PSObject.Properties["mainTable"]) { - $mt = $mainAttr.settings.mainTable - } - } - $hasMt = -not [string]::IsNullOrEmpty("$mt") - foreach ($el in $def.elements) { - ApplyDynamicListTableHeuristic $el $mainAttr.name $hasMt - } - } -} - -# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3 -function Compute-MainAcbAutofill { - if ($script:mainAcbDef) { - if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) { - return [bool]$script:mainAcbDef.autofill - } - return $true - } - if ($def.elements) { - foreach ($el in $def.elements) { - if (HasCmdBarRecursive $el) { return $false } - } - } - return $true -} - -# --- 12. Main compilation --- - -# Title -if ($def.title) { - Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t" -} - -# Header -X '' -X "
" - -# Oops — Title was emitted before header. Need to fix the order. -# Actually, let me restructure: build the body into a separate buffer, then assemble - -# Reset and rebuild properly -$script:xml = New-Object System.Text.StringBuilder 8192 -$script:nextId = 1 - -X '' -X "" - -# 12a. Title (from def.title or properties.title — must be multilingual XML) -$formTitle = $def.title -if (-not $formTitle -and $def.properties -and $def.properties.title) { - $formTitle = $def.properties.title -} -if ($formTitle) { - Emit-MLText -tag "Title" -text "$formTitle" -indent "`t" -} - -# 12b. Properties (skip 'title' — handled above as multilingual) -# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; -# otherwise platform appends synonym → "Title: Synonym" double-titles). -$propsClone = New-Object PSObject -$hasAutoTitle = $false -if ($def.properties) { - foreach ($p in $def.properties.PSObject.Properties) { - if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true } - } -} -if ($formTitle -and -not $hasAutoTitle) { - $propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false -} -if ($def.properties) { - foreach ($p in $def.properties.PSObject.Properties) { - if ($p.Name -ne "title") { - $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value - } - } -} -Emit-Properties -props $propsClone -indent "`t" - -# 12c. CommandSet (excluded commands) -if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { - X "`t" - foreach ($cmd in $def.excludedCommands) { - X "`t`t$cmd" - } - X "`t" -} - -# 12d. AutoCommandBar (always present, id=-1) -$acbAutofill = Compute-MainAcbAutofill -$acbName = "ФормаКоманднаяПанель" -$acbHAlign = $null -if ($script:mainAcbDef) { - if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") { - $acbName = "$($script:mainAcbDef.autoCmdBar)" - } - if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") { - $acbName = "$($script:mainAcbDef.name)" - } - if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") { - $acbHAlign = "$($script:mainAcbDef.horizontalAlign)" - } -} -$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0) -$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren) -if ($acbHasInner) { - X "`t" - if ($acbHAlign) { X "`t`t$acbHAlign" } - if (-not $acbAutofill) { X "`t`tfalse" } - if ($hasAcbChildren) { - X "`t`t" - foreach ($child in $script:mainAcbDef.children) { - Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true - } - X "`t`t" - } - X "`t" -} else { - X "`t" -} - -# 12e. Events -if ($def.events) { - foreach ($p in $def.events.PSObject.Properties) { - if ($script:knownFormEvents -notcontains $p.Name) { - Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" - } - } - X "`t" - foreach ($p in $def.events.PSObject.Properties) { - X "`t`t$($p.Value)" - } - X "`t" -} - -# 12f. ChildItems (elements) -if ($def.elements -and $def.elements.Count -gt 0) { - X "`t" - foreach ($el in $def.elements) { - Emit-Element -el $el -indent "`t`t" - } - X "`t" -} - -# 12g. Attributes -Emit-Attributes -attrs $def.attributes -indent "`t" - -# 12h. Parameters -Emit-Parameters -params $def.parameters -indent "`t" - -# 12i. Commands -Emit-Commands -cmds $def.commands -indent "`t" - -# 12j. Close -X '' - -# --- 13. Write output --- - -$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } -$outDir = [System.IO.Path]::GetDirectoryName($outPath) -if (-not (Test-Path $outDir)) { - New-Item -ItemType Directory -Path $outDir -Force | Out-Null -} - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) - -# --- 13b. Auto-register form in parent object XML --- - -# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml -$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) -$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) -$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) -$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) -$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) - -$formName = [System.IO.Path]::GetFileName($formNameDir) -$objectName = [System.IO.Path]::GetFileName($objectDir) -$formsLeaf = [System.IO.Path]::GetFileName($formsDir) - -if ($formsLeaf -eq 'Forms') { - $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" - if (Test-Path $objectXmlPath) { - $objDoc = New-Object System.Xml.XmlDocument - $objDoc.PreserveWhitespace = $true - $objDoc.Load($objectXmlPath) - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) - $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) - if ($childObjects) { - $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) - if (-not $existing) { - $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") - $formElem.InnerText = $formName - - $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) - if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } - - if ($insertBefore) { - $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null - $ws = $objDoc.CreateWhitespace("`n`t`t`t") - $childObjects.InsertBefore($ws, $insertBefore) | Out-Null - } else { - $lastChild = $childObjects.LastChild - if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { - $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null - $childObjects.InsertBefore($formElem, $lastChild) | Out-Null - } else { - $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null - $childObjects.AppendChild($formElem) | Out-Null - $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null - } - } - - $regEnc = New-Object System.Text.UTF8Encoding($true) - $regSettings = New-Object System.Xml.XmlWriterSettings - $regSettings.Encoding = $regEnc - $regSettings.Indent = $false - $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) - $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) - $objDoc.Save($regWriter) - $regWriter.Close() - $regStream.Close() - - Write-Host " Registered:
$formName
in $objectName.xml" - } - } - } -} - -# --- 14. Summary --- - -$elCount = $script:nextId - 1 -Write-Host "[OK] Compiled: $OutputPath" -Write-Host " Elements+IDs: $elCount" -if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } -if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } -if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } +# form-compile v1.25 — Compile 1C managed form from JSON or object metadata +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [string]$JsonPath, + + [Parameter(Mandatory)] + [string]$OutputPath, + + [switch]$FromObject, + [string]$ObjectPath, + [string]$Purpose, + [string]$Preset = "erp-standard", + [string]$EmitDsl +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# ═══════════════════════════════════════════════════════════════════════════ +# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation +# ═══════════════════════════════════════════════════════════════════════════ + +function Parse-ObjectMeta([string]$ObjectPath) { + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($ObjectPath) + + $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + + # Detect object type from root child + $metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns) + if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 } + $typeNode = $metaRoot.FirstChild + $objType = $typeNode.LocalName # "Document", "Catalog", etc. + + $propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) + $childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) + + # Name + $objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText + + # Synonym (Russian) + $synonym = $objName + $synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + if ($synNode) { $synonym = $synNode.InnerText } + + # Helper: extract type string from md:Type + $extractType = { + param($typeParent) + if (-not $typeParent) { return "string" } + $types = @() + foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) { + $types += $t.InnerText + } + if ($types.Count -eq 0) { return "string" } + return ($types -join " | ") + } + + # Helper: check if type is a reference + $isRefType = { + param([string]$t) + return ($t -match 'Ref\.' -or $t -match 'ссылка\.') + } + + # Helper: extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag) + $extractFields = { + param($parentNode, [string]$tagName) + $result = @() + if (-not $parentNode) { return $result } + foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) { + $fp = $fieldNode.SelectSingleNode("md:Properties", $ns) + $fName = $fp.SelectSingleNode("md:Name", $ns).InnerText + $fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName } + $fTypeNode = $fp.SelectSingleNode("md:Type", $ns) + $fType = & $extractType $fTypeNode + $result += @{ + Name = $fName + Synonym = $fSyn + Type = $fType + IsRef = (& $isRefType $fType) + } + } + return $result + } + + # Attributes + $attributes = @(& $extractFields $childObjs "Attribute") + + # Tabular sections + $tabularSections = @() + if ($childObjs) { + foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) { + $tsp = $tsNode.SelectSingleNode("md:Properties", $ns) + $tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText + $tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName } + $tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns) + $tsCols = @(& $extractFields $tsCo "Attribute") + $tabularSections += @{ + Name = $tsName + Synonym = $tsSyn + Columns = $tsCols + } + } + } + + $meta = @{ + Type = $objType + Name = $objName + Synonym = $synonym + Attributes = $attributes + TabularSections = $tabularSections + } + + # Type-specific properties + switch ($objType) { + "Document" { + $ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns) + $meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" } + } + "Catalog" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + # Owners + $owners = @() + foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { + $owners += $ow.InnerText + } + $meta.Owners = $owners + } + "InformationRegister" { + $meta.Dimensions = @(& $extractFields $childObjs "Dimension") + $meta.Resources = @(& $extractFields $childObjs "Resource") + $prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) + $meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" } + $wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns) + $meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" } + } + "AccumulationRegister" { + $meta.Dimensions = @(& $extractFields $childObjs "Dimension") + $meta.Resources = @(& $extractFields $childObjs "Resource") + $rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns) + $meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" } + } + "ChartOfCharacteristicTypes" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + $owners = @() + foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { + $owners += $ow.InnerText + } + $meta.Owners = $owners + $meta.HasValueType = $true + } + "ExchangePlan" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $meta.Hierarchical = $false + $meta.HierarchyType = $null + $meta.Owners = @() + } + "ChartOfAccounts" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $meta.Hierarchical = $true + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + $meta.Owners = @() + $maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) + $meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 } + $meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag") + $meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag") + } + } + + return $meta +} + +function Load-Preset([string]$PresetName, [string]$ScriptDir) { + # Hardcoded defaults (ERP-oriented) + $defaults = @{ + "document.item" = @{ + header = @{ position = "insidePage"; layout = "2col"; distribute = "even"; dateTitle = "от" } + footer = @{ fields = @("Комментарий"); position = "insidePage" } + tabularSections = @{ container = "pages"; exclude = @("ДополнительныеРеквизиты"); lineNumber = $true } + additional = @{ position = "page"; layout = "2col"; bspGroup = $true } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + commandBar = "auto" + properties = @{ autoTitle = $false } + } + "document.list" = @{ + columns = "all"; columnType = "labelField"; hiddenRef = $true + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "document.choice" = @{ + basedOn = "document.list" + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "catalog.item" = @{ + header = @{ layout = "1col"; distribute = "left" } + codeDescription = @{ layout = "horizontal"; order = "descriptionFirst" } + parent = @{ title = "Входит в группу"; position = "afterCodeDescription" } + owner = @{ readOnly = $true; position = "first" } + tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); lineNumber = $true } + footer = @{ fields = @(); position = "none" } + additional = @{ position = "none"; bspGroup = $true } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + commandBar = "auto" + properties = @{} + } + "catalog.folder" = @{ + parent = @{ title = "Входит в группу" } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "catalog.list" = @{ + columns = "all"; columnType = "labelField"; hiddenRef = $true + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "catalog.choice" = @{ + basedOn = "catalog.list"; choiceMode = $true + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + # ─── Register defaults ─── + "informationRegister.record" = @{ + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "informationRegister.list" = @{ + columns = "all"; columnType = "labelField" + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "accumulationRegister.list" = @{ + columns = "all"; columnType = "labelField" + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + # ─── Catalog-like type defaults ─── + "chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" } + "chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" } + "chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" } + "chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" } + "exchangePlan.item" = @{ basedOn = "catalog.item" } + "exchangePlan.list" = @{ basedOn = "catalog.list" } + "exchangePlan.choice" = @{ basedOn = "catalog.choice" } + # ─── ChartOfAccounts defaults ─── + "chartOfAccounts.item" = @{ + parent = @{ title = "Подчинен счету" } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + properties = @{} + } + "chartOfAccounts.folder" = @{ + parent = @{ title = "Подчинен счету" } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "chartOfAccounts.list" = @{ basedOn = "catalog.list" } + "chartOfAccounts.choice" = @{ basedOn = "catalog.choice" } + } + + # Deep merge helper + $deepMerge = { + param($base, $overlay) + if (-not $overlay) { return $base } + if (-not $base) { return $overlay } + $result = @{} + foreach ($k in $base.Keys) { $result[$k] = $base[$k] } + foreach ($k in $overlay.Keys) { + if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { + $result[$k] = & $deepMerge $result[$k] $overlay[$k] + } else { + $result[$k] = $overlay[$k] + } + } + return $result + } + + # Try built-in preset + $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 + # Convert PSCustomObject to hashtable recursively + $toHash = { + param($obj) + if ($obj -is [System.Management.Automation.PSCustomObject]) { + $h = @{} + foreach ($p in $obj.PSObject.Properties) { + $h[$p.Name] = & $toHash $p.Value + } + return $h + } + if ($obj -is [System.Object[]]) { + return @($obj | ForEach-Object { & $toHash $_ }) + } + return $obj + } + $presetHash = & $toHash $presetJson + foreach ($k in @($presetHash.Keys)) { + $defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k] + } + } + + # Try project-level preset (scan up from output path) + $scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved) + 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 + $projHash = & $toHash $projJson + foreach ($k in @($projHash.Keys)) { + $defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k] + } + break + } + $parentDir = Split-Path $scanDir -Parent + if ($parentDir -eq $scanDir) { break } + $scanDir = $parentDir + } + + # Resolve basedOn references + foreach ($k in @($defaults.Keys)) { + $sect = $defaults[$k] + if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) { + $baseName = $sect["basedOn"] + if ($defaults.ContainsKey($baseName)) { + $merged = & $deepMerge $defaults[$baseName] $sect + $merged.Remove("basedOn") + $defaults[$k] = $merged + } + } + } + + return $defaults +} + +# --- Helper: build a field element DSL entry --- +# Non-displayable types — cannot be bound to form elements +$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения') + +function Test-DisplayableType([string]$typeStr) { + foreach ($nd in $script:nonDisplayableTypes) { + if ($typeStr -match [regex]::Escape($nd)) { return $false } + } + return $true +} + +function New-FieldElement { + param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps) + + $isRef = ($attrType -match 'Ref\.') + $isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean') + + # Determine element type + $elType = "input" + if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") { + $elType = "check" + } + + $el = [ordered]@{ $elType = $attrName; path = $dataPath } + + # Apply ref defaults + if ($isRef -and $fieldDefaults -and $fieldDefaults.ref) { + if ($fieldDefaults.ref.choiceButton -eq $true) { $el["choiceButton"] = $true } + } + + # Extra props + if ($extraProps) { + foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] } + } + + return $el +} + +# --- Catalog DSL generators --- +function Generate-CatalogDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + + $purposeKey = "catalog.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } + $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } + + switch ($purpose) { + "Folder" { return Generate-CatalogFolderDSL $meta $p } + "List" { return Generate-CatalogListDSL $meta $p } + "Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData } + "Item" { return Generate-CatalogItemDSL $meta $p $fd } + } +} + +function Generate-CatalogFolderDSL($meta, [hashtable]$p) { + $elements = @() + # Code (if CodeLength > 0) + if ($meta.CodeLength -gt 0) { + $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + # Description + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + # Parent + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } + $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } + if ($parentTitle) { $parentEl["title"] = $parentTitle } + $elements += $parentEl + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + $formProps = [ordered]@{ useForFoldersAndItems = "Folders" } + foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $elements + attributes = @( + [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } + ) + } +} + +function Generate-CatalogListDSL($meta, [hashtable]$p) { + # Columns + $columns = @() + # Description always first + $columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" } + # Code if present + if ($meta.CodeLength -gt 0) { + $columns += [ordered]@{ labelField = "Код"; path = "Список.Code" } + } + # Custom attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } + } + # Hidden ref + if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { + $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + # Hierarchical properties + if ($meta.Hierarchical) { + $tableEl["initialTreeView"] = "ExpandTopLevel" + $tableEl["enableStartDrag"] = $true + $tableEl["enableDrag"] = $true + } + + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = @($tableEl) + attributes = @( + [ordered]@{ + name = "Список"; type = "DynamicList"; main = $true + settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true } + } + ) + } +} + +function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { + # Start from list + $listKey = "catalog.list" + $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } + $dsl = Generate-CatalogListDSL $meta $lp + + # Add choice-specific properties + $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" + if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } + + # Set ChoiceMode on table + $dsl.elements[0]["choiceMode"] = $true + + return $dsl +} + +function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) { + $headerChildren = @() + + # Owner (if subordinate) + if ($meta.Owners -and $meta.Owners.Count -gt 0) { + $ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true } + $headerChildren += $ownerEl + } + + # Code + Description + $cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" } + $cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" } + $hasCode = ($meta.CodeLength -gt 0) + + if ($cdLayout -eq "horizontal" -and $hasCode) { + $cdChildren = @() + $descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" } + $codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" } + if ($cdOrder -eq "descriptionFirst") { + $cdChildren = @($descEl, $codeEl) + } else { + $cdChildren = @($codeEl, $descEl) + } + $headerChildren += [ordered]@{ + group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false + representation = "none"; children = $cdChildren + } + } else { + # Vertical or no code + $headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + if ($hasCode) { + $headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + } + + # Parent (for hierarchical catalogs) + $parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" } + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } + if ($meta.Hierarchical) { + $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } + if ($parentTitle) { $parentEl["title"] = $parentTitle } + if ($parentPos -eq "beforeCodeDescription") { + # Insert before Code/Description (after Owner if present) + $insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 } + $newChildren = @() + for ($i = 0; $i -lt $headerChildren.Count; $i++) { + if ($i -eq $insertIdx) { $newChildren += $parentEl } + $newChildren += $headerChildren[$i] + } + $headerChildren = $newChildren + } else { + # afterCodeDescription (default) + $headerChildren += $parentEl + } + } + + # Custom attributes → header + $footerFieldNames = @() + if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) } + + foreach ($attr in $meta.Attributes) { + if ($footerFieldNames -contains $attr.Name) { continue } + if (-not (Test-DisplayableType $attr.Type)) { continue } + $headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Build root elements + $rootElements = @() + + # ГруппаШапка + $rootElements += [ordered]@{ + group = "vertical"; name = "ГруппаШапка"; showTitle = $false + representation = "none"; children = $headerChildren + } + + # Tabular sections + $tsExclude = @("ДополнительныеРеквизиты", "Представления") + if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } + $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } + $tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" } + + $visibleTS = @() + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $visibleTS += $ts + } + + foreach ($ts in $visibleTS) { + $tsCols = @() + if ($tsLineNumber) { + $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } + } + foreach ($col in $ts.Columns) { + $colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{} + $tsCols += $colEl + } + $tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + $rootElements += $tableEl + } + + # Footer fields + foreach ($fn in $footerFieldNames) { + $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } + if ($fAttr) { + $rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + + # BSP group + $bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } + if ($bspGroup) { + $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + + # Properties + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + # UseForFoldersAndItems + if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") { + $formProps["useForFoldersAndItems"] = "Items" + } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $rootElements + attributes = @( + [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } + ) + } +} + +# --- Document DSL generators --- +function Generate-DocumentDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + + $purposeKey = "document.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } + $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } + + switch ($purpose) { + "List" { return Generate-DocumentListDSL $meta $p } + "Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData } + "Item" { return Generate-DocumentItemDSL $meta $p $fd } + } +} + +function Generate-DocumentListDSL($meta, [hashtable]$p) { + $columns = @() + # Standard columns: Number + Date + $columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" } + $columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" } + # All custom attributes as labelField + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } + } + # Hidden ref + if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { + $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = @($tableEl) + attributes = @( + [ordered]@{ + name = "Список"; type = "DynamicList"; main = $true + settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true } + } + ) + } +} + +function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { + $listKey = "document.list" + $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } + $dsl = Generate-DocumentListDSL $meta $lp + + $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" + if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } + + return $dsl +} + +function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) { + $headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" } + $headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" } + $headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" } + $dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" } + + $footerFields = @() + if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) } + $footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" } + + $addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" } + $addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" } + $addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } + $addLeft = @(); $addRight = @() + if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) } + if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) } + + $headerRight = @() + if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) } + + $tsExclude = @("ДополнительныеРеквизиты") + if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } + $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } + + # Classify attributes + $claimed = @{} + foreach ($fn in $footerFields) { $claimed[$fn] = "footer" } + foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" } + foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" } + foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" } + + $unclaimed = @() + foreach ($attr in $meta.Attributes) { + if (-not $claimed.ContainsKey($attr.Name) -and (Test-DisplayableType $attr.Type)) { $unclaimed += $attr } + } + + # Distribute unclaimed + $leftAttrs = @(); $rightExtraAttrs = @() + switch ($headerDistribute) { + "left" { $leftAttrs = $unclaimed } + "right" { $rightExtraAttrs = $unclaimed } + default { # "even" + $half = [Math]::Ceiling($unclaimed.Count / 2) + for ($i = 0; $i -lt $unclaimed.Count; $i++) { + if ($i -lt $half) { $leftAttrs += $unclaimed[$i] } + else { $rightExtraAttrs += $unclaimed[$i] } + } + } + } + + # Build ГруппаНомерДата + $numDateChildren = @( + [ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 } + [ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle } + ) + $numDateGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren + } + + # Build left column + $leftChildren = @($numDateGroup) + foreach ($attr in $leftAttrs) { + $leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Build right column + $rightChildren = @() + foreach ($rn in $headerRight) { + $rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn } + if ($rAttr) { + $rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + foreach ($attr in $rightExtraAttrs) { + $rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Header group + $headerGroup = $null + if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) { + $headerGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren } + [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren } + ) + } + } else { + # 1col or no right items + $allHeaderFields = $leftChildren + $rightChildren + $headerGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields } + ) + } + } + + # Footer elements + $footerElements = @() + foreach ($fn in $footerFields) { + $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } + if ($fAttr -and (Test-DisplayableType $fAttr.Type)) { + $footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + + # Visible tabular sections + $visibleTS = @() + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $visibleTS += $ts + } + + # Additional page content + $additionalPage = $null + if ($addPos -eq "page") { + $addLeftEls = @(); $addRightEls = @() + foreach ($aln in $addLeft) { + $alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln } + if ($alAttr) { + $addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + foreach ($arn in $addRight) { + $arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn } + if ($arAttr) { + $addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + $addPageChildren = @() + if ($addLayout -eq "2col") { + $addPageChildren += [ordered]@{ + group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls } + [ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls } + ) + } + } else { + $addPageChildren += @($addLeftEls + $addRightEls) + } + if ($addBspGroup) { + $addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + $additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren } + } + + # Build TS page elements + $tsPages = @() + foreach ($ts in $visibleTS) { + $tsCols = @() + if ($tsLineNumber) { + $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } + } + foreach ($col in $ts.Columns) { + $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}) + } + $tsPages += [ordered]@{ + page = "Группа$($ts.Name)"; title = $ts.Synonym + children = @( + [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + ) + } + } + + # Assemble root elements + $rootElements = @() + + if ($visibleTS.Count -eq 0) { + # Simple form — no Pages + $rootElements += $headerGroup + if ($footerElements.Count -gt 0) { $rootElements += $footerElements } + if ($addBspGroup -and $addPos -ne "none") { + $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + } else { + # Pages form + if ($headerPos -eq "abovePages") { + $rootElements += $headerGroup + $pagesChildren = @() + $pagesChildren += $tsPages + if ($additionalPage) { $pagesChildren += $additionalPage } + $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } + } else { + # insidePage (default) + $osnovnoeChildren = @($headerGroup) + if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) { + $osnovnoeChildren += $footerElements + } + $pagesChildren = @() + $pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren } + $pagesChildren += $tsPages + if ($additionalPage) { $pagesChildren += $additionalPage } + $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } + } + + # Footer below pages + if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) { + $rootElements += $footerElements + } + } + + # Properties + $formProps = [ordered]@{ autoTitle = $false } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $rootElements + attributes = @( + [ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true } + ) + } +} + +# ─── InformationRegister ────────────────────────────────────────────────── + +function Generate-InformationRegisterDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "informationRegister.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } + switch ($purpose) { + "Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd } + "List" { return Generate-InformationRegisterListDSL $meta $p } + } +} + +function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) { + $elements = @() + $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" + + # Period first (if periodic) + if ($isPeriodic) { + $elements += [ordered]@{ input = "Период"; path = "Запись.Period" } + } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd) + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd) + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) + } + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = $elements + attributes = @( + @{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-InformationRegisterListDSL($meta, [hashtable]$p) { + $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" + $isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate" + + $columns = @() + # Period + if ($isPeriodic) { + $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } + } + # Recorder/LineNumber for subordinate registers + if ($isRecorderSubordinate) { + $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } + $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } + } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elKey = "labelField" + if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elKey = "labelField" + if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = @($tableEl) + attributes = @( + @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } } + ) + } +} + +# ─── AccumulationRegister ───────────────────────────────────────────────── + +function Generate-AccumulationRegisterDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "accumulationRegister.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + switch ($purpose) { + "List" { return Generate-AccumulationRegisterListDSL $meta $p } + } +} + +function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) { + $columns = @() + # AccumulationRegisters always have Period, Recorder, LineNumber + $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } + $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } + $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elKey = "labelField" + if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elKey = "labelField" + if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = @($tableEl) + attributes = @( + @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } } + ) + } +} + +# ─── ChartOfCharacteristicTypes (delegates to Catalog) ──────────────────── + +function Generate-ChartOfCharacteristicTypesDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + # Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc. + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose + + # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types + $catObjType = "CatalogObject.$($meta.Name)" + $ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)" + $catListType = "Catalog.$($meta.Name)" + $ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)" + + foreach ($a in $dsl.attributes) { + if ($a.type -eq $catObjType) { $a.type = $ccoctObjType } + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { + $a.settings.mainTable = $ccoctListType + } + } + + # For Item forms: inject ValueType field after Code/Description + if ($purpose -eq "Item" -and $dsl.elements) { + $vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" } + $newElements = @() + $inserted = $false + foreach ($el in $dsl.elements) { + $newElements += $el + if (-not $inserted) { + $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } + if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { + $newElements += $vtEl + $inserted = $true + } + } + } + if (-not $inserted) { $newElements += $vtEl } + $dsl.elements = $newElements + } + + return $dsl +} + +# ─── ExchangePlan (delegates to Catalog) ────────────────────────────────── + +function Generate-ExchangePlanDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + # ExchangePlans are not hierarchical and have no Folder form + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose + + # Post-patch: replace Catalog types with ExchangePlan types + $catObjType = "CatalogObject.$($meta.Name)" + $epObjType = "ExchangePlanObject.$($meta.Name)" + $catListType = "Catalog.$($meta.Name)" + $epListType = "ExchangePlan.$($meta.Name)" + + foreach ($a in $dsl.attributes) { + if ($a.type -eq $catObjType) { $a.type = $epObjType } + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { + $a.settings.mainTable = $epListType + } + } + + # For Item forms: inject SentNo, ReceivedNo after Code/Description + if ($purpose -eq "Item" -and $dsl.elements) { + $sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true } + $recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true } + $newElements = @() + $inserted = $false + foreach ($el in $dsl.elements) { + $newElements += $el + if (-not $inserted) { + $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } + if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { + $newElements += $sentEl + $newElements += $recvEl + $inserted = $true + } + } + } + if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl } + $dsl.elements = $newElements + } + + return $dsl +} + +# ─── ChartOfAccounts ────────────────────────────────────────────────────── + +function Generate-ChartOfAccountsDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "chartOfAccounts.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } + switch ($purpose) { + "Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData } + "Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p } + "List" { return Generate-ChartOfAccountsListDSL $meta $presetData } + "Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData } + } +} + +function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) { + $elements = @() + + # Header: Code + Parent + $headerLeftChildren = @() + if ($meta.CodeLength -gt 0) { + $headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + $headerRightChildren = @() + if ($meta.Hierarchical) { + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } + $headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } + } + + if ($headerRightChildren.Count -gt 0) { + $elements += [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren } + [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren } + ) + } + } elseif ($headerLeftChildren.Count -gt 0) { + $elements += $headerLeftChildren + } + + # Description + if ($meta.DescriptionLength -gt 0) { + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + } + + # OffBalance + $elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" } + + # AccountingFlags as checkboxes + if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) { + $flagChildren = @() + foreach ($flag in $meta.AccountingFlags) { + $flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" } + } + $elements += [ordered]@{ + group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета" + children = $flagChildren + } + } + + # ExtDimensionTypes table + if ($meta.MaxExtDimensionCount -gt 0) { + $edCols = @() + $edCols += [ordered]@{ input = "ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } + $edCols += [ordered]@{ check = "ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } + if ($meta.ExtDimensionAccountingFlags) { + foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { + $edCols += [ordered]@{ check = $edFlag.Name; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } + } + } + $elements += [ordered]@{ + table = "ВидыСубконто" + path = "Объект.ExtDimensionTypes" + columns = $edCols + } + } + + # Custom attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) + } + + # Tabular sections + $tsExclude = @("ДополнительныеРеквизиты","Представления") + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $tsCols = @() + foreach ($col in $ts.Columns) { + if (-not (Test-DisplayableType $col.Type)) { continue } + $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd) + } + $elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = $elements + attributes = @( + @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) { + $elements = @() + if ($meta.CodeLength -gt 0) { + $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + if ($meta.DescriptionLength -gt 0) { + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + } + if ($meta.Hierarchical) { + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } + $elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } + } + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + useForFoldersAndItems = "Folders" + properties = $props + elements = $elements + attributes = @( + @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) { + # Delegate to Catalog List and patch types + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List" + foreach ($a in $dsl.attributes) { + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { + $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" + } + } + return $dsl +} + +function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) { + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice" + foreach ($a in $dsl.attributes) { + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { + $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" + } + } + return $dsl +} + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + +# --- Detect XML format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } +$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved)) + +# --- 0. Path normalization and mode dispatch --- + +# Form name → purpose mapping +$script:formNameToPurpose = @{ + "ФормаДокумента" = "Item" + "ФормаЭлемента" = "Item" + "ФормаСписка" = "List" + "ФормаВыбора" = "Choice" + "ФормаГруппы" = "Folder" + "ФормаЗаписи" = "Record" + "ФормаСчета" = "Item" + "ФормаУзла" = "Item" +} + +if ($FromObject -and $JsonPath) { + Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode." + exit 1 +} +if (-not $FromObject -and -not $JsonPath) { + Write-Error "Either -JsonPath or -FromObject is required." + exit 1 +} + +if ($FromObject) { + # Normalize OutputPath: append /Ext/Form.xml if missing + $outNorm = $OutputPath -replace '[\\/]$', '' + if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') { + if ($outNorm -match '[/\\]Ext$') { + $OutputPath = "$outNorm/Form.xml" + } else { + $OutputPath = "$outNorm/Ext/Form.xml" + } + Write-Host "[resolved] OutputPath -> $OutputPath" + } + + # Resolve object path and purpose from OutputPath convention: + # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + $outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } + $pathParts = $outAbs -split '[/\\]' + # Find "Forms" segment + $formsIdx = -1 + for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { + if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break } + } + + $resolvedObjectPath = $null + $resolvedPurpose = $null + + if ($formsIdx -ge 2) { + $formName = $pathParts[$formsIdx + 1] + $objectName = $pathParts[$formsIdx - 1] + $typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar + + # Derive purpose from form name + if ($script:formNameToPurpose.ContainsKey($formName)) { + $resolvedPurpose = $script:formNameToPurpose[$formName] + } + + # Derive object XML path + $candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml" + if (Test-Path $candidateObjPath) { + $resolvedObjectPath = $candidateObjPath + } + } + + # Apply: explicit -ObjectPath / -Purpose override resolved values + $fromObjPath = $null + if ($ObjectPath) { + $fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath } + # Append .xml if missing + if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" } + } elseif ($resolvedObjectPath) { + $fromObjPath = $resolvedObjectPath + Write-Host "[resolved] ObjectPath -> $fromObjPath" + } else { + Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly." + exit 1 + } + + if (-not (Test-Path $fromObjPath)) { + Write-Error "Object file not found: $fromObjPath" + exit 1 + } + + $effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" } + if ($resolvedPurpose -and -not $Purpose) { + Write-Host "[resolved] Purpose -> $effectivePurpose" + } + + $meta = Parse-ObjectMeta $fromObjPath + Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)" + + $presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot + + $supportedPurposes = switch ($meta.Type) { + "Document" { @("Item","List","Choice") } + "Catalog" { @("Item","Folder","List","Choice") } + "InformationRegister" { @("Record","List") } + "AccumulationRegister" { @("List") } + "ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") } + "ExchangePlan" { @("Item","List","Choice") } + "ChartOfAccounts" { @("Item","Folder","List","Choice") } + default { @() } + } + if ($supportedPurposes.Count -eq 0) { + Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts." + exit 1 + } + if ($supportedPurposes -notcontains $effectivePurpose) { + Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')" + exit 1 + } + + # Generate DSL + $dsl = switch ($meta.Type) { + "Document" { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "Catalog" { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ChartOfAccounts" { Generate-ChartOfAccountsDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + } + + # Emit DSL if requested + if ($EmitDsl) { + $dslJson = $dsl | ConvertTo-Json -Depth 20 + $dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl } + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($dslPath, $dslJson, $enc) + Write-Host "[from-object] DSL saved: $dslPath" + } + + # Feed DSL into existing compiler + $dslJson = $dsl | ConvertTo-Json -Depth 20 + $def = $dslJson | ConvertFrom-Json +} else { + # --- 1. Load and validate JSON (original mode) --- + + if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 + } + + $json = Get-Content -Raw -Encoding UTF8 $JsonPath + $def = $json | ConvertFrom-Json +} + +# --- 2. ID allocator --- + +$script:nextId = 1 +function New-Id { + $id = $script:nextId + $script:nextId++ + return $id +} + +# --- 3. XML helper --- + +$script:xml = New-Object System.Text.StringBuilder 8192 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +# --- 4. Multilang helper --- + +function Emit-MLText { + param([string]$tag, [string]$text, [string]$indent) + X "$indent<$tag>" + X "$indent`t" + X "$indent`t`tru" + X "$indent`t`t$(Esc-Xml $text)" + X "$indent`t" + X "$indent" +} + +# --- 5. Type emitter --- + +$script:formTypeSynonyms = New-Object System.Collections.Hashtable +$script:formTypeSynonyms["строка"] = "string" +$script:formTypeSynonyms["число"] = "decimal" +$script:formTypeSynonyms["булево"] = "boolean" +$script:formTypeSynonyms["дата"] = "date" +$script:formTypeSynonyms["датавремя"]= "dateTime" +$script:formTypeSynonyms["number"] = "decimal" +$script:formTypeSynonyms["bool"] = "boolean" +$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" +$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" +$script:formTypeSynonyms["документссылка"] = "DocumentRef" +$script:formTypeSynonyms["документобъект"] = "DocumentObject" +$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" +$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:formTypeSynonyms["задачассылка"] = "TaskRef" +$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" + +# Known invalid types (runtime/UI types that don't exist in XDTO schema) +$script:knownInvalidTypes = @{ + "FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)" + "FormDataCollection" = "Runtime type. Use ValueTable" + "FormDataTree" = "Runtime type. Use ValueTree" + "FormDataTreeItem" = "Runtime type, not valid in XML" + "FormDataCollectionItem"= "Runtime type, not valid in XML" + "FormGroup" = "UI element type, not a data type" + "FormField" = "UI element type, not a data type" + "FormButton" = "UI element type, not a data type" + "FormDecoration" = "UI element type, not a data type" + "FormTable" = "UI element type, not a data type" +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) + if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $base = $Matches[1].Trim(); $params = $Matches[2] + $r = $script:formTypeSynonyms[$base.ToLower()] + if ($r) { return "$r($params)" } + return $typeStr + } + if ($typeStr.Contains('.')) { + $i = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) + $r = $script:formTypeSynonyms[$prefix.ToLower()] + if ($r) { return "$r$suffix" } + return $typeStr + } + $r = $script:formTypeSynonyms[$typeStr.ToLower()] + if ($r) { return $r } + return $typeStr +} + +function Emit-Type { + param($typeStr, [string]$indent) + + if (-not $typeStr) { + X "$indent" + return + } + + $typeString = "$typeStr" + + # Composite type: "Type1 | Type2" or "Type1 + Type2" + $parts = $typeString -split '\s*[|+]\s*' + + X "$indent" + foreach ($part in $parts) { + $part = $part.Trim() + Emit-SingleType -typeStr $part -indent "$indent`t" + } + X "$indent" +} + +function Emit-SingleType { + param([string]$typeStr, [string]$indent) + + $typeStr = Resolve-TypeStr $typeStr + + # boolean + if ($typeStr -eq "boolean") { + X "$indentxs:boolean" + return + } + + # string or string(N) + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + X "$indentxs:string" + X "$indent" + X "$indent`t$len" + X "$indent`tVariable" + X "$indent" + return + } + + # decimal(D,F) or decimal(D,F,nonneg) + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indentxs:decimal" + X "$indent" + X "$indent`t$digits" + X "$indent`t$fraction" + X "$indent`t$sign" + X "$indent" + return + } + + # date / dateTime / time + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + "time" { "Time" } + } + X "$indentxs:dateTime" + X "$indent" + X "$indent`t$fractions" + X "$indent" + return + } + + # ValueTable, ValueTree, ValueList, etc. + $v8Types = @{ + "ValueTable" = "v8:ValueTable" + "ValueTree" = "v8:ValueTree" + "ValueList" = "v8:ValueListType" + "TypeDescription" = "v8:TypeDescription" + "Universal" = "v8:Universal" + "FixedArray" = "v8:FixedArray" + "FixedStructure" = "v8:FixedStructure" + } + if ($v8Types.ContainsKey($typeStr)) { + X "$indent$($v8Types[$typeStr])" + return + } + + # UI types + $uiTypes = @{ + "FormattedString" = "v8ui:FormattedString" + "Picture" = "v8ui:Picture" + "Color" = "v8ui:Color" + "Font" = "v8ui:Font" + } + if ($uiTypes.ContainsKey($typeStr)) { + X "$indent$($uiTypes[$typeStr])" + return + } + + # DCS types + if ($typeStr -match '^DataComposition') { + $dcsMap = @{ + "DataCompositionSettings" = "dcsset:DataCompositionSettings" + "DataCompositionSchema" = "dcssch:DataCompositionSchema" + "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" + } + if ($dcsMap.ContainsKey($typeStr)) { + X "$indent$($dcsMap[$typeStr])" + return + } + } + + # DynamicList + if ($typeStr -eq "DynamicList") { + X "$indentcfg:DynamicList" + return + } + + # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) + if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') { + X "$indentcfg:$typeStr" + return + } + + # Fallback with validation + if ($script:knownInvalidTypes.ContainsKey($typeStr)) { + throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" + } + if ($typeStr.Contains('.')) { + X "$indentcfg:$typeStr" + } else { + Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" + X "$indent$typeStr" + } +} + +# --- 6. Event handler name generator --- + +$script:eventSuffixMap = @{ + "OnChange" = "ПриИзменении" + "StartChoice" = "НачалоВыбора" + "ChoiceProcessing" = "ОбработкаВыбора" + "AutoComplete" = "АвтоПодбор" + "Clearing" = "Очистка" + "Opening" = "Открытие" + "Click" = "Нажатие" + "OnActivateRow" = "ПриАктивизацииСтроки" + "BeforeAddRow" = "ПередНачаломДобавления" + "BeforeDeleteRow" = "ПередУдалением" + "BeforeRowChange" = "ПередНачаломИзменения" + "OnStartEdit" = "ПриНачалеРедактирования" + "OnEndEdit" = "ПриОкончанииРедактирования" + "Selection" = "ВыборСтроки" + "OnCurrentPageChange" = "ПриСменеСтраницы" + "TextEditEnd" = "ОкончаниеВводаТекста" + "URLProcessing" = "ОбработкаНавигационнойСсылки" + "DragStart" = "НачалоПеретаскивания" + "Drag" = "Перетаскивание" + "DragCheck" = "ПроверкаПеретаскивания" + "Drop" = "Помещение" + "AfterDeleteRow" = "ПослеУдаления" +} + +function Get-HandlerName { + param([string]$elementName, [string]$eventName) + $suffix = $script:eventSuffixMap[$eventName] + if ($suffix) { + return "$elementName$suffix" + } + return "$elementName$eventName" +} + +# --- 7. Element emitters --- + +function Get-ElementName { + param($el, [string]$typeKey) + if ($el.name) { return "$($el.name)" } + return "$($el.$typeKey)" +} + +$script:knownEvents = @{ + "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") + "check" = @("OnChange") + "radio" = @("OnChange") + "label" = @("Click","URLProcessing") + "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") + "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") + "pages" = @("OnCurrentPageChange") + "page" = @("OnCurrentPageChange") + "button" = @("Click") + "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") + "calendar" = @("OnChange","OnActivate") + "picture" = @("Click") + "cmdBar" = @() + "popup" = @() + "group" = @() +} +$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") + +function Emit-Events { + param($el, [string]$elementName, [string]$indent, [string]$typeKey) + + if (-not $el.on) { return } + + # Validate event names + if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { + $allowed = $script:knownEvents[$typeKey] + foreach ($evt in $el.on) { + if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") { + Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + } + } + } + + X "$indent" + foreach ($evt in $el.on) { + $evtName = "$evt" + $handler = if ($el.handlers -and $el.handlers.$evtName) { + "$($el.handlers.$evtName)" + } else { + Get-HandlerName -elementName $elementName -eventName $evtName + } + X "$indent`t$handler" + } + X "$indent" +} + +function Emit-Companion { + param([string]$tag, [string]$name, [string]$indent) + $id = New-Id + X "$indent<$tag name=`"$name`" id=`"$id`"/>" +} + +function Emit-Element { + param($el, [string]$indent, [bool]$inCmdBar = $false) + + # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). + # Maps any synonym to canonical short DSL key. + $synonyms = @{ + "commandBar" = "cmdBar" + "autoCommandBar" = "autoCmdBar" + "КоманднаяПанель" = "cmdBar" + "InputField" = "input" + "ПолеВвода" = "input" + "CheckBoxField" = "check" + "ПолеФлажка" = "check" + "RadioButtonField" = "radio" + "ПолеПереключателя" = "radio" + "radioButton" = "radio" + "PictureField" = "picField" + "ПолеКартинки" = "picField" + "LabelField" = "labelField" + "ПолеНадписи" = "labelField" + "CalendarField" = "calendar" + "ПолеКалендаря" = "calendar" + "LabelDecoration" = "label" + "Надпись" = "label" + "PictureDecoration" = "picture" + "Картинка" = "picture" + "UsualGroup" = "group" + "Группа" = "group" + "ОбычнаяГруппа" = "group" + "ColumnGroup" = "columnGroup" + "ГруппаКолонок" = "columnGroup" + "Pages" = "pages" + "ГруппаСтраниц" = "pages" + "Page" = "page" + "Страница" = "page" + "Table" = "table" + "Таблица" = "table" + "Button" = "button" + "Кнопка" = "button" + "Popup" = "popup" + "ВсплывающееМеню" = "popup" + } + foreach ($pair in $synonyms.GetEnumerator()) { + if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { + $val = $el.($pair.Key) + $el.PSObject.Properties.Remove($pair.Key) | Out-Null + $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force + } + } + + # Determine element type from key + $typeKey = $null + $xmlTag = $null + + foreach ($key in @("columnGroup","buttonGroup","group","input","check","radio","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { + $typeKey = $key + break + } + } + + if (-not $typeKey) { + Write-Warning "Unknown element type, skipping" + return + } + + # Validate known keys — warn about typos and unknown properties + $knownKeys = @{ + # type keys + "group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + # columnGroup-specific + "showInHeader"=1 + # radio-specific + "radioButtonType"=1;"choiceList"=1;"columnsCount"=1 + # naming & binding + "name"=1;"path"=1;"title"=1 + # visibility & state + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 + # events + "on"=1;"handlers"=1 + # layout + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + "maxWidth"=1;"maxHeight"=1 + "groupHorizontalAlign"=1;"groupVerticalAlign"=1;"horizontalAlign"=1 + # input-specific + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + "textEdit"=1 + # label/hyperlink + "hyperlink"=1 + # group-specific + "showTitle"=1;"united"=1;"collapsed"=1 + # hierarchy + "children"=1;"columns"=1 + # table-specific + "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1 + "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 + "rowPictureDataPath"=1;"tableAutofill"=1 + # pages-specific + "pagesRepresentation"=1 + # button-specific + "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 + # picture/decoration + "src"=1;"valuesPicture"=1;"loadTransparent"=1 + # cmdBar-specific + "autofill"=1 + } + foreach ($p in $el.PSObject.Properties) { + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." + } + } + + $name = Get-ElementName -el $el -typeKey $typeKey + $id = New-Id + + switch ($typeKey) { + "group" { Emit-Group -el $el -name $name -id $id -indent $indent } + "columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent } + "buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent } + "input" { Emit-Input -el $el -name $name -id $id -indent $indent } + "check" { Emit-Check -el $el -name $name -id $id -indent $indent } + "radio" { Emit-Radio -el $el -name $name -id $id -indent $indent } + "label" { Emit-Label -el $el -name $name -id $id -indent $indent } + "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } + "table" { Emit-Table -el $el -name $name -id $id -indent $indent } + "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } + "page" { Emit-Page -el $el -name $name -id $id -indent $indent } + "button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar } + "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } + "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } + "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } + "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } + "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } + } +} + +function Emit-CommonFlags { + param($el, [string]$indent) + if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } + if ($el.userVisible -eq $false) { + X "$indent" + X "$indent`tfalse" + X "$indent" + } + if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } + if ($el.readOnly -eq $true) { X "$indenttrue" } +} + +# Общие layout-свойства — применимы ко всем элементам. Порядок согласован с +# историческим выводом input/label, чтобы не сдвигать существующие снапшоты. +# -skipHeight: для Table (height → HeightInTableRows, эмитится в Emit-Table). +# -multiLineDefault: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. +function Emit-Layout { + param($el, [string]$indent, [switch]$skipHeight, [bool]$multiLineDefault = $false) + if ($el.skipOnInput -eq $true) { X "$indenttrue" } + $amwExplicit = ($el.PSObject.Properties.Name -contains 'autoMaxWidth') + if ($amwExplicit) { + if ($el.autoMaxWidth -eq $false) { X "$indentfalse" } + } elseif ($multiLineDefault) { + X "$indentfalse" + } + if ($null -ne $el.maxWidth) { X "$indent$($el.maxWidth)" } + if ($el.autoMaxHeight -eq $false) { X "$indentfalse" } + if ($null -ne $el.maxHeight) { X "$indent$($el.maxHeight)" } + if ($el.width) { X "$indent$($el.width)" } + if (-not $skipHeight -and $el.height) { X "$indent$($el.height)" } + if ($el.horizontalStretch -eq $true) { X "$indenttrue" } + if ($el.verticalStretch -eq $true) { X "$indenttrue" } + if ($el.groupHorizontalAlign) { X "$indent$($el.groupHorizontalAlign)" } + if ($el.groupVerticalAlign) { X "$indent$($el.groupVerticalAlign)" } + if ($el.horizontalAlign) { X "$indent$($el.horizontalAlign)" } +} + +function Title-FromName { + param([string]$name) + if (-not $name) { return '' } + $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') + $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') + $parts = $s -split ' ' + if ($parts.Count -eq 0) { return $s } + $out = New-Object System.Collections.ArrayList + [void]$out.Add($parts[0]) + for ($i = 1; $i -lt $parts.Count; $i++) { + $p = $parts[$i] + if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { + [void]$out.Add($p) + } else { + [void]$out.Add($p.ToLower()) + } + } + return ($out -join ' ') +} + +function Emit-Title { + param($el, [string]$name, [string]$indent, [switch]$auto) + $title = $el.title + if (-not $title -and $auto -and $name) { + $title = Title-FromName -name $name + } + if ($title) { + Emit-MLText -tag "Title" -text "$title" -indent $indent + } +} + +function Emit-Group { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation + $groupVal = "$($el.group)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } + "alwaysVertical" { "AlwaysVertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + + # Behavior + if ($groupVal -eq "collapsible") { + X "$innerVertical" + X "$innerCollapsible" + if ($el.collapsed -eq $true) { X "$innertrue" } + } + + # Representation + if ($el.representation) { + $repr = switch ("$($el.representation)") { + "none" { "None" } + "normal" { "NormalSeparation" } + "weak" { "WeakSeparation" } + "strong" { "StrongSeparation" } + default { "$($el.representation)" } + } + X "$inner$repr" + } + + # ShowTitle + if ($el.showTitle -eq $false) { X "$innerfalse" } + + # United + if ($el.united -eq $false) { X "$innerfalse" } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-ColumnGroup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation (horizontal / vertical / inCell — последнее только здесь) + $groupVal = "$($el.columnGroup)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "inCell" { "InCell" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + + if ($el.showTitle -eq $false) { X "$innerfalse" } + if ($null -ne $el.showInHeader) { + $shVal = if ($el.showInHeader) { "true" } else { "false" } + X "$inner$shVal" + } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Input { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + default { "$($el.titleLocation)" } + } + X "$inner$loc" + } + + if ($el.multiLine -eq $true) { X "$innertrue" } + if ($el.passwordMode -eq $true) { X "$innertrue" } + if ($el.choiceButton -eq $false) { X "$innerfalse" } + elseif ($el.choiceButton -eq $true -and ($el.on -contains 'StartChoice')) { X "$innertrue" } + if ($el.clearButton -eq $true) { X "$innertrue" } + if ($el.spinButton -eq $true) { X "$innertrue" } + if ($el.dropListButton -eq $true) { X "$innertrue" } + if ($el.markIncomplete -eq $true) { X "$innertrue" } + if ($el.textEdit -eq $false) { X "$innerfalse" } + Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true)) + + if ($el.inputHint) { + Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner + } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" + + X "$indent" +} + +function Emit-Check { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + $tl = if ($el.titleLocation) { "$($el.titleLocation)" } else { "Right" } + X "$inner$tl" + + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" + + X "$indent" +} + +# Maps Russian/English root of a typed reference path to canonical English root. +# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y". +$script:refRootSynonyms = @{ + "Перечисление" = "Enum" + "Справочник" = "Catalog" + "Документ" = "Document" + "ПланСчетов" = "ChartOfAccounts" + "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" + "ПланВидовРасчета" = "ChartOfCalculationTypes" + "ПланВидовРасчёта" = "ChartOfCalculationTypes" + "ПланОбмена" = "ExchangePlan" + "БизнесПроцесс" = "BusinessProcess" + "Задача" = "Task" + "РегистрСведений" = "InformationRegister" + "РегистрНакопления" = "AccumulationRegister" + "РегистрБухгалтерии" = "AccountingRegister" + "РегистрРасчета" = "CalculationRegister" + "РегистрРасчёта" = "CalculationRegister" +} +$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления") + +# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." } +function Normalize-ChoiceValue { + param($value) + + # Booleans + if ($value -is [bool]) { + return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } } + } + # Numbers (int / decimal / double) + if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) { + return @{ XsiType = "xs:decimal"; Text = "$value" } + } + + $s = "$value" + if ([string]::IsNullOrEmpty($s)) { + return @{ XsiType = "xs:string"; Text = "" } + } + + # Try to detect typed reference path: ".[..]" + $parts = $s -split '\.' + if ($parts.Count -ge 2) { + $root = $parts[0] + $canonRoot = $null + if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] } + elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root } + + if ($canonRoot) { + $typeName = $parts[1] + $normalized = $null + + if ($canonRoot -eq "Enum") { + if ($parts.Count -eq 2) { + # "Enum.X" alone — not a value, treat as string + } elseif ($parts.Count -eq 3) { + # "Enum.X.Y" — insert .EnumValue. + $normalized = "Enum.$typeName.EnumValue.$($parts[2])" + } else { + # "Enum.X..Y..." — replace member with EnumValue (handles ЗначениеПеречисления too) + $member = $parts[2] + if ($script:enumValueSynonyms -contains $member) { + $rest = $parts[3..($parts.Count-1)] -join '.' + $normalized = "Enum.$typeName.EnumValue.$rest" + } else { + $rest = $parts[2..($parts.Count-1)] -join '.' + $normalized = "Enum.$typeName.EnumValue.$rest" + } + } + } else { + # Other ref roots: just translate root, keep tail as-is + if ($parts.Count -ge 3) { + $tail = $parts[1..($parts.Count-1)] -join '.' + $normalized = "$canonRoot.$tail" + } + } + + if ($normalized) { + return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized } + } + } + } + + return @{ XsiType = "xs:string"; Text = $s } +} + +# Emit Presentation block for a choiceList item. +# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}. +# Empty/null → emits empty . +function Emit-ChoicePresentation { + param($pres, [string]$indent) + if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) { + X "$indent" + return + } + + $pairs = @() + if ($pres -is [string]) { + $pairs += ,@("ru", $pres) + } elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) { + foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") } + } elseif ($pres.PSObject -and $pres.PSObject.Properties) { + foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") } + } else { + $pairs += ,@("ru", "$pres") + } + + X "$indent" + foreach ($pair in $pairs) { + X "$indent`t" + X "$indent`t`t$($pair[0])" + X "$indent`t`t$(Esc-Xml $pair[1])" + X "$indent`t" + } + X "$indent" +} + +function Emit-Radio { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + # TitleLocation default is None for radio (matches typical configurator behavior) + $tl = if ($el.titleLocation) { + switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + default { "$($el.titleLocation)" } + } + } else { "None" } + X "$inner$tl" + + # RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms. + $rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" } + $rbt = switch -Regex ($rbtRaw.ToLower()) { + '^(auto|авто)$' { "Auto"; break } + '^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break } + '^(tumbler|тумблер)$' { "Tumbler"; break } + default { $rbtRaw } + } + X "$inner$rbt" + + if ($null -ne $el.columnsCount) { + X "$inner$($el.columnsCount)" + } + + # ChoiceList + if ($el.choiceList -and $el.choiceList.Count -gt 0) { + X "$inner" + $itemIndent = "$inner`t" + foreach ($item in $el.choiceList) { + # Pull value (and tolerate Russian synonym "значение") + $valRaw = $null + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("value")) { $valRaw = $item["value"] } + elseif ($item.Contains("значение")) { $valRaw = $item["значение"] } + } else { + if ($item.PSObject.Properties["value"]) { $valRaw = $item.value } + elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" } + } + + # Pull presentation (presentation OR title synonym) + $presRaw = $null + $hasPres = $false + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true } + elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true } + elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true } + } else { + if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true } + elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true } + elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true } + } + + $norm = Normalize-ChoiceValue -value $valRaw + + # Auto-derive presentation if missing + if (-not $hasPres) { + if ($norm.XsiType -eq "xr:DesignTimeRef") { + $tail = ($norm.Text -split '\.')[-1] + $presRaw = Title-FromName -name $tail + } elseif ($norm.XsiType -eq "xs:string") { + $presRaw = $norm.Text + } else { + $presRaw = $norm.Text + } + } + + X "$itemIndent" + $valIndent = "$itemIndent`t" + X "$valIndent" + X "$valIndent0" + X "$valIndent" + Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t" + X "$valIndent`t$(Esc-Xml $norm.Text)" + X "$valIndent" + X "$itemIndent" + } + X "$inner" + } + + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio" + + X "$indent" +} + +function Emit-Label { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + $labelTitle = if ($el.title) { "$($el.title)" } else { Title-FromName -name $name } + if ($labelTitle) { + $formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" } + X "$inner" + X "$inner`t<v8:item>" + X "$inner`t`t<v8:lang>ru</v8:lang>" + X "$inner`t`t<v8:content>$(Esc-Xml "$labelTitle")</v8:content>" + X "$inner`t</v8:item>" + X "$inner" + } + + Emit-CommonFlags -el $el -indent $inner + + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" + + X "$indent" +} + +function Emit-LabelField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" + + X "$indent" +} + +function Emit-Table { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + if ($el.changeRowSet -eq $true) { X "$innertrue" } + if ($el.changeRowOrder -eq $true) { X "$innertrue" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.header -eq $false) { X "$inner
false
" } + if ($el.footer -eq $true) { X "$inner
true
" } + + if ($el.commandBarLocation) { + X "$inner$($el.commandBarLocation)" + } + if ($el.searchStringLocation) { + X "$inner$($el.searchStringLocation)" + } + if ($el.choiceMode -eq $true) { X "$innertrue" } + if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } + if ($el.enableStartDrag -eq $true) { X "$innertrue" } + if ($el.enableDrag -eq $true) { X "$innertrue" } + if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } + Emit-Layout -el $el -indent $inner -skipHeight + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + # AutoCommandBar — with optional Autofill control + if ($null -ne $el.tableAutofill) { + $acbId = New-Id + X "$inner" + $afVal = if ($el.tableAutofill) { "true" } else { "false" } + X "$inner`t$afVal" + X "$inner" + } else { + Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + } + Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner + Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner + Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner + + # Columns + if ($el.columns -and $el.columns.Count -gt 0) { + X "$inner" + foreach ($col in $el.columns) { + Emit-Element -el $col -indent "$inner`t" + } + X "$inner" + } + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" + + X "$indent
" +} + +function Emit-Pages { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.pagesRepresentation) { + X "$inner$($el.pagesRepresentation)" + } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" + + # Children (pages) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Page { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner -auto + Emit-CommonFlags -el $el -indent $inner + + if ($el.group) { + $orientation = switch ("$($el.group)") { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } + "alwaysVertical" { "AlwaysVertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + } + Emit-Layout -el $el -indent $inner + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Button { + param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false) + + X "$indent" +} + +function Emit-PictureDecoration { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.picture -or $el.src) { + $ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" } + $lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" } + X "$inner" + X "$inner`t$ref" + X "$inner`t$lt" + X "$inner" + } + + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" + + X "$indent" +} + +function Emit-PictureField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + # ValuesPicture — picture (collection) used to render the field's value. + # Required for a Boolean-bound PictureField to actually show an icon. + # loadTransparent emitted only when true (1С default is false). + if ($el.valuesPicture) { + X "$inner" + X "$inner`t$($el.valuesPicture)" + if ($el.loadTransparent) { X "$inner`ttrue" } + X "$inner" + } + + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" + + X "$indent" +} + +function Emit-Calendar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" + + X "$indent" +} + +function Emit-CommandBar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.autofill -eq $true) { X "$innertrue" } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +function Emit-ButtonGroup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + # Children (кнопки в контексте командной панели) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +function Emit-Popup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner -auto + Emit-CommonFlags -el $el -indent $inner + + if ($el.picture) { + X "$inner" + X "$inner`t$($el.picture)" + X "$inner`ttrue" + X "$inner" + } + + if ($el.representation) { + X "$inner$($el.representation)" + } + Emit-Layout -el $el -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +# --- 8. Attribute emitter --- + +function Emit-Attributes { + param($attrs, [string]$indent) + + if (-not $attrs -or $attrs.Count -eq 0) { return } + + X "$indent" + foreach ($attr in $attrs) { + $attrId = New-Id + $attrName = "$($attr.name)" + + X "$indent`t" + $inner = "$indent`t`t" + + $attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' } + if ($attrTitle) { + Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner + } + + # Type + if ($attr.type) { + Emit-Type -typeStr "$($attr.type)" -indent $inner + } else { + X "$inner" + } + + if ($attr.main -eq $true) { + X "$innertrue" + } + $mainSaved = $false + if ($attr.main -eq $true -and $attr.type) { + $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' + } + if ($attr.savedData -eq $true -or $mainSaved) { + X "$innertrue" + } + if ($attr.fillChecking) { + X "$inner$($attr.fillChecking)" + } + + # Columns (for ValueTable/ValueTree) + if ($attr.columns -and $attr.columns.Count -gt 0) { + X "$inner" + foreach ($col in $attr.columns) { + $colId = New-Id + X "$inner`t" + if ($col.title) { + Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" + } + Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" + X "$inner`t" + } + X "$inner" + } + + # Settings (for DynamicList) + if ($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" } + X "$si$mq" + $ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" } + X "$si$ddr" + X "$inner" + } + + X "$indent`t" + } + X "$indent" +} + +# --- 9. Parameter emitter --- + +function Emit-Parameters { + param($params, [string]$indent) + + if (-not $params -or $params.Count -eq 0) { return } + + X "$indent" + foreach ($param in $params) { + X "$indent`t" + $inner = "$indent`t`t" + + Emit-Type -typeStr "$($param.type)" -indent $inner + + if ($param.key -eq $true) { + X "$innertrue" + } + + X "$indent`t" + } + X "$indent" +} + +# --- 10. Command emitter --- + +function Emit-Commands { + param($cmds, [string]$indent) + + if (-not $cmds -or $cmds.Count -eq 0) { return } + + X "$indent" + foreach ($cmd in $cmds) { + $cmdId = New-Id + X "$indent`t" + $inner = "$indent`t`t" + + $cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" } + if ($cmdTitle) { + Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner + } + + if ($cmd.tooltip) { + Emit-MLText -tag "ToolTip" -text "$($cmd.tooltip)" -indent $inner + } + + if ($cmd.action) { + X "$inner$($cmd.action)" + } + + if ($cmd.currentRowUse) { + X "$inner$($cmd.currentRowUse)" + } + + if ($cmd.shortcut) { + X "$inner$($cmd.shortcut)" + } + + if ($cmd.picture) { + X "$inner" + X "$inner`t$($cmd.picture)" + X "$inner`ttrue" + X "$inner" + } + + if ($cmd.representation) { + X "$inner$($cmd.representation)" + } + + X "$indent`t" + } + X "$indent" +} + +# --- 11. Properties emitter --- + +function Emit-Properties { + param($props, [string]$indent) + + if (-not $props) { return } + + # camelCase -> PascalCase mapping for known properties + $propMap = @{ + "autoTitle" = "AutoTitle" + "windowOpeningMode" = "WindowOpeningMode" + "commandBarLocation" = "CommandBarLocation" + "saveDataInSettings" = "SaveDataInSettings" + "autoSaveDataInSettings" = "AutoSaveDataInSettings" + "autoTime" = "AutoTime" + "usePostingMode" = "UsePostingMode" + "repostOnWrite" = "RepostOnWrite" + "autoURL" = "AutoURL" + "autoFillCheck" = "AutoFillCheck" + "customizable" = "Customizable" + "enterKeyBehavior" = "EnterKeyBehavior" + "verticalScroll" = "VerticalScroll" + "scalingMode" = "ScalingMode" + "useForFoldersAndItems" = "UseForFoldersAndItems" + "reportResult" = "ReportResult" + "detailsData" = "DetailsData" + "reportFormType" = "ReportFormType" + "autoShowState" = "AutoShowState" + "width" = "Width" + "height" = "Height" + "group" = "Group" + } + + foreach ($p in $props.PSObject.Properties) { + $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { + # Auto PascalCase: first letter uppercase + $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) + } + # Convert boolean to lowercase string (PS renders as True/False) + $val = $p.Value + if ($val -is [bool]) { + $val = if ($val) { "true" } else { "false" } + } + X "$indent<$xmlName>$val" + } +} + +# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- + +function Normalize-ElementSynonyms { + param($el) + if ($null -eq $el) { return } + $synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" } + foreach ($pair in $synonyms.GetEnumerator()) { + if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { + $val = $el.($pair.Key) + $el.PSObject.Properties.Remove($pair.Key) | Out-Null + $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force + } + } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { Normalize-ElementSynonyms $child } + } + if ($el.PSObject.Properties["columns"] -and $el.columns) { + foreach ($child in $el.columns) { Normalize-ElementSynonyms $child } + } +} + +function HasCmdBarRecursive { + param($el) + if ($null -eq $el) { return $false } + if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } } + } + if ($el.PSObject.Properties["columns"] -and $el.columns) { + foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } } + } + return $false +} + +function ApplyDynamicListTableHeuristic { + param($el, [string]$listName, [bool]$hasMainTable) + if ($null -eq $el) { return } + if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) { + if ($null -eq $el.PSObject.Properties["tableAutofill"]) { + $el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force + } + if ($null -eq $el.PSObject.Properties["commandBarLocation"]) { + $el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force + } + # DefaultPicture доступен только если у DynamicList есть основная таблица + if ($hasMainTable -and ($null -eq $el.PSObject.Properties["rowPictureDataPath"] -or [string]::IsNullOrEmpty("$($el.rowPictureDataPath)"))) { + $el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force + } + } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable } + } +} + +function Test-IsObjectLikeType { + param([string]$type) + if ([string]::IsNullOrEmpty($type)) { return $false } + if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true } + $objectSuffixes = @( + "CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject", + "ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject", + "TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject", + "ChartOfCalculationTypesObject", "ExchangePlanObject" + ) + $recordSetPrefixes = @( + "InformationRegisterRecordSet", "AccumulationRegisterRecordSet", + "AccountingRegisterRecordSet", "CalculationRegisterRecordSet", + "InformationRegisterRecordManager" + ) + foreach ($suffix in $objectSuffixes) { + if ($type -like "$suffix.*") { return $true } + } + foreach ($prefix in $recordSetPrefixes) { + if ($type -like "$prefix.*") { return $true } + } + return $false +} + +# 11b.1: Normalize synonyms recursively +if ($def.elements) { + foreach ($el in $def.elements) { Normalize-ElementSynonyms $el } +} + +# 11b.2: Extract autoCmdBar element from def.elements +$script:mainAcbDef = $null +if ($def.elements) { + $autoBars = @() + $rest = @() + foreach ($el in $def.elements) { + if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) { + $autoBars += $el + } else { + $rest += $el + } + } + if ($autoBars.Count -gt 1) { + Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed." + exit 1 + } + if ($autoBars.Count -eq 1) { + $script:mainAcbDef = $autoBars[0] + # Replace def.elements with the filtered list + $def.PSObject.Properties.Remove("elements") | Out-Null + $def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force + } +} + +# 11b.3: Infer main attribute (only if no attribute has main:true) +if ($def.attributes) { + $hasExplicitMain = $false + foreach ($attr in $def.attributes) { + if ($attr.main -eq $true) { $hasExplicitMain = $true; break } + } + if (-not $hasExplicitMain) { + $candidates = @() + foreach ($attr in $def.attributes) { + # Skip if user explicitly opted out via main:false + if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue } + if (Test-IsObjectLikeType "$($attr.type)") { + $candidates += $attr + } + } + if ($candidates.Count -eq 1) { + $candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force + Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))" + } elseif ($candidates.Count -gt 1) { + $names = ($candidates | ForEach-Object { $_.name }) -join ", " + Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly" + } + } +} + +# 11b.4: DynamicList → table heuristic +if ($def.attributes -and $def.elements) { + $mainAttr = $null + foreach ($attr in $def.attributes) { + if ($attr.main -eq $true) { $mainAttr = $attr; break } + } + if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") { + $mt = $null + if ($mainAttr.PSObject.Properties["settings"] -and $null -ne $mainAttr.settings) { + if ($mainAttr.settings -is [hashtable]) { + if ($mainAttr.settings.ContainsKey("mainTable")) { $mt = $mainAttr.settings["mainTable"] } + } elseif ($mainAttr.settings.PSObject.Properties["mainTable"]) { + $mt = $mainAttr.settings.mainTable + } + } + $hasMt = -not [string]::IsNullOrEmpty("$mt") + foreach ($el in $def.elements) { + ApplyDynamicListTableHeuristic $el $mainAttr.name $hasMt + } + } +} + +# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3 +function Compute-MainAcbAutofill { + if ($script:mainAcbDef) { + if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) { + return [bool]$script:mainAcbDef.autofill + } + return $true + } + if ($def.elements) { + foreach ($el in $def.elements) { + if (HasCmdBarRecursive $el) { return $false } + } + } + return $true +} + +# --- 12. Main compilation --- + +# Title +if ($def.title) { + Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t" +} + +# Header +X '' +X "
" + +# Oops — Title was emitted before header. Need to fix the order. +# Actually, let me restructure: build the body into a separate buffer, then assemble + +# Reset and rebuild properly +$script:xml = New-Object System.Text.StringBuilder 8192 +$script:nextId = 1 + +X '' +X "" + +# 12a. Title (from def.title or properties.title — must be multilingual XML) +$formTitle = $def.title +if (-not $formTitle -and $def.properties -and $def.properties.title) { + $formTitle = $def.properties.title +} +if ($formTitle) { + Emit-MLText -tag "Title" -text "$formTitle" -indent "`t" +} + +# 12b. Properties (skip 'title' — handled above as multilingual) +# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; +# otherwise platform appends synonym → "Title: Synonym" double-titles). +$propsClone = New-Object PSObject +$hasAutoTitle = $false +if ($def.properties) { + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true } + } +} +if ($formTitle -and -not $hasAutoTitle) { + $propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false +} +if ($def.properties) { + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -ne "title") { + $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value + } + } +} +Emit-Properties -props $propsClone -indent "`t" + +# 12c. CommandSet (excluded commands) +if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { + X "`t" + foreach ($cmd in $def.excludedCommands) { + X "`t`t$cmd" + } + X "`t" +} + +# 12d. AutoCommandBar (always present, id=-1) +$acbAutofill = Compute-MainAcbAutofill +$acbName = "ФормаКоманднаяПанель" +$acbHAlign = $null +if ($script:mainAcbDef) { + if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") { + $acbName = "$($script:mainAcbDef.autoCmdBar)" + } + if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") { + $acbName = "$($script:mainAcbDef.name)" + } + if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") { + $acbHAlign = "$($script:mainAcbDef.horizontalAlign)" + } +} +$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0) +$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren) +if ($acbHasInner) { + X "`t" + if ($acbHAlign) { X "`t`t$acbHAlign" } + if (-not $acbAutofill) { X "`t`tfalse" } + if ($hasAcbChildren) { + X "`t`t" + foreach ($child in $script:mainAcbDef.children) { + Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true + } + X "`t`t" + } + X "`t" +} else { + X "`t" +} + +# 12e. Events +if ($def.events) { + foreach ($p in $def.events.PSObject.Properties) { + if ($script:knownFormEvents -notcontains $p.Name) { + Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" + } + } + X "`t" + foreach ($p in $def.events.PSObject.Properties) { + X "`t`t$($p.Value)" + } + X "`t" +} + +# 12f. ChildItems (elements) +if ($def.elements -and $def.elements.Count -gt 0) { + X "`t" + foreach ($el in $def.elements) { + Emit-Element -el $el -indent "`t`t" + } + X "`t" +} + +# 12g. Attributes +Emit-Attributes -attrs $def.attributes -indent "`t" + +# 12h. Parameters +Emit-Parameters -params $def.parameters -indent "`t" + +# 12i. Commands +Emit-Commands -cmds $def.commands -indent "`t" + +# 12j. Close +X '' + +# --- 13. Write output --- + +$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } +$outDir = [System.IO.Path]::GetDirectoryName($outPath) +if (-not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) + +# --- 13b. Auto-register form in parent object XML --- + +# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml +$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) +$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) +$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) +$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) +$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) + +$formName = [System.IO.Path]::GetFileName($formNameDir) +$objectName = [System.IO.Path]::GetFileName($objectDir) +$formsLeaf = [System.IO.Path]::GetFileName($formsDir) + +if ($formsLeaf -eq 'Forms') { + $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" + if (Test-Path $objectXmlPath) { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objectXmlPath) + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) + if ($childObjects) { + $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) + if (-not $existing) { + $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formElem.InnerText = $formName + + $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) + if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } + + if ($insertBefore) { + $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null + $ws = $objDoc.CreateWhitespace("`n`t`t`t") + $childObjects.InsertBefore($ws, $insertBefore) | Out-Null + } else { + $lastChild = $childObjects.LastChild + if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { + $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null + $childObjects.InsertBefore($formElem, $lastChild) | Out-Null + } else { + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null + $childObjects.AppendChild($formElem) | Out-Null + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null + } + } + + $regEnc = New-Object System.Text.UTF8Encoding($true) + $regSettings = New-Object System.Xml.XmlWriterSettings + $regSettings.Encoding = $regEnc + $regSettings.Indent = $false + $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) + $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) + $objDoc.Save($regWriter) + $regWriter.Close() + $regStream.Close() + + Write-Host " Registered:
$formName
in $objectName.xml" + } + } + } +} + +# --- 14. Summary --- + +$elCount = $script:nextId - 1 +Write-Host "[OK] Compiled: $OutputPath" +Write-Host " Elements+IDs: $elCount" +if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } +if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } +if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index b4fc7ea1..79b0f054 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,3179 +1,3191 @@ -#!/usr/bin/env python3 -# form-compile v1.24 — Compile 1C managed form from JSON or object metadata -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -import argparse -import copy -import json -import os -import re -import sys -import uuid -import xml.etree.ElementTree as ET -from collections import OrderedDict - -# ═══════════════════════════════════════════════════════════════════════════ -# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation -# ═══════════════════════════════════════════════════════════════════════════ - -NS = { - 'md': 'http://v8.1c.ru/8.3/MDClasses', - 'xr': 'http://v8.1c.ru/8.3/xcf/readable', - 'v8': 'http://v8.1c.ru/8.1/data/core', -} - - -def _et_find(node, path): - """Find with namespace map.""" - return node.find(path, NS) - - -def _et_findall(node, path): - """Findall with namespace map.""" - return node.findall(path, NS) - - -def _et_text(node, path, default=''): - """Get text of a sub-element, or default.""" - el = node.find(path, NS) - return el.text if el is not None and el.text else default - - -def parse_object_meta(object_path): - """Parse 1C metadata XML and return dict with Type, Name, Synonym, Attributes, TabularSections, etc.""" - tree = ET.parse(object_path) - root = tree.getroot() - - # Detect object type from root child - meta_root = _et_find(root, '.') - # Root is MetaDataObject; first child is the type node - type_node = None - for child in root: - type_node = child - break - if type_node is None: - print("Not a 1C metadata XML: " + object_path, file=sys.stderr) - sys.exit(1) - - # Extract local name (strip namespace) - obj_type = type_node.tag.split('}')[-1] if '}' in type_node.tag else type_node.tag - - props_node = _et_find(type_node, 'md:Properties') - child_objs = _et_find(type_node, 'md:ChildObjects') - - # Name - obj_name = _et_text(props_node, 'md:Name') - - # Synonym (Russian) - synonym = obj_name - syn_node = _et_find(props_node, "md:Synonym/v8:item[v8:lang='ru']/v8:content") - if syn_node is not None and syn_node.text: - synonym = syn_node.text - - def extract_type(type_parent): - """Extract type string from md:Type element.""" - if type_parent is None: - return 'string' - types = [] - for t in _et_findall(type_parent, 'v8:Type'): - if t.text: - types.append(t.text) - if not types: - return 'string' - return ' | '.join(types) - - def is_ref_type(t): - return bool(re.search(r'Ref\.', t) or re.search(r'\u0441\u0441\u044b\u043b\u043a\u0430\.', t)) - - def extract_fields(parent_node, tag_name='Attribute'): - """Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag).""" - result = [] - if parent_node is None: - return result - for field_node in _et_findall(parent_node, f'md:{tag_name}'): - fp = _et_find(field_node, 'md:Properties') - f_name = _et_text(fp, 'md:Name') - f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") - f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name - f_type_node = _et_find(fp, 'md:Type') - f_type = extract_type(f_type_node) - result.append({ - 'Name': f_name, - 'Synonym': f_syn, - 'Type': f_type, - 'IsRef': is_ref_type(f_type), - }) - return result - - # Attributes - attributes = extract_fields(child_objs, 'Attribute') - - # Tabular sections - tabular_sections = [] - if child_objs is not None: - for ts_node in _et_findall(child_objs, 'md:TabularSection'): - tsp = _et_find(ts_node, 'md:Properties') - ts_name = _et_text(tsp, 'md:Name') - ts_syn_node = _et_find(tsp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") - ts_syn = ts_syn_node.text if ts_syn_node is not None and ts_syn_node.text else ts_name - ts_co = _et_find(ts_node, 'md:ChildObjects') - ts_cols = extract_fields(ts_co, 'Attribute') - tabular_sections.append({ - 'Name': ts_name, - 'Synonym': ts_syn, - 'Columns': ts_cols, - }) - - meta = { - 'Type': obj_type, - 'Name': obj_name, - 'Synonym': synonym, - 'Attributes': attributes, - 'TabularSections': tabular_sections, - } - - # Type-specific properties - if obj_type == 'Document': - nt_node = _et_find(props_node, 'md:NumberType') - meta['NumberType'] = nt_node.text if nt_node is not None and nt_node.text else 'String' - elif obj_type == 'Catalog': - cl_node = _et_find(props_node, 'md:CodeLength') - meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 - dl_node = _et_find(props_node, 'md:DescriptionLength') - meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 - hi_node = _et_find(props_node, 'md:Hierarchical') - meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true') - ht_node = _et_find(props_node, 'md:HierarchyType') - meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' - owners = [] - for ow in _et_findall(props_node, 'md:Owners/xr:Item'): - if ow.text: - owners.append(ow.text) - meta['Owners'] = owners - elif obj_type == 'InformationRegister': - meta['Dimensions'] = extract_fields(child_objs, 'Dimension') - meta['Resources'] = extract_fields(child_objs, 'Resource') - prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity') - meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical' - wm_node = _et_find(props_node, 'md:WriteMode') - meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent' - elif obj_type == 'AccumulationRegister': - meta['Dimensions'] = extract_fields(child_objs, 'Dimension') - meta['Resources'] = extract_fields(child_objs, 'Resource') - rt_node = _et_find(props_node, 'md:RegisterType') - meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances' - elif obj_type == 'ChartOfCharacteristicTypes': - cl_node = _et_find(props_node, 'md:CodeLength') - meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 - dl_node = _et_find(props_node, 'md:DescriptionLength') - meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 - hi_node = _et_find(props_node, 'md:Hierarchical') - meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true') - ht_node = _et_find(props_node, 'md:HierarchyType') - meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' - owners = [] - for ow in _et_findall(props_node, 'md:Owners/xr:Item'): - if ow.text: - owners.append(ow.text) - meta['Owners'] = owners - meta['HasValueType'] = True - elif obj_type == 'ExchangePlan': - cl_node = _et_find(props_node, 'md:CodeLength') - meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 - dl_node = _et_find(props_node, 'md:DescriptionLength') - meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 - meta['Hierarchical'] = False - meta['HierarchyType'] = None - meta['Owners'] = [] - elif obj_type == 'ChartOfAccounts': - cl_node = _et_find(props_node, 'md:CodeLength') - meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 - dl_node = _et_find(props_node, 'md:DescriptionLength') - meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 - meta['Hierarchical'] = True - ht_node = _et_find(props_node, 'md:HierarchyType') - meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' - meta['Owners'] = [] - max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount') - meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0 - meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag') - meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag') - - return meta - - -def _deep_merge(base, overlay): - """Deep merge two dicts. overlay wins on conflicts.""" - if not overlay: - return base - if not base: - return overlay - result = {} - for k in base: - result[k] = base[k] - for k in overlay: - if k in result and isinstance(result[k], dict) and isinstance(overlay[k], dict): - result[k] = _deep_merge(result[k], overlay[k]) - else: - result[k] = overlay[k] - return result - - -def load_preset(preset_name, script_dir, out_path_resolved): - """Load preset: hardcoded defaults -> built-in JSON -> project-level JSON, with deep merge.""" - defaults = { - 'document.item': { - 'header': {'position': 'insidePage', 'layout': '2col', 'distribute': 'even', 'dateTitle': '\u043e\u0442'}, - 'footer': {'fields': ['\u041a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439'], 'position': 'insidePage'}, - 'tabularSections': {'container': 'pages', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'], 'lineNumber': True}, - 'additional': {'position': 'page', 'layout': '2col', 'bspGroup': True}, - 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, - 'commandBar': 'auto', - 'properties': {'autoTitle': False}, - }, - 'document.list': { - 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True, - 'tableCommandBar': 'none', 'commandBar': 'auto', - 'properties': {}, - }, - 'document.choice': { - 'basedOn': 'document.list', - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - 'catalog.item': { - 'header': {'layout': '1col', 'distribute': 'left'}, - 'codeDescription': {'layout': 'horizontal', 'order': 'descriptionFirst'}, - 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443', 'position': 'afterCodeDescription'}, - 'owner': {'readOnly': True, 'position': 'first'}, - 'tabularSections': {'container': 'inline', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'], 'lineNumber': True}, - 'footer': {'fields': [], 'position': 'none'}, - 'additional': {'position': 'none', 'bspGroup': True}, - 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, - 'commandBar': 'auto', - 'properties': {}, - }, - 'catalog.folder': { - 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443'}, - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - 'catalog.list': { - 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True, - 'tableCommandBar': 'none', 'commandBar': 'auto', - 'properties': {}, - }, - 'catalog.choice': { - 'basedOn': 'catalog.list', 'choiceMode': True, - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - # --- Register defaults --- - 'informationRegister.record': { - 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - 'informationRegister.list': { - 'columns': 'all', 'columnType': 'labelField', - 'tableCommandBar': 'none', 'commandBar': 'auto', - 'properties': {}, - }, - 'accumulationRegister.list': { - 'columns': 'all', 'columnType': 'labelField', - 'tableCommandBar': 'none', 'commandBar': 'auto', - 'properties': {}, - }, - # --- Catalog-like type defaults --- - 'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'}, - 'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'}, - 'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'}, - 'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'}, - 'exchangePlan.item': {'basedOn': 'catalog.item'}, - 'exchangePlan.list': {'basedOn': 'catalog.list'}, - 'exchangePlan.choice': {'basedOn': 'catalog.choice'}, - # --- ChartOfAccounts defaults --- - 'chartOfAccounts.item': { - 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, - 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, - 'properties': {}, - }, - 'chartOfAccounts.folder': { - 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - 'chartOfAccounts.list': {'basedOn': 'catalog.list'}, - 'chartOfAccounts.choice': {'basedOn': 'catalog.choice'}, - } - - # Try built-in preset - 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 = json.load(f) - for k in list(preset_data.keys()): - defaults[k] = _deep_merge(defaults.get(k), preset_data[k]) - - # Try project-level preset (scan up from output path) - scan_dir = os.path.dirname(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) - for k in list(proj_data.keys()): - defaults[k] = _deep_merge(defaults.get(k), proj_data[k]) - break - parent_dir = os.path.dirname(scan_dir) - if parent_dir == scan_dir: - break - scan_dir = parent_dir - - # Resolve basedOn references - for k in list(defaults.keys()): - sect = defaults[k] - if isinstance(sect, dict) and 'basedOn' in sect: - base_name = sect['basedOn'] - if base_name in defaults: - merged = _deep_merge(defaults[base_name], sect) - merged.pop('basedOn', None) - defaults[k] = merged - - return defaults - - -# Non-displayable types — cannot be bound to form elements -NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения') - -def is_displayable_type(type_str): - return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES) - -def new_field_element(attr_name, data_path, attr_type, field_defaults, extra_props=None): - """Build a field element DSL entry.""" - is_ref = bool(re.search(r'Ref\.', attr_type)) - is_bool = bool(re.match(r'^\s*xs:boolean\s*$', attr_type) or attr_type == 'boolean' or re.search(r'Boolean', attr_type)) - - el_type = 'input' - if is_bool and field_defaults and field_defaults.get('boolean') and field_defaults['boolean'].get('element') == 'check': - el_type = 'check' - - el = OrderedDict() - el[el_type] = attr_name - el['path'] = data_path - - # Apply ref defaults - if is_ref and field_defaults and field_defaults.get('ref'): - if field_defaults['ref'].get('choiceButton') is True: - el['choiceButton'] = True - - # Extra props - if extra_props: - for k in extra_props: - el[k] = extra_props[k] - - return el - - -# --- Catalog DSL generators --- - -def generate_catalog_dsl(meta, preset_data, purpose): - purpose_key = f"catalog.{purpose.lower()}" - p = preset_data.get(purpose_key, {}) - fd = p.get('fieldDefaults', {}) - - dispatch = { - 'Folder': lambda: generate_catalog_folder_dsl(meta, p), - 'List': lambda: generate_catalog_list_dsl(meta, p), - 'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data), - 'Item': lambda: generate_catalog_item_dsl(meta, p, fd), - } - return dispatch[purpose]() - - -def generate_catalog_folder_dsl(meta, p): - elements = [] - # Code (if CodeLength > 0) - if meta.get('CodeLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - # Description - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - # Parent - parent_title = p.get('parent', {}).get('title') - parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) - if parent_title: - parent_el['title'] = parent_title - elements.append(parent_el) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - form_props = OrderedDict([('useForFoldersAndItems', 'Folders')]) - for k in props: - form_props[k] = props[k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', elements), - ('attributes', [ - OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) - ]), - ]) - - -def generate_catalog_list_dsl(meta, p): - columns = [] - # Description always first - columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')])) - # Code if present - if meta.get('CodeLength', 0) > 0: - columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')])) - # Custom attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - # Hidden ref - if p.get('hiddenRef', True) is not False: - columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns), - ]) - # Hierarchical properties - if meta.get('Hierarchical'): - table_el['initialTreeView'] = 'ExpandTopLevel' - table_el['enableStartDrag'] = True - table_el['enableDrag'] = True - - form_props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', [table_el]), - ('attributes', [ - OrderedDict([ - ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), - ('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])), - ]) - ]), - ]) - - -def generate_catalog_choice_dsl(meta, p, preset_data): - # Start from list - list_key = 'catalog.list' - lp = preset_data.get(list_key, {}) - dsl = generate_catalog_list_dsl(meta, lp) - - # Add choice-specific properties - dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' - if p.get('properties'): - for k in p['properties']: - dsl['properties'][k] = p['properties'][k] - - # Set ChoiceMode on table - dsl['elements'][0]['choiceMode'] = True - - return dsl - - -def generate_catalog_item_dsl(meta, p, fd): - header_children = [] - - # Owner (if subordinate) - if meta.get('Owners') and len(meta['Owners']) > 0: - owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)]) - header_children.append(owner_el) - - # Code + Description - cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal') - cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst') - has_code = meta.get('CodeLength', 0) > 0 - - if cd_layout == 'horizontal' and has_code: - cd_children = [] - desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]) - code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]) - if cd_order == 'descriptionFirst': - cd_children = [desc_el, code_el] - else: - cd_children = [code_el, desc_el] - header_children.append(OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False), - ('representation', 'none'), ('children', cd_children), - ])) - else: - # Vertical or no code - header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - if has_code: - header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - - # Parent (for hierarchical catalogs) - parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription') - parent_title = (p.get('parent') or {}).get('title') - if meta.get('Hierarchical'): - parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) - if parent_title: - parent_el['title'] = parent_title - if parent_pos == 'beforeCodeDescription': - insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0 - header_children.insert(insert_idx, parent_el) - else: - # afterCodeDescription (default) - header_children.append(parent_el) - - # Custom attributes -> header - footer_field_names = (p.get('footer') or {}).get('fields', []) - - for attr in meta['Attributes']: - if attr['Name'] in footer_field_names: - continue - if not is_displayable_type(attr['Type']): - continue - header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Build root elements - root_elements = [] - - # ГруппаШапка - root_elements.append(OrderedDict([ - ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), - ('representation', 'none'), ('children', header_children), - ])) - - # Tabular sections - ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] - if (p.get('tabularSections') or {}).get('exclude'): - ts_exclude = p['tabularSections']['exclude'] - ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) - - visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] - - for ts in visible_ts: - ts_cols = [] - if ts_line_number: - ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) - for col in ts['Columns']: - ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) - root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) - - # Footer fields - for fn in footer_field_names: - f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) - if f_attr: - root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) - - # BSP group - bsp_group = (p.get('additional') or {}).get('bspGroup', True) - if bsp_group: - root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) - - # Properties - form_props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - # UseForFoldersAndItems - if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems': - form_props['useForFoldersAndItems'] = 'Items' - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', root_elements), - ('attributes', [ - OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) - ]), - ]) - - -# --- Document DSL generators --- - -def generate_document_dsl(meta, preset_data, purpose): - purpose_key = f"document.{purpose.lower()}" - p = preset_data.get(purpose_key, {}) - fd = p.get('fieldDefaults', {}) - - dispatch = { - 'List': lambda: generate_document_list_dsl(meta, p), - 'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data), - 'Item': lambda: generate_document_item_dsl(meta, p, fd), - } - return dispatch[purpose]() - - -def generate_document_list_dsl(meta, p): - columns = [] - # Standard columns: Number + Date - columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) - columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) - # All custom attributes as labelField - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - # Hidden ref - if p.get('hiddenRef', True): - columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns), - ]) - - form_props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', [table_el]), - ('attributes', [ - OrderedDict([ - ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), - ('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])), - ]) - ]), - ]) - - -def generate_document_choice_dsl(meta, p, preset_data): - list_key = 'document.list' - lp = preset_data.get(list_key, {}) - dsl = generate_document_list_dsl(meta, lp) - - dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' - if p.get('properties'): - for k in p['properties']: - dsl['properties'][k] = p['properties'][k] - - return dsl - - -def generate_document_item_dsl(meta, p, fd): - header_pos = (p.get('header') or {}).get('position', 'insidePage') - header_layout = (p.get('header') or {}).get('layout', '2col') - header_distribute = (p.get('header') or {}).get('distribute', 'even') - date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442') - - footer_fields = (p.get('footer') or {}).get('fields', []) - footer_pos = (p.get('footer') or {}).get('position', 'insidePage') - - add_pos = (p.get('additional') or {}).get('position', 'page') - add_layout = (p.get('additional') or {}).get('layout', '2col') - add_bsp_group = (p.get('additional') or {}).get('bspGroup', True) - add_left = (p.get('additional') or {}).get('left', []) - add_right = (p.get('additional') or {}).get('right', []) - - header_right = (p.get('header') or {}).get('right', []) - - ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'] - if (p.get('tabularSections') or {}).get('exclude'): - ts_exclude = p['tabularSections']['exclude'] - ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) - - # Classify attributes - claimed = {} - for fn in footer_fields: - claimed[fn] = 'footer' - for fn in header_right: - claimed[fn] = 'header.right' - for fn in add_left: - claimed[fn] = 'additional.left' - for fn in add_right: - claimed[fn] = 'additional.right' - - unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed and is_displayable_type(attr['Type'])] - - # Distribute unclaimed - left_attrs = [] - right_extra_attrs = [] - if header_distribute == 'left': - left_attrs = unclaimed - elif header_distribute == 'right': - right_extra_attrs = unclaimed - else: # "even" - import math - half = math.ceil(len(unclaimed) / 2) if unclaimed else 0 - left_attrs = unclaimed[:half] - right_extra_attrs = unclaimed[half:] - - # Build ГруппаНомерДата - num_date_children = [ - OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]), - OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]), - ] - num_date_group = OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children), - ]) - - # Build left column - left_children = [num_date_group] - for attr in left_attrs: - left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Build right column - right_children = [] - for rn in header_right: - r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None) - if r_attr: - right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd)) - for attr in right_extra_attrs: - right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Header group - if header_layout == '2col' and len(right_children) > 0: - header_group = OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]), - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]), - ]), - ]) - else: - # 1col or no right items - all_header_fields = left_children + right_children - header_group = OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]), - ]), - ]) - - # Footer elements - footer_elements = [] - for fn in footer_fields: - f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) - if f_attr: - footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) - - # Visible tabular sections - visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] - - # Additional page content - additional_page = None - if add_pos == 'page': - add_left_els = [] - add_right_els = [] - for aln in add_left: - al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None) - if al_attr: - add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd)) - for arn in add_right: - ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None) - if ar_attr: - add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd)) - add_page_children = [] - if add_layout == '2col': - add_page_children.append(OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]), - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]), - ]), - ])) - else: - add_page_children.extend(add_left_els + add_right_els) - if add_bsp_group: - add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) - additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)]) - - # Build TS page elements - ts_pages = [] - for ts in visible_ts: - ts_cols = [] - if ts_line_number: - ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) - for col in ts['Columns']: - ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) - ts_pages.append(OrderedDict([ - ('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']), - ('children', [ - OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]) - ]), - ])) - - # Assemble root elements - root_elements = [] - - if len(visible_ts) == 0: - # Simple form - no Pages - root_elements.append(header_group) - if footer_elements: - root_elements.extend(footer_elements) - if add_bsp_group and add_pos != 'none': - root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) - else: - # Pages form - if header_pos == 'abovePages': - root_elements.append(header_group) - pages_children = list(ts_pages) - if additional_page: - pages_children.append(additional_page) - root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) - else: - # insidePage (default) - osnovnoe_children = [header_group] - if footer_pos == 'insidePage' and footer_elements: - osnovnoe_children.extend(footer_elements) - pages_children = [] - pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)])) - pages_children.extend(ts_pages) - if additional_page: - pages_children.append(additional_page) - root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) - - # Footer below pages - if footer_pos == 'belowPages' and footer_elements: - root_elements.extend(footer_elements) - - # Properties - form_props = OrderedDict([('autoTitle', False)]) - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', root_elements), - ('attributes', [ - OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)]) - ]), - ]) - - -# --- InformationRegister DSL generators --- - -def generate_information_register_dsl(meta, preset_data, purpose): - p_key = f"informationRegister.{purpose.lower()}" - p = preset_data.get(p_key, {}) - fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} - dispatch = { - 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), - 'List': lambda: generate_information_register_list_dsl(meta, p), - } - return dispatch[purpose]() - - -def generate_information_register_record_dsl(meta, p, fd): - elements = OrderedDict() - is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' - - # Period first (if periodic) - if is_periodic: - elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_information_register_list_dsl(meta, p): - is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' - is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' - - columns_list = [] - # Period - if is_periodic: - columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) - # Recorder/LineNumber for subordinate registers - if is_recorder_subordinate: - columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) - columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns_list), - ]) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', [table_el]), - ('attributes', [ - {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} - ]), - ]) - - -# --- AccumulationRegister DSL generators --- - -def generate_accumulation_register_dsl(meta, preset_data, purpose): - p_key = f"accumulationRegister.{purpose.lower()}" - p = preset_data.get(p_key, {}) - dispatch = { - 'List': lambda: generate_accumulation_register_list_dsl(meta, p), - } - return dispatch[purpose]() - - -def generate_accumulation_register_list_dsl(meta, p): - columns_list = [] - # AccumulationRegisters always have Period, Recorder, LineNumber - columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) - columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) - columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns_list), - ]) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', [table_el]), - ('attributes', [ - {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} - ]), - ]) - - -# --- ChartOfCharacteristicTypes (delegates to Catalog) --- - -def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): - # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. - dsl = generate_catalog_dsl(meta, preset_data, purpose) - - # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types - cat_obj_type = f"CatalogObject.{meta['Name']}" - ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" - cat_list_type = f"Catalog.{meta['Name']}" - ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" - - for a in dsl['attributes']: - if a.get('type') == cat_obj_type: - a['type'] = ccoct_obj_type - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: - a['settings']['mainTable'] = ccoct_list_type - - # For Item forms: inject ValueType field after Description/ГруппаКодНаименование - if purpose == 'Item' and dsl.get('elements'): - vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) - els = dsl['elements'] - if isinstance(els, list): - inserted = False - new_els = [] - for el in els: - new_els.append(el) - if not inserted and isinstance(el, dict): - name = el.get('input') or el.get('group') or '' - if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): - new_els.append(vt_el) - inserted = True - if not inserted: - new_els.append(vt_el) - dsl['elements'] = new_els - - return dsl - - -# --- ExchangePlan (delegates to Catalog) --- - -def generate_exchange_plan_dsl(meta, preset_data, purpose): - # ExchangePlans are not hierarchical and have no Folder form - dsl = generate_catalog_dsl(meta, preset_data, purpose) - - # Post-patch: replace Catalog types with ExchangePlan types - cat_obj_type = f"CatalogObject.{meta['Name']}" - ep_obj_type = f"ExchangePlanObject.{meta['Name']}" - cat_list_type = f"Catalog.{meta['Name']}" - ep_list_type = f"ExchangePlan.{meta['Name']}" - - for a in dsl['attributes']: - if a.get('type') == cat_obj_type: - a['type'] = ep_obj_type - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: - a['settings']['mainTable'] = ep_list_type - - # For Item forms: inject SentNo, ReceivedNo after Code/Description - if purpose == 'Item' and dsl.get('elements'): - sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) - recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) - els = dsl['elements'] - if isinstance(els, list): - inserted = False - new_els = [] - for el in els: - new_els.append(el) - if not inserted and isinstance(el, dict): - name = el.get('input') or el.get('group') or '' - if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): - new_els.append(sent_el) - new_els.append(recv_el) - inserted = True - if not inserted: - new_els.append(sent_el) - new_els.append(recv_el) - dsl['elements'] = new_els - - return dsl - - -# --- ChartOfAccounts DSL generators --- - -def generate_chart_of_accounts_dsl(meta, preset_data, purpose): - p_key = f"chartOfAccounts.{purpose.lower()}" - p = preset_data.get(p_key, {}) - fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} - dispatch = { - 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), - 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), - 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), - 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), - } - return dispatch[purpose]() - - -def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): - elements = [] - - # Header: Code + Parent - header_left_children = [] - if meta.get('CodeLength', 0) > 0: - header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - header_right_children = [] - if meta.get('Hierarchical'): - parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') - header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) - - if len(header_right_children) > 0: - elements.append(OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]), - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]), - ]), - ])) - elif len(header_left_children) > 0: - elements.extend(header_left_children) - - # Description - if meta.get('DescriptionLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - - # OffBalance - elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) - - # AccountingFlags as checkboxes - if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: - flag_children = [] - for flag in meta['AccountingFlags']: - flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) - elements.append(OrderedDict([ - ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), - ('children', flag_children), - ])) - - # ExtDimensionTypes table - if meta.get('MaxExtDimensionCount', 0) > 0: - ed_cols = [] - ed_cols.append(OrderedDict([('input', '\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) - ed_cols.append(OrderedDict([('check', '\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) - if meta.get('ExtDimensionAccountingFlags'): - for ed_flag in meta['ExtDimensionAccountingFlags']: - ed_cols.append(OrderedDict([('check', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) - elements.append(OrderedDict([ - ('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), - ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), - ('columns', ed_cols), - ])) - - # Custom attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Tabular sections - ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] - for ts in meta['TabularSections']: - if ts['Name'] in ts_exclude: - continue - ts_cols = [] - for col in ts['Columns']: - if not is_displayable_type(col['Type']): - continue - ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) - elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_chart_of_accounts_folder_dsl(meta, p): - elements = [] - if meta.get('CodeLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - if meta.get('DescriptionLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - if meta.get('Hierarchical'): - parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') - elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('useForFoldersAndItems', 'Folders'), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_chart_of_accounts_list_dsl(meta, preset_data): - # Delegate to Catalog List and patch types - dsl = generate_catalog_dsl(meta, preset_data, 'List') - for a in dsl['attributes']: - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": - a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" - return dsl - - -def generate_chart_of_accounts_choice_dsl(meta, preset_data): - dsl = generate_catalog_dsl(meta, preset_data, 'Choice') - for a in dsl['attributes']: - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": - a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" - return dsl - - -# ═══════════════════════════════════════════════════════════════════════════ -# END OF FROM-OBJECT MODE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════ - - -def esc_xml(s): - return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') - - -def emit_mltext(lines, indent, tag, text): - if not text: - lines.append(f"{indent}<{tag}/>") - return - lines.append(f"{indent}<{tag}>") - lines.append(f"{indent}\t") - lines.append(f"{indent}\t\tru") - lines.append(f"{indent}\t\t{esc_xml(text)}") - lines.append(f"{indent}\t") - lines.append(f"{indent}") - - -def new_uuid(): - return str(uuid.uuid4()) - - -def write_utf8_bom(path, content): - with open(path, 'w', encoding='utf-8-sig', newline='') as f: - f.write(content) - - -# --- ID allocator --- -_next_id = 0 - -def new_id(): - global _next_id - _next_id += 1 - return _next_id - - -# --- Event handler name generator --- - -EVENT_SUFFIX_MAP = { - "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", - "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430", - "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430", - "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440", - "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430", - "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435", - "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435", - "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438", - "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f", - "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c", - "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f", - "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438", - "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b", - "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430", - "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438", - "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", - "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435", - "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", - "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435", - "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f", -} - -KNOWN_EVENTS = { - "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], - "check": ["OnChange"], - "radio": ["OnChange"], - "label": ["Click", "URLProcessing"], - "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"], - "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"], - "pages": ["OnCurrentPageChange"], - "page": ["OnCurrentPageChange"], - "button": ["Click"], - "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"], - "calendar": ["OnChange", "OnActivate"], - "picture": ["Click"], - "cmdBar": [], - "popup": [], - "group": [], -} - -KNOWN_FORM_EVENTS = [ - "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing", - "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer", - "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer", - "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer", - "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening", -] - -KNOWN_KEYS = { - "group", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", - "button", "picture", "picField", "calendar", "cmdBar", "popup", - "showInHeader", - "radioButtonType", "choiceList", "columnsCount", - "name", "path", "title", - "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", - "on", "handlers", - "titleLocation", "representation", "width", "height", - "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", - "maxWidth", "maxHeight", - "multiLine", "passwordMode", "choiceButton", "clearButton", - "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", - "textEdit", - "hyperlink", - "showTitle", "united", "collapsed", - "children", "columns", - "changeRowSet", "changeRowOrder", "header", "footer", - "commandBarLocation", "searchStringLocation", - "pagesRepresentation", - "type", "command", "stdCommand", "defaultButton", "locationInCommandBar", - "src", "valuesPicture", "loadTransparent", - "autofill", - "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", - "rowPictureDataPath", "tableAutofill", -} - -TYPE_KEYS = ["columnGroup", "buttonGroup", "group", "input", "check", "radio", "label", "labelField", "table", "pages", "page", - "button", "picture", "picField", "calendar", "cmdBar", "popup"] - -# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) -ELEMENT_TYPE_SYNONYMS = { - "commandBar": "cmdBar", - "autoCommandBar": "autoCmdBar", - "КоманднаяПанель": "cmdBar", - "InputField": "input", - "ПолеВвода": "input", - "CheckBoxField": "check", - "ПолеФлажка": "check", - "RadioButtonField": "radio", - "ПолеПереключателя": "radio", - "radioButton": "radio", - "PictureField": "picField", - "ПолеКартинки": "picField", - "LabelField": "labelField", - "ПолеНадписи": "labelField", - "CalendarField": "calendar", - "ПолеКалендаря": "calendar", - "LabelDecoration": "label", - "Надпись": "label", - "PictureDecoration": "picture", - "Картинка": "picture", - "UsualGroup": "group", - "Группа": "group", - "ОбычнаяГруппа": "group", - "ColumnGroup": "columnGroup", - "ГруппаКолонок": "columnGroup", - "Pages": "pages", - "ГруппаСтраниц": "pages", - "Page": "page", - "Страница": "page", - "Table": "table", - "Таблица": "table", - "Button": "button", - "Кнопка": "button", - "Popup": "popup", - "ВсплывающееМеню": "popup", -} - -# Maps Russian/English root of typed reference path to canonical English root -REF_ROOT_SYNONYMS = { - "Перечисление": "Enum", - "Справочник": "Catalog", - "Документ": "Document", - "ПланСчетов": "ChartOfAccounts", - "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", - "ПланВидовРасчета": "ChartOfCalculationTypes", - "ПланВидовРасчёта": "ChartOfCalculationTypes", - "ПланОбмена": "ExchangePlan", - "БизнесПроцесс": "BusinessProcess", - "Задача": "Task", - "РегистрСведений": "InformationRegister", - "РегистрНакопления": "AccumulationRegister", - "РегистрБухгалтерии": "AccountingRegister", - "РегистрРасчета": "CalculationRegister", - "РегистрРасчёта": "CalculationRegister", -} -ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} - - -def normalize_choice_value(value): - """Returns dict {xsi_type, text} for a choiceList item value.""" - if isinstance(value, bool): - return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} - if isinstance(value, (int, float)): - return {"xsi_type": "xs:decimal", "text": str(value)} - - s = "" if value is None else str(value) - if not s: - return {"xsi_type": "xs:string", "text": ""} - - parts = s.split(".") - if len(parts) >= 2: - root = parts[0] - canon_root = None - if root in REF_ROOT_SYNONYMS: - canon_root = REF_ROOT_SYNONYMS[root] - elif root in REF_ROOT_SYNONYMS.values(): - canon_root = root - - if canon_root: - type_name = parts[1] - normalized = None - if canon_root == "Enum": - if len(parts) == 3: - normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" - elif len(parts) >= 4: - member = parts[2] - if member in ENUM_VALUE_SYNONYMS: - rest = ".".join(parts[3:]) - else: - rest = ".".join(parts[2:]) - normalized = f"Enum.{type_name}.EnumValue.{rest}" - else: - if len(parts) >= 3: - tail = ".".join(parts[1:]) - normalized = f"{canon_root}.{tail}" - - if normalized: - return {"xsi_type": "xr:DesignTimeRef", "text": normalized} - - return {"xsi_type": "xs:string", "text": s} - - -def emit_choice_presentation(lines, pres, indent): - """Accepts None/empty → ; str → ru only; dict → multi-lang.""" - if pres is None or (isinstance(pres, str) and pres == ""): - lines.append(f"{indent}") - return - - if isinstance(pres, str): - pairs = [("ru", pres)] - elif isinstance(pres, dict): - pairs = [(str(k), str(v)) for k, v in pres.items()] - else: - pairs = [("ru", str(pres))] - - lines.append(f"{indent}") - for lang, content in pairs: - lines.append(f"{indent}\t") - lines.append(f"{indent}\t\t{lang}") - lines.append(f"{indent}\t\t{esc_xml(content)}") - lines.append(f"{indent}\t") - lines.append(f"{indent}") - - -def normalize_radio_button_type(raw): - if not raw: - return "Auto" - s = str(raw).strip().lower() - if s in ("auto", "авто"): - return "Auto" - if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): - return "RadioButtons" - if s in ("tumbler", "тумблер"): - return "Tumbler" - return str(raw).strip() - - -def get_handler_name(element_name, event_name): - suffix = EVENT_SUFFIX_MAP.get(event_name) - if suffix: - return f"{element_name}{suffix}" - return f"{element_name}{event_name}" - - -def get_element_name(el, type_key): - if el.get('name'): - return str(el['name']) - return str(el.get(type_key, '')) - - -def emit_events(lines, el, element_name, indent, type_key): - if not el.get('on'): - return - - # Validate event names - if type_key and type_key in KNOWN_EVENTS: - allowed = KNOWN_EVENTS[type_key] - for evt in el['on']: - if allowed and str(evt) not in allowed: - print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") - - lines.append(f"{indent}") - for evt in el['on']: - evt_name = str(evt) - handlers = el.get('handlers') - if handlers and handlers.get(evt_name): - handler = str(handlers[evt_name]) - else: - handler = get_handler_name(element_name, evt_name) - lines.append(f'{indent}\t{handler}') - lines.append(f"{indent}") - - -def emit_companion(lines, tag, name, indent): - cid = new_id() - lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') - - -def emit_common_flags(lines, el, indent): - if el.get('visible') is False or el.get('hidden') is True: - lines.append(f"{indent}false") - if el.get('userVisible') is False: - lines.append(f"{indent}") - lines.append(f"{indent}\tfalse") - lines.append(f"{indent}") - if el.get('enabled') is False or el.get('disabled') is True: - lines.append(f"{indent}false") - if el.get('readOnly') is True: - lines.append(f"{indent}true") - - -def title_from_name(name): - """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" - if not name: - return '' - s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) - s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) - parts = s.split(' ') - if not parts: - return s - out = [parts[0]] - for p in parts[1:]: - out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) - return ' '.join(out) - - -def emit_title(lines, el, name, indent, auto=False): - title = el.get('title') - if not title and auto and name: - title = title_from_name(name) - if title: - emit_mltext(lines, indent, 'Title', str(title)) - - -# --- Type emitter --- - -V8_TYPES = { - "ValueTable": "v8:ValueTable", - "ValueTree": "v8:ValueTree", - "ValueList": "v8:ValueListType", - "TypeDescription": "v8:TypeDescription", - "Universal": "v8:Universal", - "FixedArray": "v8:FixedArray", - "FixedStructure": "v8:FixedStructure", -} - -UI_TYPES = { - "FormattedString": "v8ui:FormattedString", - "Picture": "v8ui:Picture", - "Color": "v8ui:Color", - "Font": "v8ui:Font", -} - -DCS_MAP = { - "DataCompositionSettings": "dcsset:DataCompositionSettings", - "DataCompositionSchema": "dcssch:DataCompositionSchema", - "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType", -} - -CFG_REF_PATTERN = re.compile( - r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|' - r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' - r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' - r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' - r'InformationRegisterRecordSet|InformationRegisterRecordManager|' - r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' - r'ConstantsSet|DataProcessorObject|ReportObject)\.' -) - -KNOWN_INVALID_TYPES = { - 'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', - 'FormDataCollection': 'Runtime type. Use ValueTable', - 'FormDataTree': 'Runtime type. Use ValueTree', - 'FormDataTreeItem': 'Runtime type, not valid in XML', - 'FormDataCollectionItem': 'Runtime type, not valid in XML', - 'FormGroup': 'UI element type, not a data type', - 'FormField': 'UI element type, not a data type', - 'FormButton': 'UI element type, not a data type', - 'FormDecoration': 'UI element type, not a data type', - 'FormTable': 'UI element type, not a data type', -} - - -_FORM_TYPE_SYNONYMS = { - "строка": "string", "число": "decimal", "булево": "boolean", - "дата": "date", "датавремя": "dateTime", - "number": "decimal", "bool": "boolean", - "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject", - "документссылка": "DocumentRef", "документобъект": "DocumentObject", - "перечислениессылка": "EnumRef", - "плансчетовссылка": "ChartOfAccountsRef", - "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", - "планвидоврасчётассылка": "ChartOfCalculationTypesRef", - "планвидоврасчетассылка": "ChartOfCalculationTypesRef", - "планобменассылка": "ExchangePlanRef", - "бизнеспроцессссылка": "BusinessProcessRef", - "задачассылка": "TaskRef", - "определяемыйтип": "DefinedType", -} - - -def resolve_type_str(type_str): - if not type_str: - return type_str - # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) - if type_str.startswith('cfg:'): - type_str = type_str[4:] - m = re.match(r'^([^(]+)\((.+)\)$', type_str) - if m: - base, params = m.group(1).strip(), m.group(2) - r = _FORM_TYPE_SYNONYMS.get(base.lower()) - return f"{r}({params})" if r else type_str - if '.' in type_str: - i = type_str.index('.') - prefix, suffix = type_str[:i], type_str[i:] - r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) - return f"{r}{suffix}" if r else type_str - r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) - return r if r else type_str - - -def emit_single_type(lines, type_str, indent): - type_str = resolve_type_str(type_str) - # boolean - if type_str == 'boolean': - lines.append(f'{indent}xs:boolean') - return - - # string or string(N) - m = re.match(r'^string(\((\d+)\))?$', type_str) - if m: - length = m.group(2) if m.group(2) else '0' - lines.append(f'{indent}xs:string') - lines.append(f'{indent}') - lines.append(f'{indent}\t{length}') - lines.append(f'{indent}\tVariable') - lines.append(f'{indent}') - return - - # decimal(D,F) or decimal(D,F,nonneg) - m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) - if m: - digits = m.group(1) - fraction = m.group(2) - sign = 'Nonnegative' if m.group(3) else 'Any' - lines.append(f'{indent}xs:decimal') - lines.append(f'{indent}') - lines.append(f'{indent}\t{digits}') - lines.append(f'{indent}\t{fraction}') - lines.append(f'{indent}\t{sign}') - lines.append(f'{indent}') - return - - # date / dateTime / time - m = re.match(r'^(date|dateTime|time)$', type_str) - if m: - fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'} - fractions = fractions_map[type_str] - lines.append(f'{indent}xs:dateTime') - lines.append(f'{indent}') - lines.append(f'{indent}\t{fractions}') - lines.append(f'{indent}') - return - - # V8 types - if type_str in V8_TYPES: - lines.append(f'{indent}{V8_TYPES[type_str]}') - return - - # UI types - if type_str in UI_TYPES: - lines.append(f'{indent}{UI_TYPES[type_str]}') - return - - # DCS types - if type_str.startswith('DataComposition'): - if type_str in DCS_MAP: - lines.append(f'{indent}{DCS_MAP[type_str]}') - return - - # DynamicList - if type_str == 'DynamicList': - lines.append(f'{indent}cfg:DynamicList') - return - - # cfg: references - if CFG_REF_PATTERN.match(type_str): - lines.append(f'{indent}cfg:{type_str}') - return - - # Fallback with validation - if type_str in KNOWN_INVALID_TYPES: - raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") - if '.' in type_str: - lines.append(f'{indent}cfg:{type_str}') - else: - print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) - lines.append(f'{indent}{type_str}') - - -def emit_type(lines, type_str, indent): - if not type_str: - lines.append(f'{indent}') - return - - type_string = str(type_str) - parts = [p.strip() for p in re.split(r'[|+]', type_string)] - - lines.append(f'{indent}') - for part in parts: - emit_single_type(lines, part, f'{indent}\t') - lines.append(f'{indent}') - - -# --- Element emitters --- - -def emit_element(lines, el, indent, in_cmd_bar=False): - # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) - for src, dst in ELEMENT_TYPE_SYNONYMS.items(): - if src in el and dst not in el: - el[dst] = el.pop(src) - - type_key = None - for key in TYPE_KEYS: - if el.get(key) is not None: - type_key = key - break - - if not type_key: - print("WARNING: Unknown element type, skipping", file=sys.stderr) - return - - # Validate known keys - for p_name in el.keys(): - if p_name not in KNOWN_KEYS: - print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) - - name = get_element_name(el, type_key) - eid = new_id() - - emitters = { - 'group': emit_group, - 'columnGroup': emit_column_group, - 'buttonGroup': emit_button_group, - 'input': emit_input, - 'check': emit_check, - 'radio': emit_radio_button_field, - 'label': emit_label, - 'labelField': emit_label_field, - 'table': emit_table, - 'pages': emit_pages, - 'page': emit_page, - 'button': emit_button, - 'picture': emit_picture_decoration, - 'picField': emit_picture_field, - 'calendar': emit_calendar, - 'cmdBar': emit_command_bar, - 'popup': emit_popup, - } - - emitter = emitters.get(type_key) - if emitter: - if type_key == 'button': - emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) - else: - emitter(lines, el, name, eid, indent) - - -def emit_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - # Group orientation - group_val = str(el.get('group', '')) - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'alwaysHorizontal': 'AlwaysHorizontal', - 'alwaysVertical': 'AlwaysVertical', - } - orientation = orientation_map.get(group_val) - if orientation: - lines.append(f'{inner}{orientation}') - - # Behavior - if group_val == 'collapsible': - lines.append(f'{inner}Vertical') - lines.append(f'{inner}Collapsible') - if el.get('collapsed') is True: - lines.append(f'{inner}true') - - # Representation - if el.get('representation'): - repr_map = { - 'none': 'None', - 'normal': 'NormalSeparation', - 'weak': 'WeakSeparation', - 'strong': 'StrongSeparation', - } - repr_val = repr_map.get(str(el['representation']), str(el['representation'])) - lines.append(f'{inner}{repr_val}') - - # ShowTitle - if el.get('showTitle') is False: - lines.append(f'{inner}false') - - # United - if el.get('united') is False: - lines.append(f'{inner}false') - - emit_common_flags(lines, el, inner) - - # Companion: ExtendedTooltip - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_column_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - group_val = str(el.get('columnGroup', '')) - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'inCell': 'InCell', - } - orientation = orientation_map.get(group_val) - if orientation: - lines.append(f'{inner}{orientation}') - - if el.get('showTitle') is False: - lines.append(f'{inner}false') - if el.get('showInHeader') is not None: - sh_val = 'true' if el['showInHeader'] else 'false' - lines.append(f'{inner}{sh_val}') - if el.get('width'): - lines.append(f'{inner}{el["width"]}') - - emit_common_flags(lines, el, inner) - - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) - - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_input(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('titleLocation'): - loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} - loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) - lines.append(f'{inner}{loc}') - - if el.get('multiLine') is True: - lines.append(f'{inner}true') - if el.get('passwordMode') is True: - lines.append(f'{inner}true') - if el.get('choiceButton') is False: - lines.append(f'{inner}false') - elif el.get('choiceButton') is True and 'StartChoice' in (el.get('on') or []): - lines.append(f'{inner}true') - if el.get('clearButton') is True: - lines.append(f'{inner}true') - if el.get('spinButton') is True: - lines.append(f'{inner}true') - if el.get('dropListButton') is True: - lines.append(f'{inner}true') - if el.get('markIncomplete') is True: - lines.append(f'{inner}true') - if el.get('textEdit') is False: - lines.append(f'{inner}false') - if el.get('skipOnInput') is True: - lines.append(f'{inner}true') - if 'autoMaxWidth' in el: - if el['autoMaxWidth'] is False: - lines.append(f'{inner}false') - elif el.get('multiLine') is True: - lines.append(f'{inner}false') - if el.get('maxWidth') is not None: - lines.append(f'{inner}{el["maxWidth"]}') - if el.get('autoMaxHeight') is False: - lines.append(f'{inner}false') - if el.get('maxHeight') is not None: - lines.append(f'{inner}{el["maxHeight"]}') - if el.get('width'): - lines.append(f'{inner}{el["width"]}') - if el.get('height'): - lines.append(f'{inner}{el["height"]}') - if el.get('horizontalStretch') is True: - lines.append(f'{inner}true') - if el.get('verticalStretch') is True: - lines.append(f'{inner}true') - - if el.get('inputHint'): - emit_mltext(lines, inner, 'InputHint', str(el['inputHint'])) - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'input') - - lines.append(f'{indent}') - - -def emit_check(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - tl = el.get('titleLocation') or 'Right' - lines.append(f'{inner}{tl}') - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'check') - - lines.append(f'{indent}') - - -def emit_radio_button_field(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - tl_raw = el.get('titleLocation') - if tl_raw: - loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} - tl = loc_map.get(str(tl_raw), str(tl_raw)) - else: - tl = 'None' - lines.append(f'{inner}{tl}') - - rbt = normalize_radio_button_type(el.get('radioButtonType')) - lines.append(f'{inner}{rbt}') - - if el.get('columnsCount') is not None: - lines.append(f'{inner}{el["columnsCount"]}') - - choice_list = el.get('choiceList') or [] - if choice_list: - lines.append(f'{inner}') - item_indent = f'{inner}\t' - for item in choice_list: - if not isinstance(item, dict): - continue - val_raw = item.get('value', item.get('значение')) - has_pres = any(k in item for k in ('presentation', 'представление', 'title')) - pres_raw = item.get('presentation', item.get('представление', item.get('title'))) - - norm = normalize_choice_value(val_raw) - - if not has_pres: - if norm['xsi_type'] == 'xr:DesignTimeRef': - tail = norm['text'].split('.')[-1] - pres_raw = title_from_name(tail) - else: - pres_raw = norm['text'] - - lines.append(f'{item_indent}') - val_indent = f'{item_indent}\t' - lines.append(f'{val_indent}') - lines.append(f'{val_indent}0') - lines.append(f'{val_indent}') - emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') - lines.append(f'{val_indent}\t{esc_xml(norm["text"])}') - lines.append(f'{val_indent}') - lines.append(f'{item_indent}') - lines.append(f'{inner}') - - emit_companion(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) - - emit_events(lines, el, name, inner, 'radio') - - lines.append(f'{indent}') - - -def emit_label(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - label_title = el.get('title') or title_from_name(name) - if label_title: - formatted = 'true' if el.get('hyperlink') is True else 'false' - lines.append(f'{inner}') - lines.append(f'{inner}\t<v8:item>') - lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>') - lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(label_title))}</v8:content>') - lines.append(f'{inner}\t</v8:item>') - lines.append(f'{inner}') - - emit_common_flags(lines, el, inner) - - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - if el.get('autoMaxWidth') is False: - lines.append(f'{inner}false') - if el.get('maxWidth') is not None: - lines.append(f'{inner}{el["maxWidth"]}') - if el.get('autoMaxHeight') is False: - lines.append(f'{inner}false') - if el.get('maxHeight') is not None: - lines.append(f'{inner}{el["maxHeight"]}') - if el.get('width'): - lines.append(f'{inner}{el["width"]}') - if el.get('height'): - lines.append(f'{inner}{el["height"]}') - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'label') - - lines.append(f'{indent}') - - -def emit_label_field(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'labelField') - - lines.append(f'{indent}') - - -def emit_table(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - if el.get('changeRowSet') is True: - lines.append(f'{inner}true') - if el.get('changeRowOrder') is True: - lines.append(f'{inner}true') - if el.get('height'): - lines.append(f'{inner}{el["height"]}') - if el.get('header') is False: - lines.append(f'{inner}
false
') - if el.get('footer') is True: - lines.append(f'{inner}
true
') - - if el.get('commandBarLocation'): - lines.append(f'{inner}{el["commandBarLocation"]}') - if el.get('searchStringLocation'): - lines.append(f'{inner}{el["searchStringLocation"]}') - - if el.get('choiceMode') is True: - lines.append(f'{inner}true') - if el.get('initialTreeView'): - lines.append(f'{inner}{el["initialTreeView"]}') - if el.get('enableStartDrag') is True: - lines.append(f'{inner}true') - if el.get('enableDrag') is True: - lines.append(f'{inner}true') - if el.get('rowPictureDataPath'): - lines.append(f'{inner}{el["rowPictureDataPath"]}') - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - # AutoCommandBar — with optional Autofill control - if el.get('tableAutofill') is not None: - acb_id = new_id() - acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' - af_val = 'true' if el['tableAutofill'] else 'false' - lines.append(f'{inner}') - lines.append(f'{inner}\t{af_val}') - lines.append(f'{inner}') - else: - emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner) - emit_companion(lines, 'SearchStringAddition', f'{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430', inner) - emit_companion(lines, 'ViewStatusAddition', f'{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430', inner) - emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner) - - # Columns - if el.get('columns') and len(el['columns']) > 0: - lines.append(f'{inner}') - for col in el['columns']: - emit_element(lines, col, f'{inner}\t') - lines.append(f'{inner}') - - emit_events(lines, el, name, inner, 'table') - - lines.append(f'{indent}
') - - -def emit_pages(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('pagesRepresentation'): - lines.append(f'{inner}{el["pagesRepresentation"]}') - - emit_common_flags(lines, el, inner) - - # Companion - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'pages') - - # Children (pages) - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_page(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner, auto=True) - emit_common_flags(lines, el, inner) - - if el.get('group'): - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'alwaysHorizontal': 'AlwaysHorizontal', - 'alwaysVertical': 'AlwaysVertical', - } - orientation = orientation_map.get(str(el['group'])) - if orientation: - lines.append(f'{inner}{orientation}') - - # Companion - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_button(lines, el, name, eid, indent, in_cmd_bar=False): - lines.append(f'{indent}') - - -def emit_picture_decoration(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - emit_common_flags(lines, el, inner) - - if el.get('picture') or el.get('src'): - ref = str(el.get('src') or el.get('picture')) - lines.append(f'{inner}') - lines.append(f'{inner}\t{ref}') - lines.append(f'{inner}\ttrue') - lines.append(f'{inner}') - - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - if el.get('width'): - lines.append(f'{inner}{el["width"]}') - if el.get('height'): - lines.append(f'{inner}{el["height"]}') - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'picture') - - lines.append(f'{indent}') - - -def emit_picture_field(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner) - emit_common_flags(lines, el, inner) - - # ValuesPicture \u2014 picture (collection) used to render the field's value. - # Required for a Boolean-bound PictureField to actually show an icon. - # loadTransparent emitted only when true (1\u0421 default is false). - if el.get('valuesPicture'): - lines.append(f'{inner}') - lines.append(f'{inner}\t{el["valuesPicture"]}') - if el.get('loadTransparent'): - lines.append(f'{inner}\ttrue') - lines.append(f'{inner}') - - if el.get('width'): - lines.append(f'{inner}{el["width"]}') - if el.get('height'): - lines.append(f'{inner}{el["height"]}') - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'picField') - - lines.append(f'{indent}') - - -def emit_calendar(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - # Companions - emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) - - emit_events(lines, el, name, inner, 'calendar') - - lines.append(f'{indent}') - - -def emit_command_bar(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('autofill') is True: - lines.append(f'{inner}true') - - emit_common_flags(lines, el, inner) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_popup(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner, auto=True) - emit_common_flags(lines, el, inner) - - if el.get('picture'): - lines.append(f'{inner}') - lines.append(f'{inner}\t{el["picture"]}') - lines.append(f'{inner}\ttrue') - lines.append(f'{inner}') - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_button_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - - emit_common_flags(lines, el, inner) - - # Companion: ExtendedTooltip - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) - - # Children (кнопки в контексте командной панели) - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -# --- Attribute emitter --- - -def emit_attributes(lines, attrs, indent): - if not attrs or len(attrs) == 0: - return - - lines.append(f'{indent}') - for attr in attrs: - attr_id = new_id() - attr_name = str(attr['name']) - - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - attr_title = attr.get('title') - if not attr_title and attr.get('main') is not True: - attr_title = title_from_name(attr_name) - if attr_title: - emit_mltext(lines, inner, 'Title', str(attr_title)) - - # Type - if attr.get('type'): - emit_type(lines, str(attr['type']), inner) - else: - lines.append(f'{inner}') - - if attr.get('main') is True: - lines.append(f'{inner}true') - main_saved = False - if attr.get('main') is True and attr.get('type'): - t = str(attr['type']) - main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) - if attr.get('savedData') is True or main_saved: - lines.append(f'{inner}true') - if attr.get('fillChecking'): - lines.append(f'{inner}{attr["fillChecking"]}') - - # Columns (for ValueTable/ValueTree) - if attr.get('columns') and len(attr['columns']) > 0: - lines.append(f'{inner}') - for col in attr['columns']: - col_id = new_id() - lines.append(f'{inner}\t') - if col.get('title'): - emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title'])) - emit_type(lines, str(col.get('type', '')), f'{inner}\t\t') - lines.append(f'{inner}\t') - lines.append(f'{inner}') - - # Settings (for DynamicList) - 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' - lines.append(f'{si}{mq}') - ddr = 'true' if s.get('dynamicDataRead') else 'false' - lines.append(f'{si}{ddr}') - lines.append(f'{inner}') - - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# --- Parameter emitter --- - -def emit_parameters(lines, params, indent): - if not params or len(params) == 0: - return - - lines.append(f'{indent}') - for param in params: - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - emit_type(lines, str(param.get('type', '')), inner) - - if param.get('key') is True: - lines.append(f'{inner}true') - - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# --- Command emitter --- - -def emit_commands(lines, cmds, indent): - if not cmds or len(cmds) == 0: - return - - lines.append(f'{indent}') - for cmd in cmds: - cmd_id = new_id() - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - cmd_title = cmd.get('title') or title_from_name(str(cmd['name'])) - if cmd_title: - emit_mltext(lines, inner, 'Title', str(cmd_title)) - - if cmd.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', str(cmd['tooltip'])) - - if cmd.get('action'): - lines.append(f'{inner}{cmd["action"]}') - - if cmd.get('currentRowUse'): - lines.append(f'{inner}{cmd["currentRowUse"]}') - - if cmd.get('shortcut'): - lines.append(f'{inner}{cmd["shortcut"]}') - - if cmd.get('picture'): - lines.append(f'{inner}') - lines.append(f'{inner}\t{cmd["picture"]}') - lines.append(f'{inner}\ttrue') - lines.append(f'{inner}') - - if cmd.get('representation'): - lines.append(f'{inner}{cmd["representation"]}') - - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# --- Properties emitter --- - -PROP_MAP = { - "autoTitle": "AutoTitle", - "windowOpeningMode": "WindowOpeningMode", - "commandBarLocation": "CommandBarLocation", - "saveDataInSettings": "SaveDataInSettings", - "autoSaveDataInSettings": "AutoSaveDataInSettings", - "autoTime": "AutoTime", - "usePostingMode": "UsePostingMode", - "repostOnWrite": "RepostOnWrite", - "autoURL": "AutoURL", - "autoFillCheck": "AutoFillCheck", - "customizable": "Customizable", - "enterKeyBehavior": "EnterKeyBehavior", - "verticalScroll": "VerticalScroll", - "scalingMode": "ScalingMode", - "useForFoldersAndItems": "UseForFoldersAndItems", - "reportResult": "ReportResult", - "detailsData": "DetailsData", - "reportFormType": "ReportFormType", - "autoShowState": "AutoShowState", - "width": "Width", - "height": "Height", - "group": "Group", -} - - -def emit_properties(lines, props, indent): - if not props: - return - - for p_name, p_value in props.items(): - xml_name = PROP_MAP.get(p_name) - if not xml_name: - # Auto PascalCase - xml_name = p_name[0].upper() + p_name[1:] - - # Convert boolean to lowercase - if isinstance(p_value, bool): - val = 'true' if p_value else 'false' - else: - val = str(p_value) - lines.append(f'{indent}<{xml_name}>{val}') - - - -def detect_format_version(d): - while d: - cfg_path = os.path.join(d, "Configuration.xml") - if os.path.isfile(cfg_path): - with open(cfg_path, "r", encoding="utf-8-sig") as f: - head = f.read(2000) - m = re.search(r']+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 _normalize_elements(defn): - """Convert dict-style elements from --from-object generators to list-style expected by compiler. - Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} - Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] - Also handles nested 'elements' in groups and 'columns' in tables recursively. - """ - def convert_elements(els): - if isinstance(els, list): - # Already list format — but may have nested dicts inside groups - result = [] - for el in els: - if isinstance(el, dict): - el = dict(el) # copy - if 'elements' in el and isinstance(el['elements'], dict): - el['elements'] = convert_elements(el['elements']) - if 'columns' in el and isinstance(el['columns'], dict): - el['columns'] = convert_columns(el['columns']) - result.append(el) - return result - if isinstance(els, dict): - result = [] - for name, props in els.items(): - if not isinstance(props, dict): - continue - new_el = {} - el_type = props.get('element', 'input') - # Map element type to the key name used in JSON DSL - type_map = { - 'input': 'input', 'check': 'check', 'labelField': 'labelField', - 'table': 'table', 'group': 'group', 'pages': 'pages', - 'page': 'page', 'label': 'label', 'button': 'button', - 'checkBox': 'check', 'radioButton': 'radioButton', - 'pictureField': 'pictureField', - } - mapped_type = type_map.get(el_type, el_type) - new_el[mapped_type] = name - for k, v in props.items(): - if k == 'element': - continue - if k == 'elements' and isinstance(v, dict): - new_el['elements'] = convert_elements(v) - elif k == 'columns' and isinstance(v, dict): - new_el['columns'] = convert_columns(v) - elif k == 'groupType': - # groupType → group property in DSL - new_el['group'] = v - elif k == 'showTitle': - new_el['showTitle'] = v - elif k == 'representation': - new_el['representation'] = v - elif k == 'autoCommandBar': - new_el['autoCommandBar'] = v - elif k == 'commandBarLocation': - new_el['commandBarLocation'] = v - else: - new_el[k] = v - result.append(new_el) - return result - return els - - def convert_columns(cols): - if isinstance(cols, list): - return cols - if isinstance(cols, dict): - result = [] - for name, props in cols.items(): - if not isinstance(props, dict): - continue - new_col = {} - el_type = props.get('element', 'input') - type_map = { - 'input': 'input', 'check': 'check', 'labelField': 'labelField', - 'checkBox': 'check', - } - mapped_type = type_map.get(el_type, el_type) - new_col[mapped_type] = name - for k, v in props.items(): - if k == 'element': - continue - new_col[k] = v - result.append(new_col) - return result - return cols - - if 'elements' in defn: - defn['elements'] = convert_elements(defn['elements']) - return defn - - -def main(): - sys.stdout.reconfigure(encoding="utf-8") - sys.stderr.reconfigure(encoding="utf-8") - global _next_id - - parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False) - parser.add_argument('-JsonPath', type=str, default=None) - parser.add_argument('-OutputPath', type=str, required=True) - parser.add_argument('-FromObject', action='store_true', default=False) - parser.add_argument('-ObjectPath', type=str, default=None) - parser.add_argument('-Purpose', type=str, default=None) - parser.add_argument('-Preset', type=str, default='erp-standard') - parser.add_argument('-EmitDsl', type=str, default=None) - args = parser.parse_args() - - # Form name -> purpose mapping - _FORM_NAME_TO_PURPOSE = { - '\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента - '\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента - '\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка - '\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора - '\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы - '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи - '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета - '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла - } - - # Mutual exclusion validation - if args.FromObject and args.JsonPath: - print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr) - sys.exit(1) - if not args.FromObject and not args.JsonPath: - print("Either -JsonPath or -FromObject is required.", file=sys.stderr) - sys.exit(1) - - # Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing - if args.FromObject: - out_norm = args.OutputPath.rstrip('/\\') - if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm): - if re.search(r'[/\\]Ext$', out_norm): - args.OutputPath = out_norm + '/Form.xml' - else: - args.OutputPath = out_norm + '/Ext/Form.xml' - print(f"[resolved] OutputPath -> {args.OutputPath}") - - # --- Detect XML format version --- - out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath) - format_version = detect_format_version(os.path.dirname(out_path_resolved)) - - # --- 0. From-object mode --- - if args.FromObject: - # Resolve object path and purpose from OutputPath convention: - # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml - out_abs = out_path_resolved - parts = re.split(r'[/\\]', out_abs) - forms_idx = -1 - for i in range(len(parts) - 1, -1, -1): - if parts[i] == 'Forms': - forms_idx = i - break - - resolved_object_path = None - resolved_purpose = None - - if forms_idx >= 2: - form_name = parts[forms_idx + 1] - object_name = parts[forms_idx - 1] - type_plural_and_above = os.sep.join(parts[:forms_idx - 1]) - - if form_name in _FORM_NAME_TO_PURPOSE: - resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name] - - candidate = os.path.join(type_plural_and_above, f'{object_name}.xml') - if os.path.exists(candidate): - resolved_object_path = candidate - - # Apply: explicit -ObjectPath / -Purpose override resolved - from_obj_path = None - if args.ObjectPath: - from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath) - if not from_obj_path.endswith('.xml'): - from_obj_path += '.xml' - elif resolved_object_path: - from_obj_path = resolved_object_path - print(f"[resolved] ObjectPath -> {from_obj_path}") - else: - print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr) - sys.exit(1) - - if not os.path.exists(from_obj_path): - print(f"Object file not found: {from_obj_path}", file=sys.stderr) - sys.exit(1) - - purpose = args.Purpose or resolved_purpose or 'Item' - if resolved_purpose and not args.Purpose: - print(f"[resolved] Purpose -> {purpose}") - - meta = parse_object_meta(from_obj_path) - print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}") - - preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved) - - supported = { - 'Document': ['Item', 'List', 'Choice'], - 'Catalog': ['Item', 'Folder', 'List', 'Choice'], - 'InformationRegister': ['Record', 'List'], - 'AccumulationRegister': ['List'], - 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], - 'ExchangePlan': ['Item', 'List', 'Choice'], - 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], - } - if meta['Type'] not in supported: - print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", file=sys.stderr) - sys.exit(1) - if purpose not in supported[meta['Type']]: - print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr) - sys.exit(1) - - dsl_dispatch = { - 'Document': generate_document_dsl, - 'Catalog': generate_catalog_dsl, - 'InformationRegister': generate_information_register_dsl, - 'AccumulationRegister': generate_accumulation_register_dsl, - 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, - 'ExchangePlan': generate_exchange_plan_dsl, - 'ChartOfAccounts': generate_chart_of_accounts_dsl, - } - dsl = dsl_dispatch[meta['Type']](meta, preset_data, purpose) - - if args.EmitDsl: - dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl) - os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True) - with open(dsl_path, 'w', encoding='utf-8') as f: - json.dump(dsl, f, ensure_ascii=False, indent=2) - print(f"[from-object] DSL saved: {dsl_path}") - - defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict - # Convert dict-style elements (from generators) to list-style (expected by compiler) - defn = _normalize_elements(defn) - else: - # --- 1. Load and validate JSON --- - json_path = args.JsonPath - if not os.path.exists(json_path): - print(f"File not found: {json_path}", file=sys.stderr) - sys.exit(1) - - with open(json_path, 'r', encoding='utf-8-sig') as f: - defn = json.load(f) - - # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- - def _normalize_synonyms(el): - if not isinstance(el, dict): - return - synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'} - for src, dst in synonyms.items(): - if src in el and dst not in el: - el[dst] = el.pop(src) - if isinstance(el.get('children'), list): - for child in el['children']: - _normalize_synonyms(child) - if isinstance(el.get('columns'), list): - for child in el['columns']: - _normalize_synonyms(child) - - def _has_cmd_bar_recursive(el): - if not isinstance(el, dict): - return False - if el.get('cmdBar') is not None: - return True - if isinstance(el.get('children'), list): - for child in el['children']: - if _has_cmd_bar_recursive(child): - return True - if isinstance(el.get('columns'), list): - for child in el['columns']: - if _has_cmd_bar_recursive(child): - return True - return False - - def _apply_dlist_table_heuristic(el, list_name, has_main_table): - if not isinstance(el, dict): - return - if el.get('table') is not None and str(el.get('path', '')) == list_name: - if 'tableAutofill' not in el: - el['tableAutofill'] = False - if 'commandBarLocation' not in el: - el['commandBarLocation'] = 'None' - # DefaultPicture доступен только если у DynamicList есть основная таблица - if has_main_table and not el.get('rowPictureDataPath'): - el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' - if isinstance(el.get('children'), list): - for child in el['children']: - _apply_dlist_table_heuristic(child, list_name, has_main_table) - - def _is_object_like_type(t): - if not t: - return False - if t == 'DynamicList' or t == 'ConstantsSet': - return True - object_suffixes = ( - 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', - 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', - 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', - 'ChartOfCalculationTypesObject', 'ExchangePlanObject', - ) - record_set_prefixes = ( - 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', - 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', - 'InformationRegisterRecordManager', - ) - for s in object_suffixes: - if t.startswith(s + '.'): - return True - for s in record_set_prefixes: - if t.startswith(s + '.'): - return True - return False - - # 1b.1: Normalize synonyms recursively - if isinstance(defn.get('elements'), list): - for el in defn['elements']: - _normalize_synonyms(el) - - # 1b.2: Extract autoCmdBar element from defn['elements'] - main_acb_def = None - if isinstance(defn.get('elements'), list): - auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] - if len(auto_bars) > 1: - print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) - sys.exit(1) - if len(auto_bars) == 1: - main_acb_def = auto_bars[0] - defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] - - # 1b.3: Infer main attribute - if isinstance(defn.get('attributes'), list): - has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) - if not has_explicit_main: - candidates = [] - for a in defn['attributes']: - if not isinstance(a, dict): - continue - if 'main' in a and a.get('main') is False: - continue - if _is_object_like_type(str(a.get('type', ''))): - candidates.append(a) - if len(candidates) == 1: - candidates[0]['main'] = True - print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") - elif len(candidates) > 1: - names = ', '.join(c.get('name', '') for c in candidates) - print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") - - # 1b.4: DynamicList → table heuristic - if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): - main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None) - if main_attr and str(main_attr.get('type', '')) == 'DynamicList': - settings = main_attr.get('settings') or {} - has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) - for el in defn['elements']: - _apply_dlist_table_heuristic(el, main_attr.get('name', ''), has_mt) - - # 1b.5: Compute main AutoCommandBar Autofill (B3) - def _compute_main_acb_autofill(): - if main_acb_def is not None: - if 'autofill' in main_acb_def: - return bool(main_acb_def.get('autofill')) - return True - if isinstance(defn.get('elements'), list): - for el in defn['elements']: - if _has_cmd_bar_recursive(el): - return False - return True - - # --- 2. Main compilation --- - _next_id = 0 - lines = [] - - lines.append('') - lines.append(f'
') - - # Title - form_title = defn.get('title') - if not form_title and defn.get('properties') and defn['properties'].get('title'): - form_title = defn['properties']['title'] - if form_title: - emit_mltext(lines, '\t', 'Title', str(form_title)) - - # Properties (skip 'title' — handled above) - # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; - # otherwise platform appends synonym → "Title: Synonym" double-titles). - props_src = defn.get('properties') or {} - props_clone = OrderedDict() - if form_title and 'autoTitle' not in props_src: - props_clone['autoTitle'] = False - for k, v in props_src.items(): - if k != 'title': - props_clone[k] = v - emit_properties(lines, props_clone, '\t') - - # CommandSet (excluded commands) - if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: - lines.append('\t') - for cmd in defn['excludedCommands']: - lines.append(f'\t\t{cmd}') - lines.append('\t') - - # AutoCommandBar (always present, id=-1) - acb_autofill = _compute_main_acb_autofill() - acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' - acb_halign = None - if main_acb_def is not None: - v = main_acb_def.get('autoCmdBar') - if v: - acb_name = str(v) - if main_acb_def.get('name'): - acb_name = str(main_acb_def['name']) - if main_acb_def.get('horizontalAlign'): - acb_halign = str(main_acb_def['horizontalAlign']) - has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) - has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children - if has_inner: - lines.append(f'\t') - if acb_halign: - lines.append(f'\t\t{acb_halign}') - if not acb_autofill: - lines.append('\t\tfalse') - if has_acb_children: - lines.append('\t\t') - for child in main_acb_def['children']: - emit_element(lines, child, '\t\t\t', in_cmd_bar=True) - lines.append('\t\t') - lines.append('\t') - else: - lines.append(f'\t') - - # Events - if defn.get('events'): - for evt_name in defn['events']: - if evt_name not in KNOWN_FORM_EVENTS: - print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}") - lines.append('\t') - for evt_name, evt_handler in defn['events'].items(): - lines.append(f'\t\t{evt_handler}') - lines.append('\t') - - # ChildItems (elements) - if defn.get('elements') and len(defn['elements']) > 0: - lines.append('\t') - for el in defn['elements']: - emit_element(lines, el, '\t\t') - lines.append('\t') - - # Attributes - emit_attributes(lines, defn.get('attributes'), '\t') - - # Parameters - emit_parameters(lines, defn.get('parameters'), '\t') - - # Commands - emit_commands(lines, defn.get('commands'), '\t') - - # Close - lines.append('') - - # --- 3. Write output --- - out_path = args.OutputPath - if not os.path.isabs(out_path): - out_path = os.path.join(os.getcwd(), out_path) - out_dir = os.path.dirname(out_path) - if out_dir and not os.path.exists(out_dir): - os.makedirs(out_dir, exist_ok=True) - - content = '\n'.join(lines) + '\n' - write_utf8_bom(out_path, content) - - # --- 4. Auto-register form in parent object XML --- - # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml - form_xml_dir = os.path.dirname(out_path) # Ext - form_name_dir = os.path.dirname(form_xml_dir) # FormName - forms_dir = os.path.dirname(form_name_dir) # Forms - object_dir = os.path.dirname(forms_dir) # ObjectName - type_plural_dir = os.path.dirname(object_dir) # TypePlural - - form_name = os.path.basename(form_name_dir) - object_name = os.path.basename(object_dir) - forms_leaf = os.path.basename(forms_dir) - - if forms_leaf == 'Forms': - object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') - if os.path.exists(object_xml_path): - with open(object_xml_path, 'r', encoding='utf-8-sig') as f: - raw_text = f.read() - - # Check if already registered - if f'
{form_name}
' not in raw_text: - # Insert before - if '' in raw_text: - insert_line = f'\t\t\t
{form_name}
\n' - raw_text = raw_text.replace('', insert_line + '\t\t', 1) - elif '' in raw_text: - replacement = f'\n\t\t\t
{form_name}
\n\t\t
' - raw_text = raw_text.replace('', replacement, 1) - - write_utf8_bom(object_xml_path, raw_text) - print(f" Registered:
{form_name}
in {object_name}.xml") - - # --- 5. Summary --- - el_count = _next_id - print(f"[OK] Compiled: {args.OutputPath}") - print(f" Elements+IDs: {el_count}") - if defn.get('attributes'): - print(f" Attributes: {len(defn['attributes'])}") - if defn.get('commands'): - print(f" Commands: {len(defn['commands'])}") - if defn.get('parameters'): - print(f" Parameters: {len(defn['parameters'])}") - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +# form-compile v1.25 — Compile 1C managed form from JSON or object metadata +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +import argparse +import copy +import json +import os +import re +import sys +import uuid +import xml.etree.ElementTree as ET +from collections import OrderedDict + +# ═══════════════════════════════════════════════════════════════════════════ +# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation +# ═══════════════════════════════════════════════════════════════════════════ + +NS = { + 'md': 'http://v8.1c.ru/8.3/MDClasses', + 'xr': 'http://v8.1c.ru/8.3/xcf/readable', + 'v8': 'http://v8.1c.ru/8.1/data/core', +} + + +def _et_find(node, path): + """Find with namespace map.""" + return node.find(path, NS) + + +def _et_findall(node, path): + """Findall with namespace map.""" + return node.findall(path, NS) + + +def _et_text(node, path, default=''): + """Get text of a sub-element, or default.""" + el = node.find(path, NS) + return el.text if el is not None and el.text else default + + +def parse_object_meta(object_path): + """Parse 1C metadata XML and return dict with Type, Name, Synonym, Attributes, TabularSections, etc.""" + tree = ET.parse(object_path) + root = tree.getroot() + + # Detect object type from root child + meta_root = _et_find(root, '.') + # Root is MetaDataObject; first child is the type node + type_node = None + for child in root: + type_node = child + break + if type_node is None: + print("Not a 1C metadata XML: " + object_path, file=sys.stderr) + sys.exit(1) + + # Extract local name (strip namespace) + obj_type = type_node.tag.split('}')[-1] if '}' in type_node.tag else type_node.tag + + props_node = _et_find(type_node, 'md:Properties') + child_objs = _et_find(type_node, 'md:ChildObjects') + + # Name + obj_name = _et_text(props_node, 'md:Name') + + # Synonym (Russian) + synonym = obj_name + syn_node = _et_find(props_node, "md:Synonym/v8:item[v8:lang='ru']/v8:content") + if syn_node is not None and syn_node.text: + synonym = syn_node.text + + def extract_type(type_parent): + """Extract type string from md:Type element.""" + if type_parent is None: + return 'string' + types = [] + for t in _et_findall(type_parent, 'v8:Type'): + if t.text: + types.append(t.text) + if not types: + return 'string' + return ' | '.join(types) + + def is_ref_type(t): + return bool(re.search(r'Ref\.', t) or re.search(r'\u0441\u0441\u044b\u043b\u043a\u0430\.', t)) + + def extract_fields(parent_node, tag_name='Attribute'): + """Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag).""" + result = [] + if parent_node is None: + return result + for field_node in _et_findall(parent_node, f'md:{tag_name}'): + fp = _et_find(field_node, 'md:Properties') + f_name = _et_text(fp, 'md:Name') + f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") + f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name + f_type_node = _et_find(fp, 'md:Type') + f_type = extract_type(f_type_node) + result.append({ + 'Name': f_name, + 'Synonym': f_syn, + 'Type': f_type, + 'IsRef': is_ref_type(f_type), + }) + return result + + # Attributes + attributes = extract_fields(child_objs, 'Attribute') + + # Tabular sections + tabular_sections = [] + if child_objs is not None: + for ts_node in _et_findall(child_objs, 'md:TabularSection'): + tsp = _et_find(ts_node, 'md:Properties') + ts_name = _et_text(tsp, 'md:Name') + ts_syn_node = _et_find(tsp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") + ts_syn = ts_syn_node.text if ts_syn_node is not None and ts_syn_node.text else ts_name + ts_co = _et_find(ts_node, 'md:ChildObjects') + ts_cols = extract_fields(ts_co, 'Attribute') + tabular_sections.append({ + 'Name': ts_name, + 'Synonym': ts_syn, + 'Columns': ts_cols, + }) + + meta = { + 'Type': obj_type, + 'Name': obj_name, + 'Synonym': synonym, + 'Attributes': attributes, + 'TabularSections': tabular_sections, + } + + # Type-specific properties + if obj_type == 'Document': + nt_node = _et_find(props_node, 'md:NumberType') + meta['NumberType'] = nt_node.text if nt_node is not None and nt_node.text else 'String' + elif obj_type == 'Catalog': + cl_node = _et_find(props_node, 'md:CodeLength') + meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 + dl_node = _et_find(props_node, 'md:DescriptionLength') + meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 + hi_node = _et_find(props_node, 'md:Hierarchical') + meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true') + ht_node = _et_find(props_node, 'md:HierarchyType') + meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' + owners = [] + for ow in _et_findall(props_node, 'md:Owners/xr:Item'): + if ow.text: + owners.append(ow.text) + meta['Owners'] = owners + elif obj_type == 'InformationRegister': + meta['Dimensions'] = extract_fields(child_objs, 'Dimension') + meta['Resources'] = extract_fields(child_objs, 'Resource') + prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity') + meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical' + wm_node = _et_find(props_node, 'md:WriteMode') + meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent' + elif obj_type == 'AccumulationRegister': + meta['Dimensions'] = extract_fields(child_objs, 'Dimension') + meta['Resources'] = extract_fields(child_objs, 'Resource') + rt_node = _et_find(props_node, 'md:RegisterType') + meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances' + elif obj_type == 'ChartOfCharacteristicTypes': + cl_node = _et_find(props_node, 'md:CodeLength') + meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 + dl_node = _et_find(props_node, 'md:DescriptionLength') + meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 + hi_node = _et_find(props_node, 'md:Hierarchical') + meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true') + ht_node = _et_find(props_node, 'md:HierarchyType') + meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' + owners = [] + for ow in _et_findall(props_node, 'md:Owners/xr:Item'): + if ow.text: + owners.append(ow.text) + meta['Owners'] = owners + meta['HasValueType'] = True + elif obj_type == 'ExchangePlan': + cl_node = _et_find(props_node, 'md:CodeLength') + meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 + dl_node = _et_find(props_node, 'md:DescriptionLength') + meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 + meta['Hierarchical'] = False + meta['HierarchyType'] = None + meta['Owners'] = [] + elif obj_type == 'ChartOfAccounts': + cl_node = _et_find(props_node, 'md:CodeLength') + meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 + dl_node = _et_find(props_node, 'md:DescriptionLength') + meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 + meta['Hierarchical'] = True + ht_node = _et_find(props_node, 'md:HierarchyType') + meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' + meta['Owners'] = [] + max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount') + meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0 + meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag') + meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag') + + return meta + + +def _deep_merge(base, overlay): + """Deep merge two dicts. overlay wins on conflicts.""" + if not overlay: + return base + if not base: + return overlay + result = {} + for k in base: + result[k] = base[k] + for k in overlay: + if k in result and isinstance(result[k], dict) and isinstance(overlay[k], dict): + result[k] = _deep_merge(result[k], overlay[k]) + else: + result[k] = overlay[k] + return result + + +def load_preset(preset_name, script_dir, out_path_resolved): + """Load preset: hardcoded defaults -> built-in JSON -> project-level JSON, with deep merge.""" + defaults = { + 'document.item': { + 'header': {'position': 'insidePage', 'layout': '2col', 'distribute': 'even', 'dateTitle': '\u043e\u0442'}, + 'footer': {'fields': ['\u041a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439'], 'position': 'insidePage'}, + 'tabularSections': {'container': 'pages', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'], 'lineNumber': True}, + 'additional': {'position': 'page', 'layout': '2col', 'bspGroup': True}, + 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, + 'commandBar': 'auto', + 'properties': {'autoTitle': False}, + }, + 'document.list': { + 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True, + 'tableCommandBar': 'none', 'commandBar': 'auto', + 'properties': {}, + }, + 'document.choice': { + 'basedOn': 'document.list', + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + 'catalog.item': { + 'header': {'layout': '1col', 'distribute': 'left'}, + 'codeDescription': {'layout': 'horizontal', 'order': 'descriptionFirst'}, + 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443', 'position': 'afterCodeDescription'}, + 'owner': {'readOnly': True, 'position': 'first'}, + 'tabularSections': {'container': 'inline', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'], 'lineNumber': True}, + 'footer': {'fields': [], 'position': 'none'}, + 'additional': {'position': 'none', 'bspGroup': True}, + 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, + 'commandBar': 'auto', + 'properties': {}, + }, + 'catalog.folder': { + 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443'}, + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + 'catalog.list': { + 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True, + 'tableCommandBar': 'none', 'commandBar': 'auto', + 'properties': {}, + }, + 'catalog.choice': { + 'basedOn': 'catalog.list', 'choiceMode': True, + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + # --- Register defaults --- + 'informationRegister.record': { + 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + 'informationRegister.list': { + 'columns': 'all', 'columnType': 'labelField', + 'tableCommandBar': 'none', 'commandBar': 'auto', + 'properties': {}, + }, + 'accumulationRegister.list': { + 'columns': 'all', 'columnType': 'labelField', + 'tableCommandBar': 'none', 'commandBar': 'auto', + 'properties': {}, + }, + # --- Catalog-like type defaults --- + 'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'}, + 'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'}, + 'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'}, + 'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'}, + 'exchangePlan.item': {'basedOn': 'catalog.item'}, + 'exchangePlan.list': {'basedOn': 'catalog.list'}, + 'exchangePlan.choice': {'basedOn': 'catalog.choice'}, + # --- ChartOfAccounts defaults --- + 'chartOfAccounts.item': { + 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, + 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, + 'properties': {}, + }, + 'chartOfAccounts.folder': { + 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + 'chartOfAccounts.list': {'basedOn': 'catalog.list'}, + 'chartOfAccounts.choice': {'basedOn': 'catalog.choice'}, + } + + # Try built-in preset + 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 = json.load(f) + for k in list(preset_data.keys()): + defaults[k] = _deep_merge(defaults.get(k), preset_data[k]) + + # Try project-level preset (scan up from output path) + scan_dir = os.path.dirname(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) + for k in list(proj_data.keys()): + defaults[k] = _deep_merge(defaults.get(k), proj_data[k]) + break + parent_dir = os.path.dirname(scan_dir) + if parent_dir == scan_dir: + break + scan_dir = parent_dir + + # Resolve basedOn references + for k in list(defaults.keys()): + sect = defaults[k] + if isinstance(sect, dict) and 'basedOn' in sect: + base_name = sect['basedOn'] + if base_name in defaults: + merged = _deep_merge(defaults[base_name], sect) + merged.pop('basedOn', None) + defaults[k] = merged + + return defaults + + +# Non-displayable types — cannot be bound to form elements +NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения') + +def is_displayable_type(type_str): + return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES) + +def new_field_element(attr_name, data_path, attr_type, field_defaults, extra_props=None): + """Build a field element DSL entry.""" + is_ref = bool(re.search(r'Ref\.', attr_type)) + is_bool = bool(re.match(r'^\s*xs:boolean\s*$', attr_type) or attr_type == 'boolean' or re.search(r'Boolean', attr_type)) + + el_type = 'input' + if is_bool and field_defaults and field_defaults.get('boolean') and field_defaults['boolean'].get('element') == 'check': + el_type = 'check' + + el = OrderedDict() + el[el_type] = attr_name + el['path'] = data_path + + # Apply ref defaults + if is_ref and field_defaults and field_defaults.get('ref'): + if field_defaults['ref'].get('choiceButton') is True: + el['choiceButton'] = True + + # Extra props + if extra_props: + for k in extra_props: + el[k] = extra_props[k] + + return el + + +# --- Catalog DSL generators --- + +def generate_catalog_dsl(meta, preset_data, purpose): + purpose_key = f"catalog.{purpose.lower()}" + p = preset_data.get(purpose_key, {}) + fd = p.get('fieldDefaults', {}) + + dispatch = { + 'Folder': lambda: generate_catalog_folder_dsl(meta, p), + 'List': lambda: generate_catalog_list_dsl(meta, p), + 'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data), + 'Item': lambda: generate_catalog_item_dsl(meta, p, fd), + } + return dispatch[purpose]() + + +def generate_catalog_folder_dsl(meta, p): + elements = [] + # Code (if CodeLength > 0) + if meta.get('CodeLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + # Description + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + # Parent + parent_title = p.get('parent', {}).get('title') + parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) + if parent_title: + parent_el['title'] = parent_title + elements.append(parent_el) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + form_props = OrderedDict([('useForFoldersAndItems', 'Folders')]) + for k in props: + form_props[k] = props[k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', elements), + ('attributes', [ + OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) + ]), + ]) + + +def generate_catalog_list_dsl(meta, p): + columns = [] + # Description always first + columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')])) + # Code if present + if meta.get('CodeLength', 0) > 0: + columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')])) + # Custom attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + # Hidden ref + if p.get('hiddenRef', True) is not False: + columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns), + ]) + # Hierarchical properties + if meta.get('Hierarchical'): + table_el['initialTreeView'] = 'ExpandTopLevel' + table_el['enableStartDrag'] = True + table_el['enableDrag'] = True + + form_props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', [table_el]), + ('attributes', [ + OrderedDict([ + ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), + ('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])), + ]) + ]), + ]) + + +def generate_catalog_choice_dsl(meta, p, preset_data): + # Start from list + list_key = 'catalog.list' + lp = preset_data.get(list_key, {}) + dsl = generate_catalog_list_dsl(meta, lp) + + # Add choice-specific properties + dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' + if p.get('properties'): + for k in p['properties']: + dsl['properties'][k] = p['properties'][k] + + # Set ChoiceMode on table + dsl['elements'][0]['choiceMode'] = True + + return dsl + + +def generate_catalog_item_dsl(meta, p, fd): + header_children = [] + + # Owner (if subordinate) + if meta.get('Owners') and len(meta['Owners']) > 0: + owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)]) + header_children.append(owner_el) + + # Code + Description + cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal') + cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst') + has_code = meta.get('CodeLength', 0) > 0 + + if cd_layout == 'horizontal' and has_code: + cd_children = [] + desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]) + code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]) + if cd_order == 'descriptionFirst': + cd_children = [desc_el, code_el] + else: + cd_children = [code_el, desc_el] + header_children.append(OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False), + ('representation', 'none'), ('children', cd_children), + ])) + else: + # Vertical or no code + header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + if has_code: + header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + + # Parent (for hierarchical catalogs) + parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription') + parent_title = (p.get('parent') or {}).get('title') + if meta.get('Hierarchical'): + parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) + if parent_title: + parent_el['title'] = parent_title + if parent_pos == 'beforeCodeDescription': + insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0 + header_children.insert(insert_idx, parent_el) + else: + # afterCodeDescription (default) + header_children.append(parent_el) + + # Custom attributes -> header + footer_field_names = (p.get('footer') or {}).get('fields', []) + + for attr in meta['Attributes']: + if attr['Name'] in footer_field_names: + continue + if not is_displayable_type(attr['Type']): + continue + header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Build root elements + root_elements = [] + + # ГруппаШапка + root_elements.append(OrderedDict([ + ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), + ('representation', 'none'), ('children', header_children), + ])) + + # Tabular sections + ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] + if (p.get('tabularSections') or {}).get('exclude'): + ts_exclude = p['tabularSections']['exclude'] + ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) + + visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] + + for ts in visible_ts: + ts_cols = [] + if ts_line_number: + ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) + for col in ts['Columns']: + ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) + root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) + + # Footer fields + for fn in footer_field_names: + f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) + if f_attr: + root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) + + # BSP group + bsp_group = (p.get('additional') or {}).get('bspGroup', True) + if bsp_group: + root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) + + # Properties + form_props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + # UseForFoldersAndItems + if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems': + form_props['useForFoldersAndItems'] = 'Items' + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', root_elements), + ('attributes', [ + OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) + ]), + ]) + + +# --- Document DSL generators --- + +def generate_document_dsl(meta, preset_data, purpose): + purpose_key = f"document.{purpose.lower()}" + p = preset_data.get(purpose_key, {}) + fd = p.get('fieldDefaults', {}) + + dispatch = { + 'List': lambda: generate_document_list_dsl(meta, p), + 'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data), + 'Item': lambda: generate_document_item_dsl(meta, p, fd), + } + return dispatch[purpose]() + + +def generate_document_list_dsl(meta, p): + columns = [] + # Standard columns: Number + Date + columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) + columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) + # All custom attributes as labelField + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + # Hidden ref + if p.get('hiddenRef', True): + columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns), + ]) + + form_props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', [table_el]), + ('attributes', [ + OrderedDict([ + ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), + ('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])), + ]) + ]), + ]) + + +def generate_document_choice_dsl(meta, p, preset_data): + list_key = 'document.list' + lp = preset_data.get(list_key, {}) + dsl = generate_document_list_dsl(meta, lp) + + dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' + if p.get('properties'): + for k in p['properties']: + dsl['properties'][k] = p['properties'][k] + + return dsl + + +def generate_document_item_dsl(meta, p, fd): + header_pos = (p.get('header') or {}).get('position', 'insidePage') + header_layout = (p.get('header') or {}).get('layout', '2col') + header_distribute = (p.get('header') or {}).get('distribute', 'even') + date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442') + + footer_fields = (p.get('footer') or {}).get('fields', []) + footer_pos = (p.get('footer') or {}).get('position', 'insidePage') + + add_pos = (p.get('additional') or {}).get('position', 'page') + add_layout = (p.get('additional') or {}).get('layout', '2col') + add_bsp_group = (p.get('additional') or {}).get('bspGroup', True) + add_left = (p.get('additional') or {}).get('left', []) + add_right = (p.get('additional') or {}).get('right', []) + + header_right = (p.get('header') or {}).get('right', []) + + ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'] + if (p.get('tabularSections') or {}).get('exclude'): + ts_exclude = p['tabularSections']['exclude'] + ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) + + # Classify attributes + claimed = {} + for fn in footer_fields: + claimed[fn] = 'footer' + for fn in header_right: + claimed[fn] = 'header.right' + for fn in add_left: + claimed[fn] = 'additional.left' + for fn in add_right: + claimed[fn] = 'additional.right' + + unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed and is_displayable_type(attr['Type'])] + + # Distribute unclaimed + left_attrs = [] + right_extra_attrs = [] + if header_distribute == 'left': + left_attrs = unclaimed + elif header_distribute == 'right': + right_extra_attrs = unclaimed + else: # "even" + import math + half = math.ceil(len(unclaimed) / 2) if unclaimed else 0 + left_attrs = unclaimed[:half] + right_extra_attrs = unclaimed[half:] + + # Build ГруппаНомерДата + num_date_children = [ + OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]), + OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]), + ] + num_date_group = OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children), + ]) + + # Build left column + left_children = [num_date_group] + for attr in left_attrs: + left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Build right column + right_children = [] + for rn in header_right: + r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None) + if r_attr: + right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd)) + for attr in right_extra_attrs: + right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Header group + if header_layout == '2col' and len(right_children) > 0: + header_group = OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]), + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]), + ]), + ]) + else: + # 1col or no right items + all_header_fields = left_children + right_children + header_group = OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]), + ]), + ]) + + # Footer elements + footer_elements = [] + for fn in footer_fields: + f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) + if f_attr: + footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) + + # Visible tabular sections + visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] + + # Additional page content + additional_page = None + if add_pos == 'page': + add_left_els = [] + add_right_els = [] + for aln in add_left: + al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None) + if al_attr: + add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd)) + for arn in add_right: + ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None) + if ar_attr: + add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd)) + add_page_children = [] + if add_layout == '2col': + add_page_children.append(OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]), + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]), + ]), + ])) + else: + add_page_children.extend(add_left_els + add_right_els) + if add_bsp_group: + add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) + additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)]) + + # Build TS page elements + ts_pages = [] + for ts in visible_ts: + ts_cols = [] + if ts_line_number: + ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) + for col in ts['Columns']: + ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) + ts_pages.append(OrderedDict([ + ('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']), + ('children', [ + OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]) + ]), + ])) + + # Assemble root elements + root_elements = [] + + if len(visible_ts) == 0: + # Simple form - no Pages + root_elements.append(header_group) + if footer_elements: + root_elements.extend(footer_elements) + if add_bsp_group and add_pos != 'none': + root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) + else: + # Pages form + if header_pos == 'abovePages': + root_elements.append(header_group) + pages_children = list(ts_pages) + if additional_page: + pages_children.append(additional_page) + root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) + else: + # insidePage (default) + osnovnoe_children = [header_group] + if footer_pos == 'insidePage' and footer_elements: + osnovnoe_children.extend(footer_elements) + pages_children = [] + pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)])) + pages_children.extend(ts_pages) + if additional_page: + pages_children.append(additional_page) + root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) + + # Footer below pages + if footer_pos == 'belowPages' and footer_elements: + root_elements.extend(footer_elements) + + # Properties + form_props = OrderedDict([('autoTitle', False)]) + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', root_elements), + ('attributes', [ + OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)]) + ]), + ]) + + +# --- InformationRegister DSL generators --- + +def generate_information_register_dsl(meta, preset_data, purpose): + p_key = f"informationRegister.{purpose.lower()}" + p = preset_data.get(p_key, {}) + fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} + dispatch = { + 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), + 'List': lambda: generate_information_register_list_dsl(meta, p), + } + return dispatch[purpose]() + + +def generate_information_register_record_dsl(meta, p, fd): + elements = OrderedDict() + is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' + + # Period first (if periodic) + if is_periodic: + elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_information_register_list_dsl(meta, p): + is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' + is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' + + columns_list = [] + # Period + if is_periodic: + columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) + # Recorder/LineNumber for subordinate registers + if is_recorder_subordinate: + columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) + columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns_list), + ]) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', [table_el]), + ('attributes', [ + {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} + ]), + ]) + + +# --- AccumulationRegister DSL generators --- + +def generate_accumulation_register_dsl(meta, preset_data, purpose): + p_key = f"accumulationRegister.{purpose.lower()}" + p = preset_data.get(p_key, {}) + dispatch = { + 'List': lambda: generate_accumulation_register_list_dsl(meta, p), + } + return dispatch[purpose]() + + +def generate_accumulation_register_list_dsl(meta, p): + columns_list = [] + # AccumulationRegisters always have Period, Recorder, LineNumber + columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) + columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) + columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns_list), + ]) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', [table_el]), + ('attributes', [ + {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} + ]), + ]) + + +# --- ChartOfCharacteristicTypes (delegates to Catalog) --- + +def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): + # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. + dsl = generate_catalog_dsl(meta, preset_data, purpose) + + # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types + cat_obj_type = f"CatalogObject.{meta['Name']}" + ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" + cat_list_type = f"Catalog.{meta['Name']}" + ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" + + for a in dsl['attributes']: + if a.get('type') == cat_obj_type: + a['type'] = ccoct_obj_type + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: + a['settings']['mainTable'] = ccoct_list_type + + # For Item forms: inject ValueType field after Description/ГруппаКодНаименование + if purpose == 'Item' and dsl.get('elements'): + vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) + els = dsl['elements'] + if isinstance(els, list): + inserted = False + new_els = [] + for el in els: + new_els.append(el) + if not inserted and isinstance(el, dict): + name = el.get('input') or el.get('group') or '' + if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): + new_els.append(vt_el) + inserted = True + if not inserted: + new_els.append(vt_el) + dsl['elements'] = new_els + + return dsl + + +# --- ExchangePlan (delegates to Catalog) --- + +def generate_exchange_plan_dsl(meta, preset_data, purpose): + # ExchangePlans are not hierarchical and have no Folder form + dsl = generate_catalog_dsl(meta, preset_data, purpose) + + # Post-patch: replace Catalog types with ExchangePlan types + cat_obj_type = f"CatalogObject.{meta['Name']}" + ep_obj_type = f"ExchangePlanObject.{meta['Name']}" + cat_list_type = f"Catalog.{meta['Name']}" + ep_list_type = f"ExchangePlan.{meta['Name']}" + + for a in dsl['attributes']: + if a.get('type') == cat_obj_type: + a['type'] = ep_obj_type + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: + a['settings']['mainTable'] = ep_list_type + + # For Item forms: inject SentNo, ReceivedNo after Code/Description + if purpose == 'Item' and dsl.get('elements'): + sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) + recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) + els = dsl['elements'] + if isinstance(els, list): + inserted = False + new_els = [] + for el in els: + new_els.append(el) + if not inserted and isinstance(el, dict): + name = el.get('input') or el.get('group') or '' + if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): + new_els.append(sent_el) + new_els.append(recv_el) + inserted = True + if not inserted: + new_els.append(sent_el) + new_els.append(recv_el) + dsl['elements'] = new_els + + return dsl + + +# --- ChartOfAccounts DSL generators --- + +def generate_chart_of_accounts_dsl(meta, preset_data, purpose): + p_key = f"chartOfAccounts.{purpose.lower()}" + p = preset_data.get(p_key, {}) + fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} + dispatch = { + 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), + 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), + 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), + 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), + } + return dispatch[purpose]() + + +def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): + elements = [] + + # Header: Code + Parent + header_left_children = [] + if meta.get('CodeLength', 0) > 0: + header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + header_right_children = [] + if meta.get('Hierarchical'): + parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') + header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) + + if len(header_right_children) > 0: + elements.append(OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]), + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]), + ]), + ])) + elif len(header_left_children) > 0: + elements.extend(header_left_children) + + # Description + if meta.get('DescriptionLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + + # OffBalance + elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) + + # AccountingFlags as checkboxes + if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: + flag_children = [] + for flag in meta['AccountingFlags']: + flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) + elements.append(OrderedDict([ + ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), + ('children', flag_children), + ])) + + # ExtDimensionTypes table + if meta.get('MaxExtDimensionCount', 0) > 0: + ed_cols = [] + ed_cols.append(OrderedDict([('input', '\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) + ed_cols.append(OrderedDict([('check', '\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) + if meta.get('ExtDimensionAccountingFlags'): + for ed_flag in meta['ExtDimensionAccountingFlags']: + ed_cols.append(OrderedDict([('check', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) + elements.append(OrderedDict([ + ('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), + ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), + ('columns', ed_cols), + ])) + + # Custom attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Tabular sections + ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] + for ts in meta['TabularSections']: + if ts['Name'] in ts_exclude: + continue + ts_cols = [] + for col in ts['Columns']: + if not is_displayable_type(col['Type']): + continue + ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) + elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_chart_of_accounts_folder_dsl(meta, p): + elements = [] + if meta.get('CodeLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + if meta.get('DescriptionLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + if meta.get('Hierarchical'): + parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') + elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('useForFoldersAndItems', 'Folders'), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_chart_of_accounts_list_dsl(meta, preset_data): + # Delegate to Catalog List and patch types + dsl = generate_catalog_dsl(meta, preset_data, 'List') + for a in dsl['attributes']: + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": + a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" + return dsl + + +def generate_chart_of_accounts_choice_dsl(meta, preset_data): + dsl = generate_catalog_dsl(meta, preset_data, 'Choice') + for a in dsl['attributes']: + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": + a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" + return dsl + + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + + +def esc_xml(s): + return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') + + +def emit_mltext(lines, indent, tag, text): + if not text: + lines.append(f"{indent}<{tag}/>") + return + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t") + lines.append(f"{indent}\t\tru") + lines.append(f"{indent}\t\t{esc_xml(text)}") + lines.append(f"{indent}\t") + lines.append(f"{indent}") + + +def new_uuid(): + return str(uuid.uuid4()) + + +def write_utf8_bom(path, content): + with open(path, 'w', encoding='utf-8-sig', newline='') as f: + f.write(content) + + +# --- ID allocator --- +_next_id = 0 + +def new_id(): + global _next_id + _next_id += 1 + return _next_id + + +# --- Event handler name generator --- + +EVENT_SUFFIX_MAP = { + "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", + "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430", + "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430", + "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440", + "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430", + "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435", + "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435", + "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438", + "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c", + "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f", + "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438", + "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b", + "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430", + "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438", + "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", + "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435", + "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", + "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f", +} + +KNOWN_EVENTS = { + "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], + "check": ["OnChange"], + "radio": ["OnChange"], + "label": ["Click", "URLProcessing"], + "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"], + "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"], + "pages": ["OnCurrentPageChange"], + "page": ["OnCurrentPageChange"], + "button": ["Click"], + "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"], + "calendar": ["OnChange", "OnActivate"], + "picture": ["Click"], + "cmdBar": [], + "popup": [], + "group": [], +} + +KNOWN_FORM_EVENTS = [ + "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing", + "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer", + "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer", + "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer", + "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening", +] + +KNOWN_KEYS = { + "group", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", + "button", "picture", "picField", "calendar", "cmdBar", "popup", + "showInHeader", + "radioButtonType", "choiceList", "columnsCount", + "name", "path", "title", + "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", + "on", "handlers", + "titleLocation", "representation", "width", "height", + "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", + "maxWidth", "maxHeight", + "groupHorizontalAlign", "groupVerticalAlign", "horizontalAlign", + "multiLine", "passwordMode", "choiceButton", "clearButton", + "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", + "textEdit", + "hyperlink", + "showTitle", "united", "collapsed", + "children", "columns", + "changeRowSet", "changeRowOrder", "header", "footer", + "commandBarLocation", "searchStringLocation", + "pagesRepresentation", + "type", "command", "stdCommand", "defaultButton", "locationInCommandBar", + "src", "valuesPicture", "loadTransparent", + "autofill", + "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", + "rowPictureDataPath", "tableAutofill", +} + +TYPE_KEYS = ["columnGroup", "buttonGroup", "group", "input", "check", "radio", "label", "labelField", "table", "pages", "page", + "button", "picture", "picField", "calendar", "cmdBar", "popup"] + +# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) +ELEMENT_TYPE_SYNONYMS = { + "commandBar": "cmdBar", + "autoCommandBar": "autoCmdBar", + "КоманднаяПанель": "cmdBar", + "InputField": "input", + "ПолеВвода": "input", + "CheckBoxField": "check", + "ПолеФлажка": "check", + "RadioButtonField": "radio", + "ПолеПереключателя": "radio", + "radioButton": "radio", + "PictureField": "picField", + "ПолеКартинки": "picField", + "LabelField": "labelField", + "ПолеНадписи": "labelField", + "CalendarField": "calendar", + "ПолеКалендаря": "calendar", + "LabelDecoration": "label", + "Надпись": "label", + "PictureDecoration": "picture", + "Картинка": "picture", + "UsualGroup": "group", + "Группа": "group", + "ОбычнаяГруппа": "group", + "ColumnGroup": "columnGroup", + "ГруппаКолонок": "columnGroup", + "Pages": "pages", + "ГруппаСтраниц": "pages", + "Page": "page", + "Страница": "page", + "Table": "table", + "Таблица": "table", + "Button": "button", + "Кнопка": "button", + "Popup": "popup", + "ВсплывающееМеню": "popup", +} + +# Maps Russian/English root of typed reference path to canonical English root +REF_ROOT_SYNONYMS = { + "Перечисление": "Enum", + "Справочник": "Catalog", + "Документ": "Document", + "ПланСчетов": "ChartOfAccounts", + "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", + "ПланВидовРасчета": "ChartOfCalculationTypes", + "ПланВидовРасчёта": "ChartOfCalculationTypes", + "ПланОбмена": "ExchangePlan", + "БизнесПроцесс": "BusinessProcess", + "Задача": "Task", + "РегистрСведений": "InformationRegister", + "РегистрНакопления": "AccumulationRegister", + "РегистрБухгалтерии": "AccountingRegister", + "РегистрРасчета": "CalculationRegister", + "РегистрРасчёта": "CalculationRegister", +} +ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} + + +def normalize_choice_value(value): + """Returns dict {xsi_type, text} for a choiceList item value.""" + if isinstance(value, bool): + return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} + if isinstance(value, (int, float)): + return {"xsi_type": "xs:decimal", "text": str(value)} + + s = "" if value is None else str(value) + if not s: + return {"xsi_type": "xs:string", "text": ""} + + parts = s.split(".") + if len(parts) >= 2: + root = parts[0] + canon_root = None + if root in REF_ROOT_SYNONYMS: + canon_root = REF_ROOT_SYNONYMS[root] + elif root in REF_ROOT_SYNONYMS.values(): + canon_root = root + + if canon_root: + type_name = parts[1] + normalized = None + if canon_root == "Enum": + if len(parts) == 3: + normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" + elif len(parts) >= 4: + member = parts[2] + if member in ENUM_VALUE_SYNONYMS: + rest = ".".join(parts[3:]) + else: + rest = ".".join(parts[2:]) + normalized = f"Enum.{type_name}.EnumValue.{rest}" + else: + if len(parts) >= 3: + tail = ".".join(parts[1:]) + normalized = f"{canon_root}.{tail}" + + if normalized: + return {"xsi_type": "xr:DesignTimeRef", "text": normalized} + + return {"xsi_type": "xs:string", "text": s} + + +def emit_choice_presentation(lines, pres, indent): + """Accepts None/empty → ; str → ru only; dict → multi-lang.""" + if pres is None or (isinstance(pres, str) and pres == ""): + lines.append(f"{indent}") + return + + if isinstance(pres, str): + pairs = [("ru", pres)] + elif isinstance(pres, dict): + pairs = [(str(k), str(v)) for k, v in pres.items()] + else: + pairs = [("ru", str(pres))] + + lines.append(f"{indent}") + for lang, content in pairs: + lines.append(f"{indent}\t") + lines.append(f"{indent}\t\t{lang}") + lines.append(f"{indent}\t\t{esc_xml(content)}") + lines.append(f"{indent}\t") + lines.append(f"{indent}") + + +def normalize_radio_button_type(raw): + if not raw: + return "Auto" + s = str(raw).strip().lower() + if s in ("auto", "авто"): + return "Auto" + if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): + return "RadioButtons" + if s in ("tumbler", "тумблер"): + return "Tumbler" + return str(raw).strip() + + +def get_handler_name(element_name, event_name): + suffix = EVENT_SUFFIX_MAP.get(event_name) + if suffix: + return f"{element_name}{suffix}" + return f"{element_name}{event_name}" + + +def get_element_name(el, type_key): + if el.get('name'): + return str(el['name']) + return str(el.get(type_key, '')) + + +def emit_events(lines, el, element_name, indent, type_key): + if not el.get('on'): + return + + # Validate event names + if type_key and type_key in KNOWN_EVENTS: + allowed = KNOWN_EVENTS[type_key] + for evt in el['on']: + if allowed and str(evt) not in allowed: + print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") + + lines.append(f"{indent}") + for evt in el['on']: + evt_name = str(evt) + handlers = el.get('handlers') + if handlers and handlers.get(evt_name): + handler = str(handlers[evt_name]) + else: + handler = get_handler_name(element_name, evt_name) + lines.append(f'{indent}\t{handler}') + lines.append(f"{indent}") + + +def emit_companion(lines, tag, name, indent): + cid = new_id() + lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') + + +def emit_common_flags(lines, el, indent): + if el.get('visible') is False or el.get('hidden') is True: + lines.append(f"{indent}false") + if el.get('userVisible') is False: + lines.append(f"{indent}") + lines.append(f"{indent}\tfalse") + lines.append(f"{indent}") + if el.get('enabled') is False or el.get('disabled') is True: + lines.append(f"{indent}false") + if el.get('readOnly') is True: + lines.append(f"{indent}true") + + +def emit_layout(lines, el, indent, skip_height=False, multi_line_default=False): + # Общие layout-свойства — применимы ко всем элементам. Порядок согласован + # с историческим выводом input/label, чтобы не сдвигать существующие снапшоты. + # skip_height: для Table (height → HeightInTableRows, эмитится в emit_table). + # multi_line_default: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. + if el.get('skipOnInput') is True: + lines.append(f"{indent}true") + if 'autoMaxWidth' in el: + if el.get('autoMaxWidth') is False: + lines.append(f"{indent}false") + elif multi_line_default: + lines.append(f"{indent}false") + if el.get('maxWidth') is not None: + lines.append(f"{indent}{el['maxWidth']}") + if el.get('autoMaxHeight') is False: + lines.append(f"{indent}false") + if el.get('maxHeight') is not None: + lines.append(f"{indent}{el['maxHeight']}") + if el.get('width'): + lines.append(f"{indent}{el['width']}") + if not skip_height and el.get('height'): + lines.append(f"{indent}{el['height']}") + if el.get('horizontalStretch') is True: + lines.append(f"{indent}true") + if el.get('verticalStretch') is True: + lines.append(f"{indent}true") + if el.get('groupHorizontalAlign'): + lines.append(f"{indent}{el['groupHorizontalAlign']}") + if el.get('groupVerticalAlign'): + lines.append(f"{indent}{el['groupVerticalAlign']}") + if el.get('horizontalAlign'): + lines.append(f"{indent}{el['horizontalAlign']}") + + +def title_from_name(name): + """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" + if not name: + return '' + s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) + s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) + parts = s.split(' ') + if not parts: + return s + out = [parts[0]] + for p in parts[1:]: + out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) + return ' '.join(out) + + +def emit_title(lines, el, name, indent, auto=False): + title = el.get('title') + if not title and auto and name: + title = title_from_name(name) + if title: + emit_mltext(lines, indent, 'Title', str(title)) + + +# --- Type emitter --- + +V8_TYPES = { + "ValueTable": "v8:ValueTable", + "ValueTree": "v8:ValueTree", + "ValueList": "v8:ValueListType", + "TypeDescription": "v8:TypeDescription", + "Universal": "v8:Universal", + "FixedArray": "v8:FixedArray", + "FixedStructure": "v8:FixedStructure", +} + +UI_TYPES = { + "FormattedString": "v8ui:FormattedString", + "Picture": "v8ui:Picture", + "Color": "v8ui:Color", + "Font": "v8ui:Font", +} + +DCS_MAP = { + "DataCompositionSettings": "dcsset:DataCompositionSettings", + "DataCompositionSchema": "dcssch:DataCompositionSchema", + "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType", +} + +CFG_REF_PATTERN = re.compile( + r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|' + r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' + r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' + r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' + r'InformationRegisterRecordSet|InformationRegisterRecordManager|' + r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' + r'ConstantsSet|DataProcessorObject|ReportObject)\.' +) + +KNOWN_INVALID_TYPES = { + 'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', + 'FormDataCollection': 'Runtime type. Use ValueTable', + 'FormDataTree': 'Runtime type. Use ValueTree', + 'FormDataTreeItem': 'Runtime type, not valid in XML', + 'FormDataCollectionItem': 'Runtime type, not valid in XML', + 'FormGroup': 'UI element type, not a data type', + 'FormField': 'UI element type, not a data type', + 'FormButton': 'UI element type, not a data type', + 'FormDecoration': 'UI element type, not a data type', + 'FormTable': 'UI element type, not a data type', +} + + +_FORM_TYPE_SYNONYMS = { + "строка": "string", "число": "decimal", "булево": "boolean", + "дата": "date", "датавремя": "dateTime", + "number": "decimal", "bool": "boolean", + "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject", + "документссылка": "DocumentRef", "документобъект": "DocumentObject", + "перечислениессылка": "EnumRef", + "плансчетовссылка": "ChartOfAccountsRef", + "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", + "планвидоврасчётассылка": "ChartOfCalculationTypesRef", + "планвидоврасчетассылка": "ChartOfCalculationTypesRef", + "планобменассылка": "ExchangePlanRef", + "бизнеспроцессссылка": "BusinessProcessRef", + "задачассылка": "TaskRef", + "определяемыйтип": "DefinedType", +} + + +def resolve_type_str(type_str): + if not type_str: + return type_str + # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) + if type_str.startswith('cfg:'): + type_str = type_str[4:] + m = re.match(r'^([^(]+)\((.+)\)$', type_str) + if m: + base, params = m.group(1).strip(), m.group(2) + r = _FORM_TYPE_SYNONYMS.get(base.lower()) + return f"{r}({params})" if r else type_str + if '.' in type_str: + i = type_str.index('.') + prefix, suffix = type_str[:i], type_str[i:] + r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) + return f"{r}{suffix}" if r else type_str + r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) + return r if r else type_str + + +def emit_single_type(lines, type_str, indent): + type_str = resolve_type_str(type_str) + # boolean + if type_str == 'boolean': + lines.append(f'{indent}xs:boolean') + return + + # string or string(N) + m = re.match(r'^string(\((\d+)\))?$', type_str) + if m: + length = m.group(2) if m.group(2) else '0' + lines.append(f'{indent}xs:string') + lines.append(f'{indent}') + lines.append(f'{indent}\t{length}') + lines.append(f'{indent}\tVariable') + lines.append(f'{indent}') + return + + # decimal(D,F) or decimal(D,F,nonneg) + m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) + if m: + digits = m.group(1) + fraction = m.group(2) + sign = 'Nonnegative' if m.group(3) else 'Any' + lines.append(f'{indent}xs:decimal') + lines.append(f'{indent}') + lines.append(f'{indent}\t{digits}') + lines.append(f'{indent}\t{fraction}') + lines.append(f'{indent}\t{sign}') + lines.append(f'{indent}') + return + + # date / dateTime / time + m = re.match(r'^(date|dateTime|time)$', type_str) + if m: + fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'} + fractions = fractions_map[type_str] + lines.append(f'{indent}xs:dateTime') + lines.append(f'{indent}') + lines.append(f'{indent}\t{fractions}') + lines.append(f'{indent}') + return + + # V8 types + if type_str in V8_TYPES: + lines.append(f'{indent}{V8_TYPES[type_str]}') + return + + # UI types + if type_str in UI_TYPES: + lines.append(f'{indent}{UI_TYPES[type_str]}') + return + + # DCS types + if type_str.startswith('DataComposition'): + if type_str in DCS_MAP: + lines.append(f'{indent}{DCS_MAP[type_str]}') + return + + # DynamicList + if type_str == 'DynamicList': + lines.append(f'{indent}cfg:DynamicList') + return + + # cfg: references + if CFG_REF_PATTERN.match(type_str): + lines.append(f'{indent}cfg:{type_str}') + return + + # Fallback with validation + if type_str in KNOWN_INVALID_TYPES: + raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") + if '.' in type_str: + lines.append(f'{indent}cfg:{type_str}') + else: + print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) + lines.append(f'{indent}{type_str}') + + +def emit_type(lines, type_str, indent): + if not type_str: + lines.append(f'{indent}') + return + + type_string = str(type_str) + parts = [p.strip() for p in re.split(r'[|+]', type_string)] + + lines.append(f'{indent}') + for part in parts: + emit_single_type(lines, part, f'{indent}\t') + lines.append(f'{indent}') + + +# --- Element emitters --- + +def emit_element(lines, el, indent, in_cmd_bar=False): + # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) + for src, dst in ELEMENT_TYPE_SYNONYMS.items(): + if src in el and dst not in el: + el[dst] = el.pop(src) + + type_key = None + for key in TYPE_KEYS: + if el.get(key) is not None: + type_key = key + break + + if not type_key: + print("WARNING: Unknown element type, skipping", file=sys.stderr) + return + + # Validate known keys + for p_name in el.keys(): + if p_name not in KNOWN_KEYS: + print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) + + name = get_element_name(el, type_key) + eid = new_id() + + emitters = { + 'group': emit_group, + 'columnGroup': emit_column_group, + 'buttonGroup': emit_button_group, + 'input': emit_input, + 'check': emit_check, + 'radio': emit_radio_button_field, + 'label': emit_label, + 'labelField': emit_label_field, + 'table': emit_table, + 'pages': emit_pages, + 'page': emit_page, + 'button': emit_button, + 'picture': emit_picture_decoration, + 'picField': emit_picture_field, + 'calendar': emit_calendar, + 'cmdBar': emit_command_bar, + 'popup': emit_popup, + } + + emitter = emitters.get(type_key) + if emitter: + if type_key == 'button': + emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) + else: + emitter(lines, el, name, eid, indent) + + +def emit_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + # Group orientation + group_val = str(el.get('group', '')) + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'alwaysHorizontal': 'AlwaysHorizontal', + 'alwaysVertical': 'AlwaysVertical', + } + orientation = orientation_map.get(group_val) + if orientation: + lines.append(f'{inner}{orientation}') + + # Behavior + if group_val == 'collapsible': + lines.append(f'{inner}Vertical') + lines.append(f'{inner}Collapsible') + if el.get('collapsed') is True: + lines.append(f'{inner}true') + + # Representation + if el.get('representation'): + repr_map = { + 'none': 'None', + 'normal': 'NormalSeparation', + 'weak': 'WeakSeparation', + 'strong': 'StrongSeparation', + } + repr_val = repr_map.get(str(el['representation']), str(el['representation'])) + lines.append(f'{inner}{repr_val}') + + # ShowTitle + if el.get('showTitle') is False: + lines.append(f'{inner}false') + + # United + if el.get('united') is False: + lines.append(f'{inner}false') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Companion: ExtendedTooltip + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_column_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + group_val = str(el.get('columnGroup', '')) + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'inCell': 'InCell', + } + orientation = orientation_map.get(group_val) + if orientation: + lines.append(f'{inner}{orientation}') + + if el.get('showTitle') is False: + lines.append(f'{inner}false') + if el.get('showInHeader') is not None: + sh_val = 'true' if el['showInHeader'] else 'false' + lines.append(f'{inner}{sh_val}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) + + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_input(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('titleLocation'): + loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} + loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) + lines.append(f'{inner}{loc}') + + if el.get('multiLine') is True: + lines.append(f'{inner}true') + if el.get('passwordMode') is True: + lines.append(f'{inner}true') + if el.get('choiceButton') is False: + lines.append(f'{inner}false') + elif el.get('choiceButton') is True and 'StartChoice' in (el.get('on') or []): + lines.append(f'{inner}true') + if el.get('clearButton') is True: + lines.append(f'{inner}true') + if el.get('spinButton') is True: + lines.append(f'{inner}true') + if el.get('dropListButton') is True: + lines.append(f'{inner}true') + if el.get('markIncomplete') is True: + lines.append(f'{inner}true') + if el.get('textEdit') is False: + lines.append(f'{inner}false') + emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) + + if el.get('inputHint'): + emit_mltext(lines, inner, 'InputHint', str(el['inputHint'])) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'input') + + lines.append(f'{indent}') + + +def emit_check(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + tl = el.get('titleLocation') or 'Right' + lines.append(f'{inner}{tl}') + + emit_layout(lines, el, inner) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'check') + + lines.append(f'{indent}') + + +def emit_radio_button_field(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + tl_raw = el.get('titleLocation') + if tl_raw: + loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} + tl = loc_map.get(str(tl_raw), str(tl_raw)) + else: + tl = 'None' + lines.append(f'{inner}{tl}') + + rbt = normalize_radio_button_type(el.get('radioButtonType')) + lines.append(f'{inner}{rbt}') + + if el.get('columnsCount') is not None: + lines.append(f'{inner}{el["columnsCount"]}') + + choice_list = el.get('choiceList') or [] + if choice_list: + lines.append(f'{inner}') + item_indent = f'{inner}\t' + for item in choice_list: + if not isinstance(item, dict): + continue + val_raw = item.get('value', item.get('значение')) + has_pres = any(k in item for k in ('presentation', 'представление', 'title')) + pres_raw = item.get('presentation', item.get('представление', item.get('title'))) + + norm = normalize_choice_value(val_raw) + + if not has_pres: + if norm['xsi_type'] == 'xr:DesignTimeRef': + tail = norm['text'].split('.')[-1] + pres_raw = title_from_name(tail) + else: + pres_raw = norm['text'] + + lines.append(f'{item_indent}') + val_indent = f'{item_indent}\t' + lines.append(f'{val_indent}') + lines.append(f'{val_indent}0') + lines.append(f'{val_indent}') + emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') + lines.append(f'{val_indent}\t{esc_xml(norm["text"])}') + lines.append(f'{val_indent}') + lines.append(f'{item_indent}') + lines.append(f'{inner}') + + emit_layout(lines, el, inner) + + emit_companion(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) + + emit_events(lines, el, name, inner, 'radio') + + lines.append(f'{indent}') + + +def emit_label(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + label_title = el.get('title') or title_from_name(name) + if label_title: + formatted = 'true' if el.get('hyperlink') is True else 'false' + lines.append(f'{inner}') + lines.append(f'{inner}\t<v8:item>') + lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>') + lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(label_title))}</v8:content>') + lines.append(f'{inner}\t</v8:item>') + lines.append(f'{inner}') + + emit_common_flags(lines, el, inner) + + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'label') + + lines.append(f'{indent}') + + +def emit_label_field(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'labelField') + + lines.append(f'{indent}') + + +def emit_table(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + if el.get('changeRowSet') is True: + lines.append(f'{inner}true') + if el.get('changeRowOrder') is True: + lines.append(f'{inner}true') + if el.get('height'): + lines.append(f'{inner}{el["height"]}') + if el.get('header') is False: + lines.append(f'{inner}
false
') + if el.get('footer') is True: + lines.append(f'{inner}
true
') + + if el.get('commandBarLocation'): + lines.append(f'{inner}{el["commandBarLocation"]}') + if el.get('searchStringLocation'): + lines.append(f'{inner}{el["searchStringLocation"]}') + + if el.get('choiceMode') is True: + lines.append(f'{inner}true') + if el.get('initialTreeView'): + lines.append(f'{inner}{el["initialTreeView"]}') + if el.get('enableStartDrag') is True: + lines.append(f'{inner}true') + if el.get('enableDrag') is True: + lines.append(f'{inner}true') + if el.get('rowPictureDataPath'): + lines.append(f'{inner}{el["rowPictureDataPath"]}') + emit_layout(lines, el, inner, skip_height=True) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + # AutoCommandBar — with optional Autofill control + if el.get('tableAutofill') is not None: + acb_id = new_id() + acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' + af_val = 'true' if el['tableAutofill'] else 'false' + lines.append(f'{inner}') + lines.append(f'{inner}\t{af_val}') + lines.append(f'{inner}') + else: + emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner) + emit_companion(lines, 'SearchStringAddition', f'{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430', inner) + emit_companion(lines, 'ViewStatusAddition', f'{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430', inner) + emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner) + + # Columns + if el.get('columns') and len(el['columns']) > 0: + lines.append(f'{inner}') + for col in el['columns']: + emit_element(lines, col, f'{inner}\t') + lines.append(f'{inner}') + + emit_events(lines, el, name, inner, 'table') + + lines.append(f'{indent}
') + + +def emit_pages(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('pagesRepresentation'): + lines.append(f'{inner}{el["pagesRepresentation"]}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Companion + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'pages') + + # Children (pages) + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_page(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner, auto=True) + emit_common_flags(lines, el, inner) + + if el.get('group'): + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'alwaysHorizontal': 'AlwaysHorizontal', + 'alwaysVertical': 'AlwaysVertical', + } + orientation = orientation_map.get(str(el['group'])) + if orientation: + lines.append(f'{inner}{orientation}') + emit_layout(lines, el, inner) + + # Companion + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_button(lines, el, name, eid, indent, in_cmd_bar=False): + lines.append(f'{indent}') + + +def emit_picture_decoration(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + emit_common_flags(lines, el, inner) + + if el.get('picture') or el.get('src'): + ref = str(el.get('src') or el.get('picture')) + lt = 'true' if el.get('loadTransparent') is True else 'false' + lines.append(f'{inner}') + lines.append(f'{inner}\t{ref}') + lines.append(f'{inner}\t{lt}') + lines.append(f'{inner}') + + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'picture') + + lines.append(f'{indent}') + + +def emit_picture_field(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner) + emit_common_flags(lines, el, inner) + + # ValuesPicture \u2014 picture (collection) used to render the field's value. + # Required for a Boolean-bound PictureField to actually show an icon. + # loadTransparent emitted only when true (1\u0421 default is false). + if el.get('valuesPicture'): + lines.append(f'{inner}') + lines.append(f'{inner}\t{el["valuesPicture"]}') + if el.get('loadTransparent'): + lines.append(f'{inner}\ttrue') + lines.append(f'{inner}') + + emit_layout(lines, el, inner) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'picField') + + lines.append(f'{indent}') + + +def emit_calendar(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) + + emit_events(lines, el, name, inner, 'calendar') + + lines.append(f'{indent}') + + +def emit_command_bar(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('autofill') is True: + lines.append(f'{inner}true') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_popup(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner, auto=True) + emit_common_flags(lines, el, inner) + + if el.get('picture'): + lines.append(f'{inner}') + lines.append(f'{inner}\t{el["picture"]}') + lines.append(f'{inner}\ttrue') + lines.append(f'{inner}') + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + emit_layout(lines, el, inner) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_button_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Companion: ExtendedTooltip + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) + + # Children (кнопки в контексте командной панели) + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +# --- Attribute emitter --- + +def emit_attributes(lines, attrs, indent): + if not attrs or len(attrs) == 0: + return + + lines.append(f'{indent}') + for attr in attrs: + attr_id = new_id() + attr_name = str(attr['name']) + + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + attr_title = attr.get('title') + if not attr_title and attr.get('main') is not True: + attr_title = title_from_name(attr_name) + if attr_title: + emit_mltext(lines, inner, 'Title', str(attr_title)) + + # Type + if attr.get('type'): + emit_type(lines, str(attr['type']), inner) + else: + lines.append(f'{inner}') + + if attr.get('main') is True: + lines.append(f'{inner}true') + main_saved = False + if attr.get('main') is True and attr.get('type'): + t = str(attr['type']) + main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) + if attr.get('savedData') is True or main_saved: + lines.append(f'{inner}true') + if attr.get('fillChecking'): + lines.append(f'{inner}{attr["fillChecking"]}') + + # Columns (for ValueTable/ValueTree) + if attr.get('columns') and len(attr['columns']) > 0: + lines.append(f'{inner}') + for col in attr['columns']: + col_id = new_id() + lines.append(f'{inner}\t') + if col.get('title'): + emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title'])) + emit_type(lines, str(col.get('type', '')), f'{inner}\t\t') + lines.append(f'{inner}\t') + lines.append(f'{inner}') + + # Settings (for DynamicList) + 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' + lines.append(f'{si}{mq}') + ddr = 'true' if s.get('dynamicDataRead') else 'false' + lines.append(f'{si}{ddr}') + lines.append(f'{inner}') + + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# --- Parameter emitter --- + +def emit_parameters(lines, params, indent): + if not params or len(params) == 0: + return + + lines.append(f'{indent}') + for param in params: + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + emit_type(lines, str(param.get('type', '')), inner) + + if param.get('key') is True: + lines.append(f'{inner}true') + + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# --- Command emitter --- + +def emit_commands(lines, cmds, indent): + if not cmds or len(cmds) == 0: + return + + lines.append(f'{indent}') + for cmd in cmds: + cmd_id = new_id() + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + cmd_title = cmd.get('title') or title_from_name(str(cmd['name'])) + if cmd_title: + emit_mltext(lines, inner, 'Title', str(cmd_title)) + + if cmd.get('tooltip'): + emit_mltext(lines, inner, 'ToolTip', str(cmd['tooltip'])) + + if cmd.get('action'): + lines.append(f'{inner}{cmd["action"]}') + + if cmd.get('currentRowUse'): + lines.append(f'{inner}{cmd["currentRowUse"]}') + + if cmd.get('shortcut'): + lines.append(f'{inner}{cmd["shortcut"]}') + + if cmd.get('picture'): + lines.append(f'{inner}') + lines.append(f'{inner}\t{cmd["picture"]}') + lines.append(f'{inner}\ttrue') + lines.append(f'{inner}') + + if cmd.get('representation'): + lines.append(f'{inner}{cmd["representation"]}') + + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# --- Properties emitter --- + +PROP_MAP = { + "autoTitle": "AutoTitle", + "windowOpeningMode": "WindowOpeningMode", + "commandBarLocation": "CommandBarLocation", + "saveDataInSettings": "SaveDataInSettings", + "autoSaveDataInSettings": "AutoSaveDataInSettings", + "autoTime": "AutoTime", + "usePostingMode": "UsePostingMode", + "repostOnWrite": "RepostOnWrite", + "autoURL": "AutoURL", + "autoFillCheck": "AutoFillCheck", + "customizable": "Customizable", + "enterKeyBehavior": "EnterKeyBehavior", + "verticalScroll": "VerticalScroll", + "scalingMode": "ScalingMode", + "useForFoldersAndItems": "UseForFoldersAndItems", + "reportResult": "ReportResult", + "detailsData": "DetailsData", + "reportFormType": "ReportFormType", + "autoShowState": "AutoShowState", + "width": "Width", + "height": "Height", + "group": "Group", +} + + +def emit_properties(lines, props, indent): + if not props: + return + + for p_name, p_value in props.items(): + xml_name = PROP_MAP.get(p_name) + if not xml_name: + # Auto PascalCase + xml_name = p_name[0].upper() + p_name[1:] + + # Convert boolean to lowercase + if isinstance(p_value, bool): + val = 'true' if p_value else 'false' + else: + val = str(p_value) + lines.append(f'{indent}<{xml_name}>{val}') + + + +def detect_format_version(d): + while d: + cfg_path = os.path.join(d, "Configuration.xml") + if os.path.isfile(cfg_path): + with open(cfg_path, "r", encoding="utf-8-sig") as f: + head = f.read(2000) + m = re.search(r']+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 _normalize_elements(defn): + """Convert dict-style elements from --from-object generators to list-style expected by compiler. + Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} + Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] + Also handles nested 'elements' in groups and 'columns' in tables recursively. + """ + def convert_elements(els): + if isinstance(els, list): + # Already list format — but may have nested dicts inside groups + result = [] + for el in els: + if isinstance(el, dict): + el = dict(el) # copy + if 'elements' in el and isinstance(el['elements'], dict): + el['elements'] = convert_elements(el['elements']) + if 'columns' in el and isinstance(el['columns'], dict): + el['columns'] = convert_columns(el['columns']) + result.append(el) + return result + if isinstance(els, dict): + result = [] + for name, props in els.items(): + if not isinstance(props, dict): + continue + new_el = {} + el_type = props.get('element', 'input') + # Map element type to the key name used in JSON DSL + type_map = { + 'input': 'input', 'check': 'check', 'labelField': 'labelField', + 'table': 'table', 'group': 'group', 'pages': 'pages', + 'page': 'page', 'label': 'label', 'button': 'button', + 'checkBox': 'check', 'radioButton': 'radioButton', + 'pictureField': 'pictureField', + } + mapped_type = type_map.get(el_type, el_type) + new_el[mapped_type] = name + for k, v in props.items(): + if k == 'element': + continue + if k == 'elements' and isinstance(v, dict): + new_el['elements'] = convert_elements(v) + elif k == 'columns' and isinstance(v, dict): + new_el['columns'] = convert_columns(v) + elif k == 'groupType': + # groupType → group property in DSL + new_el['group'] = v + elif k == 'showTitle': + new_el['showTitle'] = v + elif k == 'representation': + new_el['representation'] = v + elif k == 'autoCommandBar': + new_el['autoCommandBar'] = v + elif k == 'commandBarLocation': + new_el['commandBarLocation'] = v + else: + new_el[k] = v + result.append(new_el) + return result + return els + + def convert_columns(cols): + if isinstance(cols, list): + return cols + if isinstance(cols, dict): + result = [] + for name, props in cols.items(): + if not isinstance(props, dict): + continue + new_col = {} + el_type = props.get('element', 'input') + type_map = { + 'input': 'input', 'check': 'check', 'labelField': 'labelField', + 'checkBox': 'check', + } + mapped_type = type_map.get(el_type, el_type) + new_col[mapped_type] = name + for k, v in props.items(): + if k == 'element': + continue + new_col[k] = v + result.append(new_col) + return result + return cols + + if 'elements' in defn: + defn['elements'] = convert_elements(defn['elements']) + return defn + + +def main(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + global _next_id + + parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False) + parser.add_argument('-JsonPath', type=str, default=None) + parser.add_argument('-OutputPath', type=str, required=True) + parser.add_argument('-FromObject', action='store_true', default=False) + parser.add_argument('-ObjectPath', type=str, default=None) + parser.add_argument('-Purpose', type=str, default=None) + parser.add_argument('-Preset', type=str, default='erp-standard') + parser.add_argument('-EmitDsl', type=str, default=None) + args = parser.parse_args() + + # Form name -> purpose mapping + _FORM_NAME_TO_PURPOSE = { + '\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента + '\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента + '\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка + '\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора + '\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы + '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи + '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета + '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла + } + + # Mutual exclusion validation + if args.FromObject and args.JsonPath: + print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr) + sys.exit(1) + if not args.FromObject and not args.JsonPath: + print("Either -JsonPath or -FromObject is required.", file=sys.stderr) + sys.exit(1) + + # Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing + if args.FromObject: + out_norm = args.OutputPath.rstrip('/\\') + if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm): + if re.search(r'[/\\]Ext$', out_norm): + args.OutputPath = out_norm + '/Form.xml' + else: + args.OutputPath = out_norm + '/Ext/Form.xml' + print(f"[resolved] OutputPath -> {args.OutputPath}") + + # --- Detect XML format version --- + out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath) + format_version = detect_format_version(os.path.dirname(out_path_resolved)) + + # --- 0. From-object mode --- + if args.FromObject: + # Resolve object path and purpose from OutputPath convention: + # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + out_abs = out_path_resolved + parts = re.split(r'[/\\]', out_abs) + forms_idx = -1 + for i in range(len(parts) - 1, -1, -1): + if parts[i] == 'Forms': + forms_idx = i + break + + resolved_object_path = None + resolved_purpose = None + + if forms_idx >= 2: + form_name = parts[forms_idx + 1] + object_name = parts[forms_idx - 1] + type_plural_and_above = os.sep.join(parts[:forms_idx - 1]) + + if form_name in _FORM_NAME_TO_PURPOSE: + resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name] + + candidate = os.path.join(type_plural_and_above, f'{object_name}.xml') + if os.path.exists(candidate): + resolved_object_path = candidate + + # Apply: explicit -ObjectPath / -Purpose override resolved + from_obj_path = None + if args.ObjectPath: + from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath) + if not from_obj_path.endswith('.xml'): + from_obj_path += '.xml' + elif resolved_object_path: + from_obj_path = resolved_object_path + print(f"[resolved] ObjectPath -> {from_obj_path}") + else: + print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr) + sys.exit(1) + + if not os.path.exists(from_obj_path): + print(f"Object file not found: {from_obj_path}", file=sys.stderr) + sys.exit(1) + + purpose = args.Purpose or resolved_purpose or 'Item' + if resolved_purpose and not args.Purpose: + print(f"[resolved] Purpose -> {purpose}") + + meta = parse_object_meta(from_obj_path) + print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}") + + preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved) + + supported = { + 'Document': ['Item', 'List', 'Choice'], + 'Catalog': ['Item', 'Folder', 'List', 'Choice'], + 'InformationRegister': ['Record', 'List'], + 'AccumulationRegister': ['List'], + 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], + 'ExchangePlan': ['Item', 'List', 'Choice'], + 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], + } + if meta['Type'] not in supported: + print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", file=sys.stderr) + sys.exit(1) + if purpose not in supported[meta['Type']]: + print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr) + sys.exit(1) + + dsl_dispatch = { + 'Document': generate_document_dsl, + 'Catalog': generate_catalog_dsl, + 'InformationRegister': generate_information_register_dsl, + 'AccumulationRegister': generate_accumulation_register_dsl, + 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, + 'ExchangePlan': generate_exchange_plan_dsl, + 'ChartOfAccounts': generate_chart_of_accounts_dsl, + } + dsl = dsl_dispatch[meta['Type']](meta, preset_data, purpose) + + if args.EmitDsl: + dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl) + os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True) + with open(dsl_path, 'w', encoding='utf-8') as f: + json.dump(dsl, f, ensure_ascii=False, indent=2) + print(f"[from-object] DSL saved: {dsl_path}") + + defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict + # Convert dict-style elements (from generators) to list-style (expected by compiler) + defn = _normalize_elements(defn) + else: + # --- 1. Load and validate JSON --- + json_path = args.JsonPath + if not os.path.exists(json_path): + print(f"File not found: {json_path}", file=sys.stderr) + sys.exit(1) + + with open(json_path, 'r', encoding='utf-8-sig') as f: + defn = json.load(f) + + # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- + def _normalize_synonyms(el): + if not isinstance(el, dict): + return + synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'} + for src, dst in synonyms.items(): + if src in el and dst not in el: + el[dst] = el.pop(src) + if isinstance(el.get('children'), list): + for child in el['children']: + _normalize_synonyms(child) + if isinstance(el.get('columns'), list): + for child in el['columns']: + _normalize_synonyms(child) + + def _has_cmd_bar_recursive(el): + if not isinstance(el, dict): + return False + if el.get('cmdBar') is not None: + return True + if isinstance(el.get('children'), list): + for child in el['children']: + if _has_cmd_bar_recursive(child): + return True + if isinstance(el.get('columns'), list): + for child in el['columns']: + if _has_cmd_bar_recursive(child): + return True + return False + + def _apply_dlist_table_heuristic(el, list_name, has_main_table): + if not isinstance(el, dict): + return + if el.get('table') is not None and str(el.get('path', '')) == list_name: + if 'tableAutofill' not in el: + el['tableAutofill'] = False + if 'commandBarLocation' not in el: + el['commandBarLocation'] = 'None' + # DefaultPicture доступен только если у DynamicList есть основная таблица + if has_main_table and not el.get('rowPictureDataPath'): + el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' + if isinstance(el.get('children'), list): + for child in el['children']: + _apply_dlist_table_heuristic(child, list_name, has_main_table) + + def _is_object_like_type(t): + if not t: + return False + if t == 'DynamicList' or t == 'ConstantsSet': + return True + object_suffixes = ( + 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', + 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', + 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', + 'ChartOfCalculationTypesObject', 'ExchangePlanObject', + ) + record_set_prefixes = ( + 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', + 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', + 'InformationRegisterRecordManager', + ) + for s in object_suffixes: + if t.startswith(s + '.'): + return True + for s in record_set_prefixes: + if t.startswith(s + '.'): + return True + return False + + # 1b.1: Normalize synonyms recursively + if isinstance(defn.get('elements'), list): + for el in defn['elements']: + _normalize_synonyms(el) + + # 1b.2: Extract autoCmdBar element from defn['elements'] + main_acb_def = None + if isinstance(defn.get('elements'), list): + auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] + if len(auto_bars) > 1: + print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) + sys.exit(1) + if len(auto_bars) == 1: + main_acb_def = auto_bars[0] + defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] + + # 1b.3: Infer main attribute + if isinstance(defn.get('attributes'), list): + has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) + if not has_explicit_main: + candidates = [] + for a in defn['attributes']: + if not isinstance(a, dict): + continue + if 'main' in a and a.get('main') is False: + continue + if _is_object_like_type(str(a.get('type', ''))): + candidates.append(a) + if len(candidates) == 1: + candidates[0]['main'] = True + print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") + elif len(candidates) > 1: + names = ', '.join(c.get('name', '') for c in candidates) + print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") + + # 1b.4: DynamicList → table heuristic + if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): + main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None) + if main_attr and str(main_attr.get('type', '')) == 'DynamicList': + settings = main_attr.get('settings') or {} + has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) + for el in defn['elements']: + _apply_dlist_table_heuristic(el, main_attr.get('name', ''), has_mt) + + # 1b.5: Compute main AutoCommandBar Autofill (B3) + def _compute_main_acb_autofill(): + if main_acb_def is not None: + if 'autofill' in main_acb_def: + return bool(main_acb_def.get('autofill')) + return True + if isinstance(defn.get('elements'), list): + for el in defn['elements']: + if _has_cmd_bar_recursive(el): + return False + return True + + # --- 2. Main compilation --- + _next_id = 0 + lines = [] + + lines.append('') + lines.append(f'
') + + # Title + form_title = defn.get('title') + if not form_title and defn.get('properties') and defn['properties'].get('title'): + form_title = defn['properties']['title'] + if form_title: + emit_mltext(lines, '\t', 'Title', str(form_title)) + + # Properties (skip 'title' — handled above) + # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; + # otherwise platform appends synonym → "Title: Synonym" double-titles). + props_src = defn.get('properties') or {} + props_clone = OrderedDict() + if form_title and 'autoTitle' not in props_src: + props_clone['autoTitle'] = False + for k, v in props_src.items(): + if k != 'title': + props_clone[k] = v + emit_properties(lines, props_clone, '\t') + + # CommandSet (excluded commands) + if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: + lines.append('\t') + for cmd in defn['excludedCommands']: + lines.append(f'\t\t{cmd}') + lines.append('\t') + + # AutoCommandBar (always present, id=-1) + acb_autofill = _compute_main_acb_autofill() + acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' + acb_halign = None + if main_acb_def is not None: + v = main_acb_def.get('autoCmdBar') + if v: + acb_name = str(v) + if main_acb_def.get('name'): + acb_name = str(main_acb_def['name']) + if main_acb_def.get('horizontalAlign'): + acb_halign = str(main_acb_def['horizontalAlign']) + has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) + has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children + if has_inner: + lines.append(f'\t') + if acb_halign: + lines.append(f'\t\t{acb_halign}') + if not acb_autofill: + lines.append('\t\tfalse') + if has_acb_children: + lines.append('\t\t') + for child in main_acb_def['children']: + emit_element(lines, child, '\t\t\t', in_cmd_bar=True) + lines.append('\t\t') + lines.append('\t') + else: + lines.append(f'\t') + + # Events + if defn.get('events'): + for evt_name in defn['events']: + if evt_name not in KNOWN_FORM_EVENTS: + print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}") + lines.append('\t') + for evt_name, evt_handler in defn['events'].items(): + lines.append(f'\t\t{evt_handler}') + lines.append('\t') + + # ChildItems (elements) + if defn.get('elements') and len(defn['elements']) > 0: + lines.append('\t') + for el in defn['elements']: + emit_element(lines, el, '\t\t') + lines.append('\t') + + # Attributes + emit_attributes(lines, defn.get('attributes'), '\t') + + # Parameters + emit_parameters(lines, defn.get('parameters'), '\t') + + # Commands + emit_commands(lines, defn.get('commands'), '\t') + + # Close + lines.append('') + + # --- 3. Write output --- + out_path = args.OutputPath + if not os.path.isabs(out_path): + out_path = os.path.join(os.getcwd(), out_path) + out_dir = os.path.dirname(out_path) + if out_dir and not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + + content = '\n'.join(lines) + '\n' + write_utf8_bom(out_path, content) + + # --- 4. Auto-register form in parent object XML --- + # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + form_xml_dir = os.path.dirname(out_path) # Ext + form_name_dir = os.path.dirname(form_xml_dir) # FormName + forms_dir = os.path.dirname(form_name_dir) # Forms + object_dir = os.path.dirname(forms_dir) # ObjectName + type_plural_dir = os.path.dirname(object_dir) # TypePlural + + form_name = os.path.basename(form_name_dir) + object_name = os.path.basename(object_dir) + forms_leaf = os.path.basename(forms_dir) + + if forms_leaf == 'Forms': + object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') + if os.path.exists(object_xml_path): + with open(object_xml_path, 'r', encoding='utf-8-sig') as f: + raw_text = f.read() + + # Check if already registered + if f'
{form_name}
' not in raw_text: + # Insert before
+ if '
' in raw_text: + insert_line = f'\t\t\t
{form_name}
\n' + raw_text = raw_text.replace('', insert_line + '\t\t', 1) + elif '' in raw_text: + replacement = f'\n\t\t\t
{form_name}
\n\t\t
' + raw_text = raw_text.replace('', replacement, 1) + + write_utf8_bom(object_xml_path, raw_text) + print(f" Registered:
{form_name}
in {object_name}.xml") + + # --- 5. Summary --- + el_count = _next_id + print(f"[OK] Compiled: {args.OutputPath}") + print(f" Elements+IDs: {el_count}") + if defn.get('attributes'): + print(f" Attributes: {len(defn['attributes'])}") + if defn.get('commands'): + print(f" Commands: {len(defn['commands'])}") + if defn.get('parameters'): + print(f" Parameters: {len(defn['parameters'])}") + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 205dacb2..4acaf79e 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.3 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.4 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -184,6 +184,25 @@ function Convert-TypedValue { } } +# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора). +# Вызывается один раз для любого элемента. Height тут — пиксельная высота +# (); Table хранит высоту в строках () и ловит её сам. +function Add-Layout { + param($obj, $node) + if ((Get-Child $node 'SkipOnInput') -eq 'true') { $obj['skipOnInput'] = $true } + if ((Get-Child $node 'AutoMaxWidth') -eq 'false') { $obj['autoMaxWidth'] = $false } + $mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw } + if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false } + $mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh } + $w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w } + $h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h } + if ((Get-Child $node 'HorizontalStretch') -eq 'true') { $obj['horizontalStretch'] = $true } + if ((Get-Child $node 'VerticalStretch') -eq 'true') { $obj['verticalStretch'] = $true } + $gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha } + $gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva } + $ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha } +} + # Суффиксы авто-имён обработчиков (инверсия компилятора) $HANDLER_SUFFIX = @{ 'OnChange'='ПриИзменении'; 'StartChoice'='НачалоВыбора'; 'ChoiceProcessing'='ОбработкаВыбора'; @@ -396,6 +415,7 @@ function Decompile-Element { $obj[$key] = $name Add-CommonProps $obj $node $name $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['src'] = $ref.InnerText } + $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true } if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } } 'PictureField' { @@ -418,6 +438,7 @@ function Decompile-Element { if ((Get-Child $node 'ChangeRowOrder') -eq 'true') { $obj['changeRowOrder'] = $true } if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false } if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true } + $htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['height'] = [int]$htr } $cbl = Get-Child $node 'CommandBarLocation'; if ($cbl) { $obj['commandBarLocation'] = $cbl } $cols = Decompile-Children $node if ($cols) { $obj['columns'] = $cols } @@ -478,6 +499,7 @@ function Decompile-Element { if ($kids) { $obj['children'] = $kids } } } + Add-Layout $obj $node return $obj } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 6aa63707..af486a02 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -117,6 +117,25 @@ | `on` | string[] | Массив имён событий | | `handlers` | object | Явные имена обработчиков: `{"OnChange": "МойОбработчик"}` | +### 4.1a. Общие layout-свойства + +Применимы к любому элементу (размеры, растягивание, выравнивание внутри родителя). Эмитятся только при указании. + +| Свойство | XML | Значения | +|----------|-----|----------| +| `width` | `` | число | +| `height` | `` | число (у `table` → ``, высота в строках) | +| `horizontalStretch` | `` | `true` | +| `verticalStretch` | `` | `true` | +| `autoMaxWidth` | `` | `false` (у `input` при `multiLine` подставляется автоматически) | +| `autoMaxHeight` | `` | `false` | +| `maxWidth` | `` | число | +| `maxHeight` | `` | число | +| `groupHorizontalAlign` | `` | `Left`, `Center`, `Right` | +| `groupVerticalAlign` | `` | `Top`, `Center`, `Bottom` | +| `horizontalAlign` | `` | `Left`, `Center`, `Right` | +| `skipOnInput` | `` | `true` | + ### 4.2. Автоименование обработчиков При указании `"on"` без `"handlers"` имя обработчика генерируется автоматически: @@ -345,6 +364,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs | Свойство | Тип | Описание | |----------|-----|----------| | `src` или `picture` (как свойство) | string | Ссылка на картинку | +| `loadTransparent` | bool | `true` → загружать прозрачной. По умолчанию `false` | | `hyperlink` | bool | Режим гиперссылки | | `width` | int | Ширина | | `height` | int | Высота | diff --git a/tests/skills/cases/form-compile/groups.json b/tests/skills/cases/form-compile/groups.json index fa474f58..78581318 100644 --- a/tests/skills/cases/form-compile/groups.json +++ b/tests/skills/cases/form-compile/groups.json @@ -17,9 +17,10 @@ "title": "Группы", "elements": [ { "cmdBar": "КоманднаяПанель", "autofill": true }, - { "group": "horizontal", "name": "ГруппаШапка", "showTitle": true, "title": "Шапка", "children": [ - { "input": "Поле1", "path": "Поле1", "title": "Поле 1" }, - { "input": "Поле2", "path": "Поле2", "title": "Поле 2" } + { "group": "horizontal", "name": "ГруппаШапка", "showTitle": true, "title": "Шапка", "horizontalStretch": true, "groupHorizontalAlign": "Right", "children": [ + { "input": "Поле1", "path": "Поле1", "title": "Поле 1", "width": 20, "skipOnInput": true }, + { "input": "Поле2", "path": "Поле2", "title": "Поле 2" }, + { "labelField": "Метка", "path": "Поле1", "groupVerticalAlign": "Center" } ]}, { "group": "vertical", "name": "ГруппаПодвал", "children": [ { "input": "Поле3", "path": "Поле3", "title": "Поле 3" } diff --git a/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml index 6215e9b1..dc89ea9e 100644 --- a/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/groups/DataProcessors/СГруппами/Forms/Форма/Ext/Form.xml @@ -22,6 +22,8 @@ Horizontal + true + Right @@ -32,6 +34,8 @@ Поле 1 + true + 20 @@ -46,13 +50,19 @@ + + Поле1 + Center + + + - + Vertical - + - + Поле3 <v8:item> @@ -60,20 +70,20 @@ <v8:content>Поле 3</v8:content> </v8:item> - - + + - + cfg:DataProcessorObject.СГруппами true - + <v8:item> <v8:lang>ru</v8:lang> @@ -88,7 +98,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="Поле2" id="17"> + <Attribute name="Поле2" id="20"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -104,7 +114,7 @@ </v8:NumberQualifiers> </Type> </Attribute> - <Attribute name="Поле3" id="18"> + <Attribute name="Поле3" id="21"> <Title> <v8:item> <v8:lang>ru</v8:lang>