feat(cfe-borrow): add -BorrowMainAttribute for borrowing object attributes with form

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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-25 19:56:26 +03:00
parent 252105396b
commit f7695a9534
5 changed files with 1308 additions and 55 deletions
+23 -1
View File
@@ -1,7 +1,7 @@
---
name: cfe-borrow
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты"
argument-hint: -ExtensionPath <path> -ConfigPath <path> -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. **Регистрация**`<Form>` в 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
```
## Верификация
+641 -24
View File
@@ -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 '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
# Strip ExcludedCommand (references to standard commands invalid in extension)
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
# Strip DataPath in AutoCommandBar buttons (e.g. Объект.Ref — invalid in extension)
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
# Strip DataPath in AutoCommandBar buttons
if ($BorrowMainAttr) {
# Keep only Объект.* DataPaths
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
} else {
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
}
}
# 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, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Strip DataPath (references base form attributes not in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
# Strip TitleDataPath (e.g. Объект.Товары.RowsCount — invalid without base attributes)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
# Strip RowPictureDataPath (e.g. Список.СостояниеДокумента — invalid in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
# Strip DataPath, TitleDataPath, RowPictureDataPath
if ($BorrowMainAttr) {
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
} else {
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
}
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
# 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*<Events>.*?</Events>', '')
# Collect CommonPicture references from ChildItems
$picRefs = [regex]::Matches($childItemsXml, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')
# Collect CommonPicture references from ChildItems and AutoCommandBar
$referencedPictures = @{}
$picRefs = [regex]::Matches($childItemsXml, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')
foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true }
if ($autoCmdXml) {
$picRefs2 = [regex]::Matches($autoCmdXml, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')
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*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '')
# 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*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '')
}
# Auto-borrow StyleItems referenced in ChildItems
# Pattern 1: <Font ref="style:XXX" kind="StyleItem"/>, <TitleFont ref="style:XXX" ... kind="StyleItem"/>
# Pattern 2: <BackColor>style:XXX</BackColor>, <TextColor>style:XXX</TextColor>, 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<Attributes/>") | 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<Attributes>`r`n") | Out-Null
$formXmlSb.Append("`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
$formXmlSb.Append("`t</Attributes>") | Out-Null
} else {
$formXmlSb.Append("`t<Attributes/>") | 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<Attributes/>") | Out-Null
# BaseForm Attributes: same as main section
if ($BorrowMainAttr) {
$formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
} else {
$formXmlSb.Append("`t`t<Attributes/>") | Out-Null
}
$formXmlSb.Append("`r`n") | Out-Null
$formXmlSb.Append("`t</BaseForm>") | 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, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
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, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
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}<Attribute uuid=`"${newUuid}`">") | Out-Null
$sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null
$sb.AppendLine("${indent}`t<Properties>") | Out-Null
$sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
$sb.AppendLine("${indent}`t`t<Name>${name}</Name>") | Out-Null
$sb.AppendLine("${indent}`t`t<Comment/>") | Out-Null
$sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null
$sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null
$sb.AppendLine("${indent}`t</Properties>") | Out-Null
$sb.Append("${indent}</Attribute>") | 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}<TabularSection uuid=`"${newUuid}`">") | Out-Null
# InternalInfo with GeneratedTypes (new UUIDs, referencing source names)
if ($generatedTypes -and $generatedTypes.Count -gt 0) {
$sb.AppendLine("${indent}`t<InternalInfo>") | Out-Null
foreach ($gt in $generatedTypes) {
$newTid = [guid]::NewGuid().ToString()
$newVid = [guid]::NewGuid().ToString()
$sb.AppendLine("${indent}`t`t<xr:GeneratedType name=`"$($gt.Name)`" category=`"$($gt.Category)`">") | Out-Null
$sb.AppendLine("${indent}`t`t`t<xr:TypeId>${newTid}</xr:TypeId>") | Out-Null
$sb.AppendLine("${indent}`t`t`t<xr:ValueId>${newVid}</xr:ValueId>") | Out-Null
$sb.AppendLine("${indent}`t`t</xr:GeneratedType>") | Out-Null
}
$sb.AppendLine("${indent}`t</InternalInfo>") | Out-Null
} else {
$sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null
}
$sb.AppendLine("${indent}`t<Properties>") | Out-Null
$sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
$sb.AppendLine("${indent}`t`t<Name>${tsName}</Name>") | Out-Null
$sb.AppendLine("${indent}`t`t<Comment/>") | Out-Null
$sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null
$sb.AppendLine("${indent}`t</Properties>") | Out-Null
# ChildObjects with all attributes
if ($childAttrs -and $childAttrs.Count -gt 0) {
$sb.AppendLine("${indent}`t<ChildObjects>") | 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</ChildObjects>") | Out-Null
} else {
$sb.AppendLine("${indent}`t<ChildObjects/>") | Out-Null
}
$sb.Append("${indent}</TabularSection>") | 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 </ChildObjects>
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
$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])</${pName}>") | Out-Null
}
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
}
# Replace empty ChildObjects with adopted content
if ($adoptedContent) {
# Handle <ChildObjects/> (self-closing)
if ($objContent -match '<ChildObjects\s*/>') {
$objContent = $objContent -replace '<ChildObjects\s*/>', "<ChildObjects>`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elseif ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
$existingInner = $Matches[1]
$objContent = $objContent -replace '(?s)<ChildObjects>(.*?)</ChildObjects>', "<ChildObjects>${existingInner}`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
}
$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
+551 -18
View File
@@ -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'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', 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'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', 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}<Attribute uuid="{new_uuid_val}">',
f'{indent}\t<InternalInfo/>',
f'{indent}\t<Properties>',
f'{indent}\t\t<ObjectBelonging>Adopted</ObjectBelonging>',
f'{indent}\t\t<Name>{name}</Name>',
f'{indent}\t\t<Comment/>',
f'{indent}\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>',
f'{indent}\t\t{type_xml}',
f'{indent}\t</Properties>',
f'{indent}</Attribute>',
]
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}<TabularSection uuid="{new_uuid_val}">']
# InternalInfo with GeneratedTypes (new UUIDs, referencing source names)
if generated_types:
lines.append(f'{indent}\t<InternalInfo>')
for gt in generated_types:
new_tid = new_guid()
new_vid = new_guid()
lines.append(f'{indent}\t\t<xr:GeneratedType name="{gt["Name"]}" category="{gt["Category"]}">')
lines.append(f'{indent}\t\t\t<xr:TypeId>{new_tid}</xr:TypeId>')
lines.append(f'{indent}\t\t\t<xr:ValueId>{new_vid}</xr:ValueId>')
lines.append(f'{indent}\t\t</xr:GeneratedType>')
lines.append(f'{indent}\t</InternalInfo>')
else:
lines.append(f'{indent}\t<InternalInfo/>')
lines.append(f'{indent}\t<Properties>')
lines.append(f'{indent}\t\t<ObjectBelonging>Adopted</ObjectBelonging>')
lines.append(f'{indent}\t\t<Name>{ts_name}</Name>')
lines.append(f'{indent}\t\t<Comment/>')
lines.append(f'{indent}\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>')
lines.append(f'{indent}\t</Properties>')
# ChildObjects with all attributes
if child_attrs:
lines.append(f'{indent}\t<ChildObjects>')
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</ChildObjects>')
else:
lines.append(f'{indent}\t<ChildObjects/>')
lines.append(f'{indent}</TabularSection>')
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'<Name>(\w+)</Name>', 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 <ChildObjects/> and <ChildObjects>...</ChildObjects>
if re.search(r'<ChildObjects\s*/>', obj_content):
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
else:
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
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}</{p_name}>"
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
# Replace empty ChildObjects with adopted content
if adopted_content:
# Handle <ChildObjects/> (self-closing)
if re.search(r'<ChildObjects\s*/>', obj_content):
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>\r\n{adopted_content}\r\n\t\t</ChildObjects>", obj_content)
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elif re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content):
m = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
existing_inner = m.group(1)
obj_content = obj_content.replace(
f"<ChildObjects>{existing_inner}</ChildObjects>",
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
)
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 CommandName0), Autofillfalse
# 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('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension)
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons (e.g. Объект.Ref — invalid in extension)
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons
if borrow_main_attr:
# Keep only Объект.* DataPaths
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
else:
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', 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'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip DataPath
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
# Strip TitleDataPath
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
# Strip RowPictureDataPath (e.g. Список.СостояниеДокумента — invalid in extension)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', 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*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
else:
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', 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*<Events>.*?</Events>', '', child_items_xml, flags=re.DOTALL)
# Auto-borrow referenced CommonPictures
pic_refs = re.findall(r'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', 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'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', child_items_xml):
referenced_pictures[name] = True
if auto_cmd_xml:
for name in re.findall(r'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', 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*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '', 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*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '', 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<Attributes/>\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<Attributes>\r\n")
parts.append('\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1000001">\r\n')
parts.append(f"\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
parts.append("\t\t\t<MainAttribute>true</MainAttribute>\r\n")
parts.append("\t\t\t<SavedData>true</SavedData>\r\n")
parts.append("\t\t</Attribute>\r\n")
parts.append("\t</Attributes>")
else:
parts.append("\t<Attributes/>")
parts.append("\r\n")
# BaseForm: same content, indented one more level
parts.append(f'\t<BaseForm version="{form_version}">\r\n')
@@ -881,7 +1387,18 @@ def main():
parts.append(f"\t{line}")
parts.append("\r\n")
parts.append("\t\t<Attributes/>\r\n")
# BaseForm Attributes: same as main section
if borrow_main_attr:
parts.append("\t\t<Attributes>\r\n")
parts.append('\t\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1000001">\r\n')
parts.append(f"\t\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
parts.append("\t\t\t\t<MainAttribute>true</MainAttribute>\r\n")
parts.append("\t\t\t\t<SavedData>true</SavedData>\r\n")
parts.append("\t\t\t</Attribute>\r\n")
parts.append("\t\t</Attributes>")
else:
parts.append("\t\t<Attributes/>")
parts.append("\r\n")
parts.append("\t</BaseForm>\r\n")
parts.append("</Form>")
@@ -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}...")
+65 -11
View File
@@ -310,7 +310,47 @@ Enums/ # Перечисления
Собственный реквизит **не имеет** `ObjectBelonging` и `ExtendedConfigurationObject`. Содержит полный набор свойств.
### 5.3. Заимствованные значения перечислений
### 5.3. Заимствование реквизитов объекта для работы с формой
Когда в расширении заимствуется форма объекта и требуется сохранить привязки к данным (`DataPath`), необходимо заимствовать реквизиты объекта, на которые форма ссылается. Это позволяет добавлять новые элементы на форму через расширение.
**Что заимствуется**:
- Реквизиты объекта, на которые ссылается форма через `Объект.XXX` в `<DataPath>`
- Табличные части целиком (все колонки), если форма обращается к `Объект.ТЧ`
- Зависимые объекты по типам реквизитов (`CatalogRef.X` → заимствуется `Catalog.X` как оболочка)
- Глубокие ссылки: `Объект.A.B` → реквизит B заимствуется внутри каталога A
**Обогащение Properties**: заимствованный объект с реквизитами содержит дополнительные свойства из источника (`Hierarchical`, `CodeLength`, `DescriptionLength`, `CodeType`, `CodeAllowedLength`), которые отсутствуют у простой оболочки.
**Заимствованная табличная часть** содержит `InternalInfo` с `GeneratedType` (категории `TabularSection` и `TabularSectionRow`):
```xml
<TabularSection uuid="...">
<InternalInfo>
<xr:GeneratedType name="CatalogTabularSection.Номенклатура.ДрагоценныеМатериалы" category="TabularSection">
<xr:TypeId>...</xr:TypeId>
<xr:ValueId>...</xr:ValueId>
</xr:GeneratedType>
<xr:GeneratedType name="CatalogTabularSectionRow.Номенклатура.ДрагоценныеМатериалы" category="TabularSectionRow">
<xr:TypeId>...</xr:TypeId>
<xr:ValueId>...</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>ДрагоценныеМатериалы</Name>
<Comment/>
<ExtendedConfigurationObject>...</ExtendedConfigurationObject>
</Properties>
<ChildObjects>
<!-- все реквизиты ТЧ — adopted -->
</ChildObjects>
</TabularSection>
```
**Типы в реквизитах**: `v8:Type` для обычных типов (`cfg:CatalogRef.X`, `cfg:EnumRef.X`), `v8:TypeSet` для определяемых типов (`cfg:DefinedType.X`).
### 5.4. Заимствованные значения перечислений
```xml
<EnumValue uuid="9bc7380f-...">
@@ -324,7 +364,7 @@ Enums/ # Перечисления
</EnumValue>
```
### 5.4. Формы в расширениях
### 5.5. Формы в расширениях
В расширении существуют **два принципиально разных сценария** работы с формами:
@@ -335,7 +375,7 @@ Enums/ # Перечисления
> **Как отличить:** Если файл метаданных формы (`.xml`) содержит `<ObjectBelonging>Adopted</ObjectBelonging>` — это заимствованная форма. Собственные формы не имеют `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"`, но без `<ChildItems>` (кнопки удаляются). `<Autofill>` = `false`.
3. **Attributes** — пустой `<Attributes/>` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1.
3. **Attributes**по умолчанию пустой `<Attributes/>` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1.
**Вариант с заимствованным основным реквизитом**: если реквизиты объекта заимствованы (см. раздел 5.3), секция `<Attributes>` содержит основной реквизит формы:
```xml
<Attributes>
<Attribute name="Объект" id="1000001">
<Type><v8:Type>cfg:CatalogObject.Номенклатура</v8:Type></Type>
<MainAttribute>true</MainAttribute>
<SavedData>true</SavedData>
</Attribute>
</Attributes>
```
Присутствует в обеих секциях (Part 1 и BaseForm). Тип зависит от родительского объекта: `CatalogObject`, `DocumentObject` и т.д.
4. **BaseForm** — последний элемент в `<Form>`, атрибут `version`. В BaseForm **нет** Events, Commands, Parameters.
5. **DataPath: удаление** (вариант B) — все `<DataPath>` из базовых элементов удаляются в обеих секциях. DataPath ссылается на реквизиты, не включённые в расширение.
5. **DataPath** — два варианта в зависимости от наличия заимствованного основного реквизита:
- **Без основного реквизита**: все `<DataPath>` удаляются в обеих секциях (ссылаются на реквизиты, не включённые в расширение).
- **С основным реквизитом**: `<DataPath>`, начинающиеся с `Объект.` **сохраняются** (реквизиты заимствованы и привязки валидны). DataPath формовых реквизитов (не начинающиеся с `Объект.`) — удаляются.
6. **TitleDataPath: удаление** (вариант B) — все `<TitleDataPath>` удаляются (напр. `Объект.Товары.RowsCount` — путь недействителен без базовых атрибутов).
6. **TitleDataPath** — аналогично DataPath: удаляются без основного реквизита, `Объект.*` сохраняются с ним.
7. **TypeLink: удаление** (вариант B) — блоки `<TypeLink>` с `<xr:DataPath>Items.*</xr:DataPath>` удаляются (человекочитаемые пути, которые нельзя преобразовать в UUID-формат Конфигуратора).
@@ -435,7 +489,7 @@ Form.xml заимствованной формы — **двухчастный ф
12. **Авто-заимствование Enums + EnumValues** — `<ChoiceParameters>` могут содержать `<Value xsi:type="xr:DesignTimeRef">Enum.XXX.EnumValue.YYY</Value>`. Перечисление `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 заимствованной формы — **двухчастный ф
> **Отличие от обычной формы:** В обычной форме (конфигурации или собственной форме расширения) у `<Event>` и `<Action>` **нет** атрибута `callType` — обработчик вызывается напрямую.
#### 5.4.5. Собственная форма на заимствованном объекте
#### 5.5.5. Собственная форма на заимствованном объекте
Расширение может добавить к заимствованному объекту **собственную форму**, не существующую в базовой конфигурации. Такая форма:
@@ -543,7 +597,7 @@ Form.xml заимствованной формы — **двухчастный ф
</Form>
```
#### 5.4.6. Модуль заимствованной формы
#### 5.5.6. Модуль заимствованной формы
Модуль формы (`Forms/Имя/Ext/Form/Module.bsl`) в заимствованной форме использует те же декораторы перехвата, что и другие модули расширений (см. раздел 7.2):
+28 -1
View File
@@ -7,7 +7,7 @@
| Навык | Параметры | Описание |
|-------|-----------|----------|
| `/cfe-init` | `<Name> [-Purpose Patch\|Customization\|AddOn] [-CompatibilityMode]` | Создание расширения (scaffold XML-исходников) |
| `/cfe-borrow` | `-ExtensionPath <path> -ConfigPath <path> -Object "Type.Name"` | Заимствование объектов из конфигурации |
| `/cfe-borrow` | `-ExtensionPath <path> -ConfigPath <path> -Object "Type.Name" [-BorrowMainAttribute]` | Заимствование объектов из конфигурации |
| `/cfe-patch-method` | `-ExtensionPath <path> -ModulePath "Type.Name.Module" -MethodName "X" -InterceptorType Before` | Генерация перехватчика метода |
| `/cfe-validate` | `<ExtensionPath> [-MaxErrors 30]` | Валидация структурной корректности (9 проверок) |
| `/cfe-diff` | `-ExtensionPath <path> -ConfigPath <path> [-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` файл с декоратором перехвата для заимствованного объекта.