From f7695a95347e7b9ab5223afe1e668bde6c412e8e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 25 Mar 2026 19:56:26 +0300 Subject: [PATCH] feat(cfe-borrow): add -BorrowMainAttribute for borrowing object attributes with form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When adding a new attribute to a borrowed form, -BorrowMainAttribute borrows the form's main attribute ("Объект") and all referenced object attributes, tabular sections, and their transitive type dependencies. Two modes: Form (default — only attributes referenced by form DataPath) and All (all object attributes). Deep paths like Объект.A.B are resolved transitively. Already-borrowed objects are not overwritten. Also fixed: CommonPicture auto-borrow from AutoCommandBar, form-attribute DataPath stripping (keep only Объект.* paths). Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/cfe-borrow/SKILL.md | 24 +- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 665 +++++++++++++++++- .../skills/cfe-borrow/scripts/cfe-borrow.py | 569 ++++++++++++++- docs/1c-extension-spec.md | 76 +- docs/cfe-guide.md | 29 +- 5 files changed, 1308 insertions(+), 55 deletions(-) diff --git a/.claude/skills/cfe-borrow/SKILL.md b/.claude/skills/cfe-borrow/SKILL.md index 0bf747f5..8d98ba12 100644 --- a/.claude/skills/cfe-borrow/SKILL.md +++ b/.claude/skills/cfe-borrow/SKILL.md @@ -1,7 +1,7 @@ --- name: cfe-borrow description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации -argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты" +argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute allowed-tools: - Bash - Read @@ -31,6 +31,7 @@ allowed-tools: | `ExtensionPath` | Путь к каталогу расширения (обязат.) | | `ConfigPath` | Путь к конфигурации-источнику (обязат.) | | `Object` | Что заимствовать (обязат.), batch через `;;` | +| `BorrowMainAttribute` | Используй при добавлении нового реквизита на заимствованную форму. `Form` (по умолч.) — реквизиты с формы, `All` — все реквизиты объекта | ## Формат -Object @@ -52,6 +53,21 @@ allowed-tools: 3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` 4. **Регистрация** — `
` в ChildObjects родительского объекта +### Заимствование основного реквизита формы (-BorrowMainAttribute) + +**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`. + +**Два режима**: +- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев +- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет + +**Типовой сценарий** (добавление реквизита + вывод на форму): +1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами +2. `/meta-edit` — добавить новый реквизит в объект расширения +3. `/form-edit` — вывести реквизит на заимствованную форму + +**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее. + ## Команда ```powershell @@ -69,6 +85,12 @@ powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 # Несколько объектов за раз ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" + +# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute + +# Заимствовать форму с ВСЕМИ реквизитами объекта +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All ``` ## Верификация diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index e8c49180..c186c3f9 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -1,9 +1,10 @@ -# cfe-borrow v1.1 — Borrow objects from configuration into extension (CFE) +# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][string]$ExtensionPath, [Parameter(Mandatory)][string]$ConfigPath, - [Parameter(Mandatory)][string]$Object + [Parameter(Mandatory)][string]$Object, + [string]$BorrowMainAttribute ) $ErrorActionPreference = "Stop" @@ -256,6 +257,9 @@ $script:generatedTypes = @{ @{ prefix = "DataProcessorObject"; category = "Object" } @{ prefix = "DataProcessorManager"; category = "Manager" } ) + "DefinedType" = @( + @{ prefix = "DefinedType"; category = "DefinedType" } + ) } # Types that need ChildObjects element @@ -269,6 +273,9 @@ $typesWithChildObjects = @( # CommonModule properties to copy from source $commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") +# Standard system fields to skip when collecting DataPath references +$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName") + # --- 7. XML manipulation helpers (from cf-edit) --- function Get-ChildIndent($container) { foreach ($child in $container.ChildNodes) { @@ -324,6 +331,23 @@ if ($items.Count -eq 0) { exit 1 } +# --- 9b. Validate -BorrowMainAttribute --- +if ($BorrowMainAttribute) { + # PS treats -BorrowMainAttribute without value as "True" + if ($BorrowMainAttribute -eq "True") { $BorrowMainAttribute = "Form" } + if ($BorrowMainAttribute -notin @("Form","All")) { + Write-Error "-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)" + exit 1 + } + # Validate: only with .Form. pattern + $hasForm = $false + foreach ($item in $items) { if ($item -match '\.Form\.') { $hasForm = $true; break } } + if (-not $hasForm) { + Write-Error "-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')" + exit 1 + } +} + # --- 10. Helper: read source object XML --- function Read-SourceObject { param([string]$typeName, [string]$objName) @@ -421,7 +445,7 @@ function Read-SourceFormUuid { # --- 10c. Helper: borrow a form --- function Borrow-Form { - param([string]$typeName, [string]$objName, [string]$formName) + param([string]$typeName, [string]$objName, [string]$formName, [switch]$BorrowMainAttr) $dirName = $childTypeDirMap[$typeName] $enc = New-Object System.Text.UTF8Encoding($true) @@ -509,8 +533,13 @@ function Borrow-Form { $autoCmdXml = $autoCmdXml -replace 'true', 'false' # Strip ExcludedCommand (references to standard commands invalid in extension) $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') - # Strip DataPath in AutoCommandBar buttons (e.g. Объект.Ref — invalid in extension) - $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') + # Strip DataPath in AutoCommandBar buttons + if ($BorrowMainAttr) { + # Keep only Объект.* DataPaths + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*(?!Объект\.)[^<]*', '') + } else { + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') + } } # ChildItems: copy full tree, clean up base-config references @@ -520,12 +549,17 @@ function Borrow-Form { $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') # Replace all CommandName values with 0 $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') - # Strip DataPath (references base form attributes not in extension) - $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') - # Strip TitleDataPath (e.g. Объект.Товары.RowsCount — invalid without base attributes) - $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') - # Strip RowPictureDataPath (e.g. Список.СостояниеДокумента — invalid in extension) - $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip DataPath, TitleDataPath, RowPictureDataPath + if ($BorrowMainAttr) { + # Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*(?!Объект\.)[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*(?!Объект\.)[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + } else { + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + } # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) @@ -533,10 +567,14 @@ function Borrow-Form { # Strip element-level Events (base form handlers not in extension) $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') - # Collect CommonPicture references from ChildItems - $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') + # Collect CommonPicture references from ChildItems and AutoCommandBar $referencedPictures = @{} + $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true } + if ($autoCmdXml) { + $picRefs2 = [regex]::Matches($autoCmdXml, 'CommonPicture\.(\w+)') + foreach ($m in $picRefs2) { $referencedPictures[$m.Groups[1].Value] = $true } + } # Auto-borrow referenced CommonPictures (if not already borrowed) $autoBorrowedPics = @() @@ -555,7 +593,7 @@ function Borrow-Form { [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) Add-ToChildObjects "CommonPicture" $picName $autoBorrowedPics += $picName - $borrowedFiles += $targetFile + $script:borrowedFiles += $targetFile Info " Auto-borrowed: CommonPicture.${picName}" } else { Warn " CommonPicture.${picName} not found in source config — will strip from form" @@ -584,6 +622,19 @@ function Borrow-Form { # Strip StdPicture blocks (except Print) $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + # Same Picture strip for AutoCommandBar + if ($autoCmdXml) { + $acPicMatches = [regex]::Matches($autoCmdXml, $picBlockPattern) + for ($mi = $acPicMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $acPicMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $autoCmdXml = $autoCmdXml.Remove($pm.Index, $pm.Length) + } + } + $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + } + # Auto-borrow StyleItems referenced in ChildItems # Pattern 1: , # Pattern 2: style:XXX, style:XXX, etc. @@ -607,7 +658,7 @@ function Borrow-Form { $encBom = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) Add-ToChildObjects "StyleItem" $styleName - $borrowedFiles += $targetFile + $script:borrowedFiles += $targetFile Info " Auto-borrowed: StyleItem.${styleName}" } else { Warn " StyleItem.${styleName} not found in source config" @@ -680,7 +731,7 @@ function Borrow-Form { $encBom = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) Add-ToChildObjects "Enum" $enumName - $borrowedFiles += $targetFile + $script:borrowedFiles += $targetFile Info " Auto-borrowed: Enum.${enumName} (with $($enumValueXmls.Count) EnumValue(s))" } else { Warn " Enum.${enumName} not found in source config" @@ -715,7 +766,22 @@ function Borrow-Form { $formXmlSb.Append("`t$childItemsXml") | Out-Null $formXmlSb.Append("`r`n") | Out-Null } - $formXmlSb.Append("`t") | Out-Null + # Attributes: empty or with MainAttribute when BorrowMainAttr + if ($BorrowMainAttr) { + $objTypePrefix = "" + $gtList = $script:generatedTypes[$typeName] + if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } } + $mainAttrType = "cfg:${objTypePrefix}.${objName}" + $formXmlSb.Append("`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t${mainAttrType}`r`n") | Out-Null + $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + } else { + $formXmlSb.Append("`t") | Out-Null + } $formXmlSb.Append("`r`n") | Out-Null # BaseForm: same content, indented one more level @@ -744,7 +810,18 @@ function Borrow-Form { } } - $formXmlSb.Append("`t`t") | Out-Null + # BaseForm Attributes: same as main section + if ($BorrowMainAttr) { + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`t${mainAttrType}`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t") | Out-Null + } else { + $formXmlSb.Append("`t`t") | Out-Null + } $formXmlSb.Append("`r`n") | Out-Null $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null @@ -904,6 +981,540 @@ function Build-InternalInfoXml { return $sb.ToString() } +# --- 11b. Collect DataPath references from source Form.xml --- +function Collect-FormDataPaths { + param([string]$formXmlPath) + + $enc = New-Object System.Text.UTF8Encoding($true) + $content = [System.IO.File]::ReadAllText($formXmlPath, $enc) + + $firstLevel = @{} + $deepPaths = @() + + $matches2 = [regex]::Matches($content, '[^<]*\bОбъект\.(\w+(?:\.\w+)*)') + foreach ($m in $matches2) { + $path = $m.Groups[1].Value + $segments = $path.Split(".") + $seg0 = $segments[0] + if ($script:standardFields -contains $seg0) { continue } + $firstLevel[$seg0] = $true + if ($segments.Count -ge 2) { + $seg1 = $segments[1] + if ($script:standardFields -contains $seg1) { continue } + $deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1 } + } + } + + # Also collect from TitleDataPath + $matches3 = [regex]::Matches($content, '[^<]*\bОбъект\.(\w+(?:\.\w+)*)') + foreach ($m in $matches3) { + $path = $m.Groups[1].Value + $segments = $path.Split(".") + $seg0 = $segments[0] + if ($script:standardFields -contains $seg0) { continue } + $firstLevel[$seg0] = $true + } + + # Deduplicate deep paths + $seen = @{} + $uniqueDeep = @() + foreach ($dp in $deepPaths) { + $key = "$($dp.ObjectAttr).$($dp.SubAttr)" + if (-not $seen.ContainsKey($key)) { + $seen[$key] = $true + $uniqueDeep += $dp + } + } + + return @{ FirstLevel = $firstLevel; DeepPaths = $uniqueDeep } +} + +# --- 11c. Resolve source attributes and tabular sections --- +function Resolve-SourceAttributes { + param([string]$typeName, [string]$objName, $firstLevelNames) + # $firstLevelNames: hashtable of names, or $null for "all" + + $dirName = $childTypeDirMap[$typeName] + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", $script:mdNs) + $srcNs.AddNamespace("xr", $script:xrNs) + $srcNs.AddNamespace("v8", $script:v8Ns) + + $srcEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { Write-Error "No metadata element in source: $srcFile"; exit 1 } + + $childObjs = $srcEl.SelectSingleNode("md:ChildObjects", $srcNs) + if (-not $childObjs) { return @{ Attributes = @(); TabularSections = @(); ExtraProps = @{} } } + + $attrs = @() + $tabSections = @() + + foreach ($child in $childObjs.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + + if ($child.LocalName -eq 'Attribute') { + $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $nameNode) { continue } + $attrName = $nameNode.InnerText + if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($attrName)) { continue } + + $uuid = $child.GetAttribute("uuid") + $typeNode = $child.SelectSingleNode("md:Properties/md:Type", $srcNs) + $typeXml = if ($typeNode) { $typeNode.OuterXml } else { "" } + # Strip namespace declarations from Type + $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + + $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } + } + elseif ($child.LocalName -eq 'TabularSection') { + $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $nameNode) { continue } + $tsName = $nameNode.InnerText + if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($tsName)) { continue } + + $tsUuid = $child.GetAttribute("uuid") + + # Extract GeneratedTypes from InternalInfo + $tsGenTypes = @() + $iiNode = $child.SelectSingleNode("md:InternalInfo", $srcNs) + if ($iiNode) { + $gtNodes = $iiNode.SelectNodes("xr:GeneratedType", $srcNs) + foreach ($gt in $gtNodes) { + $tsGenTypes += @{ + Name = $gt.GetAttribute("name") + Category = $gt.GetAttribute("category") + TypeId = $gt.SelectSingleNode("xr:TypeId", $srcNs).InnerText + ValueId = $gt.SelectSingleNode("xr:ValueId", $srcNs).InnerText + } + } + } + + # Extract ALL child attributes of TabularSection + $tsAttrs = @() + $tsChildObjs = $child.SelectSingleNode("md:ChildObjects", $srcNs) + if ($tsChildObjs) { + foreach ($tsChild in $tsChildObjs.ChildNodes) { + if ($tsChild.NodeType -ne 'Element' -or $tsChild.LocalName -ne 'Attribute') { continue } + $tsAttrName = $tsChild.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $tsAttrName) { continue } + $tsAttrUuid = $tsChild.GetAttribute("uuid") + $tsTypeNode = $tsChild.SelectSingleNode("md:Properties/md:Type", $srcNs) + $tsTypeXml = if ($tsTypeNode) { $tsTypeNode.OuterXml } else { "" } + $tsTypeXml = [regex]::Replace($tsTypeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + $tsAttrs += @{ Name = $tsAttrName.InnerText; Uuid = $tsAttrUuid; TypeXml = $tsTypeXml } + } + } + + $tabSections += @{ Name = $tsName; Uuid = $tsUuid; GeneratedTypes = $tsGenTypes; Attributes = $tsAttrs } + } + } + + # Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.) + $extraProps = @{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + $propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", + "NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity") + foreach ($pName in $propsToExtract) { + $pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs) + if ($pNode) { $extraProps[$pName] = $pNode.InnerText } + } + } + + return @{ Attributes = $attrs; TabularSections = $tabSections; ExtraProps = $extraProps } +} + +# --- 11d. Build adopted attribute XML --- +function Build-AdoptedAttributeXml { + param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) + + $newUuid = [guid]::NewGuid().ToString() + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`tAdopted") | Out-Null + $sb.AppendLine("${indent}`t`t${name}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null + $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11e. Build adopted tabular section XML --- +function Build-AdoptedTabularSectionXml { + param([string]$tsName, [string]$sourceUuid, $generatedTypes, $childAttrs, [string]$indent) + + $newUuid = [guid]::NewGuid().ToString() + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # InternalInfo with GeneratedTypes (new UUIDs, referencing source names) + if ($generatedTypes -and $generatedTypes.Count -gt 0) { + $sb.AppendLine("${indent}`t") | Out-Null + foreach ($gt in $generatedTypes) { + $newTid = [guid]::NewGuid().ToString() + $newVid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t`t${newTid}") | Out-Null + $sb.AppendLine("${indent}`t`t`t${newVid}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + } + $sb.AppendLine("${indent}`t") | Out-Null + } else { + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`tAdopted") | Out-Null + $sb.AppendLine("${indent}`t`t${tsName}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + + # ChildObjects with all attributes + if ($childAttrs -and $childAttrs.Count -gt 0) { + $sb.AppendLine("${indent}`t") | Out-Null + foreach ($ca in $childAttrs) { + $caXml = Build-AdoptedAttributeXml $ca.Name $ca.Uuid $ca.TypeXml "${indent}`t`t" + $sb.AppendLine($caXml) | Out-Null + } + $sb.AppendLine("${indent}`t") | Out-Null + } else { + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11f. Collect reference types from attribute Type XML strings --- +function Collect-ReferenceTypes { + param([string[]]$typeXmls) + + $result = @{} + foreach ($typeXml in $typeXmls) { + # cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc. + $refMatches = [regex]::Matches($typeXml, 'cfg:(\w+)Ref\.(\w+)') + foreach ($m in $refMatches) { + $refPrefix = $m.Groups[1].Value # e.g. "Catalog", "Enum", "Document" + $objName = $m.Groups[2].Value + $key = "${refPrefix}.${objName}" + if (-not $result.ContainsKey($key)) { + $result[$key] = @{ TypeName = $refPrefix; ObjName = $objName } + } + } + # cfg:DefinedType.XXX (via v8:TypeSet or v8:Type) + $dtMatches = [regex]::Matches($typeXml, 'cfg:DefinedType\.(\w+)') + foreach ($m in $dtMatches) { + $dtName = $m.Groups[1].Value + $key = "DefinedType.${dtName}" + if (-not $result.ContainsKey($key)) { + $result[$key] = @{ TypeName = "DefinedType"; ObjName = $dtName } + } + } + } + return @($result.Values) +} + +# --- 11g. Merge adopted attributes into existing extension object XML --- +function Merge-AttributesIntoObject { + param([string]$typeName, [string]$objName, $attrsToAdd) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + if (-not (Test-Path $objFile)) { + Warn "Cannot merge attributes: $objFile not found" + return + } + + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", $script:mdNs) + + $objEl = $null + foreach ($c in $objDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { Warn "No type element in $objFile"; return } + + $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if (-not $childObjs) { + $childObjs = $objDoc.CreateElement("ChildObjects", $script:mdNs) + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null + $objEl.AppendChild($childObjs) | Out-Null + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null + } + + # Collect existing attribute names for dedup + $existingNames = @{} + foreach ($c in $childObjs.ChildNodes) { + if ($c.NodeType -ne 'Element' -or $c.LocalName -ne 'Attribute') { continue } + $nameNode = $c.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($nameNode) { $existingNames[$nameNode.InnerText] = $true } + } + + $added = 0 + foreach ($attr in $attrsToAdd) { + if ($existingNames.ContainsKey($attr.Name)) { continue } + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + + # Expand self-closing ChildObjects if needed + if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { + $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") + $childObjs.AppendChild($closeWs) | Out-Null + } + + $added++ + } + + if ($added -gt 0) { + # Build all adopted attributes as text and do string-level insertion + $allAttrXml = "" + foreach ($attr in $attrsToAdd) { + if ($existingNames.ContainsKey($attr.Name)) { continue } + $allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") + } + + # Save via text manipulation to avoid namespace issues with InnerXml + $settings3 = New-Object System.Xml.XmlWriterSettings + $settings3.Encoding = New-Object System.Text.UTF8Encoding($true) + $settings3.Indent = $false + $settings3.NewLineHandling = [System.Xml.NewLineHandling]::None + $memStream3 = New-Object System.IO.MemoryStream + $writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3) + $objDoc.Save($writer3) + $writer3.Flush(); $writer3.Close() + $bytes3 = $memStream3.ToArray() + $memStream3.Close() + $text3 = [System.Text.Encoding]::UTF8.GetString($bytes3) + if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) } + $text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"') + + # Insert attributes before + $text3 = $text3 -replace '', "${allAttrXml}`r`n`t`t" + + $utf8Bom3 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3) + Info " Merged $added attribute(s) into: $objFile" + } +} + +# --- 11h. Borrow-MainAttribute orchestrator --- +function Borrow-MainAttribute { + param([string]$typeName, [string]$objName, [string]$formName, [string]$mode) + + $dirName = $childTypeDirMap[$typeName] + Info "Borrowing main attribute for ${typeName}.${objName} (mode: $mode)..." + + # Step 1: Collect DataPaths (Form mode) or take all (All mode) + $firstLevelNames = $null + $deepPaths = @() + if ($mode -eq "Form") { + $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + if (-not (Test-Path $srcFormXmlPath)) { + Write-Error "Source Form.xml not found: $srcFormXmlPath" + exit 1 + } + $dp = Collect-FormDataPaths $srcFormXmlPath + $firstLevelNames = $dp.FirstLevel + $deepPaths = $dp.DeepPaths + Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" + } else { + Info " Mode All: borrowing all attributes and tabular sections" + } + + # Step 2: Resolve source attributes + $resolved = Resolve-SourceAttributes $typeName $objName $firstLevelNames + $srcAttrs = $resolved.Attributes + $srcTS = $resolved.TabularSections + $extraProps = $resolved.ExtraProps + Info " Resolved: $($srcAttrs.Count) attributes, $($srcTS.Count) tabular section(s)" + + # Identify which FirstLevel names are TabularSections (for deep path filtering) + $tsNames = @{} + foreach ($ts in $srcTS) { $tsNames[$ts.Name] = $true } + + # Step 3: Build the adopted content and insert into main object XML + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + + # Generate full object XML with attributes and TS + $contentSb = New-Object System.Text.StringBuilder + foreach ($attr in $srcAttrs) { + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + $contentSb.AppendLine($attrXml) | Out-Null + } + foreach ($ts in $srcTS) { + $tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t" + $contentSb.AppendLine($tsXml) | Out-Null + } + $adoptedContent = $contentSb.ToString().TrimEnd() + + # Read existing object XML and inject + $objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) + + # Inject extra properties after ExtendedConfigurationObject + if ($extraProps.Count -gt 0) { + $propsSb = New-Object System.Text.StringBuilder + foreach ($pName in $extraProps.Keys) { + $propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])") | Out-Null + } + $objContent = $objContent -replace '()', "`$1$($propsSb.ToString())" + } + + # Replace empty ChildObjects with adopted content + if ($adoptedContent) { + # Handle (self-closing) + if ($objContent -match '') { + $objContent = $objContent -replace '', "`r`n${adoptedContent}`r`n`t`t" + } + # Handle ... (may already have Form entry) + elseif ($objContent -match '(?s)(.*?)') { + $existingInner = $Matches[1] + $objContent = $objContent -replace '(?s)(.*?)', "${existingInner}`r`n${adoptedContent}`r`n`t`t" + } + } + + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $objContent, $encBom) + Info " Enriched object: $objFile" + + # Step 4: Collect all reference types and borrow as shells + $allTypeXmls = @() + foreach ($a in $srcAttrs) { $allTypeXmls += $a.TypeXml } + foreach ($ts in $srcTS) { + foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml } + } + $refTypes = Collect-ReferenceTypes $allTypeXmls + Info " Reference types to borrow: $($refTypes.Count)" + + foreach ($rt in $refTypes) { + if (-not $childTypeDirMap.ContainsKey($rt.TypeName)) { + Warn " Unknown reference type: $($rt.TypeName).$($rt.ObjName)" + continue + } + if (Test-ObjectBorrowed $rt.TypeName $rt.ObjName) { + Info " Already borrowed: $($rt.TypeName).$($rt.ObjName)" + continue + } + $rtSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$rt.TypeName]) "$($rt.ObjName).xml" + if (-not (Test-Path $rtSrcFile)) { + Warn " Source not found: $($rt.TypeName).$($rt.ObjName)" + continue + } + $src = Read-SourceObject $rt.TypeName $rt.ObjName + $borrowedXml = Build-BorrowedObjectXml $rt.TypeName $rt.ObjName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir $childTypeDirMap[$rt.TypeName] + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "$($rt.ObjName).xml" + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects $rt.TypeName $rt.ObjName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: $($rt.TypeName).$($rt.ObjName)" + } + + # Step 5: Handle deep paths (Form mode only) + if ($mode -eq "Form" -and $deepPaths.Count -gt 0) { + # Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs) + $realDeep = @() + foreach ($dp in $deepPaths) { + if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp } + } + + if ($realDeep.Count -gt 0) { + Info " Processing $($realDeep.Count) deep path(s)..." + + # Group by ObjectAttr → target catalog + $deepByAttr = @{} + foreach ($dp in $realDeep) { + if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() } + $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr + } + + foreach ($attrName in $deepByAttr.Keys) { + # Find the attribute's type to determine target catalog + $attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1 + if (-not $attrInfo) { continue } + + # Extract catalog name from type: cfg:CatalogRef.XXX + $catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') + if (-not $catMatch.Success) { continue } + + $targetTypeName = $catMatch.Groups[1].Value + $targetObjName = $catMatch.Groups[2].Value + + # Ensure target is borrowed + if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) { + $tSrc = Read-SourceObject $targetTypeName $targetObjName + $tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties + $tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName] + if (-not (Test-Path $tTargetDir)) { + New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null + } + $tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml" + [System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom) + Add-ToChildObjects $targetTypeName $targetObjName + $script:borrowedFiles += $tTargetFile + Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}" + } + + # Resolve sub-attributes in target catalog + $subNames = @{} + foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true } + $subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames + + if ($subResolved.Attributes.Count -gt 0) { + Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes + + # Collect and borrow ref types from deep attributes + $subTypeXmls = @() + foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml } + $subRefTypes = Collect-ReferenceTypes $subTypeXmls + foreach ($srt in $subRefTypes) { + if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue } + if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue } + $sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml" + if (-not (Test-Path $sSrcFile)) { continue } + $sSrc = Read-SourceObject $srt.TypeName $srt.ObjName + $sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties + $sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName] + if (-not (Test-Path $sTargetDir)) { + New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null + } + $sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml" + [System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom) + Add-ToChildObjects $srt.TypeName $srt.ObjName + $script:borrowedFiles += $sTargetFile + Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)" + } + } + } + } + } + + Info " Main attribute borrowing complete" +} + # --- 12. Helper: build borrowed object XML --- function Build-BorrowedObjectXml { param( @@ -1016,7 +1627,7 @@ function Add-ToChildObjects { } # --- 14. Process each item --- -$borrowedFiles = @() +$script:borrowedFiles = @() $borrowedCount = 0 foreach ($item in $items) { @@ -1070,13 +1681,19 @@ foreach ($item in $items) { Info " Created: $targetFile" Add-ToChildObjects $typeName $objName - $borrowedFiles += $targetFile + $script:borrowedFiles += $targetFile } # Borrow the form - $formFiles = Borrow-Form $typeName $objName $formName - $borrowedFiles += $formFiles + $hasBMA = [bool]$BorrowMainAttribute + $formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA + $script:borrowedFiles += $formFiles $borrowedCount++ + + # Borrow main attribute if requested + if ($hasBMA) { + Borrow-MainAttribute $typeName $objName $formName $BorrowMainAttribute + } } else { # --- Object borrowing (existing logic) --- Info "Borrowing ${typeName}.${objName}..." @@ -1098,7 +1715,7 @@ foreach ($item in $items) { Add-ToChildObjects $typeName $objName - $borrowedFiles += $targetFile + $script:borrowedFiles += $targetFile $borrowedCount++ } } @@ -1130,7 +1747,7 @@ Write-Host "=== cfe-borrow summary ===" Write-Host " Extension: $extDir" Write-Host " Config: $cfgDir" Write-Host " Borrowed: $borrowedCount object(s)" -foreach ($f in $borrowedFiles) { +foreach ($f in $script:borrowedFiles) { Write-Host " - $f" } exit 0 diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index f8dc220b..cec322c9 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-borrow v1.1 — Borrow objects from configuration into extension (CFE) +# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -221,6 +221,9 @@ GENERATED_TYPES = { {"prefix": "DataProcessorObject", "category": "Object"}, {"prefix": "DataProcessorManager", "category": "Manager"}, ], + "DefinedType": [ + {"prefix": "DefinedType", "category": "DefinedType"}, + ], } TYPES_WITH_CHILD_OBJECTS = [ @@ -232,6 +235,12 @@ TYPES_WITH_CHILD_OBJECTS = [ COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"] +# Standard system fields to skip when collecting DataPath references +STANDARD_FIELDS = [ + "Code", "Description", "Ref", "Parent", "DeletionMark", + "Predefined", "IsFolder", "LineNumber", "RowsCount", "PredefinedDataName", +] + XMLNS_DECL = ( 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" ' 'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" ' @@ -318,6 +327,7 @@ def main(): parser.add_argument("-ExtensionPath", required=True) parser.add_argument("-ConfigPath", required=True) parser.add_argument("-Object", required=True) + parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None) args = parser.parse_args() # --- 1. Resolve paths --- @@ -392,6 +402,9 @@ def main(): break info(f"Extension NamePrefix: {name_prefix}") + # Module-level list for borrowed files (used by both main loop and borrow_main_attribute) + borrowed_files = [] + # --- Helper functions --- def read_source_object(type_name, obj_name): dir_name = CHILD_TYPE_DIR_MAP.get(type_name) @@ -603,7 +616,456 @@ def main(): save_xml_bom(obj_tree, obj_file) info(f" Registered form in: {obj_file}") - def borrow_form(type_name, obj_name, form_name): + # --- 11b. Collect DataPath references from source Form.xml --- + def collect_form_data_paths(form_xml_path): + with open(form_xml_path, "r", encoding="utf-8-sig") as fh: + content = fh.read() + + first_level = {} + deep_paths = [] + + for m in re.finditer(r'[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)', content): + path = m.group(1) + segments = path.split(".") + seg0 = segments[0] + if seg0 in STANDARD_FIELDS: + continue + first_level[seg0] = True + if len(segments) >= 2: + seg1 = segments[1] + if seg1 in STANDARD_FIELDS: + continue + deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1}) + + # Also collect from TitleDataPath + for m in re.finditer(r'[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)', content): + path = m.group(1) + segments = path.split(".") + seg0 = segments[0] + if seg0 in STANDARD_FIELDS: + continue + first_level[seg0] = True + + # Deduplicate deep paths + seen = set() + unique_deep = [] + for dp in deep_paths: + key = f"{dp['ObjectAttr']}.{dp['SubAttr']}" + if key not in seen: + seen.add(key) + unique_deep.append(dp) + + return {"FirstLevel": first_level, "DeepPaths": unique_deep} + + # --- 11c. Resolve source attributes and tabular sections --- + def resolve_source_attributes(type_name, obj_name, first_level_names): + # first_level_names: dict of names, or None for "all" + dir_name = CHILD_TYPE_DIR_MAP[type_name] + src_file = os.path.join(cfg_dir, dir_name, f"{obj_name}.xml") + if not os.path.isfile(src_file): + print(f"Source object not found: {src_file}", file=sys.stderr) + sys.exit(1) + + src_parser = etree.XMLParser(remove_blank_text=True) + src_tree = etree.parse(src_file, src_parser) + src_root = src_tree.getroot() + + ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + + src_el = None + for c in src_root: + if isinstance(c.tag, str): + src_el = c + break + if src_el is None: + print(f"No metadata element in source: {src_file}", file=sys.stderr) + sys.exit(1) + + child_objs = src_el.find(f"{{{MD_NS}}}ChildObjects") + if child_objs is None: + return {"Attributes": [], "TabularSections": [], "ExtraProps": {}} + + attrs = [] + tab_sections = [] + + for child in child_objs: + if not isinstance(child.tag, str): + continue + ln = localname(child) + + if ln == "Attribute": + name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") + if name_node is None: + continue + attr_name = (name_node.text or "").strip() + if first_level_names is not None and attr_name not in first_level_names: + continue + + attr_uuid = child.get("uuid", "") + type_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Type") + type_xml = "" + if type_node is not None: + type_xml = etree.tostring(type_node, encoding="unicode") + type_xml = ns_strip.sub("", type_xml) + + attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml}) + + elif ln == "TabularSection": + name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") + if name_node is None: + continue + ts_name = (name_node.text or "").strip() + if first_level_names is not None and ts_name not in first_level_names: + continue + + ts_uuid = child.get("uuid", "") + + # Extract GeneratedTypes from InternalInfo + ts_gen_types = [] + ii_node = child.find(f"{{{MD_NS}}}InternalInfo") + if ii_node is not None: + for gt in ii_node: + if isinstance(gt.tag, str) and localname(gt) == "GeneratedType": + gt_name = gt.get("name", "") + gt_category = gt.get("category", "") + tid_el = gt.find(f"{{{XR_NS}}}TypeId") + vid_el = gt.find(f"{{{XR_NS}}}ValueId") + ts_gen_types.append({ + "Name": gt_name, + "Category": gt_category, + "TypeId": (tid_el.text or "") if tid_el is not None else "", + "ValueId": (vid_el.text or "") if vid_el is not None else "", + }) + + # Extract ALL child attributes of TabularSection + ts_attrs = [] + ts_child_objs = child.find(f"{{{MD_NS}}}ChildObjects") + if ts_child_objs is not None: + for ts_child in ts_child_objs: + if not isinstance(ts_child.tag, str) or localname(ts_child) != "Attribute": + continue + ts_attr_name_el = ts_child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") + if ts_attr_name_el is None: + continue + ts_attr_uuid = ts_child.get("uuid", "") + ts_type_node = ts_child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Type") + ts_type_xml = "" + if ts_type_node is not None: + ts_type_xml = etree.tostring(ts_type_node, encoding="unicode") + ts_type_xml = ns_strip.sub("", ts_type_xml) + ts_attrs.append({ + "Name": (ts_attr_name_el.text or "").strip(), + "Uuid": ts_attr_uuid, + "TypeXml": ts_type_xml, + }) + + tab_sections.append({ + "Name": ts_name, "Uuid": ts_uuid, + "GeneratedTypes": ts_gen_types, "Attributes": ts_attrs, + }) + + # Extract extra Properties for main object enrichment + extra_props = {} + props_node = src_el.find(f"{{{MD_NS}}}Properties") + if props_node is not None: + props_to_extract = [ + "Hierarchical", "FoldersOnTop", "CodeLength", "DescriptionLength", + "CodeType", "CodeAllowedLength", "NumberType", "NumberLength", + "NumberAllowedLength", "NumberPeriodicity", + ] + for p_name in props_to_extract: + p_node = props_node.find(f"{{{MD_NS}}}{p_name}") + if p_node is not None: + extra_props[p_name] = (p_node.text or "").strip() + + return {"Attributes": attrs, "TabularSections": tab_sections, "ExtraProps": extra_props} + + # --- 11d. Build adopted attribute XML --- + def build_adopted_attribute_xml(name, source_uuid, type_xml, indent): + new_uuid_val = new_guid() + lines = [ + f'{indent}', + f'{indent}\t', + f'{indent}\t', + f'{indent}\t\tAdopted', + f'{indent}\t\t{name}', + f'{indent}\t\t', + f'{indent}\t\t{source_uuid}', + f'{indent}\t\t{type_xml}', + f'{indent}\t', + f'{indent}', + ] + return "\n".join(lines) + + # --- 11e. Build adopted tabular section XML --- + def build_adopted_tabular_section_xml(ts_name, source_uuid, generated_types, child_attrs, indent): + new_uuid_val = new_guid() + lines = [f'{indent}'] + + # InternalInfo with GeneratedTypes (new UUIDs, referencing source names) + if generated_types: + lines.append(f'{indent}\t') + for gt in generated_types: + new_tid = new_guid() + new_vid = new_guid() + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{new_tid}') + lines.append(f'{indent}\t\t\t{new_vid}') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t') + else: + lines.append(f'{indent}\t') + + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\tAdopted') + lines.append(f'{indent}\t\t{ts_name}') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t{source_uuid}') + lines.append(f'{indent}\t') + + # ChildObjects with all attributes + if child_attrs: + lines.append(f'{indent}\t') + for ca in child_attrs: + ca_xml = build_adopted_attribute_xml(ca["Name"], ca["Uuid"], ca["TypeXml"], f"{indent}\t\t") + lines.append(ca_xml) + lines.append(f'{indent}\t') + else: + lines.append(f'{indent}\t') + + lines.append(f'{indent}') + return "\n".join(lines) + + # --- 11f. Collect reference types from attribute Type XML strings --- + def collect_reference_types(type_xmls): + result = {} + for type_xml in type_xmls: + # cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc. + for m in re.finditer(r'cfg:(\w+)Ref\.(\w+)', type_xml): + ref_prefix = m.group(1) + obj_n = m.group(2) + key = f"{ref_prefix}.{obj_n}" + if key not in result: + result[key] = {"TypeName": ref_prefix, "ObjName": obj_n} + # cfg:DefinedType.XXX + for m in re.finditer(r'cfg:DefinedType\.(\w+)', type_xml): + dt_name = m.group(1) + key = f"DefinedType.{dt_name}" + if key not in result: + result[key] = {"TypeName": "DefinedType", "ObjName": dt_name} + return list(result.values()) + + # --- 11g. Merge adopted attributes into existing extension object XML --- + def merge_attributes_into_object(type_name, obj_name, attrs_to_add): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") + if not os.path.isfile(obj_file): + warn(f"Cannot merge attributes: {obj_file} not found") + return + + with open(obj_file, "r", encoding="utf-8-sig") as fh: + obj_content = fh.read() + + # Collect existing attribute names for dedup (text-based) + existing_names = set() + for m in re.finditer(r'(\w+)', obj_content): + existing_names.add(m.group(1)) + + all_attr_xml = "" + added = 0 + for attr in attrs_to_add: + if attr["Name"] in existing_names: + continue + all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") + added += 1 + + if added > 0: + # Insert attributes — handle both and ... + if re.search(r'', obj_content): + obj_content = re.sub(r'', f"{all_attr_xml}\r\n\t\t", obj_content) + else: + obj_content = obj_content.replace("", f"{all_attr_xml}\r\n\t\t") + save_text_bom(obj_file, obj_content) + info(f" Merged {added} attribute(s) into: {obj_file}") + + # --- 11h. Borrow main attribute orchestrator --- + def borrow_main_attribute(type_name, obj_name, form_name, mode): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + info(f"Borrowing main attribute for {type_name}.{obj_name} (mode: {mode})...") + + # Step 1: Collect DataPaths (Form mode) or take all (All mode) + first_level_names = None + deep_paths = [] + if mode == "Form": + src_form_xml_path = os.path.join(cfg_dir, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml") + if not os.path.isfile(src_form_xml_path): + print(f"Source Form.xml not found: {src_form_xml_path}", file=sys.stderr) + sys.exit(1) + dp = collect_form_data_paths(src_form_xml_path) + first_level_names = dp["FirstLevel"] + deep_paths = dp["DeepPaths"] + info(f" Collected {len(first_level_names)} first-level DataPath references, {len(deep_paths)} deep paths") + else: + info(" Mode All: borrowing all attributes and tabular sections") + + # Step 2: Resolve source attributes + resolved = resolve_source_attributes(type_name, obj_name, first_level_names) + src_attrs = resolved["Attributes"] + src_ts = resolved["TabularSections"] + extra_props = resolved["ExtraProps"] + info(f" Resolved: {len(src_attrs)} attributes, {len(src_ts)} tabular section(s)") + + # Identify which FirstLevel names are TabularSections (for deep path filtering) + ts_names = {ts["Name"]: True for ts in src_ts} + + # Step 3: Build the adopted content and insert into main object XML + obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") + + # Generate full object XML with attributes and TS + content_parts = [] + for attr in src_attrs: + attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") + content_parts.append(attr_xml) + for ts in src_ts: + ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t") + content_parts.append(ts_xml) + adopted_content = "\n".join(content_parts).rstrip() + + # Read existing object XML and inject + with open(obj_file, "r", encoding="utf-8-sig") as fh: + obj_content = fh.read() + + # Inject extra properties after ExtendedConfigurationObject + if extra_props: + props_xml = "" + for p_name, p_val in extra_props.items(): + props_xml += f"\r\n\t\t\t<{p_name}>{p_val}" + obj_content = obj_content.replace("", f"{props_xml}") + + # Replace empty ChildObjects with adopted content + if adopted_content: + # Handle (self-closing) + if re.search(r'', obj_content): + obj_content = re.sub(r'', f"\r\n{adopted_content}\r\n\t\t", obj_content) + # Handle ... (may already have Form entry) + elif re.search(r'(?s)(.*?)', obj_content): + m = re.search(r'(?s)(.*?)', obj_content) + existing_inner = m.group(1) + obj_content = obj_content.replace( + f"{existing_inner}", + f"{existing_inner}\r\n{adopted_content}\r\n\t\t" + ) + + save_text_bom(obj_file, obj_content) + info(f" Enriched object: {obj_file}") + + # Step 4: Collect all reference types and borrow as shells + all_type_xmls = [] + for a in src_attrs: + all_type_xmls.append(a["TypeXml"]) + for ts in src_ts: + for tsa in ts["Attributes"]: + all_type_xmls.append(tsa["TypeXml"]) + ref_types = collect_reference_types(all_type_xmls) + info(f" Reference types to borrow: {len(ref_types)}") + + for rt in ref_types: + if rt["TypeName"] not in CHILD_TYPE_DIR_MAP: + warn(f" Unknown reference type: {rt['TypeName']}.{rt['ObjName']}") + continue + if test_object_borrowed(rt["TypeName"], rt["ObjName"]): + info(f" Already borrowed: {rt['TypeName']}.{rt['ObjName']}") + continue + rt_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]], f"{rt['ObjName']}.xml") + if not os.path.isfile(rt_src_file): + warn(f" Source not found: {rt['TypeName']}.{rt['ObjName']}") + continue + src = read_source_object(rt["TypeName"], rt["ObjName"]) + borrowed_xml = build_borrowed_object_xml(rt["TypeName"], rt["ObjName"], src["Uuid"], src["Properties"]) + target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]]) + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects(rt["TypeName"], rt["ObjName"]) + borrowed_files.append(target_file) + info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}") + + # Step 5: Handle deep paths (Form mode only) + if mode == "Form" and deep_paths: + # Filter out deep paths where ObjectAttr is a TabularSection + real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names] + + if real_deep: + info(f" Processing {len(real_deep)} deep path(s)...") + + # Group by ObjectAttr -> target catalog + deep_by_attr = {} + for dp in real_deep: + if dp["ObjectAttr"] not in deep_by_attr: + deep_by_attr[dp["ObjectAttr"]] = [] + deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"]) + + for attr_name, sub_attr_names in deep_by_attr.items(): + # Find the attribute's type to determine target catalog + attr_info = None + for a in src_attrs: + if a["Name"] == attr_name: + attr_info = a + break + if not attr_info: + continue + + # Extract catalog name from type: cfg:CatalogRef.XXX + cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"]) + if not cat_match: + continue + + target_type_name = cat_match.group(1) + target_obj_name = cat_match.group(2) + + # Ensure target is borrowed + if not test_object_borrowed(target_type_name, target_obj_name): + t_src = read_source_object(target_type_name, target_obj_name) + t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"]) + t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) + os.makedirs(t_target_dir, exist_ok=True) + t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") + save_text_bom(t_target_file, t_borrowed_xml) + add_to_child_objects(target_type_name, target_obj_name) + borrowed_files.append(t_target_file) + info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") + + # Resolve sub-attributes in target catalog + sub_names = {sn: True for sn in sub_attr_names} + sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names) + + if sub_resolved["Attributes"]: + merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"]) + + # Collect and borrow ref types from deep attributes + sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]] + sub_ref_types = collect_reference_types(sub_type_xmls) + for srt in sub_ref_types: + if srt["TypeName"] not in CHILD_TYPE_DIR_MAP: + continue + if test_object_borrowed(srt["TypeName"], srt["ObjName"]): + continue + s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml") + if not os.path.isfile(s_src_file): + continue + s_src = read_source_object(srt["TypeName"], srt["ObjName"]) + s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"]) + s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]]) + os.makedirs(s_target_dir, exist_ok=True) + s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml") + save_text_bom(s_target_file, s_borrowed_xml) + add_to_child_objects(srt["TypeName"], srt["ObjName"]) + borrowed_files.append(s_target_file) + info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}") + + info(" Main attribute borrowing complete") + + def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False): dir_name = CHILD_TYPE_DIR_MAP[type_name] # 1. Read source form UUID @@ -671,7 +1133,7 @@ def main(): ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') - # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false + # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false auto_cmd_xml = "" if src_auto_cmd is not None: auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") @@ -680,8 +1142,12 @@ def main(): auto_cmd_xml = auto_cmd_xml.replace('true', 'false') # Strip ExcludedCommand (references to standard commands invalid in extension) auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) - # Strip DataPath in AutoCommandBar buttons (e.g. Объект.Ref — invalid in extension) - auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) + # Strip DataPath in AutoCommandBar buttons + if borrow_main_attr: + # Keep only Объект.* DataPaths + auto_cmd_xml = re.sub(r'\s*(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*', '', auto_cmd_xml) + else: + auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) # ChildItems: copy full tree, clean up base-config references child_items_xml = "" @@ -696,12 +1162,16 @@ def main(): child_items_xml = ns_strip_pattern.sub("", child_items_xml) # Replace all CommandName values with 0 child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) - # Strip DataPath - child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) - # Strip TitleDataPath - child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) - # Strip RowPictureDataPath (e.g. Список.СостояниеДокумента — invalid in extension) - child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip DataPath / TitleDataPath / RowPictureDataPath + if borrow_main_attr: + # Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed) + child_items_xml = re.sub(r'\s*(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + else: + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) # Strip TypeLink blocks with human-readable DataPath (Items.XXX) @@ -709,10 +1179,15 @@ def main(): # Strip element-level Events child_items_xml = re.sub(r'\s*.*?', '', child_items_xml, flags=re.DOTALL) - # Auto-borrow referenced CommonPictures - pic_refs = re.findall(r'CommonPicture\.(\w+)', child_items_xml) - referenced_pictures = {name: True for name in pic_refs} + # Collect CommonPicture references from ChildItems and AutoCommandBar + referenced_pictures = {} + for name in re.findall(r'CommonPicture\.(\w+)', child_items_xml): + referenced_pictures[name] = True + if auto_cmd_xml: + for name in re.findall(r'CommonPicture\.(\w+)', auto_cmd_xml): + referenced_pictures[name] = True + # Auto-borrow referenced CommonPictures auto_borrowed_pics = [] for pic_name in referenced_pictures: if not test_object_borrowed("CommonPicture", pic_name): @@ -726,6 +1201,7 @@ def main(): save_text_bom(target_file, borrowed_xml) add_to_child_objects("CommonPicture", pic_name) auto_borrowed_pics.append(pic_name) + borrowed_files.append(target_file) info(f" Auto-borrowed: CommonPicture.{pic_name}") else: warn(f" CommonPicture.{pic_name} not found in source config — will strip from form") @@ -746,6 +1222,15 @@ def main(): # Strip StdPicture blocks (except Print) child_items_xml = re.sub(r'\s*\s*StdPicture\.(?!Print\b)\w+.*?', '', child_items_xml, flags=re.DOTALL) + # Same Picture strip for AutoCommandBar + if auto_cmd_xml: + ac_pic_matches = list(pic_block_pattern.finditer(auto_cmd_xml)) + for pm in reversed(ac_pic_matches): + cp_name = pm.group(1) + if cp_name not in borrowed_pic_set: + auto_cmd_xml = auto_cmd_xml[:pm.start()] + auto_cmd_xml[pm.end():] + auto_cmd_xml = re.sub(r'\s*\s*StdPicture\.(?!Print\b)\w+.*?', '', auto_cmd_xml, flags=re.DOTALL) + # Auto-borrow StyleItems referenced in ChildItems referenced_styles = set() for m in re.finditer(r'ref="style:(\w+)"[^>]*kind="StyleItem"', child_items_xml): @@ -764,6 +1249,7 @@ def main(): target_file = os.path.join(target_dir, f"{style_name}.xml") save_text_bom(target_file, borrowed_xml) add_to_child_objects("StyleItem", style_name) + borrowed_files.append(target_file) info(f" Auto-borrowed: StyleItem.{style_name}") else: warn(f" StyleItem.{style_name} not found in source config") @@ -827,6 +1313,7 @@ def main(): target_file = os.path.join(target_dir, f"{enum_name}.xml") save_text_bom(target_file, borrowed_xml) add_to_child_objects("Enum", enum_name) + borrowed_files.append(target_file) info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))") else: warn(f" Enum.{enum_name} not found in source config") @@ -856,7 +1343,26 @@ def main(): parts.append(f"\t{auto_cmd_xml}\r\n") if child_items_xml: parts.append(f"\t{child_items_xml}\r\n") - parts.append("\t\r\n") + + # Attributes: empty or with MainAttribute when borrow_main_attr + if borrow_main_attr: + obj_type_prefix = "" + gt_list = GENERATED_TYPES.get(type_name, []) + for g in gt_list: + if g["category"] == "Object": + obj_type_prefix = g["prefix"] + break + main_attr_type = f"cfg:{obj_type_prefix}.{obj_name}" + parts.append("\t\r\n") + parts.append('\t\t\r\n') + parts.append(f"\t\t\t{main_attr_type}\r\n") + parts.append("\t\t\ttrue\r\n") + parts.append("\t\t\ttrue\r\n") + parts.append("\t\t\r\n") + parts.append("\t") + else: + parts.append("\t") + parts.append("\r\n") # BaseForm: same content, indented one more level parts.append(f'\t\r\n') @@ -881,7 +1387,18 @@ def main(): parts.append(f"\t{line}") parts.append("\r\n") - parts.append("\t\t\r\n") + # BaseForm Attributes: same as main section + if borrow_main_attr: + parts.append("\t\t\r\n") + parts.append('\t\t\t\r\n') + parts.append(f"\t\t\t\t{main_attr_type}\r\n") + parts.append("\t\t\t\ttrue\r\n") + parts.append("\t\t\t\ttrue\r\n") + parts.append("\t\t\t\r\n") + parts.append("\t\t") + else: + parts.append("\t\t") + parts.append("\r\n") parts.append("\t\r\n") parts.append("") @@ -914,8 +1431,19 @@ def main(): print("No objects specified in -Object", file=sys.stderr) sys.exit(1) + # --- 9b. Validate -BorrowMainAttribute --- + borrow_main_attribute_mode = args.BorrowMainAttribute + if borrow_main_attribute_mode is not None: + if borrow_main_attribute_mode not in ("Form", "All"): + print("-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)", file=sys.stderr) + sys.exit(1) + # Validate: only with .Form. pattern + has_form = any(".Form." in item for item in items) + if not has_form: + print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr) + sys.exit(1) + # --- 10. Process each item --- - borrowed_files = [] borrowed_count = 0 for item in items: @@ -963,9 +1491,14 @@ def main(): add_to_child_objects(type_name, obj_name) borrowed_files.append(target_file) - form_files = borrow_form(type_name, obj_name, form_name) + has_bma = borrow_main_attribute_mode is not None + form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma) borrowed_files.extend(form_files) borrowed_count += 1 + + # Borrow main attribute if requested + if has_bma: + borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode) else: # --- Object borrowing --- info(f"Borrowing {type_name}.{obj_name}...") diff --git a/docs/1c-extension-spec.md b/docs/1c-extension-spec.md index aaa2cfb2..75b65fbf 100644 --- a/docs/1c-extension-spec.md +++ b/docs/1c-extension-spec.md @@ -310,7 +310,47 @@ Enums/ # Перечисления Собственный реквизит **не имеет** `ObjectBelonging` и `ExtendedConfigurationObject`. Содержит полный набор свойств. -### 5.3. Заимствованные значения перечислений +### 5.3. Заимствование реквизитов объекта для работы с формой + +Когда в расширении заимствуется форма объекта и требуется сохранить привязки к данным (`DataPath`), необходимо заимствовать реквизиты объекта, на которые форма ссылается. Это позволяет добавлять новые элементы на форму через расширение. + +**Что заимствуется**: +- Реквизиты объекта, на которые ссылается форма через `Объект.XXX` в `` +- Табличные части целиком (все колонки), если форма обращается к `Объект.ТЧ` +- Зависимые объекты по типам реквизитов (`CatalogRef.X` → заимствуется `Catalog.X` как оболочка) +- Глубокие ссылки: `Объект.A.B` → реквизит B заимствуется внутри каталога A + +**Обогащение Properties**: заимствованный объект с реквизитами содержит дополнительные свойства из источника (`Hierarchical`, `CodeLength`, `DescriptionLength`, `CodeType`, `CodeAllowedLength`), которые отсутствуют у простой оболочки. + +**Заимствованная табличная часть** содержит `InternalInfo` с `GeneratedType` (категории `TabularSection` и `TabularSectionRow`): + +```xml + + + + ... + ... + + + ... + ... + + + + Adopted + ДрагоценныеМатериалы + + ... + + + + + +``` + +**Типы в реквизитах**: `v8:Type` для обычных типов (`cfg:CatalogRef.X`, `cfg:EnumRef.X`), `v8:TypeSet` для определяемых типов (`cfg:DefinedType.X`). + +### 5.4. Заимствованные значения перечислений ```xml @@ -324,7 +364,7 @@ Enums/ # Перечисления ``` -### 5.4. Формы в расширениях +### 5.5. Формы в расширениях В расширении существуют **два принципиально разных сценария** работы с формами: @@ -335,7 +375,7 @@ Enums/ # Перечисления > **Как отличить:** Если файл метаданных формы (`.xml`) содержит `Adopted` — это заимствованная форма. Собственные формы не имеют `ObjectBelonging`. -#### 5.4.1. Метаданные заимствованной формы +#### 5.5.1. Метаданные заимствованной формы Файл `.xml` в каталоге `Forms/`: @@ -354,7 +394,7 @@ Enums/ # Перечисления Содержимое формы хранится в `Forms/ФормаСписка/Ext/Form.xml`, модуль формы — в `Forms/ФормаСписка/Ext/Form/Module.bsl`. -#### 5.4.2. Структура Form.xml заимствованной формы +#### 5.5.2. Структура Form.xml заимствованной формы Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Существуют **два варианта** в зависимости от наличия модификаций модуля формы. @@ -415,13 +455,27 @@ Form.xml заимствованной формы — **двухчастный ф 2. **AutoCommandBar** — присутствует всегда с `id="-1"`, но без `` (кнопки удаляются). `` = `false`. -3. **Attributes** — пустой `` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1. +3. **Attributes** — по умолчанию пустой `` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1. + + **Вариант с заимствованным основным реквизитом**: если реквизиты объекта заимствованы (см. раздел 5.3), секция `` содержит основной реквизит формы: + ```xml + + + cfg:CatalogObject.Номенклатура + true + true + + + ``` + Присутствует в обеих секциях (Part 1 и BaseForm). Тип зависит от родительского объекта: `CatalogObject`, `DocumentObject` и т.д. 4. **BaseForm** — последний элемент в `
`, атрибут `version`. В BaseForm **нет** Events, Commands, Parameters. -5. **DataPath: удаление** (вариант B) — все `` из базовых элементов удаляются в обеих секциях. DataPath ссылается на реквизиты, не включённые в расширение. +5. **DataPath** — два варианта в зависимости от наличия заимствованного основного реквизита: + - **Без основного реквизита**: все `` удаляются в обеих секциях (ссылаются на реквизиты, не включённые в расширение). + - **С основным реквизитом**: ``, начинающиеся с `Объект.` **сохраняются** (реквизиты заимствованы и привязки валидны). DataPath формовых реквизитов (не начинающиеся с `Объект.`) — удаляются. -6. **TitleDataPath: удаление** (вариант B) — все `` удаляются (напр. `Объект.Товары.RowsCount` — путь недействителен без базовых атрибутов). +6. **TitleDataPath** — аналогично DataPath: удаляются без основного реквизита, `Объект.*` сохраняются с ним. 7. **TypeLink: удаление** (вариант B) — блоки `` с `Items.*` удаляются (человекочитаемые пути, которые нельзя преобразовать в UUID-формат Конфигуратора). @@ -435,7 +489,7 @@ Form.xml заимствованной формы — **двухчастный ф 12. **Авто-заимствование Enums + EnumValues** — `` могут содержать `Enum.XXX.EnumValue.YYY`. Перечисление `Enum.XXX` заимствуется вместе с конкретными `EnumValue` (borrowed с `ExtendedConfigurationObject` указывающим на UUID оригинального значения). -#### 5.4.3. Нумерация ID элементов +#### 5.5.3. Нумерация ID элементов | Диапазон | Принадлежность | |----------|---------------| @@ -445,7 +499,7 @@ Form.xml заимствованной формы — **двухчастный ф > **Важно:** Визуальные элементы форм (элементы в `ChildItems`), добавленные расширением в тело базовой формы, могут использовать ID из обычного диапазона (продолжая нумерацию базовой формы). Диапазон 1000000+ гарантирован для `Attributes` и `Commands`. -#### 5.4.4. Атрибут callType — перехват событий и команд +#### 5.5.4. Атрибут callType — перехват событий и команд В заимствованных формах события и действия команд используют атрибут `callType` для определения момента перехвата: @@ -509,7 +563,7 @@ Form.xml заимствованной формы — **двухчастный ф > **Отличие от обычной формы:** В обычной форме (конфигурации или собственной форме расширения) у `` и `` **нет** атрибута `callType` — обработчик вызывается напрямую. -#### 5.4.5. Собственная форма на заимствованном объекте +#### 5.5.5. Собственная форма на заимствованном объекте Расширение может добавить к заимствованному объекту **собственную форму**, не существующую в базовой конфигурации. Такая форма: @@ -543,7 +597,7 @@ Form.xml заимствованной формы — **двухчастный ф ``` -#### 5.4.6. Модуль заимствованной формы +#### 5.5.6. Модуль заимствованной формы Модуль формы (`Forms/Имя/Ext/Form/Module.bsl`) в заимствованной форме использует те же декораторы перехвата, что и другие модули расширений (см. раздел 7.2): diff --git a/docs/cfe-guide.md b/docs/cfe-guide.md index 32a61bfa..0b5f9366 100644 --- a/docs/cfe-guide.md +++ b/docs/cfe-guide.md @@ -7,7 +7,7 @@ | Навык | Параметры | Описание | |-------|-----------|----------| | `/cfe-init` | ` [-Purpose Patch\|Customization\|AddOn] [-CompatibilityMode]` | Создание расширения (scaffold XML-исходников) | -| `/cfe-borrow` | `-ExtensionPath -ConfigPath -Object "Type.Name"` | Заимствование объектов из конфигурации | +| `/cfe-borrow` | `-ExtensionPath -ConfigPath -Object "Type.Name" [-BorrowMainAttribute]` | Заимствование объектов из конфигурации | | `/cfe-patch-method` | `-ExtensionPath -ModulePath "Type.Name.Module" -MethodName "X" -InterceptorType Before` | Генерация перехватчика метода | | `/cfe-validate` | ` [-MaxErrors 30]` | Валидация структурной корректности (9 проверок) | | `/cfe-diff` | `-ExtensionPath -ConfigPath [-Mode A\|B]` | Анализ расширения и проверка переноса | @@ -44,6 +44,21 @@ Claude выполнит: 4. `/cfe-patch-method` — создать перехватчик нужного метода 5. `/cfe-validate` — проверить результат +### Добавление реквизита в объект и вывод на форму + +``` +> Добавь реквизит "ОсновнойПоставщик" (тип СправочникСсылка.Партнеры) + в справочник Номенклатура и выведи на форму элемента. + Конфигурация ERP в C:\cfsrc\erp +``` + +Claude выполнит: +1. `/cfe-init` — создать расширение +2. `/cfe-borrow -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute` — заимствовать форму с реквизитами объекта +3. `/meta-edit` — добавить новый реквизит `Расш1_ОсновнойПоставщик` в Номенклатура +4. `/form-edit` — вывести реквизит на форму +5. `/cfe-validate` — проверить результат + ### Анализ существующего расширения ``` @@ -103,6 +118,18 @@ Claude вызовет `/cfe-diff -Mode B` — найдёт блоки `#Вста Поддерживаемые типы: Catalog, Document, Enum, CommonModule, Report, DataProcessor, ExchangePlan, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, и другие (44 типа). +### Заимствование формы с реквизитами объекта (-BorrowMainAttribute) + +При добавлении нового реквизита на заимствованную форму нужна опция `-BorrowMainAttribute`: +- Без неё форма заимствуется "пустой" — только визуальные элементы, без привязки к данным +- С ней — форма сохраняет `DataPath` привязки к реквизитам объекта (`Объект.XXX`) + +Два режима: +- `Form` (по умолчанию) — заимствует только реквизиты, выведенные на форму +- `All` — заимствует все реквизиты объекта (включая не выведенные на форму) + +Каскадно заимствует зависимые объекты по типам реквизитов (справочники, перечисления, определяемые типы) как оболочки. Если зависимый объект уже заимствован с содержимым — не перезаписывает его. + ## cfe-patch-method — перехват методов Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта.