Merge branch 'dev'

This commit is contained in:
Nick Shirokov
2026-02-21 17:40:38 +03:00
19 changed files with 1329 additions and 66 deletions
+22
View File
@@ -16,6 +16,14 @@ allowed-tools:
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры
| Параметр | Описание |
@@ -30,9 +38,20 @@ allowed-tools:
- `CommonModule.РаботаСФайлами` — общий модуль
- `Document.РеализацияТоваров` — документ
- `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
Создаётся:
1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
4. **Регистрация**`<Form>` в ChildObjects родительского объекта
## Команда
```powershell
@@ -45,6 +64,9 @@ powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1
# Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
```
+361 -28
View File
@@ -249,10 +249,12 @@ $script:generatedTypes = @{
@{ prefix = "DocumentJournalManager"; category = "Manager" }
)
"Report" = @(
@{ prefix = "ReportObject"; category = "Object" }
@{ prefix = "ReportObject"; category = "Object" }
@{ prefix = "ReportManager"; category = "Manager" }
)
"DataProcessor" = @(
@{ prefix = "DataProcessorObject"; category = "Object" }
@{ prefix = "DataProcessorObject"; category = "Object" }
@{ prefix = "DataProcessorManager"; category = "Manager" }
)
}
@@ -384,6 +386,300 @@ function Read-SourceObject {
}
}
# --- 10b. Helper: read source form UUID ---
function Read-SourceFormUuid {
param([string]$typeName, [string]$objName, [string]$formName)
$dirName = $childTypeDirMap[$typeName]
$srcFile = Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") "${formName}.xml"
if (-not (Test-Path $srcFile)) {
Write-Error "Source form not found: $srcFile"
exit 1
}
$srcDoc = New-Object System.Xml.XmlDocument
$srcDoc.PreserveWhitespace = $false
$srcDoc.Load($srcFile)
$srcEl = $null
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $srcEl = $c; break }
}
if (-not $srcEl) {
Write-Error "No metadata element found in source form: $srcFile"
exit 1
}
$srcUuid = $srcEl.GetAttribute("uuid")
if (-not $srcUuid) {
Write-Error "No uuid attribute on source form element: $srcFile"
exit 1
}
return $srcUuid
}
# --- 10c. Helper: borrow a form ---
function Borrow-Form {
param([string]$typeName, [string]$objName, [string]$formName)
$dirName = $childTypeDirMap[$typeName]
$enc = New-Object System.Text.UTF8Encoding($true)
# 1. Read source form UUID
$formUuid = Read-SourceFormUuid $typeName $objName $formName
Info " Source form UUID: $formUuid"
# 2. Read source Form.xml content
$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
}
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
# 3. Generate form metadata XML (ФормаЭлемента.xml)
$newFormUuid = [guid]::NewGuid().ToString()
$formMetaSb = New-Object System.Text.StringBuilder
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<Name>${formName}</Name>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<Comment/>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<ExtendedConfigurationObject>${formUuid}</ExtendedConfigurationObject>") | Out-Null
$formMetaSb.AppendLine("`t`t`t<FormType>Managed</FormType>") | Out-Null
$formMetaSb.AppendLine("`t`t</Properties>") | Out-Null
$formMetaSb.AppendLine("`t</Form>") | Out-Null
$formMetaSb.Append("</MetaDataObject>") | Out-Null
# 4. Create directories
$formMetaDir = Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms"
if (-not (Test-Path $formMetaDir)) {
New-Item -ItemType Directory -Path $formMetaDir -Force | Out-Null
}
# Write form metadata
$formMetaFile = Join-Path $formMetaDir "${formName}.xml"
[System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc)
Info " Created: $formMetaFile"
# 5. Generate Form.xml with BaseForm (visual elements only)
# Parse source Form.xml as XmlDocument
$srcFormDoc = New-Object System.Xml.XmlDocument
$srcFormDoc.PreserveWhitespace = $true
$srcFormDoc.Load($srcFormXmlPath)
$srcFormEl = $srcFormDoc.DocumentElement
$formVersion = $srcFormEl.GetAttribute("version")
if (-not $formVersion) { $formVersion = "2.17" }
# Find direct children: AutoCommandBar, ChildItems (visual elements only)
$srcAutoCmd = $null
$srcChildItems = $null
foreach ($fc in $srcFormEl.ChildNodes) {
if ($fc.NodeType -ne 'Element') { continue }
if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { $srcAutoCmd = $fc }
elseif ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { $srcChildItems = $fc }
}
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
$autoCmdXml = ""
if ($srcAutoCmd) {
$autoCmdXml = $srcAutoCmd.OuterXml
$autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '')
# Replace all CommandName values with 0 (base form buttons lose command refs)
$autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Replace Autofill true → false
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
}
$childItemsXml = ""
if ($srcChildItems) {
$childItemsXml = $srcChildItems.OuterXml
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
# Replace all CommandName values with 0 in ChildItems too
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
} else {
$childItemsXml = "<ChildItems/>"
}
# Extract the <Form ...> opening tag from source text (preserves namespace declarations)
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
$formTag = "<Form version=`"${formVersion}`">"
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
if ($srcFormContent -match '(<Form[^>]*>)') { $formTag = $Matches[1] }
# Build output Form.xml
$formXmlSb = New-Object System.Text.StringBuilder
$formXmlSb.Append($xmlDecl) | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
$formXmlSb.Append($formTag) | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
# Part 1: visual elements (add leading tab to first line of each block)
if ($autoCmdXml) {
$formXmlSb.Append("`t$autoCmdXml") | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
}
$formXmlSb.Append("`t$childItemsXml") | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
$formXmlSb.Append("`t<Attributes/>") | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
# BaseForm: same visual elements, indented one more level
$formXmlSb.Append("`t<BaseForm version=`"${formVersion}`">") | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
if ($autoCmdXml) {
# Reindent for BaseForm: first line gets 2 tabs, other lines get +1 tab
$acLines = $autoCmdXml -split "`r?`n"
for ($li = 0; $li -lt $acLines.Count; $li++) {
if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null }
else { $formXmlSb.Append("`t$($acLines[$li])") | Out-Null }
$formXmlSb.Append("`r`n") | Out-Null
}
}
$ciLines = $childItemsXml -split "`r?`n"
for ($li = 0; $li -lt $ciLines.Count; $li++) {
if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null }
else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null }
$formXmlSb.Append("`r`n") | Out-Null
}
$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
$formXmlSb.Append("</Form>") | Out-Null
# Write Form.xml
$formXmlDir = Join-Path (Join-Path $formMetaDir $formName) "Ext"
if (-not (Test-Path $formXmlDir)) {
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
}
$formXmlFile = Join-Path $formXmlDir "Form.xml"
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
Info " Created: $formXmlFile"
# 6. Create empty Module.bsl
$moduleDir = Join-Path $formXmlDir "Form"
if (-not (Test-Path $moduleDir)) {
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
}
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
Info " Created: $moduleBslFile"
# 7. Register form in parent object ChildObjects
Register-FormInObject $typeName $objName $formName
return @($formMetaFile, $formXmlFile, $moduleBslFile)
}
# --- 10d. Helper: register form in parent object's ChildObjects ---
function Register-FormInObject {
param([string]$typeName, [string]$objName, [string]$formName)
$dirName = $childTypeDirMap[$typeName]
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
if (-not (Test-Path $objFile)) {
Warn "Parent object file not found: $objFile — form not registered in ChildObjects"
return
}
$objDoc = New-Object System.Xml.XmlDocument
$objDoc.PreserveWhitespace = $true
$objDoc.Load($objFile)
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
# Find the type element
$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 — form not registered"
return
}
# Find or create ChildObjects
$childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs)
if (-not $childObjs) {
# Create ChildObjects element
$childObjs = $objDoc.CreateElement("ChildObjects", "http://v8.1c.ru/8.3/MDClasses")
$objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null
$objEl.AppendChild($childObjs) | Out-Null
$objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null
}
# Check dedup
foreach ($c in $childObjs.ChildNodes) {
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Form" -and $c.InnerText -eq $formName) {
Warn "Form '$formName' already in ChildObjects of ${typeName}.${objName}"
return
}
}
# Expand self-closing if needed
if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) {
$closeWs = $objDoc.CreateWhitespace("`r`n`t`t")
$childObjs.AppendChild($closeWs) | Out-Null
}
# Add <Form>formName</Form>
$formEl = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formEl.InnerText = $formName
$trailing = $childObjs.LastChild
$ws = $objDoc.CreateWhitespace("`r`n`t`t`t")
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
$childObjs.InsertBefore($ws, $trailing) | Out-Null
$childObjs.InsertBefore($formEl, $trailing) | Out-Null
} else {
$childObjs.AppendChild($ws) | Out-Null
$childObjs.AppendChild($formEl) | Out-Null
}
# Save object XML
$settings2 = New-Object System.Xml.XmlWriterSettings
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings2.Indent = $false
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream2 = New-Object System.IO.MemoryStream
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
$objDoc.Save($writer2)
$writer2.Flush(); $writer2.Close()
$bytes2 = $memStream2.ToArray()
$memStream2.Close()
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
Info " Registered form in: $objFile"
}
# --- 10e. Helper: check if object is already borrowed in extension ---
function Test-ObjectBorrowed {
param([string]$typeName, [string]$objName)
$dirName = $childTypeDirMap[$typeName]
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
return (Test-Path $objFile)
}
# --- 11. Helper: generate InternalInfo XML ---
function Build-InternalInfoXml {
param([string]$typeName, [string]$objName, [string]$indent)
@@ -534,11 +830,11 @@ $borrowedCount = 0
foreach ($item in $items) {
$dotIdx = $item.IndexOf(".")
if ($dotIdx -lt 1) {
Write-Error "Invalid format '${item}', expected 'Type.Name'"
Write-Error "Invalid format '${item}', expected 'Type.Name' or 'Type.Name.Form.FormName'"
exit 1
}
$typeName = $item.Substring(0, $dotIdx)
$objName = $item.Substring($dotIdx + 1)
$remainder = $item.Substring($dotIdx + 1)
# Resolve Russian synonym to English type name
if ($synonymMap.ContainsKey($typeName)) { $typeName = $synonymMap[$typeName] }
@@ -548,34 +844,71 @@ foreach ($item in $items) {
exit 1
}
$dirName = $childTypeDirMap[$typeName]
Info "Borrowing ${typeName}.${objName}..."
# Read source object
$src = Read-SourceObject $typeName $objName
Info " Source UUID: $($src.Uuid)"
# Build borrowed object XML
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
# Create directory in extension if needed
$targetDir = Join-Path $extDir $dirName
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
# Check for .Form. pattern: Type.ObjName.Form.FormName
$formName = $null
$formIdx = $remainder.IndexOf(".Form.")
if ($formIdx -gt 0) {
$objName = $remainder.Substring(0, $formIdx)
$formName = $remainder.Substring($formIdx + 6) # skip ".Form."
} else {
$objName = $remainder
}
# Write borrowed object XML with UTF-8 BOM
$targetFile = Join-Path $targetDir "${objName}.xml"
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
Info " Created: $targetFile"
$dirName = $childTypeDirMap[$typeName]
# Add to ChildObjects
Add-ToChildObjects $typeName $objName
if ($formName) {
# --- Form borrowing ---
Info "Borrowing form ${typeName}.${objName}.Form.${formName}..."
$borrowedFiles += $targetFile
$borrowedCount++
# Auto-borrow parent object if not yet borrowed
if (-not (Test-ObjectBorrowed $typeName $objName)) {
Info " Parent object ${typeName}.${objName} not yet borrowed — borrowing first..."
$src = Read-SourceObject $typeName $objName
Info " Source UUID: $($src.Uuid)"
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
$targetDir = Join-Path $extDir $dirName
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
$targetFile = Join-Path $targetDir "${objName}.xml"
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
Info " Created: $targetFile"
Add-ToChildObjects $typeName $objName
$borrowedFiles += $targetFile
}
# Borrow the form
$formFiles = Borrow-Form $typeName $objName $formName
$borrowedFiles += $formFiles
$borrowedCount++
} else {
# --- Object borrowing (existing logic) ---
Info "Borrowing ${typeName}.${objName}..."
$src = Read-SourceObject $typeName $objName
Info " Source UUID: $($src.Uuid)"
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
$targetDir = Join-Path $extDir $dirName
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
$targetFile = Join-Path $targetDir "${objName}.xml"
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
Info " Created: $targetFile"
Add-ToChildObjects $typeName $objName
$borrowedFiles += $targetFile
$borrowedCount++
}
}
# --- 15. Save modified Configuration.xml ---
+9
View File
@@ -38,9 +38,18 @@ powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -Ex
&ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — line 4 in ...
&Перед("ЗагрузитьКурсыВалют") — line 13 in ...
ChildObjects: 1 own attrs, 1 own TS, 3 own forms
Form.ФормаЭлемента (borrowed):
Event:OnCreateAtServer [After] -> Расш1_ПриСозданииПосле
Command:Подбор [Before] -> Расш1_ПодборПеред
Form.Расш1_МояФорма (own)
[OWN] Catalog.Расш5_Справочник1
```
Для каждой формы заимствованного объекта показывается:
- `(borrowed)` / `(own)` — заимствованная или собственная форма
- callType-события формы и элементов
- callType на командах
## Mode B — проверка переноса
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
+80 -1
View File
@@ -210,6 +210,63 @@ function Get-InsertionBlocks {
return $blocks
}
# --- Helper: analyze form for callType events and commands ---
function Get-FormInterceptors {
param([string]$formXmlPath)
if (-not (Test-Path $formXmlPath)) { return $null }
$formDoc = New-Object System.Xml.XmlDocument
$formDoc.PreserveWhitespace = $false
try { $formDoc.Load($formXmlPath) } catch { return $null }
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$fRoot = $formDoc.DocumentElement
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
$isBorrowed = ($baseForm -ne $null)
$interceptors = @()
# Form-level events with callType
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
if ($eventsNode) {
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
$ct = $evt.GetAttribute("callType")
if ($ct) {
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
}
}
}
# Element-level events with callType (scan all elements recursively)
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
if ($childItems) {
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
$elName = $evtNode.GetAttribute("name")
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
$ct = $evt.GetAttribute("callType")
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
}
}
}
# Commands with callType on Action
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
$cmdName = $cmd.GetAttribute("name")
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
$ct = $action.GetAttribute("callType")
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
}
}
return @{
IsBorrowed = $isBorrowed
Interceptors = $interceptors
}
}
# ============================================================
# MODE A: Extension overview
# ============================================================
@@ -255,6 +312,7 @@ if ($Mode -eq "A") {
$ownForms = 0
$ownTS = 0
$borrowedItems = 0
$formNames = @()
foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
@@ -268,7 +326,7 @@ if ($Mode -eq "A") {
switch ($c.LocalName) {
"Attribute" { $ownAttrs++ }
"TabularSection" { $ownTS++ }
"Form" { $ownForms++ }
"Form" { $formNames += $c.InnerText; $ownForms++ }
}
}
$parts = @()
@@ -279,6 +337,27 @@ if ($Mode -eq "A") {
if ($parts.Count -gt 0) {
Write-Host " ChildObjects: $($parts -join ', ')"
}
# Analyze forms
$borrowedFormCount = 0
$ownFormCount = 0
foreach ($fn in $formNames) {
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
$fi = Get-FormInterceptors $formXmlPath
if (-not $fi) {
Write-Host " Form.$fn (?)"
continue
}
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
if ($fi.Interceptors.Count -gt 0) {
Write-Host " Form.$fn ($formTag):"
foreach ($ic in $fi.Interceptors) {
Write-Host " $ic"
}
} else {
Write-Host " Form.$fn ($formTag)"
}
}
}
}
} else {
+15 -7
View File
@@ -1,7 +1,7 @@
---
name: cfe-init
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
argument-hint: <Name> [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
allowed-tools:
- Bash
- Read
@@ -14,13 +14,17 @@ allowed-tools:
## Подготовка
Перед созданием расширения рекомендуется получить версию и режим совместимости базовой конфигурации:
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
```
/cf-info <ConfigPath> -Mode brief
```
### Авто-определение ConfigPath
Это даст `CompatibilityMode` (передать в `-CompatibilityMode`) и версию конфигурации (для `-Version`, например `<ВерсияКонфигурации>.1`).
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
## Параметры
@@ -34,6 +38,7 @@ allowed-tools:
| `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false |
## Команда
@@ -56,7 +61,10 @@ powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Na
## Примеры
```powershell
# Расширение-исправление для ERP
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
# Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
# Расширение-доработка с версией
+54 -1
View File
@@ -11,6 +11,7 @@ param(
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24",
[string]$ConfigPath,
[switch]$NoRole
)
@@ -34,6 +35,57 @@ if (Test-Path $cfgFile) {
exit 1
}
# --- Resolve ConfigPath ---
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
if ($ConfigPath) {
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
}
if (Test-Path $ConfigPath -PathType Container) {
$candidate = Join-Path $ConfigPath "Configuration.xml"
if (Test-Path $candidate) { $ConfigPath = $candidate }
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
}
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
# 3a. Read Language UUID from base config
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
if (Test-Path $baseLangFile) {
$baseLangDoc = New-Object System.Xml.XmlDocument
$baseLangDoc.PreserveWhitespace = $false
$baseLangDoc.Load($baseLangFile)
$langEl = $null
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
}
if ($langEl) {
$baseLangUuid = $langEl.GetAttribute("uuid")
Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
} else {
Write-Host "[WARN] No <Language> element in $baseLangFile"
}
} else {
Write-Host "[WARN] Base config language not found: $baseLangFile"
}
# 3b. Read CompatibilityMode from base config
$baseCfgDoc = New-Object System.Xml.XmlDocument
$baseCfgDoc.PreserveWhitespace = $false
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
if ($compatNode -and $compatNode.InnerText) {
$CompatibilityMode = $compatNode.InnerText.Trim()
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
} else {
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
}
} else {
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
}
# --- Generate UUIDs ---
$uuidCfg = [guid]::NewGuid().ToString()
$uuidLang = [guid]::NewGuid().ToString()
@@ -149,7 +201,7 @@ $langXml = @"
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>Русский</Name>
<Comment/>
<ExtendedConfigurationObject>00000000-0000-0000-0000-000000000000</ExtendedConfigurationObject>
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
<LanguageCode>ru</LanguageCode>
</Properties>
</Language>
@@ -201,6 +253,7 @@ Write-Host "[OK] Создано расширение: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Назначение: $Purpose"
Write-Host " Префикс: $NamePrefix"
Write-Host " Совместимость: $CompatibilityMode"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
if (-not $NoRole) {
@@ -157,6 +157,21 @@ $bslCode += "$endKeyword"
$bslText = ($bslCode -join "`r`n") + "`r`n"
# --- Check form borrowing for .Form. paths ---
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
$formName = $parts[3]
$dirName = $typeDirMap[$objType]
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
Write-Host " Run /cfe-borrow first:"
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
Write-Host ""
}
}
# --- Check if file exists and append ---
$bslDir = Split-Path $bslFile -Parent
if (-not (Test-Path $bslDir)) {
+10 -2
View File
@@ -271,7 +271,11 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
"@
}
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else {
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
}
# --- 3c. Module.bsl ---
@@ -304,7 +308,11 @@ $moduleBsl = @"
#КонецОбласти
"@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
if (Test-Path $modulePath) {
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
} else {
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
}
# --- Фаза 4: Регистрация в родительском объекте ---
+6
View File
@@ -395,6 +395,12 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
## Workflow
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
3. **Проверка**: `/form-validate`, `/form-info`.
## Верификация
```
@@ -1137,6 +1137,71 @@ if (-not (Test-Path $outDir)) {
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc)
# --- 13b. Auto-register form in parent object XML ---
# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath)
$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir)
$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir)
$objectDir = [System.IO.Path]::GetDirectoryName($formsDir)
$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir)
$formName = [System.IO.Path]::GetFileName($formNameDir)
$objectName = [System.IO.Path]::GetFileName($objectDir)
$formsLeaf = [System.IO.Path]::GetFileName($formsDir)
if ($formsLeaf -eq 'Forms') {
$objectXmlPath = Join-Path $typePluralDir "$objectName.xml"
if (Test-Path $objectXmlPath) {
$objDoc = New-Object System.Xml.XmlDocument
$objDoc.PreserveWhitespace = $true
$objDoc.Load($objectXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if ($childObjects) {
$existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr)
if (-not $existing) {
$formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $formName
$insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr)
if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) }
if ($insertBefore) {
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
$ws = $objDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
} else {
$lastChild = $childObjects.LastChild
if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
$regEnc = New-Object System.Text.UTF8Encoding($true)
$regSettings = New-Object System.Xml.XmlWriterSettings
$regSettings.Encoding = $regEnc
$regSettings.Indent = $false
$regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create)
$regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings)
$objDoc.Save($regWriter)
$regWriter.Close()
$regStream.Close()
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
}
}
}
}
# --- 14. Summary ---
$elCount = $script:nextId - 1
+44 -2
View File
@@ -50,6 +50,32 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -
}
```
### Расширения (extension-формы)
Для заимствованных форм (с `<BaseForm>`) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции:
```json
{
"formEvents": [
{ "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" },
{ "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" }
],
"elementEvents": [
{ "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" }
],
"commands": [
{ "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" },
{ "name": "Запрос", "actions": [
{ "callType": "Before", "handler": "Расш1_ЗапросПеред" },
{ "callType": "After", "handler": "Расш1_ЗапросПосле" }
]}
],
"elements": [
{ "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] }
]
}
```
### Позиционирование элементов
| Ключ | По умолчанию | Описание |
@@ -96,19 +122,35 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -
`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной).
### Секции расширений
| Секция | Назначение |
|--------|-----------|
| `formEvents` | События уровня формы с `callType` (Before/After/Override) |
| `elementEvents` | События на существующих элементах заимствованной формы |
| `callType` на `commands` | callType на Action команды |
| `callType` на `on` | callType на событиях новых элементов (объектный формат) |
Все extension-секции опциональны — без них навык работает как с обычными формами.
## Вывод
```
=== form-edit: Форма ===
[EXTENSION] BaseForm detected — IDs start at 1000000+
Added form events:
+ OnCreateAtServer[After] -> Расш1_ПриСозданииПосле
Added elements (into ГруппаШапка, after Контрагент):
+ [Input] Склад -> Объект.Склад {OnChange}
Added attributes:
+ СуммаИтого: decimal(15,2) (id=12)
+ СуммаИтого: decimal(15,2) (id=1000000)
---
Total: 1 element(s) (+2 companions), 1 attribute(s)
Total: 1 form event(s), 1 element(s) (+2 companions), 1 attribute(s)
Run /form-validate to verify.
```
+182 -8
View File
@@ -110,6 +110,16 @@ $script:nextElemId++
$script:nextAttrId++
$script:nextCmdId++
# --- 4b. Auto-detect extension mode (BaseForm present) ---
$script:isExtension = $false
$baseForm = $root.SelectSingleNode("f:BaseForm", $nsMgr)
if ($baseForm) {
$script:isExtension = $true
if ($script:nextAttrId -lt 1000000) { $script:nextAttrId = 1000000 }
if ($script:nextCmdId -lt 1000000) { $script:nextCmdId = 1000000 }
if ($script:nextElemId -lt 1000000) { $script:nextElemId = 1000000 }
}
function New-ElemId { $id = $script:nextElemId; $script:nextElemId++; return $id }
function New-AttrId { $id = $script:nextAttrId; $script:nextAttrId++; return $id }
function New-CmdId { $id = $script:nextCmdId; $script:nextCmdId++; return $id }
@@ -260,18 +270,29 @@ function Emit-Events {
if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) {
$allowed = $script:knownEvents[$typeKey]
foreach ($evt in $el.on) {
if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") {
Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')"
$evtStr = if ($evt -is [string]) { "$evt" } else { "$($evt.event)" }
if ($allowed.Count -gt 0 -and $allowed -notcontains $evtStr) {
Write-Host "[WARN] Unknown event '$evtStr' for $typeKey '$elementName'. Known: $($allowed -join ', ')"
}
}
}
X "$indent<Events>"
foreach ($evt in $el.on) {
$evtName = "$evt"
$handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" }
else { Get-HandlerName -elementName $elementName -eventName $evtName }
X "$indent`t<Event name=`"$evtName`">$handler</Event>"
# Support both string ("OnChange") and object ({ "event": "OnChange", "callType": "After" })
if ($evt -is [string] -or -not $evt.event) {
$evtName = "$evt"
$handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" }
else { Get-HandlerName -elementName $elementName -eventName $evtName }
X "$indent`t<Event name=`"$evtName`">$handler</Event>"
} else {
$evtName = "$($evt.event)"
$handler = if ($evt.handler) { "$($evt.handler)" }
elseif ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" }
else { Get-HandlerName -elementName $elementName -eventName $evtName }
$callTypeAttr = if ($evt.callType) { " callType=`"$($evt.callType)`"" } else { "" }
X "$indent`t<Event name=`"$evtName`"$callTypeAttr>$handler</Event>"
}
}
X "$indent</Events>"
}
@@ -964,7 +985,20 @@ if ($def.commands -and $def.commands.Count -gt 0) {
$inner = "$cmdChildIndent`t"
if ($cmd.title) { Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner }
if ($cmd.action) { X "$inner<Action>$($cmd.action)</Action>" }
# Support single action with optional callType, or multiple actions
if ($cmd.actions) {
# Multiple actions: [{ "callType": "Before", "handler": "..." }, ...]
foreach ($act in $cmd.actions) {
$actHandler = "$($act.handler)"
$callTypeAttr = if ($act.callType) { " callType=`"$($act.callType)`"" } else { "" }
X "$inner<Action$callTypeAttr>$actHandler</Action>"
}
} elseif ($cmd.action) {
$callTypeAttr = if ($cmd.callType) { " callType=`"$($cmd.callType)`"" } else { "" }
X "$inner<Action$callTypeAttr>$($cmd.action)</Action>"
}
if ($cmd.shortcut) { X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" }
if ($cmd.picture) {
X "$inner<Picture>"
@@ -975,7 +1009,7 @@ if ($def.commands -and $def.commands.Count -gt 0) {
if ($cmd.representation) { X "$inner<Representation>$($cmd.representation)</Representation>" }
X "$cmdChildIndent</Command>"
$actionStr = if ($cmd.action) { " -> $($cmd.action)" } else { "" }
$actionStr = if ($cmd.action) { " -> $($cmd.action)" } elseif ($cmd.actions) { " -> $($cmd.actions.Count) action(s)" } else { "" }
$addedCmds += " + ${cmdName}${actionStr} (id=$cmdId)"
}
X "</_F>"
@@ -988,6 +1022,127 @@ if ($def.commands -and $def.commands.Count -gt 0) {
}
}
# === 12b. Add form-level events ===
$addedFormEvents = @()
if ($def.formEvents -and $def.formEvents.Count -gt 0) {
$eventsSection = $root.SelectSingleNode("f:Events", $nsMgr)
if (-not $eventsSection) {
# Create Events section — insert after AutoCommandBar or at the beginning
$eventsSection = $xmlDoc.CreateElement("Events", $formNs)
$insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr)
if ($insertAfter) {
$refNode = $insertAfter
$ws = $xmlDoc.CreateWhitespace("`r`n`t")
# Insert before the AutoCommandBar (Events come before AutoCommandBar in 1C)
$root.InsertBefore($ws, $refNode) | Out-Null
$root.InsertBefore($eventsSection, $refNode) | Out-Null
} else {
$firstChild = $root.FirstChild
if ($firstChild) {
$ws = $xmlDoc.CreateWhitespace("`r`n`t")
$root.InsertBefore($eventsSection, $firstChild) | Out-Null
$root.InsertBefore($ws, $eventsSection) | Out-Null
} else {
$root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null
$root.AppendChild($eventsSection) | Out-Null
}
}
}
$evtChildIndent = Get-ChildIndent $eventsSection
if (-not $evtChildIndent -or $evtChildIndent -eq "") { $evtChildIndent = "`t`t" }
# Generate event fragments
$script:xml = New-Object System.Text.StringBuilder 512
X "<_F $allNsDecl>"
foreach ($fe in $def.formEvents) {
$feName = "$($fe.name)"
$feHandler = "$($fe.handler)"
$callTypeAttr = if ($fe.callType) { " callType=`"$($fe.callType)`"" } else { "" }
X "$evtChildIndent<Event name=`"$feName`"$callTypeAttr>$feHandler</Event>"
$ctStr = if ($fe.callType) { "[$($fe.callType)]" } else { "" }
$addedFormEvents += " + $feName${ctStr} -> $feHandler"
}
X "</_F>"
$fragDoc = Parse-Fragment $script:xml.ToString()
$importedEvents = Import-ElementNodes $fragDoc
foreach ($node in $importedEvents) {
Insert-IntoContainer -container $eventsSection -newNode $node -afterName $null -childIndent $evtChildIndent
}
}
# === 12c. Add element-level events ===
$addedElemEvents = @()
if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
if (-not $rootCI) {
$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr)
}
foreach ($ee in $def.elementEvents) {
$targetName = "$($ee.element)"
$targetEl = Find-Element $rootCI $targetName
if (-not $targetEl) {
Write-Host "[WARN] Element '$targetName' not found — skipping elementEvent"
continue
}
# Find or create Events element within the target
$targetEvents = $targetEl.SelectSingleNode("f:Events", $nsMgr)
if (-not $targetEvents) {
$targetEvents = $xmlDoc.CreateElement("Events", $formNs)
# Insert Events before closing tag (after last property, before ChildItems if any)
$ciNode = $targetEl.SelectSingleNode("f:ChildItems", $nsMgr)
if ($ciNode) {
$ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))
$targetEl.InsertBefore($ws, $ciNode) | Out-Null
$targetEl.InsertBefore($targetEvents, $ciNode) | Out-Null
} else {
$trailing = $targetEl.LastChild
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
$ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))
$targetEl.InsertBefore($ws, $trailing) | Out-Null
$targetEl.InsertBefore($targetEvents, $trailing) | Out-Null
} else {
$targetEl.AppendChild($xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))) | Out-Null
$targetEl.AppendChild($targetEvents) | Out-Null
}
}
}
$eeChildIndent = Get-ChildIndent $targetEvents
if (-not $eeChildIndent -or $eeChildIndent -eq "") {
$parentIndent = Get-ChildIndent $targetEl
$eeChildIndent = "$parentIndent`t"
}
# Create Event element
$eeName = "$($ee.name)"
$eeHandler = "$($ee.handler)"
$callTypeAttr = if ($ee.callType) { " callType=`"$($ee.callType)`"" } else { "" }
$script:xml = New-Object System.Text.StringBuilder 256
X "<_F $allNsDecl>"
X "$eeChildIndent<Event name=`"$eeName`"$callTypeAttr>$eeHandler</Event>"
X "</_F>"
$fragDoc = Parse-Fragment $script:xml.ToString()
$importedEE = Import-ElementNodes $fragDoc
foreach ($node in $importedEE) {
Insert-IntoContainer -container $targetEvents -newNode $node -afterName $null -childIndent $eeChildIndent
}
$ctStr = if ($ee.callType) { "[$($ee.callType)]" } else { "" }
$addedElemEvents += " + $targetName.$eeName${ctStr} -> $eeHandler"
}
}
# === 13. Save ===
$content = $xmlDoc.OuterXml
@@ -999,6 +1154,23 @@ $enc = New-Object System.Text.UTF8Encoding($true)
# === 14. Summary ===
if ($script:isExtension) {
Write-Host "[EXTENSION] BaseForm detected — IDs start at 1000000+"
Write-Host ""
}
if ($addedFormEvents.Count -gt 0) {
Write-Host "Added form events:"
foreach ($line in $addedFormEvents) { Write-Host $line }
Write-Host ""
}
if ($addedElemEvents.Count -gt 0) {
Write-Host "Added element events:"
foreach ($line in $addedElemEvents) { Write-Host $line }
Write-Host ""
}
if ($addedElems.Count -gt 0) {
$posStr = ""
if ($def.into) { $posStr += "into $($def.into)" }
@@ -1023,6 +1195,8 @@ if ($addedCmds.Count -gt 0) {
Write-Host "---"
$totalParts = @()
if ($addedFormEvents.Count -gt 0) { $totalParts += "$($addedFormEvents.Count) form event(s)" }
if ($addedElemEvents.Count -gt 0) { $totalParts += "$($addedElemEvents.Count) element event(s)" }
if ($addedElems.Count -gt 0) {
$compStr = if ($companionCount -gt 0) { " (+$companionCount companions)" } else { "" }
$totalParts += "$($addedElems.Count) element(s)$compStr"
+26 -1
View File
@@ -45,6 +45,11 @@ powershell.exe -NoProfile -File .claude/skills/form-info/scripts/form-info.ps1 -
=== Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) ===
```
Для заимствованных форм расширения (с `<BaseForm>`):
```
=== Form: ФормаЭлемента [EXTENSION] (Catalogs.Валюты) ===
```
Имя формы, заголовок (Title) и контекст объекта определяются из пути к файлу и XML.
### Properties — свойства формы
@@ -63,6 +68,13 @@ Events:
OnOpen -> ПриОткрытии
```
Для расширений с callType:
```
Events:
OnCreateAtServer[After] -> Расш1_ПриСозданииПосле
OnOpen[Before] -> Расш1_ПриОткрытии
```
### Elements — дерево UI-элементов
Компактное дерево с типами, привязками к данным, флагами и событиями:
@@ -114,7 +126,7 @@ Elements:
**Привязка к команде**: `-> ИмяКоманды [cmd]` — команда формы, `-> Close [std]` — стандартная команда
**События**: `{OnChange, StartChoice}` — имена обработчиков
**События**: `{OnChange, StartChoice}` — имена обработчиков; `{OnChange[Before]}` — с callType для расширений
**Заголовок**: `[title:Текст]` — только если отличается от имени элемента
@@ -151,8 +163,21 @@ Commands:
Заполнить -> ЗаполнитьОбработка
```
Для расширений с callType на Action:
```
Commands:
Подбор -> Расш1_ПодборПеред[Before], Расш1_ПодборПосле[After]
```
Формат: `Имя -> Обработчик [Сочетание]`
### BaseForm (расширения)
Для заимствованных форм в конце выводится:
```
BaseForm: present (version 2.17)
```
## Что пропускается
Скрипт убирает 80%+ XML-объёма:
+40 -6
View File
@@ -35,6 +35,10 @@ $ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings
$root = $xmlDoc.DocumentElement
# --- Detect extension (BaseForm) ---
$baseFormNode = $root.SelectSingleNode("d:BaseForm", $ns)
$isExtension = ($baseFormNode -ne $null)
# --- Helper: extract multilang text ---
function Get-MLText($node) {
@@ -140,7 +144,10 @@ function Get-EventsStr($node) {
if (-not $eventsNode) { return "" }
$evts = @()
foreach ($e in $eventsNode.SelectNodes("d:Event", $ns)) {
$evts += $e.GetAttribute("name")
$eName = $e.GetAttribute("name")
$ct = $e.GetAttribute("callType")
if ($ct) { $evts += "$eName[$ct]" }
else { $evts += $eName }
}
if ($evts.Count -eq 0) { return "" }
return " {$($evts -join ', ')}"
@@ -340,7 +347,8 @@ if ($titleNode) {
$formTitle = Get-MLText $titleNode
if (-not $formTitle) { $formTitle = $titleNode.InnerText }
}
$header = "=== Form: $formName"
$extMarker = if ($isExtension) { " [EXTENSION]" } else { "" }
$header = "=== Form: $formName$extMarker"
if ($formTitle) { $header += "`"$formTitle`"" }
if ($objectContext) { $header += " ($objectContext)" }
$header += " ==="
@@ -390,7 +398,9 @@ if ($formEvents -and $formEvents.HasChildNodes) {
foreach ($e in $formEvents.SelectNodes("d:Event", $ns)) {
$eName = $e.GetAttribute("name")
$eHandler = $e.InnerText
$lines += " $eName -> $eHandler"
$ct = $e.GetAttribute("callType")
$ctStr = if ($ct) { "[$ct]" } else { "" }
$lines += " $eName${ctStr} -> $eHandler"
}
}
@@ -491,12 +501,27 @@ if ($cmdsNode) {
$cmdLines = @()
foreach ($cmd in $cmdsNode.SelectNodes("d:Command", $ns)) {
$cName = $cmd.GetAttribute("name")
$action = $cmd.SelectSingleNode("d:Action", $ns)
$shortcut = $cmd.SelectSingleNode("d:Shortcut", $ns)
$actionStr = if ($action) { " -> $($action.InnerText)" } else { "" }
$scStr = if ($shortcut) { " [$($shortcut.InnerText)]" } else { "" }
# Collect all Action elements (may have multiple with callType)
$actions = $cmd.SelectNodes("d:Action", $ns)
if ($actions.Count -gt 1) {
$actParts = @()
foreach ($a in $actions) {
$ct = $a.GetAttribute("callType")
$ctStr = if ($ct) { "[$ct]" } else { "" }
$actParts += "$($a.InnerText)$ctStr"
}
$actionStr = " -> $($actParts -join ', ')"
} elseif ($actions.Count -eq 1) {
$ct = $actions[0].GetAttribute("callType")
$ctStr = if ($ct) { "[$ct]" } else { "" }
$actionStr = " -> $($actions[0].InnerText)$ctStr"
} else {
$actionStr = ""
}
$cmdLines += " $cName$actionStr$scStr"
}
if ($cmdLines.Count -gt 0) {
@@ -506,6 +531,15 @@ if ($cmdsNode) {
}
}
# --- BaseForm footer ---
if ($isExtension) {
$bfVersion = $baseFormNode.GetAttribute("version")
$bfStr = if ($bfVersion) { "present (version $bfVersion)" } else { "present" }
$lines += ""
$lines += "BaseForm: $bfStr"
}
# --- Truncation protection ---
$totalLines = $lines.Count
+14
View File
@@ -46,6 +46,10 @@ powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-valida
| 9 | События имеют непустые имена обработчиков | ERROR |
| 10 | Команды имеют Action (обработчик) | ERROR |
| 11 | Не более одного MainAttribute | ERROR |
| 12 | BaseForm: наличие и version (при расширении) | OK / WARN |
| 13 | callType значения: Before, After, Override | ERROR |
| 14 | ID расширения >= 1000000 для добавленных attrs/commands | WARN |
| 15 | callType без BaseForm — некорректная структура | WARN |
## Вывод
@@ -71,8 +75,18 @@ All checks passed.
Код возврата: 0 = все проверки пройдены, 1 = есть ошибки.
### Расширения
При обнаружении `<BaseForm>` автоматически активируются дополнительные проверки:
- Валидность значений `callType` (Before/After/Override)
- ID расширения >= 1000000 для добавленных атрибутов и команд
- Наличие version на `<BaseForm>`
Формы без `<BaseForm>` проверяются только стандартными проверками.
## Когда использовать
- **После `/form-compile`**: проверить корректность сгенерированной формы
- **После `/form-edit`**: проверить добавленные элементы, особенно в extension-формах
- **После ручного редактирования Form.xml**: убедиться что ID уникальны, companions на месте, ссылки валидны
- **При отладке**: выявить ошибки в структуре формы до сборки EPF
@@ -76,6 +76,9 @@ if ($parentDir) {
Write-Host "=== Validation: $formName ==="
Write-Host ""
# Early BaseForm detection (used in Check 5 to skip base element DataPath validation)
$hasBaseForm = ($root.SelectSingleNode("f:BaseForm", $nsMgr) -ne $null)
# --- Check 1: Root element and version ---
if ($root.LocalName -ne "Form") {
@@ -297,6 +300,7 @@ if (-not $stopped) {
if (-not $stopped) {
$pathErrors = 0
$pathChecked = 0
$pathBaseSkipped = 0
foreach ($el in $allElements) {
if ($stopped) { break }
@@ -309,6 +313,11 @@ if (-not $stopped) {
continue
}
# In borrowed forms, skip DataPath check for base elements (id < 1000000)
if ($hasBaseForm -and $el.Id) {
try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {}
}
$dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $dpNode) { continue }
@@ -328,9 +337,15 @@ if (-not $stopped) {
}
}
if ($pathErrors -eq 0 -and $pathChecked -gt 0) {
Report-OK "DataPath references: $pathChecked paths checked"
} elseif ($pathChecked -eq 0) {
$pathMsg = ""
if ($pathChecked -gt 0) { $pathMsg = "$pathChecked paths checked" }
if ($pathBaseSkipped -gt 0) {
$skipNote = "$pathBaseSkipped base skipped"
$pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote }
}
if ($pathErrors -eq 0 -and $pathMsg) {
Report-OK "DataPath references: $pathMsg"
} elseif ($pathErrors -eq 0) {
Report-OK "DataPath references: none"
}
}
@@ -479,6 +494,152 @@ if (-not $stopped) {
}
}
# --- Check 11: Extension-specific validations ---
$baseFormNode = $root.SelectSingleNode("f:BaseForm", $nsMgr)
$isExtension = ($baseFormNode -ne $null)
if (-not $stopped -and $isExtension) {
# 11a. BaseForm version
$bfVersion = $baseFormNode.GetAttribute("version")
if ($bfVersion) {
Report-OK "BaseForm: version=$bfVersion"
} else {
Report-Warn "BaseForm: version attribute missing"
}
# 11b. callType values validation (Before, After, Override)
$validCallTypes = @("Before", "After", "Override")
$ctErrors = 0
$ctChecked = 0
# Check form-level events
$formEventsNode = $root.SelectSingleNode("f:Events", $nsMgr)
if ($formEventsNode) {
foreach ($evt in $formEventsNode.SelectNodes("f:Event", $nsMgr)) {
$ct = $evt.GetAttribute("callType")
if ($ct) {
$ctChecked++
if ($validCallTypes -notcontains $ct) {
Report-Error "Form event '$($evt.GetAttribute('name'))': invalid callType='$ct' (expected: Before, After, Override)"
$ctErrors++
}
}
}
}
# Check element-level events
foreach ($el in $allElements) {
if ($stopped) { break }
$eventsNode = $el.Node.SelectSingleNode("f:Events", $nsMgr)
if (-not $eventsNode) { continue }
foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) {
$ct = $evt.GetAttribute("callType")
if ($ct) {
$ctChecked++
if ($validCallTypes -notcontains $ct) {
Report-Error "[$($el.Tag)] '$($el.Name)' event '$($evt.GetAttribute('name'))': invalid callType='$ct'"
$ctErrors++
}
}
}
}
# Check command actions
foreach ($cmd in $cmdNodes) {
if ($stopped) { break }
$cmdName = $cmd.GetAttribute("name")
foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) {
$ct = $action.GetAttribute("callType")
if ($ct) {
$ctChecked++
if ($validCallTypes -notcontains $ct) {
Report-Error "Command '$cmdName' Action: invalid callType='$ct'"
$ctErrors++
}
}
}
}
if (-not $stopped -and $ctErrors -eq 0 -and $ctChecked -gt 0) {
Report-OK "callType values: $ctChecked checked"
}
# 11c. Extension ID ranges — warn if extension-added attrs/commands have id < 1000000
# Collect BaseForm attribute names to distinguish added ones
$baseAttrNames = @{}
$baseCmdNames = @{}
$bfNs = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$bfNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
foreach ($bAttr in $baseFormNode.SelectNodes("f:Attributes/f:Attribute", $bfNs)) {
$baName = $bAttr.GetAttribute("name")
if ($baName) { $baseAttrNames[$baName] = $true }
}
foreach ($bCmd in $baseFormNode.SelectNodes("f:Commands/f:Command", $bfNs)) {
$bcName = $bCmd.GetAttribute("name")
if ($bcName) { $baseCmdNames[$bcName] = $true }
}
$idWarnCount = 0
foreach ($attr in $attrNodes) {
$aName = $attr.GetAttribute("name")
$aId = $attr.GetAttribute("id")
if ($aName -and -not $baseAttrNames.ContainsKey($aName) -and $aId) {
try {
$intId = [int]$aId
if ($intId -lt 1000000) {
Report-Warn "Attribute '$aName' (id=$aId): extension-added attribute has id < 1000000"
$idWarnCount++
}
} catch {}
}
}
foreach ($cmd in $cmdNodes) {
$cName = $cmd.GetAttribute("name")
$cId = $cmd.GetAttribute("id")
if ($cName -and -not $baseCmdNames.ContainsKey($cName) -and $cId) {
try {
$intId = [int]$cId
if ($intId -lt 1000000) {
Report-Warn "Command '$cName' (id=$cId): extension-added command has id < 1000000"
$idWarnCount++
}
} catch {}
}
}
if (-not $stopped -and $idWarnCount -eq 0) {
$extAttrCount = ($attrNodes | Where-Object { -not $baseAttrNames.ContainsKey($_.GetAttribute("name")) }).Count
$extCmdCount = ($cmdNodes | Where-Object { -not $baseCmdNames.ContainsKey($_.GetAttribute("name")) }).Count
if (($extAttrCount + $extCmdCount) -gt 0) {
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
}
}
}
# Check callType without BaseForm (structural warning)
if (-not $stopped -and -not $isExtension) {
$callTypeWithoutBase = $false
$feNode = $root.SelectSingleNode("f:Events", $nsMgr)
if ($feNode) {
foreach ($evt in $feNode.SelectNodes("f:Event", $nsMgr)) {
if ($evt.GetAttribute("callType")) { $callTypeWithoutBase = $true; break }
}
}
if (-not $callTypeWithoutBase) {
foreach ($cmd in $cmdNodes) {
foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) {
if ($action.GetAttribute("callType")) { $callTypeWithoutBase = $true; break }
}
if ($callTypeWithoutBase) { break }
}
}
if ($callTypeWithoutBase) {
Report-Warn "callType attributes found but no BaseForm — possible incorrect structure"
}
}
# --- Summary ---
Write-Host ""
@@ -478,10 +478,12 @@ $script:generatedTypes = @{
@{ prefix = "DocumentJournalManager"; category = "Manager" }
)
"Report" = @(
@{ prefix = "ReportObject"; category = "Object" }
@{ prefix = "ReportObject"; category = "Object" }
@{ prefix = "ReportManager"; category = "Manager" }
)
"DataProcessor" = @(
@{ prefix = "DataProcessorObject"; category = "Object" }
@{ prefix = "DataProcessorObject"; category = "Object" }
@{ prefix = "DataProcessorManager"; category = "Manager" }
)
}
+2 -2
View File
@@ -257,8 +257,8 @@ Ext/ # Расширение конфигураци
| Task | Object, Ref, Selection, List, Manager |
| ExchangePlan | Object, Ref, Selection, List, Manager |
| DocumentJournal | Selection, List, Manager |
| Report | Object |
| DataProcessor | Object |
| Report | Object, Manager |
| DataProcessor | Object, Manager |
Формат имени: `{ТипОбъектаEng}.{ИмяОбъекта}` (напр. `CatalogObject.Номенклатура`, `DocumentRef.АвансовыйОтчет`).
+216 -3
View File
@@ -324,9 +324,20 @@ Enums/ # Перечисления
</EnumValue>
```
### 5.4. Заимствованные формы
### 5.4. Формы в расширениях
Метаданные формы (файл `.xml` в каталоге `Forms/`):
В расширении существуют **два принципиально разных сценария** работы с формами:
| Сценарий | Описание | `<BaseForm>` | ID элементов | `callType` |
|----------|----------|:------------:|:------------:|:----------:|
| **Собственная форма** на заимствованном объекте | Новая форма, не существующая в базовой конфигурации | Нет | Обычные (1+) | Нет |
| **Заимствованная форма** | Расширение существующей формы базовой конфигурации | Есть | Базовые + 1000000+ | Есть |
> **Как отличить:** Если файл метаданных формы (`.xml`) содержит `<ObjectBelonging>Adopted</ObjectBelonging>` — это заимствованная форма. Собственные формы не имеют `ObjectBelonging`.
#### 5.4.1. Метаданные заимствованной формы
Файл `.xml` в каталоге `Forms/`:
```xml
<Form uuid="8fcebcc1-...">
@@ -341,7 +352,209 @@ Enums/ # Перечисления
</Form>
```
Содержимое формы (расширение) хранится в `Forms/ФормаСписка/Ext/Form.xml`, модуль формы — в `Forms/ФормаСписка/Ext/Form/Module.bsl`.
Содержимое формы хранится в `Forms/ФормаСписка/Ext/Form.xml`, модуль формы — в `Forms/ФормаСписка/Ext/Form/Module.bsl`.
#### 5.4.2. Структура Form.xml заимствованной формы
Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Обе части содержат **только визуальные элементы** — атрибуты, события, параметры и команды базовой конфигурации **НЕ включаются**.
```xml
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" ... version="2.17">
<!-- ═══ ЧАСТЬ 1: Результирующая форма (база + изменения расширения) ═══ -->
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<ChildItems>
<!-- Базовые кнопки: CommandName заменён на 0 -->
<Button name="ФормаОбработкаЗагрузитьИзФайла" id="51">
<CommandName>0</CommandName>
...
</Button>
<!-- Кнопки, добавленные расширением: CommandName указывает на команду -->
<Button name="ФормаНоваяКоманда" id="159">
<CommandName>Form.Command.НоваяКоманда</CommandName>
...
</Button>
</ChildItems>
</AutoCommandBar>
<ChildItems>
<!-- Все визуальные элементы: базовые + добавленные расширением -->
</ChildItems>
<Attributes/> <!-- пустой, ИЛИ только реквизиты расширения (id ≥ 1000000) -->
<!-- Events — только обработчики расширения с callType (если есть) -->
<Events>
<Event name="OnCreateAtServer" callType="After">Расш1_ПриСозданииПосле</Event>
</Events>
<!-- Commands — только команды расширения (id ≥ 1000000, если есть) -->
<Commands>
<Command name="НоваяКоманда" id="1000000">
<Action callType="Override">Расш1_НоваяКомандаВместо</Action>
</Command>
</Commands>
<!-- ═══ ЧАСТЬ 2: Исходная форма из базовой конфигурации ═══ -->
<BaseForm version="2.17">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<ChildItems>
<!-- Только базовые кнопки, все CommandName = 0 -->
<Button name="ФормаОбработкаЗагрузитьИзФайла" id="51">
<CommandName>0</CommandName>
...
</Button>
</ChildItems>
</AutoCommandBar>
<ChildItems>
<!-- Только визуальные элементы базовой конфигурации -->
</ChildItems>
<Attributes/> <!-- всегда пустой -->
<!-- НЕТ Events, Commands, Parameters -->
</BaseForm>
</Form>
```
**Ключевые правила:**
1. **Часть 1** (до `<BaseForm>`) — **результирующая форма**. Содержит визуальные элементы (AutoCommandBar + ChildItems) из базовой конфигурации плюс элементы расширения. Атрибуты базовой конфигурации (DynamicList, QueryText и др.) **не включаются** — только реквизиты расширения (id ≥ 1000000) или пустой `<Attributes/>`. Events и Commands — только добавленные расширением (с `callType`).
2. **Часть 2** (`<BaseForm>`) — **визуальный снимок исходной формы**. Содержит только AutoCommandBar + ChildItems + пустой `<Attributes/>`. НЕ содержит Events, Commands, Parameters. Все `<CommandName>` в кнопках заменены на `0`. Платформа использует BaseForm для контроля совместимости при обновлении конфигурации.
3. **Правило `<CommandName>0</CommandName>`**: во всех кнопках базовой формы (как в Part 1, так и в BaseForm) значение `<CommandName>` заменяется на `0`. Ссылки на команды конфигурации не сохраняются. Только кнопки, добавленные расширением, сохраняют ссылку на команду (напр. `Form.Command.XXX`).
4. Элемент `<BaseForm>` всегда идёт **последним** в `<Form>` и имеет атрибут `version`.
#### 5.4.3. Нумерация ID элементов
| Диапазон | Принадлежность |
|----------|---------------|
| `-1` | Авто-командная панель (`AutoCommandBar`) — фиксированный ID |
| `1` `999999` | Элементы базовой формы (сохраняют оригинальные ID) |
| `1000000`+ | Реквизиты (`Attributes`) и команды (`Commands`), добавленные расширением |
> **Важно:** Визуальные элементы форм (элементы в `ChildItems`), добавленные расширением в тело базовой формы, могут использовать ID из обычного диапазона (продолжая нумерацию базовой формы). Диапазон 1000000+ гарантирован для `Attributes` и `Commands`.
#### 5.4.4. Атрибут callType — перехват событий и команд
В заимствованных формах события и действия команд используют атрибут `callType` для определения момента перехвата:
| Значение | Описание |
|----------|----------|
| `Before` | Обработчик расширения вызывается **до** оригинального обработчика |
| `After` | Обработчик расширения вызывается **после** оригинального обработчика |
| `Override` | Обработчик расширения **заменяет** оригинальный обработчик |
##### События формы (form-level)
```xml
<Events>
<Event name="OnCreateAtServer" callType="After">Расш1_ПриСозданииНаСервереПосле</Event>
<Event name="OnOpen" callType="Before">Расш1_ПриОткрытииПеред</Event>
<Event name="BeforeWriteAtServer" callType="After">Расш1_ПередЗаписьюНаСервереПосле</Event>
<Event name="NotificationProcessing" callType="After">Расш1_ОбработкаОповещенияПосле</Event>
</Events>
```
##### События элементов формы (element-level)
```xml
<InputField name="Банк" id="37">
<Events>
<Event name="OnChange" callType="Before">Расш1_БанкПриИзменении</Event>
<Event name="Clearing" callType="Before">Расш1_БанкОчистка</Event>
</Events>
...
</InputField>
<Table name="СписокСпецификаций" id="102">
<Events>
<Event name="Selection" callType="Before">Расш1_СписокВыборПеред</Event>
</Events>
...
</Table>
```
##### Действия команд (Command Action)
Команда может иметь **несколько элементов `<Action>`** с разными `callType`:
```xml
<!-- Перехват существующей команды базовой формы: до + после -->
<Command name="ПодборИзКлассификатора" id="1000002">
<Action callType="Before">Расш1_ПодборИзКлассификатораПеред</Action>
<Action callType="After">Расш1_ПодборИзКлассификатораПосле</Action>
</Command>
<!-- Полная замена обработчика команды -->
<Command name="НоваяКоманда" id="1000000">
<Action callType="Override">Расш1_НоваяКомандаВместо</Action>
</Command>
<!-- Один перехват (только после) -->
<Command name="ЗапросКорректировки" id="1000005">
<Action callType="After">Расш1_ЗапросКорректировкиПосле</Action>
</Command>
```
> **Отличие от обычной формы:** В обычной форме (конфигурации или собственной форме расширения) у `<Event>` и `<Action>` **нет** атрибута `callType` — обработчик вызывается напрямую.
#### 5.4.5. Собственная форма на заимствованном объекте
Расширение может добавить к заимствованному объекту **собственную форму**, не существующую в базовой конфигурации. Такая форма:
- **Не имеет** `ObjectBelonging` и `ExtendedConfigurationObject` в метаданных формы
- **Не содержит** `<BaseForm>` в Form.xml
- **Не использует** атрибут `callType`
- Использует обычную нумерацию ID (1+)
- Формат полностью совпадает с форматом форм конфигурации (см. [1c-form-spec.md](1c-form-spec.md))
```xml
<!-- Метаданные: Forms/МояФорма.xml — без ObjectBelonging -->
<Form uuid="...">
<Properties>
<Name>Расш1_МояФорма</Name>
<Synonym>...</Synonym>
<Comment/>
<FormType>Managed</FormType>
<UsePurposes>...</UsePurposes>
</Properties>
</Form>
```
```xml
<!-- Содержимое: Forms/МояФорма/Ext/Form.xml — обычная форма без BaseForm -->
<Form ... version="2.17">
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems>...</ChildItems>
<Attributes>...</Attributes>
</Form>
```
#### 5.4.6. Модуль заимствованной формы
Модуль формы (`Forms/Имя/Ext/Form/Module.bsl`) в заимствованной форме использует те же декораторы перехвата, что и другие модули расширений (см. раздел 7.2):
```bsl
&НаСервере
&Вместо("ЗаполнитьПодменюПараметры")
Процедура Расш1_ЗаполнитьПодменюПараметры()
ПродолжитьВызов();
КонецПроцедуры
&НаКлиенте
&ИзменениеИКонтроль("ПараметрыНаЯзыке")
Функция Расш1_ПараметрыНаЯзыке(КодЯзыка)
// ... тело с #Вставка / #Удаление маркерами ...
КонецФункции
// Обработчик собственной команды расширения (без декоратора)
&НаКлиенте
Процедура Расш1_НоваяКомандаВместо(Команда)
// ...
КонецПроцедуры
```
> **Обработчики событий с `callType`** (определённые в Form.xml секции Events/Action) реализуются в модуле как обычные процедуры **без** аннотаций-декораторов — привязка к событию уже задана в XML через `callType`.
---