mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-31 16:57:46 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc291b2808 |
@@ -1,63 +0,0 @@
|
||||
# cf-edit — справочник операций
|
||||
|
||||
## modify-property
|
||||
|
||||
Свойства для редактирования:
|
||||
|
||||
### Скалярные
|
||||
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
|
||||
|
||||
### LocalString (многоязычные)
|
||||
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
|
||||
|
||||
### Enum
|
||||
| Свойство | Допустимые значения |
|
||||
|----------|---------------------|
|
||||
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
|
||||
| `ConfigurationExtensionCompatibilityMode` | то же |
|
||||
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
||||
| `ScriptVariant` | `Russian`, `English` |
|
||||
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
|
||||
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
||||
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
|
||||
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
||||
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
||||
|
||||
### Ref
|
||||
`DefaultLanguage` — значение вида `Language.Русский`
|
||||
|
||||
### Формат batch
|
||||
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
|
||||
|
||||
## add-childObject / remove-childObject
|
||||
|
||||
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
||||
|
||||
При добавлении объект вставляется в каноническую позицию:
|
||||
1. Находит последний элемент того же типа → вставляет после
|
||||
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
|
||||
3. Внутри одного типа — алфавитный порядок
|
||||
|
||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||
|
||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||
|
||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||
|
||||
`set-defaultRoles` полностью заменяет список ролей.
|
||||
|
||||
## DefinitionFile (JSON)
|
||||
|
||||
```json
|
||||
[
|
||||
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
|
||||
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
|
||||
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
|
||||
]
|
||||
```
|
||||
|
||||
## Авто-валидация
|
||||
|
||||
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
|
||||
@@ -1,523 +0,0 @@
|
||||
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ConfigPath,
|
||||
[string]$DefinitionFile,
|
||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")]
|
||||
[string]$Operation,
|
||||
[string]$Value,
|
||||
[switch]$NoValidate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Mode validation ---
|
||||
if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 }
|
||||
if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 }
|
||||
|
||||
# --- Resolve path ---
|
||||
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 directory"; exit 1 }
|
||||
}
|
||||
if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 }
|
||||
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
||||
|
||||
# --- Load XML with PreserveWhitespace ---
|
||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$script:xmlDoc.PreserveWhitespace = $true
|
||||
$script:xmlDoc.Load($resolvedPath)
|
||||
|
||||
$script:addCount = 0
|
||||
$script:removeCount = 0
|
||||
$script:modifyCount = 0
|
||||
|
||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||
|
||||
# --- Detect structure ---
|
||||
$root = $script:xmlDoc.DocumentElement
|
||||
$script:mdNs = "http://v8.1c.ru/8.3/MDClasses"
|
||||
$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
$script:v8Ns = "http://v8.1c.ru/8.1/data/core"
|
||||
|
||||
$script:cfgEl = $null
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") {
|
||||
$script:cfgEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $script:cfgEl) { Write-Error "No <Configuration> element found"; exit 1 }
|
||||
|
||||
$script:propsEl = $null
|
||||
$script:childObjsEl = $null
|
||||
foreach ($child in $script:cfgEl.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Properties") { $script:propsEl = $child }
|
||||
if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child }
|
||||
}
|
||||
|
||||
$script:objName = ""
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") {
|
||||
$script:objName = $child.InnerText.Trim(); break
|
||||
}
|
||||
}
|
||||
Info "Configuration: $($script:objName)"
|
||||
|
||||
# --- Canonical type order for ChildObjects (44 types) ---
|
||||
$script:typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
)
|
||||
|
||||
# --- XML manipulation helpers (from subsystem-edit pattern) ---
|
||||
function Get-ChildIndent($container) {
|
||||
foreach ($child in $container.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
|
||||
if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] }
|
||||
if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] }
|
||||
}
|
||||
}
|
||||
$depth = 0; $current = $container
|
||||
while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode }
|
||||
return "`t" * ($depth + 1)
|
||||
}
|
||||
|
||||
function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) {
|
||||
$ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent")
|
||||
if ($refNode) {
|
||||
$container.InsertBefore($ws, $refNode) | Out-Null
|
||||
$container.InsertBefore($newNode, $ws) | Out-Null
|
||||
} else {
|
||||
$trailing = $container.LastChild
|
||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
||||
$container.InsertBefore($ws, $trailing) | Out-Null
|
||||
$container.InsertBefore($newNode, $trailing) | Out-Null
|
||||
} else {
|
||||
$container.AppendChild($ws) | Out-Null
|
||||
$container.AppendChild($newNode) | Out-Null
|
||||
$parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" }
|
||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent")
|
||||
$container.AppendChild($closeWs) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-NodeWithWhitespace($node) {
|
||||
$parent = $node.ParentNode
|
||||
$prev = $node.PreviousSibling
|
||||
$next = $node.NextSibling
|
||||
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
|
||||
$parent.RemoveChild($prev) | Out-Null
|
||||
} elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) {
|
||||
$parent.RemoveChild($next) | Out-Null
|
||||
}
|
||||
$parent.RemoveChild($node) | Out-Null
|
||||
}
|
||||
|
||||
function Expand-SelfClosingElement($container, $parentIndent) {
|
||||
if (-not $container.HasChildNodes -or $container.IsEmpty) {
|
||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent")
|
||||
$container.AppendChild($closeWs) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Import-Fragment([string]$xmlString) {
|
||||
$wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString</_W>"
|
||||
$frag = New-Object System.Xml.XmlDocument
|
||||
$frag.PreserveWhitespace = $true
|
||||
$frag.LoadXml($wrapper)
|
||||
$nodes = @()
|
||||
foreach ($child in $frag.DocumentElement.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element') {
|
||||
$nodes += $script:xmlDoc.ImportNode($child, $true)
|
||||
}
|
||||
}
|
||||
return ,$nodes
|
||||
}
|
||||
|
||||
# --- Parse batch value (split by ;;) ---
|
||||
function Parse-BatchValue([string]$val) {
|
||||
$items = @()
|
||||
foreach ($part in $val.Split(";;")) {
|
||||
$trimmed = $part.Trim()
|
||||
if ($trimmed) { $items += $trimmed }
|
||||
}
|
||||
return ,$items
|
||||
}
|
||||
|
||||
# --- LocalString properties ---
|
||||
$mlProps = @("Synonym","BriefInformation","DetailedInformation","Copyright","VendorInformationAddress","ConfigurationInformationAddress")
|
||||
# Scalar properties
|
||||
$scalarProps = @("Name","Version","Vendor","Comment","NamePrefix","UpdateCatalogAddress")
|
||||
# Ref properties
|
||||
$refProps = @("DefaultLanguage")
|
||||
|
||||
# --- Operation: modify-property ---
|
||||
function Do-ModifyProperty([string]$batchVal) {
|
||||
$items = Parse-BatchValue $batchVal
|
||||
foreach ($item in $items) {
|
||||
$eqIdx = $item.IndexOf("=")
|
||||
if ($eqIdx -lt 1) {
|
||||
Write-Error "Invalid property format '$item', expected 'Key=Value'"
|
||||
exit 1
|
||||
}
|
||||
$propName = $item.Substring(0, $eqIdx).Trim()
|
||||
$propValue = $item.Substring($eqIdx + 1).Trim()
|
||||
|
||||
# Find property element
|
||||
$propEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) {
|
||||
$propEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $propEl) {
|
||||
Write-Error "Property '$propName' not found in Properties"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($mlProps -contains $propName) {
|
||||
# LocalString
|
||||
if (-not $propValue) {
|
||||
$propEl.InnerXml = ""
|
||||
} else {
|
||||
$indent = Get-ChildIndent $script:propsEl
|
||||
$escaped = [System.Security.SecurityElement]::Escape($propValue)
|
||||
$mlXml = "`r`n$indent`t<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$escaped</v8:content>`r`n$indent`t</v8:item>`r`n$indent"
|
||||
$propEl.InnerXml = $mlXml
|
||||
}
|
||||
} elseif ($scalarProps -contains $propName -or $refProps -contains $propName) {
|
||||
# Simple text
|
||||
if (-not $propValue) { $propEl.InnerXml = "" }
|
||||
else { $propEl.InnerText = $propValue }
|
||||
} else {
|
||||
# Enum or other — just set text
|
||||
$propEl.InnerText = $propValue
|
||||
}
|
||||
|
||||
$script:modifyCount++
|
||||
Info "Set $propName = `"$propValue`""
|
||||
}
|
||||
}
|
||||
|
||||
# --- Operation: add-childObject ---
|
||||
function Do-AddChildObject([string]$batchVal) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
$items = Parse-BatchValue $batchVal
|
||||
$cfgIndent = Get-ChildIndent $script:cfgEl
|
||||
|
||||
# Expand self-closing if needed
|
||||
if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) {
|
||||
Expand-SelfClosingElement $script:childObjsEl $cfgIndent
|
||||
}
|
||||
$childIndent = Get-ChildIndent $script:childObjsEl
|
||||
|
||||
foreach ($item in $items) {
|
||||
$dotIdx = $item.IndexOf(".")
|
||||
if ($dotIdx -lt 1) {
|
||||
Write-Error "Invalid format '$item', expected 'Type.Name'"
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$objNameVal = $item.Substring($dotIdx + 1)
|
||||
|
||||
# Check type is valid
|
||||
$typeIdx = $script:typeOrder.IndexOf($typeName)
|
||||
if ($typeIdx -lt 0) {
|
||||
Write-Error "Unknown type '$typeName'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Dedup check
|
||||
$existing = $false
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) {
|
||||
$existing = $true; break
|
||||
}
|
||||
}
|
||||
if ($existing) {
|
||||
Warn "Already exists: $typeName.$objNameVal"
|
||||
continue
|
||||
}
|
||||
|
||||
# Find insertion point: after last element of same type, or after last element of preceding type
|
||||
$insertBefore = $null
|
||||
$lastSameType = $null
|
||||
$lastPrecedingType = $null
|
||||
$currentTypeIdx = -1
|
||||
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$childTypeIdx = $script:typeOrder.IndexOf($child.LocalName)
|
||||
if ($childTypeIdx -lt 0) { continue }
|
||||
|
||||
if ($child.LocalName -eq $typeName) {
|
||||
# Same type — check alphabetical order
|
||||
if ($child.InnerText -gt $objNameVal -and -not $insertBefore) {
|
||||
# Insert before this element (alphabetical)
|
||||
$insertBefore = $child
|
||||
}
|
||||
$lastSameType = $child
|
||||
} elseif ($childTypeIdx -lt $typeIdx) {
|
||||
$lastPrecedingType = $child
|
||||
} elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) {
|
||||
# First element of a later type — insert before it
|
||||
$insertBefore = $child
|
||||
}
|
||||
}
|
||||
|
||||
# Create element
|
||||
$newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs)
|
||||
$newEl.InnerText = $objNameVal
|
||||
|
||||
if ($insertBefore) {
|
||||
Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent
|
||||
} else {
|
||||
# Append at end (or after last same/preceding type)
|
||||
Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent
|
||||
}
|
||||
|
||||
$script:addCount++
|
||||
Info "Added: $typeName.$objNameVal"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Operation: remove-childObject ---
|
||||
function Do-RemoveChildObject([string]$batchVal) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
$items = Parse-BatchValue $batchVal
|
||||
foreach ($item in $items) {
|
||||
$dotIdx = $item.IndexOf(".")
|
||||
if ($dotIdx -lt 1) {
|
||||
Write-Error "Invalid format '$item', expected 'Type.Name'"
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$objNameVal = $item.Substring($dotIdx + 1)
|
||||
|
||||
$found = $false
|
||||
foreach ($child in @($script:childObjsEl.ChildNodes)) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) {
|
||||
Remove-NodeWithWhitespace $child
|
||||
$script:removeCount++
|
||||
Info "Removed: $typeName.$objNameVal"
|
||||
$found = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found) { Warn "Not found: $typeName.$objNameVal" }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Operation: add-defaultRole ---
|
||||
function Do-AddDefaultRole([string]$batchVal) {
|
||||
$items = Parse-BatchValue $batchVal
|
||||
|
||||
# Find DefaultRoles element
|
||||
$rolesEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") {
|
||||
$rolesEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $rolesEl) { Write-Error "No <DefaultRoles> element found in Properties"; exit 1 }
|
||||
|
||||
$propsIndent = Get-ChildIndent $script:propsEl
|
||||
if (-not $rolesEl.HasChildNodes -or $rolesEl.IsEmpty) {
|
||||
Expand-SelfClosingElement $rolesEl $propsIndent
|
||||
}
|
||||
$roleIndent = Get-ChildIndent $rolesEl
|
||||
|
||||
foreach ($item in $items) {
|
||||
$roleName = $item
|
||||
if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" }
|
||||
|
||||
# Dedup
|
||||
$existing = $false
|
||||
foreach ($child in $rolesEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) {
|
||||
$existing = $true; break
|
||||
}
|
||||
}
|
||||
if ($existing) {
|
||||
Warn "DefaultRole already exists: $roleName"
|
||||
continue
|
||||
}
|
||||
|
||||
$fragXml = "<xr:Item xsi:type=`"xr:MDObjectRef`">$roleName</xr:Item>"
|
||||
$nodes = Import-Fragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent
|
||||
$script:addCount++
|
||||
Info "Added DefaultRole: $roleName"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Operation: remove-defaultRole ---
|
||||
function Do-RemoveDefaultRole([string]$batchVal) {
|
||||
$items = Parse-BatchValue $batchVal
|
||||
|
||||
$rolesEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") {
|
||||
$rolesEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $rolesEl) { Write-Error "No <DefaultRoles> element found"; exit 1 }
|
||||
|
||||
foreach ($item in $items) {
|
||||
$roleName = $item
|
||||
if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" }
|
||||
|
||||
$found = $false
|
||||
foreach ($child in @($rolesEl.ChildNodes)) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) {
|
||||
Remove-NodeWithWhitespace $child
|
||||
$script:removeCount++
|
||||
Info "Removed DefaultRole: $roleName"
|
||||
$found = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found) { Warn "DefaultRole not found: $roleName" }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Operation: set-defaultRoles ---
|
||||
function Do-SetDefaultRoles([string]$batchVal) {
|
||||
$items = Parse-BatchValue $batchVal
|
||||
|
||||
$rolesEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") {
|
||||
$rolesEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $rolesEl) { Write-Error "No <DefaultRoles> element found"; exit 1 }
|
||||
|
||||
# Clear all existing children
|
||||
while ($rolesEl.HasChildNodes) {
|
||||
$rolesEl.RemoveChild($rolesEl.FirstChild) | Out-Null
|
||||
}
|
||||
|
||||
if ($items.Count -eq 0) {
|
||||
$script:modifyCount++
|
||||
Info "Cleared DefaultRoles"
|
||||
return
|
||||
}
|
||||
|
||||
$propsIndent = Get-ChildIndent $script:propsEl
|
||||
$roleIndent = "$propsIndent`t"
|
||||
|
||||
# Add closing whitespace
|
||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$propsIndent")
|
||||
$rolesEl.AppendChild($closeWs) | Out-Null
|
||||
|
||||
foreach ($item in $items) {
|
||||
$roleName = $item
|
||||
if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" }
|
||||
|
||||
$fragXml = "<xr:Item xsi:type=`"xr:MDObjectRef`">$roleName</xr:Item>"
|
||||
$nodes = Import-Fragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent
|
||||
}
|
||||
}
|
||||
|
||||
$script:modifyCount++
|
||||
Info "Set DefaultRoles: $($items.Count) roles"
|
||||
}
|
||||
|
||||
# --- Execute operations ---
|
||||
$operations = @()
|
||||
if ($DefinitionFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
||||
}
|
||||
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
||||
$ops = $jsonText | ConvertFrom-Json
|
||||
if ($ops -is [System.Array]) {
|
||||
foreach ($op in $ops) { $operations += $op }
|
||||
} else {
|
||||
$operations += $ops
|
||||
}
|
||||
} else {
|
||||
$operations += @{ operation = $Operation; value = $Value }
|
||||
}
|
||||
|
||||
foreach ($op in $operations) {
|
||||
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
|
||||
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
|
||||
|
||||
switch ($opName) {
|
||||
"modify-property" { Do-ModifyProperty $opValue }
|
||||
"add-childObject" { Do-AddChildObject $opValue }
|
||||
"remove-childObject" { Do-RemoveChildObject $opValue }
|
||||
"add-defaultRole" { Do-AddDefaultRole $opValue }
|
||||
"remove-defaultRole" { Do-RemoveDefaultRole $opValue }
|
||||
"set-defaultRoles" { Do-SetDefaultRoles $opValue }
|
||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Save ---
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$script:xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||
Info "Saved: $resolvedPath"
|
||||
|
||||
# --- Auto-validate ---
|
||||
if (-not $NoValidate) {
|
||||
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\cf-validate") "scripts\cf-validate.ps1"
|
||||
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
|
||||
if (Test-Path $validateScript) {
|
||||
Write-Host ""
|
||||
Write-Host "--- Running cf-validate ---"
|
||||
& powershell.exe -NoProfile -File $validateScript -ConfigPath $resolvedPath
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host "=== cf-edit summary ==="
|
||||
Write-Host " Configuration: $($script:objName)"
|
||||
Write-Host " Added: $($script:addCount)"
|
||||
Write-Host " Removed: $($script:removeCount)"
|
||||
Write-Host " Modified: $($script:modifyCount)"
|
||||
exit 0
|
||||
@@ -1,516 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from html import escape as html_escape
|
||||
from lxml import etree
|
||||
|
||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
# Canonical type order for ChildObjects (44 types)
|
||||
TYPE_ORDER = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
]
|
||||
|
||||
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
||||
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
|
||||
REF_PROPS = ["DefaultLanguage"]
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
|
||||
def info(msg):
|
||||
print(f"[INFO] {msg}")
|
||||
|
||||
|
||||
def warn(msg):
|
||||
print(f"[WARN] {msg}")
|
||||
|
||||
|
||||
def get_child_indent(container):
|
||||
if container.text and "\n" in container.text:
|
||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||
if after_nl and not after_nl.strip():
|
||||
return after_nl
|
||||
for child in container:
|
||||
if child.tail and "\n" in child.tail:
|
||||
after_nl = child.tail.rsplit("\n", 1)[-1]
|
||||
if after_nl and not after_nl.strip():
|
||||
return after_nl
|
||||
depth = 0
|
||||
current = container
|
||||
while current is not None:
|
||||
depth += 1
|
||||
current = current.getparent()
|
||||
return "\t" * depth
|
||||
|
||||
|
||||
def insert_before_closing(container, new_el, child_indent):
|
||||
children = list(container)
|
||||
if len(children) == 0:
|
||||
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
|
||||
container.text = "\r\n" + child_indent
|
||||
new_el.tail = "\r\n" + parent_indent
|
||||
container.append(new_el)
|
||||
else:
|
||||
last = children[-1]
|
||||
new_el.tail = last.tail
|
||||
last.tail = "\r\n" + child_indent
|
||||
container.append(new_el)
|
||||
|
||||
|
||||
def insert_before_ref(container, new_el, ref_el, child_indent):
|
||||
"""Insert new_el before ref_el inside container."""
|
||||
idx = list(container).index(ref_el)
|
||||
prev = ref_el.getprevious()
|
||||
if prev is not None:
|
||||
new_el.tail = prev.tail
|
||||
prev.tail = "\r\n" + child_indent
|
||||
else:
|
||||
new_el.tail = container.text
|
||||
container.text = "\r\n" + child_indent
|
||||
container.insert(idx, new_el)
|
||||
|
||||
|
||||
def remove_with_indent(el):
|
||||
parent = el.getparent()
|
||||
prev = el.getprevious()
|
||||
if prev is not None:
|
||||
if el.tail:
|
||||
prev.tail = el.tail
|
||||
else:
|
||||
if el.tail:
|
||||
parent.text = el.tail
|
||||
parent.remove(el)
|
||||
|
||||
|
||||
def expand_self_closing(container, parent_indent):
|
||||
if len(container) == 0 and not (container.text and container.text.strip()):
|
||||
container.text = "\r\n" + parent_indent
|
||||
|
||||
|
||||
def import_fragment(xml_string):
|
||||
wrapper = (
|
||||
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
|
||||
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
|
||||
)
|
||||
frag = etree.fromstring(wrapper.encode("utf-8"))
|
||||
return list(frag)
|
||||
|
||||
|
||||
def parse_batch_value(val):
|
||||
items = []
|
||||
for part in val.split(";;"):
|
||||
trimmed = part.strip()
|
||||
if trimmed:
|
||||
items.append(trimmed)
|
||||
return items
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||
parser.add_argument("-ConfigPath", required=True)
|
||||
parser.add_argument("-DefinitionFile", default=None)
|
||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"])
|
||||
parser.add_argument("-Value", default=None)
|
||||
parser.add_argument("-NoValidate", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.DefinitionFile and args.Operation:
|
||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not args.DefinitionFile and not args.Operation:
|
||||
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config_path = args.ConfigPath
|
||||
if not os.path.isabs(config_path):
|
||||
config_path = os.path.join(os.getcwd(), config_path)
|
||||
if os.path.isdir(config_path):
|
||||
candidate = os.path.join(config_path, "Configuration.xml")
|
||||
if os.path.isfile(candidate):
|
||||
config_path = candidate
|
||||
else:
|
||||
print("No Configuration.xml in directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not os.path.isfile(config_path):
|
||||
print(f"File not found: {config_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
resolved_path = os.path.abspath(config_path)
|
||||
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
add_count = 0
|
||||
remove_count = 0
|
||||
modify_count = 0
|
||||
|
||||
cfg_el = None
|
||||
for child in xml_root:
|
||||
if isinstance(child.tag, str) and localname(child) == "Configuration":
|
||||
cfg_el = child
|
||||
break
|
||||
if cfg_el is None:
|
||||
print("No <Configuration> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
props_el = None
|
||||
child_objs_el = None
|
||||
for child in cfg_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if localname(child) == "Properties":
|
||||
props_el = child
|
||||
if localname(child) == "ChildObjects":
|
||||
child_objs_el = child
|
||||
|
||||
obj_name = ""
|
||||
if props_el is not None:
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "Name":
|
||||
obj_name = (child.text or "").strip()
|
||||
break
|
||||
info(f"Configuration: {obj_name}")
|
||||
|
||||
# --- Operations ---
|
||||
def do_modify_property(batch_val):
|
||||
nonlocal modify_count
|
||||
items = parse_batch_value(batch_val)
|
||||
for item in items:
|
||||
eq_idx = item.find("=")
|
||||
if eq_idx < 1:
|
||||
print(f"Invalid property format '{item}', expected 'Key=Value'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
prop_name = item[:eq_idx].strip()
|
||||
prop_value = item[eq_idx + 1:].strip()
|
||||
|
||||
prop_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == prop_name:
|
||||
prop_el = child
|
||||
break
|
||||
if prop_el is None:
|
||||
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if prop_name in ML_PROPS:
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
if not prop_value:
|
||||
prop_el.text = None
|
||||
else:
|
||||
indent = get_child_indent(props_el)
|
||||
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
|
||||
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
|
||||
lang_el.text = "ru"
|
||||
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
|
||||
content_el.text = prop_value
|
||||
prop_el.text = "\r\n" + indent + "\t"
|
||||
item_el.text = "\r\n" + indent + "\t\t"
|
||||
lang_el.tail = "\r\n" + indent + "\t\t"
|
||||
content_el.tail = "\r\n" + indent + "\t"
|
||||
item_el.tail = "\r\n" + indent
|
||||
elif prop_name in SCALAR_PROPS or prop_name in REF_PROPS:
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
if not prop_value:
|
||||
prop_el.text = None
|
||||
else:
|
||||
prop_el.text = prop_value
|
||||
else:
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
prop_el.text = prop_value
|
||||
|
||||
modify_count += 1
|
||||
info(f'Set {prop_name} = "{prop_value}"')
|
||||
|
||||
def do_add_child_object(batch_val):
|
||||
nonlocal add_count
|
||||
if child_objs_el is None:
|
||||
print("No <ChildObjects> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
items = parse_batch_value(batch_val)
|
||||
cfg_indent = get_child_indent(cfg_el)
|
||||
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
|
||||
expand_self_closing(child_objs_el, cfg_indent)
|
||||
child_indent = get_child_indent(child_objs_el)
|
||||
|
||||
for item in items:
|
||||
dot_idx = item.find(".")
|
||||
if dot_idx < 1:
|
||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_name = item[:dot_idx]
|
||||
obj_name_val = item[dot_idx + 1:]
|
||||
|
||||
if type_name not in TYPE_ORDER:
|
||||
print(f"Unknown type '{type_name}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_idx = TYPE_ORDER.index(type_name)
|
||||
|
||||
# Dedup
|
||||
exists = False
|
||||
for child in child_objs_el:
|
||||
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
|
||||
exists = True
|
||||
break
|
||||
if exists:
|
||||
warn(f"Already exists: {type_name}.{obj_name_val}")
|
||||
continue
|
||||
|
||||
# Find insertion point
|
||||
insert_before = None
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
child_type_name = localname(child)
|
||||
if child_type_name not in TYPE_ORDER:
|
||||
continue
|
||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||
|
||||
if child_type_name == type_name:
|
||||
if (child.text or "") > obj_name_val and insert_before is None:
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and insert_before is None:
|
||||
insert_before = child
|
||||
|
||||
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
||||
new_el.text = obj_name_val
|
||||
|
||||
if insert_before is not None:
|
||||
insert_before_ref(child_objs_el, new_el, insert_before, child_indent)
|
||||
else:
|
||||
insert_before_closing(child_objs_el, new_el, child_indent)
|
||||
|
||||
add_count += 1
|
||||
info(f"Added: {type_name}.{obj_name_val}")
|
||||
|
||||
def do_remove_child_object(batch_val):
|
||||
nonlocal remove_count
|
||||
if child_objs_el is None:
|
||||
print("No <ChildObjects> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
items = parse_batch_value(batch_val)
|
||||
for item in items:
|
||||
dot_idx = item.find(".")
|
||||
if dot_idx < 1:
|
||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_name = item[:dot_idx]
|
||||
obj_name_val = item[dot_idx + 1:]
|
||||
|
||||
found = False
|
||||
for child in list(child_objs_el):
|
||||
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
|
||||
remove_with_indent(child)
|
||||
remove_count += 1
|
||||
info(f"Removed: {type_name}.{obj_name_val}")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
warn(f"Not found: {type_name}.{obj_name_val}")
|
||||
|
||||
def do_add_default_role(batch_val):
|
||||
nonlocal add_count
|
||||
items = parse_batch_value(batch_val)
|
||||
|
||||
roles_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
||||
roles_el = child
|
||||
break
|
||||
if roles_el is None:
|
||||
print("No <DefaultRoles> element found in Properties", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
props_indent = get_child_indent(props_el)
|
||||
if len(roles_el) == 0 and not (roles_el.text and roles_el.text.strip()):
|
||||
expand_self_closing(roles_el, props_indent)
|
||||
role_indent = get_child_indent(roles_el)
|
||||
|
||||
for item in items:
|
||||
role_name = item
|
||||
if not role_name.startswith("Role."):
|
||||
role_name = f"Role.{role_name}"
|
||||
|
||||
exists = False
|
||||
for child in roles_el:
|
||||
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
|
||||
exists = True
|
||||
break
|
||||
if exists:
|
||||
warn(f"DefaultRole already exists: {role_name}")
|
||||
continue
|
||||
|
||||
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
|
||||
nodes = import_fragment(frag_xml)
|
||||
if nodes:
|
||||
insert_before_closing(roles_el, nodes[0], role_indent)
|
||||
add_count += 1
|
||||
info(f"Added DefaultRole: {role_name}")
|
||||
|
||||
def do_remove_default_role(batch_val):
|
||||
nonlocal remove_count
|
||||
items = parse_batch_value(batch_val)
|
||||
|
||||
roles_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
||||
roles_el = child
|
||||
break
|
||||
if roles_el is None:
|
||||
print("No <DefaultRoles> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for item in items:
|
||||
role_name = item
|
||||
if not role_name.startswith("Role."):
|
||||
role_name = f"Role.{role_name}"
|
||||
|
||||
found = False
|
||||
for child in list(roles_el):
|
||||
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
|
||||
remove_with_indent(child)
|
||||
remove_count += 1
|
||||
info(f"Removed DefaultRole: {role_name}")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
warn(f"DefaultRole not found: {role_name}")
|
||||
|
||||
def do_set_default_roles(batch_val):
|
||||
nonlocal modify_count
|
||||
items = parse_batch_value(batch_val)
|
||||
|
||||
roles_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
||||
roles_el = child
|
||||
break
|
||||
if roles_el is None:
|
||||
print("No <DefaultRoles> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Clear all existing children
|
||||
for ch in list(roles_el):
|
||||
roles_el.remove(ch)
|
||||
roles_el.text = None
|
||||
|
||||
if not items:
|
||||
modify_count += 1
|
||||
info("Cleared DefaultRoles")
|
||||
return
|
||||
|
||||
props_indent = get_child_indent(props_el)
|
||||
role_indent = props_indent + "\t"
|
||||
|
||||
roles_el.text = "\r\n" + props_indent
|
||||
|
||||
for item in items:
|
||||
role_name = item
|
||||
if not role_name.startswith("Role."):
|
||||
role_name = f"Role.{role_name}"
|
||||
|
||||
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
|
||||
nodes = import_fragment(frag_xml)
|
||||
if nodes:
|
||||
insert_before_closing(roles_el, nodes[0], role_indent)
|
||||
|
||||
modify_count += 1
|
||||
info(f"Set DefaultRoles: {len(items)} roles")
|
||||
|
||||
# --- Execute operations ---
|
||||
operations = []
|
||||
if args.DefinitionFile:
|
||||
def_file = args.DefinitionFile
|
||||
if not os.path.isabs(def_file):
|
||||
def_file = os.path.join(os.getcwd(), def_file)
|
||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||
ops = json.loads(fh.read())
|
||||
if isinstance(ops, list):
|
||||
operations = ops
|
||||
else:
|
||||
operations = [ops]
|
||||
else:
|
||||
operations = [{"operation": args.Operation, "value": args.Value or ""}]
|
||||
|
||||
for op in operations:
|
||||
op_name = op.get("operation", args.Operation or "")
|
||||
op_value = op.get("value", args.Value or "")
|
||||
|
||||
if op_name == "modify-property":
|
||||
do_modify_property(op_value)
|
||||
elif op_name == "add-childObject":
|
||||
do_add_child_object(op_value)
|
||||
elif op_name == "remove-childObject":
|
||||
do_remove_child_object(op_value)
|
||||
elif op_name == "add-defaultRole":
|
||||
do_add_default_role(op_value)
|
||||
elif op_name == "remove-defaultRole":
|
||||
do_remove_default_role(op_value)
|
||||
elif op_name == "set-defaultRoles":
|
||||
do_set_default_roles(op_value)
|
||||
else:
|
||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Save ---
|
||||
save_xml_bom(tree, resolved_path)
|
||||
info(f"Saved: {resolved_path}")
|
||||
|
||||
# --- Auto-validate ---
|
||||
if not args.NoValidate:
|
||||
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "cf-validate", "scripts", "cf-validate.py"))
|
||||
if os.path.isfile(validate_script):
|
||||
print()
|
||||
print("--- Running cf-validate ---")
|
||||
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
|
||||
|
||||
# --- Summary ---
|
||||
print()
|
||||
print("=== cf-edit summary ===")
|
||||
print(f" Configuration: {obj_name}")
|
||||
print(f" Added: {add_count}")
|
||||
print(f" Removed: {remove_count}")
|
||||
print(f" Modified: {modify_count}")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
name: cf-validate
|
||||
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
||||
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-validate — валидация конфигурации 1С
|
||||
|
||||
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|---|----------|-------------|
|
||||
| 1 | XML well-formedness, MetaDataObject/Configuration, version 2.17/2.20 | ERROR |
|
||||
| 2 | InternalInfo: 7 ContainedObject, валидные ClassId, уникальность | ERROR |
|
||||
| 3 | Properties: Name непустой, Synonym, DefaultLanguage, DefaultRunMode | ERROR/WARN |
|
||||
| 4 | Properties: enum-значения (11 свойств) | ERROR |
|
||||
| 5 | ChildObjects: валидные имена типов (44 типа), нет дубликатов, порядок типов | ERROR/WARN |
|
||||
| 6 | DefaultLanguage ссылается на существующий Language в ChildObjects | ERROR |
|
||||
| 7 | Файлы языков Languages/<name>.xml существуют | WARN |
|
||||
| 8 | Каталоги объектов из ChildObjects существуют (spot-check) | WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: cfe-patch-method
|
||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-patch-method — Генерация перехватчика метода
|
||||
|
||||
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
||||
| `Context` | Директива контекста | `НаСервере` |
|
||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
||||
|
||||
## Формат ModulePath
|
||||
|
||||
| ModulePath | Файл |
|
||||
|------------|------|
|
||||
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
|
||||
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
|
||||
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
|
||||
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
|
||||
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
|
||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||
|
||||
## Типы перехвата
|
||||
|
||||
| InterceptorType | Декоратор | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
||||
| `After` | `&После` | Код после вызова оригинального метода |
|
||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Перехват &Перед на сервере
|
||||
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
|
||||
# Перехват &После на клиенте
|
||||
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
|
||||
|
||||
# ИзменениеИКонтроль для функции
|
||||
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
|
||||
```
|
||||
|
||||
## Генерируемый код (Before)
|
||||
|
||||
```bsl
|
||||
&НаСервере
|
||||
&Перед("ПриЗаписи")
|
||||
Процедура Расш1_ПриЗаписи()
|
||||
// TODO: код перед вызовом оригинального метода
|
||||
КонецПроцедуры
|
||||
```
|
||||
@@ -1,201 +0,0 @@
|
||||
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ModulePath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$MethodName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("Before","After","ModificationAndControl")]
|
||||
[string]$InterceptorType,
|
||||
|
||||
[string]$Context = "НаСервере",
|
||||
|
||||
[switch]$IsFunction
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve extension path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) {
|
||||
$ExtensionPath = Split-Path $ExtensionPath -Parent
|
||||
}
|
||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||
if (-not (Test-Path $cfgFile)) {
|
||||
Write-Error "Configuration.xml not found in: $ExtensionPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Read NamePrefix from Configuration.xml ---
|
||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgDoc.PreserveWhitespace = $false
|
||||
$cfgDoc.Load($cfgFile)
|
||||
|
||||
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
|
||||
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
|
||||
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
|
||||
|
||||
# --- Map ModulePath to file path ---
|
||||
# ModulePath formats:
|
||||
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
|
||||
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
|
||||
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
|
||||
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
|
||||
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
|
||||
|
||||
$typeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
|
||||
}
|
||||
|
||||
$parts = $ModulePath.Split(".")
|
||||
if ($parts.Count -lt 2) {
|
||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$objType = $parts[0]
|
||||
$objName = $parts[1]
|
||||
|
||||
if (-not $typeDirMap.ContainsKey($objType)) {
|
||||
Write-Error "Unknown object type: $objType"
|
||||
exit 1
|
||||
}
|
||||
$dirName = $typeDirMap[$objType]
|
||||
|
||||
$bslFile = $null
|
||||
if ($objType -eq "CommonModule") {
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
|
||||
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
||||
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
|
||||
$formName = $parts[3]
|
||||
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
|
||||
} elseif ($parts.Count -ge 3) {
|
||||
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
|
||||
$moduleName = $parts[2]
|
||||
$moduleFileName = switch ($moduleName) {
|
||||
"ObjectModule" { "ObjectModule.bsl" }
|
||||
"ManagerModule" { "ManagerModule.bsl" }
|
||||
"RecordSetModule" { "RecordSetModule.bsl" }
|
||||
"CommandModule" { "CommandModule.bsl" }
|
||||
default { "$moduleName.bsl" }
|
||||
}
|
||||
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
|
||||
} else {
|
||||
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Map InterceptorType to decorator ---
|
||||
$decorator = switch ($InterceptorType) {
|
||||
"Before" { "&Перед" }
|
||||
"After" { "&После" }
|
||||
"ModificationAndControl" { "&ИзменениеИКонтроль" }
|
||||
}
|
||||
|
||||
# --- Map Context to annotation ---
|
||||
$contextAnnotation = switch ($Context) {
|
||||
"НаСервере" { "&НаСервере" }
|
||||
"НаКлиенте" { "&НаКлиенте" }
|
||||
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
|
||||
default { "&$Context" }
|
||||
}
|
||||
|
||||
# --- Procedure name ---
|
||||
$procName = "${namePrefix}${MethodName}"
|
||||
|
||||
# --- Generate BSL code ---
|
||||
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
|
||||
$bodyLines = @()
|
||||
switch ($InterceptorType) {
|
||||
"Before" {
|
||||
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
|
||||
}
|
||||
"After" {
|
||||
$bodyLines += "`t// TODO: код после вызова оригинального метода"
|
||||
}
|
||||
"ModificationAndControl" {
|
||||
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
|
||||
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
|
||||
}
|
||||
}
|
||||
|
||||
if ($IsFunction) {
|
||||
$bodyLines += "`t"
|
||||
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
|
||||
}
|
||||
|
||||
$bslCode = @()
|
||||
$bslCode += "$contextAnnotation"
|
||||
$bslCode += "${decorator}(`"$MethodName`")"
|
||||
$bslCode += "$keyword ${procName}()"
|
||||
$bslCode += $bodyLines
|
||||
$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)) {
|
||||
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
if (Test-Path $bslFile) {
|
||||
# Append to existing file
|
||||
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
|
||||
$separator = "`r`n"
|
||||
if ($existing -and -not $existing.EndsWith("`n")) {
|
||||
$separator = "`r`n`r`n"
|
||||
}
|
||||
$newContent = $existing + $separator + $bslText
|
||||
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
|
||||
Write-Host "[OK] Добавлен перехватчик в существующий файл"
|
||||
} else {
|
||||
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
|
||||
Write-Host "[OK] Создан файл модуля"
|
||||
}
|
||||
|
||||
Write-Host " Файл: $bslFile"
|
||||
Write-Host " Декоратор: $decorator(`"$MethodName`")"
|
||||
Write-Host " Процедура: ${procName}()"
|
||||
Write-Host " Контекст: $contextAnnotation"
|
||||
@@ -1,231 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate method interceptor for 1C extension (CFE)",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-ExtensionPath", required=True)
|
||||
parser.add_argument("-ModulePath", required=True)
|
||||
parser.add_argument("-MethodName", required=True)
|
||||
parser.add_argument(
|
||||
"-InterceptorType",
|
||||
required=True,
|
||||
choices=["Before", "After", "ModificationAndControl"],
|
||||
)
|
||||
parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере
|
||||
parser.add_argument("-IsFunction", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
extension_path = args.ExtensionPath
|
||||
module_path = args.ModulePath
|
||||
method_name = args.MethodName
|
||||
interceptor_type = args.InterceptorType
|
||||
context = args.Context
|
||||
is_function = args.IsFunction
|
||||
|
||||
# --- Resolve extension path ---
|
||||
if not os.path.isabs(extension_path):
|
||||
extension_path = os.path.join(os.getcwd(), extension_path)
|
||||
if os.path.isfile(extension_path):
|
||||
extension_path = os.path.dirname(extension_path)
|
||||
|
||||
cfg_file = os.path.join(extension_path, "Configuration.xml")
|
||||
if not os.path.isfile(cfg_file):
|
||||
print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Read NamePrefix from Configuration.xml ---
|
||||
tree = ET.parse(cfg_file)
|
||||
root = tree.getroot()
|
||||
|
||||
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
props_node = root.find(".//md:Configuration/md:Properties", ns)
|
||||
name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_
|
||||
if props_node is not None:
|
||||
prefix_node = props_node.find("md:NamePrefix", ns)
|
||||
if prefix_node is not None and prefix_node.text:
|
||||
name_prefix = prefix_node.text
|
||||
|
||||
# --- Map ModulePath to file path ---
|
||||
# ModulePath formats:
|
||||
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
|
||||
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
|
||||
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
|
||||
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
|
||||
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
|
||||
|
||||
type_dir_map = {
|
||||
"Catalog": "Catalogs",
|
||||
"Document": "Documents",
|
||||
"Enum": "Enums",
|
||||
"CommonModule": "CommonModules",
|
||||
"Report": "Reports",
|
||||
"DataProcessor": "DataProcessors",
|
||||
"ExchangePlan": "ExchangePlans",
|
||||
"ChartOfAccounts": "ChartsOfAccounts",
|
||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||
"BusinessProcess": "BusinessProcesses",
|
||||
"Task": "Tasks",
|
||||
"InformationRegister": "InformationRegisters",
|
||||
"AccumulationRegister": "AccumulationRegisters",
|
||||
"AccountingRegister": "AccountingRegisters",
|
||||
"CalculationRegister": "CalculationRegisters",
|
||||
}
|
||||
|
||||
parts = module_path.split(".")
|
||||
if len(parts) < 2:
|
||||
print(
|
||||
f"Invalid ModulePath format: {module_path}. "
|
||||
"Expected: Type.Name.Module or CommonModule.Name",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
obj_type = parts[0]
|
||||
obj_name = parts[1]
|
||||
|
||||
if obj_type not in type_dir_map:
|
||||
print(f"Unknown object type: {obj_type}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
dir_name = type_dir_map[obj_type]
|
||||
|
||||
bsl_file = None
|
||||
if obj_type == "CommonModule":
|
||||
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
|
||||
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl")
|
||||
elif len(parts) >= 4 and parts[2] == "Form":
|
||||
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
|
||||
form_name = parts[3]
|
||||
bsl_file = os.path.join(
|
||||
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"
|
||||
)
|
||||
elif len(parts) >= 3:
|
||||
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
|
||||
module_name = parts[2]
|
||||
module_file_map = {
|
||||
"ObjectModule": "ObjectModule.bsl",
|
||||
"ManagerModule": "ManagerModule.bsl",
|
||||
"RecordSetModule": "RecordSetModule.bsl",
|
||||
"CommandModule": "CommandModule.bsl",
|
||||
}
|
||||
module_file_name = module_file_map.get(module_name, f"{module_name}.bsl")
|
||||
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name)
|
||||
else:
|
||||
print(
|
||||
f"Invalid ModulePath format: {module_path}. "
|
||||
"Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Map InterceptorType to decorator ---
|
||||
decorator_map = {
|
||||
"Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед
|
||||
"After": "&\u041f\u043e\u0441\u043b\u0435", # &После
|
||||
"ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль
|
||||
}
|
||||
decorator = decorator_map[interceptor_type]
|
||||
|
||||
# --- Map Context to annotation ---
|
||||
context_map = {
|
||||
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере
|
||||
"\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте
|
||||
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста
|
||||
}
|
||||
context_annotation = context_map.get(context, f"&{context}")
|
||||
|
||||
# --- Procedure name ---
|
||||
proc_name = f"{name_prefix}{method_name}"
|
||||
|
||||
# --- Generate BSL code ---
|
||||
keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура
|
||||
end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры
|
||||
|
||||
body_lines = []
|
||||
if interceptor_type == "Before":
|
||||
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода
|
||||
elif interceptor_type == "After":
|
||||
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода
|
||||
elif interceptor_type == "ModificationAndControl":
|
||||
body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения,
|
||||
body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки
|
||||
|
||||
if is_function:
|
||||
body_lines.append("\t")
|
||||
body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение
|
||||
|
||||
bsl_code = [
|
||||
context_annotation,
|
||||
f'{decorator}("{method_name}")',
|
||||
f"{keyword} {proc_name}()",
|
||||
]
|
||||
bsl_code.extend(body_lines)
|
||||
bsl_code.append(end_keyword)
|
||||
|
||||
bsl_text = "\r\n".join(bsl_code) + "\r\n"
|
||||
|
||||
# --- Check form borrowing for .Form. paths ---
|
||||
if len(parts) >= 4 and parts[2] == "Form":
|
||||
form_name = parts[3]
|
||||
form_meta_file = os.path.join(
|
||||
extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml"
|
||||
)
|
||||
form_xml_file = os.path.join(
|
||||
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml"
|
||||
)
|
||||
|
||||
if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file):
|
||||
print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.")
|
||||
print(" Run /cfe-borrow first:")
|
||||
print(
|
||||
f" /cfe-borrow -ExtensionPath {extension_path} "
|
||||
f'-ConfigPath <ConfigPath> -Object "{obj_type}.{obj_name}.Form.{form_name}"'
|
||||
)
|
||||
print()
|
||||
|
||||
# --- Check if file exists and append ---
|
||||
bsl_dir = os.path.dirname(bsl_file)
|
||||
if not os.path.isdir(bsl_dir):
|
||||
os.makedirs(bsl_dir, exist_ok=True)
|
||||
|
||||
if os.path.isfile(bsl_file):
|
||||
# Append to existing file
|
||||
with open(bsl_file, "r", encoding="utf-8-sig", newline="") as f:
|
||||
existing = f.read()
|
||||
|
||||
separator = "\r\n"
|
||||
if existing and not existing.endswith("\n"):
|
||||
separator = "\r\n\r\n"
|
||||
new_content = existing + separator + bsl_text
|
||||
|
||||
with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f:
|
||||
f.write(new_content)
|
||||
print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл
|
||||
else:
|
||||
with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f:
|
||||
f.write(bsl_text)
|
||||
print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля
|
||||
|
||||
print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл:
|
||||
print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор:
|
||||
print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура:
|
||||
print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
name: cfe-validate
|
||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|---------------|:-----:|---------|-------------------------------------------------|
|
||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
|
||||
```
|
||||
|
||||
## Проверки (13 шагов)
|
||||
|
||||
| # | Проверка | Уровень |
|
||||
|---|----------|---------|
|
||||
| 1 | XML well-formedness, MetaDataObject/Configuration, version | ERROR |
|
||||
| 2 | InternalInfo: 7 ContainedObject, валидные ClassId | ERROR |
|
||||
| 3 | Extension properties: ObjectBelonging=Adopted, Name, Purpose, NamePrefix, KeepMapping | ERROR |
|
||||
| 4 | Enum-значения: ConfigurationExtensionCompatibilityMode, DefaultRunMode, ScriptVariant, InterfaceCompatibilityMode | ERROR |
|
||||
| 5 | ChildObjects: валидные типы (44), нет дубликатов, каноничный порядок | ERROR/WARN |
|
||||
| 6 | DefaultLanguage ссылается на Language в ChildObjects | ERROR |
|
||||
| 7 | Файлы языков существуют | WARN |
|
||||
| 8 | Каталоги объектов существуют | WARN |
|
||||
| 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN |
|
||||
| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR |
|
||||
| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm version | ERROR/WARN |
|
||||
| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN |
|
||||
| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,163 +0,0 @@
|
||||
# db-create v1.0 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Создание информационной базы 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Создаёт новую информационную базу 1С (файловую или серверную).
|
||||
Поддерживает создание из шаблона и добавление в список баз.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UseTemplate
|
||||
Путь к файлу шаблона (.cf или .dt)
|
||||
|
||||
.PARAMETER AddToList
|
||||
Добавить в список баз 1С
|
||||
|
||||
.PARAMETER ListName
|
||||
Имя базы в списке
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UseTemplate,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AddToList,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ListName
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate template ---
|
||||
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
|
||||
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("CREATEINFOBASE")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "File=`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
# --- Template ---
|
||||
if ($UseTemplate) {
|
||||
$arguments += "/UseTemplate", "`"$UseTemplate`""
|
||||
}
|
||||
|
||||
# --- Add to list ---
|
||||
if ($AddToList) {
|
||||
if ($ListName) {
|
||||
$arguments += "/AddToList", "`"$ListName`""
|
||||
} else {
|
||||
$arguments += "/AddToList"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "create_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-create v1.0 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create 1C information base",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UseTemplate", default="")
|
||||
parser.add_argument("-AddToList", action="store_true")
|
||||
parser.add_argument("-ListName", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate template ---
|
||||
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["CREATEINFOBASE"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
||||
else:
|
||||
arguments.append(f'File="{args.InfoBasePath}"')
|
||||
|
||||
# --- Template ---
|
||||
if args.UseTemplate:
|
||||
arguments.extend(["/UseTemplate", args.UseTemplate])
|
||||
|
||||
# --- Add to list ---
|
||||
if args.AddToList:
|
||||
if args.ListName:
|
||||
arguments.extend(["/AddToList", args.ListName])
|
||||
else:
|
||||
arguments.append("/AddToList")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "create_log.txt")
|
||||
arguments.extend(["/Out", out_file])
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
||||
else:
|
||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||
else:
|
||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.isdir(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,166 +0,0 @@
|
||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в CF-файл
|
||||
|
||||
.DESCRIPTION
|
||||
Выгружает конфигурацию информационной базы в бинарный CF-файл.
|
||||
Поддерживает выгрузку расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному CF-файлу
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для выгрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Выгрузить все расширения
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
$outDir = Split-Path $OutputFile -Parent
|
||||
if ($outDir -and -not (Test-Path $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/DumpCfg", "`"$OutputFile`""
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_cf_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Dump 1C configuration to CF file",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-OutputFile", required=True)
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
out_dir = os.path.dirname(args.OutputFile)
|
||||
if out_dir and not os.path.isdir(out_dir):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||
else:
|
||||
arguments.extend(["/F", args.InfoBasePath])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments.extend(["/DumpCfg", args.OutputFile])
|
||||
|
||||
# --- Extensions ---
|
||||
if args.Extension:
|
||||
arguments.extend(["-Extension", args.Extension])
|
||||
elif args.AllExtensions:
|
||||
arguments.append("-AllExtensions")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
||||
arguments.extend(["/Out", out_file])
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||
else:
|
||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.isdir(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,224 +0,0 @@
|
||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в XML-файлы
|
||||
|
||||
.DESCRIPTION
|
||||
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
|
||||
- Full: полная выгрузка всей конфигурации
|
||||
- Changes: инкрементальная выгрузка изменённых объектов
|
||||
- Partial: выгрузка конкретных объектов из списка
|
||||
- UpdateInfo: обновление только ConfigDumpInfo.xml
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог для выгрузки конфигурации
|
||||
|
||||
.PARAMETER Mode
|
||||
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
|
||||
|
||||
.PARAMETER Objects
|
||||
Имена объектов метаданных через запятую (для режима Partial)
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для выгрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Выгрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
|
||||
[string]$Mode = "Changes",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Objects,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical"
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Objects) {
|
||||
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Create output dir if needed ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
|
||||
Write-Host "Created output directory: $ConfigDir"
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
switch ($Mode) {
|
||||
"Full" {
|
||||
Write-Host "Executing full configuration dump..."
|
||||
}
|
||||
"Changes" {
|
||||
Write-Host "Executing incremental configuration dump..."
|
||||
$arguments += "-update"
|
||||
$arguments += "-force"
|
||||
}
|
||||
"Partial" {
|
||||
Write-Host "Executing partial configuration dump..."
|
||||
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
|
||||
$listFile = Join-Path $tempDir "dump_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
|
||||
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
Write-Host "Objects to dump: $($objectList.Count)"
|
||||
foreach ($obj in $objectList) { Write-Host " $obj" }
|
||||
}
|
||||
"UpdateInfo" {
|
||||
Write-Host "Updating ConfigDumpInfo.xml..."
|
||||
$arguments += "-configDumpInfoOnly"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Dump completed successfully" -ForegroundColor Green
|
||||
Write-Host "Configuration dumped to: $ConfigDir"
|
||||
} else {
|
||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Dump 1C configuration to XML files",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
||||
parser.add_argument(
|
||||
"-Mode",
|
||||
default="Changes",
|
||||
choices=["Full", "Changes", "Partial", "UpdateInfo"],
|
||||
help="Dump mode (default: Changes)",
|
||||
)
|
||||
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
||||
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
||||
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
||||
parser.add_argument(
|
||||
"-Format",
|
||||
default="Hierarchical",
|
||||
choices=["Hierarchical", "Plain"],
|
||||
help="Dump format (default: Hierarchical)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if args.Mode == "Partial" and not args.Objects:
|
||||
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Create output dir if needed ---
|
||||
if not os.path.exists(args.ConfigDir):
|
||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||
print(f"Created output directory: {args.ConfigDir}")
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
||||
else:
|
||||
arguments += ["/F", args.InfoBasePath]
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments += ["/DumpConfigToFiles", args.ConfigDir]
|
||||
arguments += ["-Format", args.Format]
|
||||
|
||||
if args.Mode == "Full":
|
||||
print("Executing full configuration dump...")
|
||||
elif args.Mode == "Changes":
|
||||
print("Executing incremental configuration dump...")
|
||||
arguments.append("-update")
|
||||
arguments.append("-force")
|
||||
elif args.Mode == "Partial":
|
||||
print("Executing partial configuration dump...")
|
||||
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
|
||||
|
||||
list_file = os.path.join(temp_dir, "dump_list.txt")
|
||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||
f.write("\n".join(object_list))
|
||||
|
||||
arguments += ["-listFile", list_file]
|
||||
print(f"Objects to dump: {len(object_list)}")
|
||||
for obj in object_list:
|
||||
print(f" {obj}")
|
||||
elif args.Mode == "UpdateInfo":
|
||||
print("Updating ConfigDumpInfo.xml...")
|
||||
arguments.append("-configDumpInfoOnly")
|
||||
|
||||
# --- Extensions ---
|
||||
if args.Extension:
|
||||
arguments += ["-Extension", args.Extension]
|
||||
elif args.AllExtensions:
|
||||
arguments.append("-AllExtensions")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||
arguments += ["/Out", out_file]
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print("Dump completed successfully")
|
||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
||||
else:
|
||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,166 +0,0 @@
|
||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка конфигурации 1С из CF-файла
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает конфигурацию из бинарного CF-файла в информационную базу.
|
||||
Поддерживает загрузку расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к CF-файлу для загрузки
|
||||
|
||||
.PARAMETER Extension
|
||||
Загрузить как расширение
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения из архива
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$InputFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate input file ---
|
||||
if (-not (Test-Path $InputFile)) {
|
||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/LoadCfg", "`"$InputFile`""
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_cf_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Load 1C configuration from CF file",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-InputFile", required=True)
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate input file ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||
else:
|
||||
arguments.extend(["/F", args.InfoBasePath])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments.extend(["/LoadCfg", args.InputFile])
|
||||
|
||||
# --- Extensions ---
|
||||
if args.Extension:
|
||||
arguments.extend(["-Extension", args.Extension])
|
||||
elif args.AllExtensions:
|
||||
arguments.append("-AllExtensions")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
||||
arguments.extend(["/Out", out_file])
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.isdir(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,359 +0,0 @@
|
||||
# db-load-git v1.3 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка изменений из Git в базу 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Определяет изменённые файлы конфигурации по данным Git и выполняет
|
||||
частичную загрузку в информационную базу.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог XML-выгрузки конфигурации (git-репозиторий)
|
||||
|
||||
.PARAMETER Source
|
||||
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
|
||||
|
||||
.PARAMETER CommitRange
|
||||
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для загрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.PARAMETER DryRun
|
||||
Только показать что будет загружено (без загрузки)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("All", "Staged", "Unstaged", "Commit")]
|
||||
[string]$Source = "All",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CommitRange,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$DryRun,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$UpdateDB
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
||||
function Get-ObjectXmlFromSubFile {
|
||||
param([string]$RelativePath)
|
||||
|
||||
$parts = $RelativePath -split '[\\/]'
|
||||
if ($parts.Count -ge 2) {
|
||||
return "$($parts[0])/$($parts[1]).xml"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Resolve V8Path (skip if DryRun) ---
|
||||
if (-not $DryRun) {
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Validate connection (skip if DryRun) ---
|
||||
if (-not $DryRun) {
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Validate config dir ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Commit mode ---
|
||||
if ($Source -eq "Commit" -and -not $CommitRange) {
|
||||
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Check git ---
|
||||
try {
|
||||
$null = git --version 2>&1
|
||||
} catch {
|
||||
Write-Host "Error: git not found in PATH" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Get changed files from Git ---
|
||||
$changedFiles = @()
|
||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||
|
||||
Push-Location $ConfigDir
|
||||
try {
|
||||
switch ($Source) {
|
||||
"Staged" {
|
||||
Write-Host "Getting staged changes..."
|
||||
$raw = git diff --cached --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
"Unstaged" {
|
||||
Write-Host "Getting unstaged changes..."
|
||||
$raw = git diff --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
$raw = git ls-files --others --exclude-standard 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
"Commit" {
|
||||
Write-Host "Getting changes from $CommitRange..."
|
||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
"All" {
|
||||
Write-Host "Getting all uncommitted changes..."
|
||||
$raw = git diff --cached --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
$raw = git diff --name-only --relative 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
$raw = git ls-files --others --exclude-standard 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
|
||||
if ($changedFiles.Count -eq 0) {
|
||||
Write-Host "No changes found"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Git changes detected: $($changedFiles.Count) files"
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
$configFiles = @()
|
||||
|
||||
foreach ($file in $changedFiles) {
|
||||
$file = $file.Trim().Replace('\', '/')
|
||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||
|
||||
# Skip service files
|
||||
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
||||
|
||||
$fullPath = Join-Path $ConfigDir $file
|
||||
|
||||
if ($file -match '\.xml$') {
|
||||
# XML file — add directly if exists
|
||||
if (Test-Path $fullPath) {
|
||||
if ($configFiles -notcontains $file) {
|
||||
$configFiles += $file
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
||||
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
|
||||
if ($objectXml) {
|
||||
$fullXmlPath = Join-Path $ConfigDir $objectXml
|
||||
if (Test-Path $fullXmlPath) {
|
||||
if ($configFiles -notcontains $objectXml) {
|
||||
$configFiles += $objectXml
|
||||
}
|
||||
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
|
||||
$configFiles += $file
|
||||
}
|
||||
|
||||
# Add all files from Ext/ directory of the object
|
||||
$parts = $file -split '[\\/]'
|
||||
if ($parts.Count -ge 2) {
|
||||
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
|
||||
if (Test-Path $extDir) {
|
||||
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
|
||||
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
|
||||
if ($configFiles -notcontains $extRelPath) {
|
||||
$configFiles += $extRelPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($configFiles.Count -eq 0) {
|
||||
Write-Host "No configuration files found in changes"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Files for loading: $($configFiles.Count)"
|
||||
foreach ($f in $configFiles) { Write-Host " $f" }
|
||||
|
||||
# --- DryRun: stop here ---
|
||||
if ($DryRun) {
|
||||
Write-Host ""
|
||||
Write-Host "DryRun mode - no changes applied"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Write list file (UTF-8 with BOM) ---
|
||||
$listFile = Join-Path $tempDir "load_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
|
||||
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
$arguments += "-Format", $Format
|
||||
$arguments += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- UpdateDB ---
|
||||
if ($UpdateDB) {
|
||||
$arguments += "/UpdateDBCfg"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host ""
|
||||
Write-Host "Executing partial configuration load..."
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
Write-Host ""
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-git v1.3 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
def get_object_xml_from_subfile(relative_path):
|
||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||
parts = re.split(r"[\\/]", relative_path)
|
||||
if len(parts) >= 2:
|
||||
return f"{parts[0]}/{parts[1]}.xml"
|
||||
return None
|
||||
|
||||
|
||||
def run_git(config_dir, git_args):
|
||||
"""Run a git command in config_dir and return output lines on success."""
|
||||
result = subprocess.run(
|
||||
["git"] + git_args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=config_dir,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return [line for line in result.stdout.splitlines() if line.strip()]
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Load Git changes into 1C database",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
|
||||
parser.add_argument(
|
||||
"-Source",
|
||||
default="All",
|
||||
choices=["All", "Staged", "Unstaged", "Commit"],
|
||||
help="Change source (default: All)",
|
||||
)
|
||||
parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD")
|
||||
parser.add_argument("-Extension", default="", help="Extension name to load")
|
||||
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
|
||||
parser.add_argument(
|
||||
"-Format",
|
||||
default="Hierarchical",
|
||||
choices=["Hierarchical", "Plain"],
|
||||
help="File format (default: Hierarchical)",
|
||||
)
|
||||
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve V8Path (skip if DryRun) ---
|
||||
v8path = None
|
||||
if not args.DryRun:
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection (skip if DryRun) ---
|
||||
if not args.DryRun:
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate config dir ---
|
||||
if not os.path.exists(args.ConfigDir):
|
||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Commit mode ---
|
||||
if args.Source == "Commit" and not args.CommitRange:
|
||||
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Check git ---
|
||||
try:
|
||||
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Error: git not found in PATH", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Get changed files from Git ---
|
||||
changed_files = []
|
||||
|
||||
if args.Source == "Staged":
|
||||
print("Getting staged changes...")
|
||||
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"])
|
||||
elif args.Source == "Unstaged":
|
||||
print("Getting unstaged changes...")
|
||||
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"])
|
||||
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
|
||||
elif args.Source == "Commit":
|
||||
print(f"Getting changes from {args.CommitRange}...")
|
||||
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative", args.CommitRange])
|
||||
elif args.Source == "All":
|
||||
print("Getting all uncommitted changes...")
|
||||
changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"])
|
||||
changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"])
|
||||
changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"])
|
||||
|
||||
# Deduplicate and filter blanks
|
||||
changed_files = list(dict.fromkeys(f for f in changed_files if f.strip()))
|
||||
|
||||
if len(changed_files) == 0:
|
||||
print("No changes found")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Git changes detected: {len(changed_files)} files")
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
config_files = []
|
||||
|
||||
for file in changed_files:
|
||||
file = file.strip().replace("\\", "/")
|
||||
if not file:
|
||||
continue
|
||||
|
||||
# Skip service files
|
||||
if file == "ConfigDumpInfo.xml":
|
||||
continue
|
||||
|
||||
full_path = os.path.join(args.ConfigDir, file)
|
||||
|
||||
if file.endswith(".xml"):
|
||||
# XML file — add directly if exists
|
||||
if os.path.exists(full_path):
|
||||
if file not in config_files:
|
||||
config_files.append(file)
|
||||
else:
|
||||
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
|
||||
object_xml = get_object_xml_from_subfile(file)
|
||||
if object_xml:
|
||||
full_xml_path = os.path.join(args.ConfigDir, object_xml)
|
||||
if os.path.exists(full_xml_path):
|
||||
if object_xml not in config_files:
|
||||
config_files.append(object_xml)
|
||||
if os.path.exists(full_path) and file not in config_files:
|
||||
config_files.append(file)
|
||||
|
||||
# Add all files from Ext/ directory of the object
|
||||
parts = re.split(r"[\\/]", file)
|
||||
if len(parts) >= 2:
|
||||
ext_dir = os.path.join(args.ConfigDir, parts[0], parts[1], "Ext")
|
||||
if os.path.isdir(ext_dir):
|
||||
for root, dirs, files in os.walk(ext_dir):
|
||||
for fname in files:
|
||||
abs_path = os.path.join(root, fname)
|
||||
rel_path = os.path.relpath(abs_path, args.ConfigDir).replace("\\", "/")
|
||||
if rel_path not in config_files:
|
||||
config_files.append(rel_path)
|
||||
|
||||
if len(config_files) == 0:
|
||||
print("No configuration files found in changes")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Files for loading: {len(config_files)}")
|
||||
for f in config_files:
|
||||
print(f" {f}")
|
||||
|
||||
# --- DryRun: stop here ---
|
||||
if args.DryRun:
|
||||
print("")
|
||||
print("DryRun mode - no changes applied")
|
||||
sys.exit(0)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_git_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Write list file (UTF-8 with BOM) ---
|
||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||
f.write("\n".join(config_files))
|
||||
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
||||
else:
|
||||
arguments += ["/F", args.InfoBasePath]
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
||||
arguments += ["-listFile", list_file]
|
||||
arguments += ["-Format", args.Format]
|
||||
arguments.append("-partial")
|
||||
arguments.append("-updateConfigDumpInfo")
|
||||
|
||||
# --- Extensions ---
|
||||
if args.Extension:
|
||||
arguments += ["-Extension", args.Extension]
|
||||
elif args.AllExtensions:
|
||||
arguments.append("-AllExtensions")
|
||||
|
||||
# --- UpdateDB ---
|
||||
if args.UpdateDB:
|
||||
arguments.append("/UpdateDBCfg")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
||||
arguments += ["/Out", out_file]
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print("")
|
||||
print("Executing partial configuration load...")
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
print("")
|
||||
if exit_code == 0:
|
||||
print("Load completed successfully")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,238 +0,0 @@
|
||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка конфигурации 1С из XML-файлов
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает конфигурацию в информационную базу из XML-файлов.
|
||||
Поддерживает полную и частичную загрузку.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER ConfigDir
|
||||
Каталог XML-исходников конфигурации
|
||||
|
||||
.PARAMETER Mode
|
||||
Режим загрузки: Full или Partial (по умолчанию Full)
|
||||
|
||||
.PARAMETER Files
|
||||
Относительные пути файлов через запятую (для режима Partial)
|
||||
|
||||
.PARAMETER ListFile
|
||||
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для загрузки
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Загрузить все расширения
|
||||
|
||||
.PARAMETER Format
|
||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Full", "Partial")]
|
||||
[string]$Mode = "Full",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Files,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ListFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$UpdateDB
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate config dir ---
|
||||
if (-not (Test-Path $ConfigDir)) {
|
||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
|
||||
if ($Mode -eq "Full") {
|
||||
Write-Host "Executing full configuration load..."
|
||||
} else {
|
||||
Write-Host "Executing partial configuration load..."
|
||||
|
||||
# Build list file
|
||||
$generatedListFile = $null
|
||||
if ($ListFile) {
|
||||
# Use provided list file
|
||||
if (-not (Test-Path $ListFile)) {
|
||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$generatedListFile = $ListFile
|
||||
} else {
|
||||
# Generate from -Files parameter
|
||||
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
||||
|
||||
Write-Host "Files to load: $($fileList.Count)"
|
||||
foreach ($f in $fileList) { Write-Host " $f" }
|
||||
}
|
||||
|
||||
$arguments += "-listFile", "`"$generatedListFile`""
|
||||
$arguments += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
}
|
||||
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- UpdateDB ---
|
||||
if ($UpdateDB) {
|
||||
$arguments += "/UpdateDBCfg"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Load 1C configuration from XML files",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
||||
parser.add_argument(
|
||||
"-Mode",
|
||||
default="Full",
|
||||
choices=["Full", "Partial"],
|
||||
help="Load mode (default: Full)",
|
||||
)
|
||||
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
|
||||
parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)")
|
||||
parser.add_argument("-Extension", default="", help="Extension name to load")
|
||||
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
|
||||
parser.add_argument(
|
||||
"-Format",
|
||||
default="Hierarchical",
|
||||
choices=["Hierarchical", "Plain"],
|
||||
help="File format (default: Hierarchical)",
|
||||
)
|
||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate config dir ---
|
||||
if not os.path.exists(args.ConfigDir):
|
||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
if args.Mode == "Partial" and not args.Files and not args.ListFile:
|
||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
||||
else:
|
||||
arguments += ["/F", args.InfoBasePath]
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
||||
|
||||
if args.Mode == "Full":
|
||||
print("Executing full configuration load...")
|
||||
else:
|
||||
print("Executing partial configuration load...")
|
||||
|
||||
# Build list file
|
||||
generated_list_file = None
|
||||
if args.ListFile:
|
||||
# Use provided list file
|
||||
if not os.path.isfile(args.ListFile):
|
||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
generated_list_file = args.ListFile
|
||||
else:
|
||||
# Generate from -Files parameter
|
||||
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||
f.write("\n".join(file_list))
|
||||
|
||||
print(f"Files to load: {len(file_list)}")
|
||||
for fl in file_list:
|
||||
print(f" {fl}")
|
||||
|
||||
arguments += ["-listFile", generated_list_file]
|
||||
arguments.append("-partial")
|
||||
arguments.append("-updateConfigDumpInfo")
|
||||
|
||||
arguments += ["-Format", args.Format]
|
||||
|
||||
# --- Extensions ---
|
||||
if args.Extension:
|
||||
arguments += ["-Extension", args.Extension]
|
||||
elif args.AllExtensions:
|
||||
arguments.append("-AllExtensions")
|
||||
|
||||
# --- UpdateDB ---
|
||||
if args.UpdateDB:
|
||||
arguments.append("/UpdateDBCfg")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
||||
arguments += ["/Out", out_file]
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print("Load completed successfully")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,145 +0,0 @@
|
||||
# db-run v1.0 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Запуск 1С:Предприятие
|
||||
|
||||
.DESCRIPTION
|
||||
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
|
||||
Запуск в фоне — не ждёт завершения процесса.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER Execute
|
||||
Путь к внешней обработке для запуска
|
||||
|
||||
.PARAMETER CParam
|
||||
Параметр запуска (/C)
|
||||
|
||||
.PARAMETER URL
|
||||
Навигационная ссылка (e1cib/...)
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Execute,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CParam,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$URL
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Build arguments as single string ---
|
||||
# Note: Start-Process without -NoNewWindow uses ShellExecute.
|
||||
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
|
||||
# re-joins elements. Single string avoids this.
|
||||
$argString = "ENTERPRISE"
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$argString += " /F `"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $argString += " /N`"$UserName`"" }
|
||||
if ($Password) { $argString += " /P`"$Password`"" }
|
||||
|
||||
# --- Optional params ---
|
||||
if ($Execute) {
|
||||
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
|
||||
if ($ext -eq ".erf") {
|
||||
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
|
||||
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
|
||||
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
|
||||
$Execute = ""
|
||||
}
|
||||
}
|
||||
if ($Execute) {
|
||||
$argString += " /Execute `"$Execute`""
|
||||
}
|
||||
if ($CParam) {
|
||||
$argString += " /C `"$CParam`""
|
||||
}
|
||||
if ($URL) {
|
||||
$argString += " /URL `"$URL`""
|
||||
}
|
||||
|
||||
$argString += " /DisableStartupDialogs"
|
||||
|
||||
# --- Execute (background, no wait) ---
|
||||
Write-Host "Running: 1cv8.exe $argString"
|
||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-run v1.0 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Launch 1C:Enterprise",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-Execute", default="")
|
||||
parser.add_argument("-CParam", default="")
|
||||
parser.add_argument("-URL", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Build arguments ---
|
||||
arguments = ["ENTERPRISE"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||
else:
|
||||
arguments.extend(["/F", args.InfoBasePath])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
# --- Optional params ---
|
||||
execute = args.Execute
|
||||
if execute:
|
||||
ext = os.path.splitext(execute)[1].lower()
|
||||
if ext == ".erf":
|
||||
print("[WARN] /Execute does not support ERF files (external reports).")
|
||||
print(f" Open the report via File -> Open: {execute}")
|
||||
print(" Launching database without /Execute.")
|
||||
execute = ""
|
||||
|
||||
if execute:
|
||||
arguments.extend(["/Execute", execute])
|
||||
if args.CParam:
|
||||
arguments.extend(["/C", args.CParam])
|
||||
if args.URL:
|
||||
arguments.extend(["/URL", args.URL])
|
||||
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute (background, no wait) ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
subprocess.Popen([v8path] + arguments)
|
||||
print("1C:Enterprise launched")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,184 +0,0 @@
|
||||
# db-update v1.0 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Обновление конфигурации базы данных 1С
|
||||
|
||||
.DESCRIPTION
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных.
|
||||
Поддерживает динамическое обновление, обновление расширений.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER Extension
|
||||
Имя расширения для обновления
|
||||
|
||||
.PARAMETER AllExtensions
|
||||
Обновить все расширения
|
||||
|
||||
.PARAMETER Dynamic
|
||||
Динамическое обновление: "+" включить, "-" отключить
|
||||
|
||||
.PARAMETER Server
|
||||
Обновление на стороне сервера
|
||||
|
||||
.PARAMETER WarningsAsErrors
|
||||
Предупреждения считать ошибками
|
||||
|
||||
.EXAMPLE
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
.EXAMPLE
|
||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Extension,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("+", "-")]
|
||||
[string]$Dynamic,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$Server,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$WarningsAsErrors
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/UpdateDBCfg"
|
||||
|
||||
# --- Options ---
|
||||
if ($Dynamic) {
|
||||
$arguments += "-Dynamic$Dynamic"
|
||||
}
|
||||
if ($Server) {
|
||||
$arguments += "-Server"
|
||||
}
|
||||
if ($WarningsAsErrors) {
|
||||
$arguments += "-WarningsAsErrors"
|
||||
}
|
||||
|
||||
# --- Extensions ---
|
||||
if ($Extension) {
|
||||
$arguments += "-Extension", "`"$Extension`""
|
||||
} elseif ($AllExtensions) {
|
||||
$arguments += "-AllExtensions"
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "update_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-update v1.0 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Update 1C database configuration",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||
parser.add_argument("-Server", action="store_true")
|
||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||
else:
|
||||
arguments.extend(["/F", args.InfoBasePath])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments.append("/UpdateDBCfg")
|
||||
|
||||
# --- Options ---
|
||||
if args.Dynamic:
|
||||
arguments.append(f"-Dynamic{args.Dynamic}")
|
||||
if args.Server:
|
||||
arguments.append("-Server")
|
||||
if args.WarningsAsErrors:
|
||||
arguments.append("-WarningsAsErrors")
|
||||
|
||||
# --- Extensions ---
|
||||
if args.Extension:
|
||||
arguments.extend(["-Extension", args.Extension])
|
||||
elif args.AllExtensions:
|
||||
arguments.append("-AllExtensions")
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "update_log.txt")
|
||||
arguments.extend(["/Out", out_file])
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print("Database configuration updated successfully")
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.isdir(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: epf-add-form
|
||||
description: Добавить управляемую форму к внешней обработке 1С
|
||||
argument-hint: <ProcessorName> <FormName> [Synonym]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# /epf-add-form — Добавление формы
|
||||
|
||||
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
|
||||
```
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---------------|:------------:|--------------|-------------------------------------------|
|
||||
| ProcessorName | да | — | Имя обработки (должна существовать) |
|
||||
| FormName | да | — | Имя формы |
|
||||
| Synonym | нет | = FormName | Синоним формы |
|
||||
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
|
||||
| SrcDir | нет | `src` | Каталог исходников |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<SrcDir>/<ProcessorName>/Forms/
|
||||
├── <FormName>.xml # Метаданные формы (1 UUID)
|
||||
└── <FormName>/
|
||||
└── Ext/
|
||||
├── Form.xml # Описание формы (logform namespace)
|
||||
└── Form/
|
||||
└── Module.bsl # BSL-модуль с 4 регионами
|
||||
```
|
||||
|
||||
## Что модифицируется
|
||||
|
||||
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
|
||||
|
||||
## Детали
|
||||
|
||||
- FormType: Managed
|
||||
- UsePurposes: PlatformApplication, MobilePlatformApplication
|
||||
- AutoCommandBar с id=-1
|
||||
- Реквизит "Объект" с MainAttribute=true
|
||||
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
|
||||
@@ -1,207 +0,0 @@
|
||||
# epf-add-form v1.0 — Add managed form to 1C processor
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ProcessorName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FormName,
|
||||
|
||||
[string]$Synonym = $FormName,
|
||||
|
||||
[switch]$Main,
|
||||
|
||||
[string]$SrcDir = "src"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Проверки ---
|
||||
|
||||
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
|
||||
if (-not (Test-Path $rootXmlPath)) {
|
||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$processorDir = Join-Path $SrcDir $ProcessorName
|
||||
$formsDir = Join-Path $processorDir "Forms"
|
||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
||||
|
||||
if (Test-Path $formMetaPath) {
|
||||
Write-Error "Форма уже существует: $formMetaPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Создание каталогов ---
|
||||
|
||||
$formDir = Join-Path $formsDir $FormName
|
||||
$formExtDir = Join-Path $formDir "Ext"
|
||||
$formModuleDir = Join-Path $formExtDir "Form"
|
||||
|
||||
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
|
||||
|
||||
# --- Кодировка ---
|
||||
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
$encNoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
|
||||
|
||||
$formUuid = [guid]::NewGuid().ToString()
|
||||
|
||||
$formMetaXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Form uuid="$formUuid">
|
||||
<Properties>
|
||||
<Name>$FormName</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>$Synonym</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<FormType>Managed</FormType>
|
||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ExtendedPresentation/>
|
||||
</Properties>
|
||||
</Form>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
||||
|
||||
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
|
||||
|
||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||
|
||||
$formXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||
<Autofill>true</Autofill>
|
||||
</AutoCommandBar>
|
||||
<ChildItems/>
|
||||
<Attributes>
|
||||
<Attribute name="Объект" id="1">
|
||||
<Type>
|
||||
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
|
||||
</Type>
|
||||
<MainAttribute>true</MainAttribute>
|
||||
</Attribute>
|
||||
</Attributes>
|
||||
</Form>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
||||
|
||||
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
||||
|
||||
$modulePath = Join-Path $formModuleDir "Module.bsl"
|
||||
|
||||
$moduleBsl = @"
|
||||
#Область ОбработчикиСобытийФормы
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ОбработчикиСобытийЭлементовФормы
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ОбработчикиКомандФормы
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область ОбработчикиОповещений
|
||||
|
||||
#КонецОбласти
|
||||
|
||||
#Область СлужебныеПроцедурыИФункции
|
||||
|
||||
#КонецОбласти
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
||||
|
||||
# --- 4. Модификация корневого XML ---
|
||||
|
||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($rootXmlFull.Path)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
||||
if (-not $childObjects) {
|
||||
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Добавить <Form> перед первым <Template>, или в конец
|
||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$formElem.InnerText = $FormName
|
||||
|
||||
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
|
||||
if ($firstTemplate) {
|
||||
# Вставить перед Template, добавив перенос строки + табуляцию
|
||||
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
|
||||
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
|
||||
} else {
|
||||
# Добавить в конец ChildObjects
|
||||
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
|
||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($formElem) | Out-Null
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
} else {
|
||||
$lastChild = $childObjects.LastChild
|
||||
# Вставить перед закрывающим whitespace (если есть), или в конец
|
||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
||||
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($formElem) | Out-Null
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
|
||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
||||
$isFirstForm = ($existingForms.Count -eq 1)
|
||||
|
||||
if ($Main -or $isFirstForm) {
|
||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
||||
if ($defaultForm) {
|
||||
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
|
||||
}
|
||||
}
|
||||
|
||||
# Сохранить с BOM
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = $encBom
|
||||
$settings.Indent = $false # Preserve original whitespace
|
||||
|
||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
||||
$xmlDoc.Save($writer)
|
||||
$writer.Close()
|
||||
$stream.Close()
|
||||
|
||||
Write-Host "[OK] Создана форма: $FormName"
|
||||
Write-Host " Метаданные: $formMetaPath"
|
||||
Write-Host " Описание: $formXmlPath"
|
||||
Write-Host " Модуль: $modulePath"
|
||||
if ($Main -or $isFirstForm) {
|
||||
Write-Host " DefaultForm обновлён"
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# add-form v1.0 — Add managed form to 1C external data processor
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def write_text_with_bom(path, text):
|
||||
"""Write text to file with UTF-8 BOM."""
|
||||
with open(path, "w", encoding="utf-8-sig") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
|
||||
parser.add_argument("-ProcessorName", required=True)
|
||||
parser.add_argument("-FormName", required=True)
|
||||
parser.add_argument("-Synonym", default=None)
|
||||
parser.add_argument("-Main", action="store_true")
|
||||
parser.add_argument("-SrcDir", default="src")
|
||||
args = parser.parse_args()
|
||||
|
||||
processor_name = args.ProcessorName
|
||||
form_name = args.FormName
|
||||
synonym = args.Synonym if args.Synonym is not None else form_name
|
||||
is_main = args.Main
|
||||
src_dir = args.SrcDir
|
||||
|
||||
# --- Checks ---
|
||||
|
||||
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
|
||||
if not os.path.exists(root_xml_path):
|
||||
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
processor_dir = os.path.join(src_dir, processor_name)
|
||||
forms_dir = os.path.join(processor_dir, "Forms")
|
||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
||||
|
||||
if os.path.exists(form_meta_path):
|
||||
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Create directories ---
|
||||
|
||||
form_dir = os.path.join(forms_dir, form_name)
|
||||
form_ext_dir = os.path.join(form_dir, "Ext")
|
||||
form_module_dir = os.path.join(form_ext_dir, "Form")
|
||||
|
||||
os.makedirs(form_module_dir, exist_ok=True)
|
||||
|
||||
# --- 1. Form metadata (Forms/<FormName>.xml) ---
|
||||
|
||||
form_uuid = str(uuid.uuid4())
|
||||
|
||||
form_meta_xml = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<MetaDataObject 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"'
|
||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
' version="2.17">\n'
|
||||
f'\t<Form uuid="{form_uuid}">\n'
|
||||
'\t\t<Properties>\n'
|
||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
||||
'\t\t\t<Synonym>\n'
|
||||
'\t\t\t\t<v8:item>\n'
|
||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
||||
'\t\t\t\t</v8:item>\n'
|
||||
'\t\t\t</Synonym>\n'
|
||||
'\t\t\t<Comment/>\n'
|
||||
'\t\t\t<FormType>Managed</FormType>\n'
|
||||
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
|
||||
'\t\t\t<UsePurposes>\n'
|
||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
||||
'\t\t\t</UsePurposes>\n'
|
||||
'\t\t\t<ExtendedPresentation/>\n'
|
||||
'\t\t</Properties>\n'
|
||||
'\t</Form>\n'
|
||||
'</MetaDataObject>'
|
||||
)
|
||||
|
||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
||||
|
||||
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
|
||||
|
||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
||||
|
||||
form_xml = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
||||
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
' version="2.17">\n'
|
||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||
'\t\t<Autofill>true</Autofill>\n'
|
||||
'\t</AutoCommandBar>\n'
|
||||
'\t<ChildItems/>\n'
|
||||
'\t<Attributes>\n'
|
||||
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
|
||||
'\t\t\t<Type>\n'
|
||||
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
|
||||
'\t\t\t</Type>\n'
|
||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||
'\t\t</Attribute>\n'
|
||||
'\t</Attributes>\n'
|
||||
'</Form>'
|
||||
)
|
||||
|
||||
write_text_with_bom(form_xml_path, form_xml)
|
||||
|
||||
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
||||
|
||||
module_path = os.path.join(form_module_dir, "Module.bsl")
|
||||
|
||||
module_bsl = (
|
||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
||||
'\n'
|
||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||
'\n'
|
||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
||||
'\n'
|
||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||
'\n'
|
||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
|
||||
'\n'
|
||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||
'\n'
|
||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
|
||||
'\n'
|
||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||
'\n'
|
||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
|
||||
'\n'
|
||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
|
||||
)
|
||||
|
||||
write_text_with_bom(module_path, module_bsl)
|
||||
|
||||
# --- 4. Modify root XML ---
|
||||
|
||||
root_xml_full = os.path.abspath(root_xml_path)
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(root_xml_full, parser_xml)
|
||||
root = tree.getroot()
|
||||
|
||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
||||
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
||||
if child_objects is None:
|
||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Add <Form> before first <Template>, or at end
|
||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
||||
form_elem.text = form_name
|
||||
|
||||
first_template = child_objects.find("md:Template", NSMAP)
|
||||
if first_template is not None:
|
||||
# Insert before Template, adding newline + indent
|
||||
idx = list(child_objects).index(first_template)
|
||||
child_objects.insert(idx, form_elem)
|
||||
# Set whitespace: form_elem gets same tail pattern
|
||||
form_elem.tail = "\n\t\t\t"
|
||||
else:
|
||||
# Add to end of ChildObjects
|
||||
children = list(child_objects)
|
||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||
# Empty ChildObjects (self-closing)
|
||||
child_objects.text = "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = "\n\t\t"
|
||||
else:
|
||||
if len(children) > 0:
|
||||
last_child = children[-1]
|
||||
old_tail = last_child.tail
|
||||
last_child.tail = "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||
else:
|
||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||
child_objects.append(form_elem)
|
||||
form_elem.tail = "\n\t\t"
|
||||
|
||||
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
|
||||
existing_forms = child_objects.findall("md:Form", NSMAP)
|
||||
is_first_form = len(existing_forms) == 1
|
||||
|
||||
if is_main or is_first_form:
|
||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
||||
if default_form is not None:
|
||||
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
|
||||
|
||||
# Save with BOM
|
||||
save_xml_with_bom(tree, root_xml_full)
|
||||
|
||||
print(f"[OK] Создана форма: {form_name}")
|
||||
print(f" Метаданные: {form_meta_path}")
|
||||
print(f" Описание: {form_xml_path}")
|
||||
print(f" Модуль: {module_path}")
|
||||
if is_main or is_first_form:
|
||||
print(" DefaultForm обновлён")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,173 +0,0 @@
|
||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||
|
||||
.DESCRIPTION
|
||||
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
|
||||
Общий скрипт для epf-build и erf-build.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER SourceFile
|
||||
Путь к корневому XML-файлу исходников
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному EPF/ERF-файлу
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$SourceFile,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
|
||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
|
||||
if ($stubProc.ExitCode -ne 0) {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$InfoBasePath = $autoBasePath
|
||||
$autoCreatedBase = $autoBasePath
|
||||
}
|
||||
|
||||
# --- Validate source file ---
|
||||
if (-not (Test-Path $SourceFile)) {
|
||||
Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
$outDir = Split-Path $OutputFile -Parent
|
||||
if ($outDir -and -not (Test-Path $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "build_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
|
||||
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build external data processor or report (EPF/ERF) from XML sources",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
||||
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
auto_created_base = None
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||
print("No database specified. Creating temporary stub database...")
|
||||
result = subprocess.run(
|
||||
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path],
|
||||
capture_output=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print("Error: failed to create stub database", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
args.InfoBasePath = auto_base_path
|
||||
auto_created_base = auto_base_path
|
||||
|
||||
# --- Validate source file ---
|
||||
if not os.path.isfile(args.SourceFile):
|
||||
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
out_dir = os.path.dirname(args.OutputFile)
|
||||
if out_dir and not os.path.exists(out_dir):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_build_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
||||
else:
|
||||
arguments += ["/F", args.InfoBasePath]
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile]
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "build_log.txt")
|
||||
arguments += ["/Out", out_file]
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print(f"Build completed successfully: {args.OutputFile}")
|
||||
else:
|
||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
if auto_created_base and os.path.exists(auto_created_base):
|
||||
shutil.rmtree(auto_created_base, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,167 +0,0 @@
|
||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||
|
||||
.DESCRIPTION
|
||||
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
|
||||
Общий скрипт для epf-dump и erf-dump.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к EPF/ERF-файлу
|
||||
|
||||
.PARAMETER OutputDir
|
||||
Каталог для выгрузки исходников
|
||||
|
||||
.PARAMETER Format
|
||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
||||
|
||||
.EXAMPLE
|
||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$InputFile,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Hierarchical", "Plain")]
|
||||
[string]$Format = "Hierarchical"
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate database connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
|
||||
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate input file ---
|
||||
if (-not (Test-Path $InputFile)) {
|
||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
|
||||
$arguments += "-Format", $Format
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Dump external data processor or report (EPF/ERF) to XML sources",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
|
||||
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
|
||||
parser.add_argument(
|
||||
"-Format",
|
||||
default="Hierarchical",
|
||||
choices=["Hierarchical", "Plain"],
|
||||
help="Dump format (default: Hierarchical)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate database connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate input file ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
if not os.path.exists(args.OutputDir):
|
||||
os.makedirs(args.OutputDir, exist_ok=True)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
||||
else:
|
||||
arguments += ["/F", args.InfoBasePath]
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
|
||||
arguments += ["-Format", args.Format]
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||
arguments += ["/Out", out_file]
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
||||
else:
|
||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: epf-validate
|
||||
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /epf-validate — валидация внешней обработки (EPF)
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|----|-------------------------------------------------------|--------------|
|
||||
| 1 | Root structure: MetaDataObject/ExternalDataProcessor | ERROR |
|
||||
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
|
||||
| 3 | Properties: Name (identifier), Synonym | ERROR / WARN |
|
||||
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
|
||||
| 5 | Cross-references: DefaultForm → Form, AuxiliaryForm | ERROR / WARN |
|
||||
| 6 | Attributes: UUID, Name, Type | ERROR |
|
||||
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
|
||||
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
|
||||
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
|
||||
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: erf-validate
|
||||
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /erf-validate — валидация внешнего отчёта (ERF)
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
|
||||
|
||||
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
|
||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|----|-------------------------------------------------------|--------------|
|
||||
| 1 | Root structure: MetaDataObject/ExternalReport | ERROR |
|
||||
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
|
||||
| 3 | Properties: Name, Synonym, MainDataCompositionSchema | ERROR / WARN |
|
||||
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
|
||||
| 5 | Cross-references: DefaultForm, MainDCS → Template | ERROR / WARN |
|
||||
| 6 | Attributes: UUID, Name, Type | ERROR |
|
||||
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
|
||||
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
|
||||
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
|
||||
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: form-validate
|
||||
description: Валидация управляемой формы 1С. Используй после создания или модификации формы для проверки корректности. При наличии BaseForm автоматически проверяет callType и ID расширений
|
||||
argument-hint: <FormPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /form-validate — валидация управляемой формы 1С
|
||||
|
||||
Проверяет Form.xml на структурные ошибки: уникальность ID, наличие companion-элементов, корректность ссылок DataPath и команд.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|-----------|:-----:|---------|-----------------------------------------|
|
||||
| FormPath | да | — | Путь к файлу Form.xml |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
|
||||
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|---|---|---|
|
||||
| 1 | Корневой элемент `<Form>`, version="2.17" | ERROR / WARN |
|
||||
| 2 | `<AutoCommandBar>` присутствует, id="-1" | ERROR |
|
||||
| 3 | Уникальность ID элементов (отдельный пул) | ERROR |
|
||||
| 4 | Уникальность ID реквизитов (отдельный пул) | ERROR |
|
||||
| 5 | Уникальность ID команд (отдельный пул) | ERROR |
|
||||
| 6 | Companion-элементы (ContextMenu, ExtendedTooltip, и др.) | ERROR |
|
||||
| 7 | DataPath → ссылается на существующий реквизит | ERROR |
|
||||
| 8 | CommandName кнопок → ссылается на существующую команду | ERROR |
|
||||
| 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 |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,117 +0,0 @@
|
||||
# help-add v1.2 — Add built-in help to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ObjectName,
|
||||
|
||||
[string]$Lang = "ru",
|
||||
|
||||
[string]$SrcDir = "src"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Проверки ---
|
||||
|
||||
$objectDir = Join-Path $SrcDir $ObjectName
|
||||
$extDir = Join-Path $objectDir "Ext"
|
||||
|
||||
if (-not (Test-Path $extDir)) {
|
||||
Write-Error "Каталог объекта не найден: $extDir. Проверьте путь ObjectName (например Catalogs/МойСправочник)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$helpXmlPath = Join-Path $extDir "Help.xml"
|
||||
if (Test-Path $helpXmlPath) {
|
||||
Write-Error "Справка уже существует: $helpXmlPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Кодировка ---
|
||||
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
# --- 1. Help.xml ---
|
||||
|
||||
$helpXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Page>$Lang</Page>
|
||||
</Help>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
|
||||
|
||||
# --- 2. Help/<lang>.html ---
|
||||
|
||||
$helpDir = Join-Path $extDir "Help"
|
||||
New-Item -ItemType Directory -Path $helpDir -Force | Out-Null
|
||||
|
||||
$helpHtmlPath = Join-Path $helpDir "$Lang.html"
|
||||
|
||||
$helpHtml = @"
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>$ObjectName</h1>
|
||||
<p>Описание.</p>
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($helpHtmlPath, $helpHtml, $encBom)
|
||||
|
||||
# --- 3. Проверка IncludeHelpInContents в метаданных форм ---
|
||||
|
||||
$formsDir = Join-Path $objectDir "Forms"
|
||||
if (Test-Path $formsDir) {
|
||||
$formMetaFiles = Get-ChildItem -Path $formsDir -Filter "*.xml" -File
|
||||
foreach ($formMeta in $formMetaFiles) {
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($formMeta.FullName)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$includeHelp = $xmlDoc.SelectSingleNode("//md:IncludeHelpInContents", $nsMgr)
|
||||
if (-not $includeHelp) {
|
||||
# Добавить после <FormType>
|
||||
$formType = $xmlDoc.SelectSingleNode("//md:FormType", $nsMgr)
|
||||
if ($formType) {
|
||||
$newElem = $xmlDoc.CreateElement("IncludeHelpInContents", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$newElem.InnerText = "false"
|
||||
$parent = $formType.ParentNode
|
||||
$nextSibling = $formType.NextSibling
|
||||
# Вставить перенос + табуляцию + элемент
|
||||
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
||||
if ($nextSibling) {
|
||||
$parent.InsertBefore($ws, $nextSibling) | Out-Null
|
||||
$parent.InsertBefore($newElem, $ws) | Out-Null
|
||||
} else {
|
||||
$parent.AppendChild($ws) | Out-Null
|
||||
$parent.AppendChild($newElem) | Out-Null
|
||||
}
|
||||
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = $encBom
|
||||
$settings.Indent = $false
|
||||
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
|
||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
||||
$xmlDoc.Save($writer)
|
||||
$writer.Close()
|
||||
$stream.Close()
|
||||
|
||||
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[OK] Создана справка: $ObjectName"
|
||||
Write-Host " Метаданные: $helpXmlPath"
|
||||
Write-Host " Страница: $helpHtmlPath"
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# add-help v1.2 — Add built-in help to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
|
||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def write_text_with_bom(path, text):
|
||||
"""Write text to file with UTF-8 BOM."""
|
||||
with open(path, "w", encoding="utf-8-sig") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False)
|
||||
parser.add_argument("-ObjectName", required=True)
|
||||
parser.add_argument("-Lang", default="ru")
|
||||
parser.add_argument("-SrcDir", default="src")
|
||||
args = parser.parse_args()
|
||||
|
||||
object_name = args.ObjectName
|
||||
lang = args.Lang
|
||||
src_dir = args.SrcDir
|
||||
|
||||
# --- Checks ---
|
||||
|
||||
object_dir = os.path.join(src_dir, object_name)
|
||||
ext_dir = os.path.join(object_dir, "Ext")
|
||||
|
||||
if not os.path.isdir(ext_dir):
|
||||
print(f"Каталог объекта не найден: {ext_dir}. Проверьте путь ObjectName (например Catalogs/МойСправочник).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
help_xml_path = os.path.join(ext_dir, "Help.xml")
|
||||
if os.path.exists(help_xml_path):
|
||||
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- 1. Help.xml ---
|
||||
|
||||
help_xml = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
|
||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
' version="2.17">\n'
|
||||
f'\t<Page>{lang}</Page>\n'
|
||||
'</Help>'
|
||||
)
|
||||
|
||||
write_text_with_bom(help_xml_path, help_xml)
|
||||
|
||||
# --- 2. Help/<lang>.html ---
|
||||
|
||||
help_dir = os.path.join(ext_dir, "Help")
|
||||
os.makedirs(help_dir, exist_ok=True)
|
||||
|
||||
help_html_path = os.path.join(help_dir, f"{lang}.html")
|
||||
|
||||
help_html = (
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
|
||||
'<html>\n'
|
||||
'<head>\n'
|
||||
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
|
||||
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
|
||||
'</head>\n'
|
||||
'<body>\n'
|
||||
f' <h1>{object_name}</h1>\n'
|
||||
' <p>Описание.</p>\n'
|
||||
'</body>\n'
|
||||
'</html>'
|
||||
)
|
||||
|
||||
write_text_with_bom(help_html_path, help_html)
|
||||
|
||||
# --- 3. Check IncludeHelpInContents in form metadata ---
|
||||
|
||||
forms_dir = os.path.join(object_dir, "Forms")
|
||||
if os.path.isdir(forms_dir):
|
||||
for entry in os.listdir(forms_dir):
|
||||
if not entry.endswith(".xml"):
|
||||
continue
|
||||
form_meta_full = os.path.join(forms_dir, entry)
|
||||
if not os.path.isfile(form_meta_full):
|
||||
continue
|
||||
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
form_tree = etree.parse(form_meta_full, parser_xml)
|
||||
form_root = form_tree.getroot()
|
||||
|
||||
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
|
||||
if include_help is not None:
|
||||
continue
|
||||
|
||||
# Add after <FormType>
|
||||
form_type = form_root.find(".//md:FormType", NSMAP)
|
||||
if form_type is None:
|
||||
continue
|
||||
|
||||
parent = form_type.getparent()
|
||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
||||
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
|
||||
new_elem.text = "false"
|
||||
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
|
||||
parent.remove(new_elem)
|
||||
|
||||
# Find index of FormType in parent
|
||||
form_type_idx = list(parent).index(form_type)
|
||||
|
||||
# Insert after FormType
|
||||
parent.insert(form_type_idx + 1, new_elem)
|
||||
|
||||
# Whitespace handling: copy FormType's tail as new_elem's tail,
|
||||
# and set FormType's tail to include newline + indent
|
||||
new_elem.tail = form_type.tail
|
||||
form_type.tail = "\n\t\t\t"
|
||||
|
||||
save_xml_with_bom(form_tree, form_meta_full)
|
||||
|
||||
print(f" IncludeHelpInContents добавлен: {entry}")
|
||||
|
||||
print(f"[OK] Создана справка: {object_name}")
|
||||
print(f" Метаданные: {help_xml_path}")
|
||||
print(f" Страница: {help_html_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: interface-edit
|
||||
description: Настройка командного интерфейса подсистемы 1С. Используй когда нужно скрыть или показать команды, разместить в группах, настроить порядок
|
||||
argument-hint: <CIPath> <Operation> <Value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /interface-edit — редактирование CommandInterface.xml
|
||||
|
||||
Операции: hide, show, place, order, subsystem-order, group-order. Подробнее: `.claude/skills/interface-edit/reference.md`
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude/skills/interface-edit/scripts/interface-edit.ps1' -CIPath '<path>' -Operation hide -Value '<cmd>'
|
||||
```
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
name: interface-validate
|
||||
description: Валидация командного интерфейса 1С. Используй после настройки командного интерфейса подсистемы для проверки корректности
|
||||
argument-hint: <CIPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /interface-validate — валидация CommandInterface.xml
|
||||
|
||||
Проверяет XML командного интерфейса на структурные ошибки: корневой элемент, допустимые секции, порядок, формат ссылок на команды, дубликаты.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|-----------|:-----:|---------|-----------------------------------------|
|
||||
| CIPath | да | — | Путь к CommandInterface.xml |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи"
|
||||
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
|
||||
```
|
||||
|
||||
## Проверки (13)
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|----|--------------------------------------------------------------|-------------|
|
||||
| 1 | XML well-formedness + root element (CommandInterface, version, namespace) | ERROR |
|
||||
| 2 | Допустимые дочерние элементы (только 5 секций) | ERROR |
|
||||
| 3 | Порядок секций корректен | ERROR |
|
||||
| 4 | Нет дублирующихся секций | ERROR |
|
||||
| 5 | CommandsVisibility — Command.name + Visibility/xr:Common | ERROR |
|
||||
| 6 | CommandsVisibility — нет дубликатов по name | WARN |
|
||||
| 7 | CommandsPlacement — Command.name + CommandGroup + Placement | ERROR |
|
||||
| 8 | CommandsOrder — Command.name + CommandGroup | ERROR |
|
||||
| 9 | SubsystemsOrder — Subsystem непустой, формат Subsystem.X | ERROR |
|
||||
| 10 | SubsystemsOrder — нет дубликатов | WARN |
|
||||
| 11 | GroupsOrder — Group непустой | ERROR |
|
||||
| 12 | GroupsOrder — нет дубликатов | WARN |
|
||||
| 13 | Формат ссылок на команды | WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
name: meta-compile
|
||||
description: Создать объект метаданных 1С. Используй когда пользователь просит создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
|
||||
argument-hint: <JsonPath> <OutputDir>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /meta-compile — генерация объектов метаданных из JSON DSL
|
||||
|
||||
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
|
||||
|
||||
## Порядок работы
|
||||
|
||||
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
|
||||
2. Запусти скрипт meta-compile
|
||||
3. Если нужно изменить созданный объект — `/meta-edit`
|
||||
4. Если нужно проверить — `/meta-validate`
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/meta-compile/scripts/meta-compile.ps1 -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
|
||||
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
|
||||
|
||||
## JSON DSL
|
||||
|
||||
### Общая структура
|
||||
|
||||
```json
|
||||
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
|
||||
```
|
||||
|
||||
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
|
||||
|
||||
### Shorthand реквизитов
|
||||
|
||||
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
|
||||
|
||||
```
|
||||
"ИмяРеквизита" → String(10) по умолчанию
|
||||
"ИмяРеквизита: Тип" → с типом
|
||||
"ИмяРеквизита: Тип | req, index" → с флагами
|
||||
```
|
||||
|
||||
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
|
||||
|
||||
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
|
||||
|
||||
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
|
||||
|
||||
### Свойства по типам
|
||||
|
||||
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл:
|
||||
|
||||
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
||||
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
|
||||
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
|
||||
- `reference/types-web.md` — HTTPService, WebService
|
||||
|
||||
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
|
||||
|
||||
## Примеры паттернов DSL
|
||||
|
||||
### Минимальный объект
|
||||
|
||||
```json
|
||||
{ "type": "Catalog", "name": "Валюты" }
|
||||
```
|
||||
|
||||
### С реквизитами
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Catalog", "name": "Организации",
|
||||
"descriptionLength": 100,
|
||||
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
|
||||
}
|
||||
```
|
||||
|
||||
### С табличной частью
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Document", "name": "ПриходнаяНакладная",
|
||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
|
||||
}
|
||||
```
|
||||
|
||||
### Регистровый паттерн (измерения + ресурсы)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
||||
}
|
||||
```
|
||||
|
||||
### Batch — несколько объектов в одном файле
|
||||
|
||||
```json
|
||||
[
|
||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
||||
{ "type": "Catalog", "name": "Валюты" },
|
||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
||||
]
|
||||
```
|
||||
|
||||
## Что генерируется
|
||||
|
||||
- `{TypePlural}/{Name}.xml` — метаданные объекта
|
||||
- `{TypePlural}/{Name}/Ext/*.bsl` — модули (ObjectModule, RecordSetModule, Module — зависит от типа)
|
||||
- `Configuration.xml` — автоматическая регистрация в `<ChildObjects>`
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
||||
|
||||
## Catalog
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `hierarchical` | `false` | Hierarchical |
|
||||
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
|
||||
| `codeLength` | `9` | CodeLength |
|
||||
| `codeType` | `String` | CodeType |
|
||||
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
|
||||
| `descriptionLength` | `25` | DescriptionLength |
|
||||
| `autonumbering` | `true` | Autonumbering |
|
||||
| `checkUnique` | `false` | CheckUnique |
|
||||
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
|
||||
| `owners` | `[]` | Owners |
|
||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
||||
|
||||
```json
|
||||
{ "type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)"] }
|
||||
```
|
||||
|
||||
## Document
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `numberType` | `String` | NumberType |
|
||||
| `numberLength` | `11` | NumberLength |
|
||||
| `numberAllowedLength` | `Variable` | NumberAllowedLength |
|
||||
| `numberPeriodicity` | `Year` | NumberPeriodicity |
|
||||
| `checkUnique` | `true` | CheckUnique |
|
||||
| `autonumbering` | `true` | Autonumbering |
|
||||
| `posting` | `Allow` | Posting |
|
||||
| `realTimePosting` | `Deny` | RealTimePosting |
|
||||
| `registerRecordsDeletion` | `AutoDelete` | RegisterRecordsDeletion |
|
||||
| `registerRecordsWritingOnPost` | `WriteModified` | RegisterRecordsWritingOnPost |
|
||||
| `postInPrivilegedMode` | `true` | PostInPrivilegedMode |
|
||||
| `unpostInPrivilegedMode` | `true` | UnpostInPrivilegedMode |
|
||||
| `registerRecords` | `[]` | RegisterRecords |
|
||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
||||
|
||||
RegisterRecords — массив строк: `"AccumulationRegister.Продажи"`, `"InformationRegister.Цены"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Document", "name": "ПриходнаяНакладная",
|
||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
|
||||
}
|
||||
```
|
||||
|
||||
## Enum
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `values` | `[]` | → EnumValue в ChildObjects |
|
||||
|
||||
```json
|
||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
|
||||
```
|
||||
|
||||
## Constant
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `valueType` | `String` | Type |
|
||||
|
||||
`valueType` принимает shorthand типа: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`.
|
||||
|
||||
```json
|
||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
||||
```
|
||||
|
||||
## DefinedType
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `valueTypes` | `[]` | Type (составной тип) |
|
||||
| `valueType` | — | Алиас для `valueTypes` (строка или массив) |
|
||||
|
||||
```json
|
||||
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
|
||||
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
|
||||
```
|
||||
|
||||
## Report
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
||||
|
||||
```json
|
||||
{ "type": "Report", "name": "ОстаткиТоваров" }
|
||||
```
|
||||
|
||||
## DataProcessor
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
||||
|
||||
```json
|
||||
{ "type": "DataProcessor", "name": "ЗагрузкаДанных", "attributes": ["ПутьКФайлу: String(500)"] }
|
||||
```
|
||||
@@ -1,136 +0,0 @@
|
||||
# Процессы и сервисные: BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
|
||||
|
||||
## BusinessProcess
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `task` | `""` | Task (ссылка `Task.XXX`) |
|
||||
| `numberType` | `String` | NumberType |
|
||||
| `numberLength` | `11` | NumberLength |
|
||||
| `checkUnique` | `true` | CheckUnique |
|
||||
| `autonumbering` | `true` | Autonumbering |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
| `tabularSections` | `{}` | → TabularSection |
|
||||
|
||||
Модули: `Ext/ObjectModule.bsl`, `Ext/Flowchart.xml`.
|
||||
|
||||
```json
|
||||
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
|
||||
```
|
||||
|
||||
## Task
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `numberType` | `String` | NumberType |
|
||||
| `numberLength` | `14` | NumberLength |
|
||||
| `checkUnique` | `true` | CheckUnique |
|
||||
| `autonumbering` | `true` | Autonumbering |
|
||||
| `descriptionLength` | `150` | DescriptionLength |
|
||||
| `addressing` | `""` | Addressing (ссылка на РС адресации) |
|
||||
| `mainAddressingAttribute` | `""` | MainAddressingAttribute |
|
||||
| `currentPerformer` | `""` | CurrentPerformer |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
| `tabularSections` | `{}` | → TabularSection |
|
||||
| `addressingAttributes` | `[]` | → AddressingAttribute (shorthand или объект) |
|
||||
|
||||
AddressingAttribute — shorthand `"Имя: Тип"` или объект `{ "name", "type", "addressingDimension" }`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Task", "name": "ЗадачаИсполнителя",
|
||||
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи"]
|
||||
}
|
||||
```
|
||||
|
||||
## ExchangePlan
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `codeLength` | `9` | CodeLength |
|
||||
| `descriptionLength` | `100` | DescriptionLength |
|
||||
| `distributedInfoBase` | `false` | DistributedInfoBase |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
| `tabularSections` | `{}` | → TabularSection |
|
||||
|
||||
Модули: `Ext/ObjectModule.bsl`, `Ext/Content.xml`.
|
||||
|
||||
```json
|
||||
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
|
||||
```
|
||||
|
||||
## CommonModule
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `context` | — | Шорткат (см. ниже) |
|
||||
| `global` | `false` | Global |
|
||||
| `server` | `false` | Server |
|
||||
| `serverCall` | `false` | ServerCall |
|
||||
| `clientManagedApplication` | `false` | ClientManagedApplication |
|
||||
| `externalConnection` | `false` | ExternalConnection |
|
||||
| `privileged` | `false` | Privileged |
|
||||
| `returnValuesReuse` | `DontUse` | ReturnValuesReuse |
|
||||
|
||||
Шорткаты `context`: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
|
||||
|
||||
```json
|
||||
{ "type": "CommonModule", "name": "ОбщиеФункции", "context": "serverClient" }
|
||||
```
|
||||
|
||||
## ScheduledJob
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `methodName` | `""` | MethodName |
|
||||
| `description` | = synonym | Description |
|
||||
| `use` | `false` | Use |
|
||||
| `predefined` | `false` | Predefined |
|
||||
| `restartCountOnFailure` | `3` | RestartCountOnFailure |
|
||||
| `restartIntervalOnFailure` | `10` | RestartIntervalOnFailure |
|
||||
|
||||
Формат `methodName`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
|
||||
|
||||
```json
|
||||
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить" }
|
||||
```
|
||||
|
||||
## EventSubscription
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `source` | `[]` | Source (массив, формат `XxxObject.Name`) |
|
||||
| `event` | `BeforeWrite` | Event |
|
||||
| `handler` | `""` | Handler |
|
||||
|
||||
Формат `handler`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
|
||||
|
||||
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing`.
|
||||
|
||||
Формат `source`: `"CatalogObject.Xxx"`, `"DocumentObject.Xxx"`.
|
||||
|
||||
```json
|
||||
{ "type": "EventSubscription", "name": "ПередЗаписью", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщиеФункции.ПередЗаписью" }
|
||||
```
|
||||
|
||||
## DocumentJournal
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `registeredDocuments` | `[]` | RegisteredDocuments (массив `"Document.Xxx"`) |
|
||||
| `columns` | `[]` | → Column |
|
||||
|
||||
Колонки — строка `"Имя"` или объект `{ "name", "synonym", "indexing": "Index"/"DontIndex", "references": ["Document.Xxx.Attribute.Yyy"] }`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "DocumentJournal", "name": "Взаимодействия",
|
||||
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
|
||||
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
|
||||
}
|
||||
```
|
||||
|
||||
## Зависимости
|
||||
|
||||
- **ScheduledJob/EventSubscription** — процедура-обработчик должна существовать в модуле (экспортная)
|
||||
- **BusinessProcess** → `Task` (задача должна существовать)
|
||||
@@ -1,174 +0,0 @@
|
||||
# Регистры и планы: InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
|
||||
|
||||
## Измерения и ресурсы (общее)
|
||||
|
||||
Синтаксис аналогичен реквизитам (shorthand `"Имя: Тип | флаги"`).
|
||||
|
||||
Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (AccumulationRegister only, default `true`).
|
||||
|
||||
```json
|
||||
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter, denyIncomplete"],
|
||||
"resources": ["Сумма: Number(15,2)"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## InformationRegister
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `writeMode` | `Independent` | WriteMode |
|
||||
| `periodicity` | `Nonperiodical` | InformationRegisterPeriodicity |
|
||||
| `mainFilterOnPeriod` | авто* | MainFilterOnPeriod |
|
||||
| `dimensions` | `[]` | → Dimension |
|
||||
| `resources` | `[]` | → Resource |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
|
||||
\* `mainFilterOnPeriod` = `true` если `periodicity` != `Nonperiodical`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
||||
}
|
||||
```
|
||||
|
||||
## AccumulationRegister
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `registerType` | `Balance` | RegisterType (`Balance` / `Turnovers`) |
|
||||
| `enableTotalsSplitting` | `true` | EnableTotalsSplitting |
|
||||
| `dimensions` | `[]` | → Dimension |
|
||||
| `resources` | `[]` | → Resource |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
|
||||
"dimensions": ["Номенклатура: CatalogRef.Номенклатура"],
|
||||
"resources": ["Количество: Number(15,3)"]
|
||||
}
|
||||
```
|
||||
|
||||
## AccountingRegister
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `chartOfAccounts` | `""` | ChartOfAccounts (**обязательная** ссылка на план счетов) |
|
||||
| `correspondence` | `false` | Correspondence |
|
||||
| `periodAdjustmentLength` | `0` | PeriodAdjustmentLength |
|
||||
| `dimensions` | `[]` | → Dimension |
|
||||
| `resources` | `[]` | → Resource |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "AccountingRegister", "name": "Хозрасчетный",
|
||||
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
|
||||
"dimensions": ["Организация: CatalogRef.Организации"],
|
||||
"resources": ["Сумма: Number(15,2)"]
|
||||
}
|
||||
```
|
||||
|
||||
## CalculationRegister
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `chartOfCalculationTypes` | `""` | ChartOfCalculationTypes (**обязательная** ссылка на ПВР) |
|
||||
| `periodicity` | `Month` | Periodicity |
|
||||
| `actionPeriod` | `false` | ActionPeriod |
|
||||
| `basePeriod` | `false` | BasePeriod |
|
||||
| `schedule` | `""` | Schedule (ссылка на РС графиков) |
|
||||
| `dimensions` | `[]` | → Dimension |
|
||||
| `resources` | `[]` | → Resource |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "CalculationRegister", "name": "Начисления",
|
||||
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления",
|
||||
"periodicity": "Month",
|
||||
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"],
|
||||
"resources": ["Сумма: Number(15,2)"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ChartOfCharacteristicTypes
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `codeLength` | `9` | CodeLength |
|
||||
| `descriptionLength` | `25` | DescriptionLength |
|
||||
| `autonumbering` | `true` | Autonumbering |
|
||||
| `checkUnique` | `false` | CheckUnique |
|
||||
| `characteristicExtValues` | `""` | CharacteristicExtValues |
|
||||
| `valueTypes` | авто* | Type (составной тип значений характеристик) |
|
||||
| `hierarchical` | `false` | Hierarchical |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
| `tabularSections` | `{}` | → TabularSection |
|
||||
|
||||
\* По умолчанию: Boolean, String(100), Number(15,2), DateTime.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ChartOfCharacteristicTypes", "name": "ВидыСубконто",
|
||||
"valueTypes": ["CatalogRef.Номенклатура", "CatalogRef.Контрагенты", "Boolean", "String", "Number(15,2)"]
|
||||
}
|
||||
```
|
||||
|
||||
## ChartOfAccounts
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `extDimensionTypes` | `""` | ExtDimensionTypes (ссылка на ПВХ) |
|
||||
| `maxExtDimensionCount` | `3` | MaxExtDimensionCount |
|
||||
| `codeMask` | `""` | CodeMask |
|
||||
| `codeLength` | `8` | CodeLength |
|
||||
| `descriptionLength` | `120` | DescriptionLength |
|
||||
| `codeSeries` | `WholeChartOfAccounts` | CodeSeries |
|
||||
| `autoOrderByCode` | `true` | AutoOrderByCode |
|
||||
| `orderLength` | `5` | OrderLength |
|
||||
| `hierarchical` | `false` | Hierarchical |
|
||||
| `accountingFlags` | `[]` | → AccountingFlag (Boolean-тип, массив имён) |
|
||||
| `extDimensionAccountingFlags` | `[]` | → ExtDimensionAccountingFlag (Boolean-тип, массив имён) |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
| `tabularSections` | `{}` | → TabularSection |
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ChartOfAccounts", "name": "Хозрасчетный",
|
||||
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
|
||||
"codeLength": 8, "codeMask": "@@@.@@.@",
|
||||
"accountingFlags": ["Валютный", "Количественный"],
|
||||
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
|
||||
}
|
||||
```
|
||||
|
||||
## ChartOfCalculationTypes
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `codeLength` | `9` | CodeLength |
|
||||
| `descriptionLength` | `25` | DescriptionLength |
|
||||
| `autonumbering` | `true` | Autonumbering |
|
||||
| `checkUnique` | `false` | CheckUnique |
|
||||
| `dependenceOnCalculationTypes` | `NotUsed` | DependenceOnCalculationTypes |
|
||||
| `actionPeriodUse` | `false` | ActionPeriodUse |
|
||||
| `attributes` | `[]` | → Attribute |
|
||||
| `tabularSections` | `{}` | → TabularSection |
|
||||
|
||||
`dependenceOnCalculationTypes`: `NotUsed`, `ExclusionAndDependence`, `ExclusionOnly`.
|
||||
|
||||
```json
|
||||
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "ExclusionAndDependence" }
|
||||
```
|
||||
|
||||
## Зависимости
|
||||
|
||||
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
|
||||
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
|
||||
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
|
||||
@@ -1,103 +0,0 @@
|
||||
# Веб-сервисы: HTTPService, WebService
|
||||
|
||||
## HTTPService
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `rootURL` | `= name.toLower()` | RootURL |
|
||||
| `reuseSessions` | `DontUse` | ReuseSessions |
|
||||
| `sessionMaxAge` | `20` | SessionMaxAge |
|
||||
| `urlTemplates` | `{}` | → URLTemplate |
|
||||
|
||||
Модули: `Ext/Module.bsl`.
|
||||
|
||||
### urlTemplates — вложенная структура
|
||||
|
||||
`urlTemplates` — объект `{ "TemplateName": templateDef, ... }`.
|
||||
|
||||
Каждый `templateDef`:
|
||||
- Строка — URL-шаблон: `"/v1/users"` (без методов)
|
||||
- Объект:
|
||||
|
||||
| Поле | Умолчание | Описание |
|
||||
|------|----------|----------|
|
||||
| `template` | `"/templatename"` | URL-путь (с параметрами `{id}`) |
|
||||
| `methods` | `{}` | Методы: `{ "MethodName": "HTTPMethod" }` |
|
||||
|
||||
Допустимые HTTPMethod: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
||||
|
||||
Обработчик метода генерируется автоматически: `{TemplateName}{MethodName}` — должен быть реализован в `Ext/Module.bsl`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "HTTPService", "name": "API", "rootURL": "api",
|
||||
"urlTemplates": {
|
||||
"Users": {
|
||||
"template": "/v1/users/{id}",
|
||||
"methods": { "Get": "GET", "Create": "POST", "Update": "PUT", "Delete": "DELETE" }
|
||||
},
|
||||
"Health": "/health"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebService
|
||||
|
||||
| Поле JSON | Умолчание | XML элемент |
|
||||
|-----------|----------|-------------|
|
||||
| `namespace` | `""` | Namespace (URI пространства имён WSDL) |
|
||||
| `xdtoPackages` | `""` | XDTOPackages |
|
||||
| `reuseSessions` | `DontUse` | ReuseSessions |
|
||||
| `sessionMaxAge` | `20` | SessionMaxAge |
|
||||
| `operations` | `{}` | → Operation |
|
||||
|
||||
Модули: `Ext/Module.bsl`.
|
||||
|
||||
### operations — вложенная структура
|
||||
|
||||
`operations` — объект `{ "OperationName": operationDef, ... }`.
|
||||
|
||||
Каждый `operationDef`:
|
||||
- Строка — тип возврата: `"xs:boolean"` (параметров нет, обработчик = имя операции)
|
||||
- Объект:
|
||||
|
||||
| Поле | Умолчание | Описание |
|
||||
|------|----------|----------|
|
||||
| `returnType` | `xs:string` | XDTO-тип возврата |
|
||||
| `nillable` | `false` | Может ли вернуть null |
|
||||
| `transactioned` | `false` | Выполнять в транзакции |
|
||||
| `handler` | `= operationName` | Имя процедуры в модуле |
|
||||
| `parameters` | `{}` | Параметры операции |
|
||||
|
||||
### parameters — параметры операции
|
||||
|
||||
`parameters` — объект `{ "ParamName": paramDef, ... }`.
|
||||
|
||||
Каждый `paramDef`:
|
||||
- Строка — XDTO-тип: `"xs:string"` (direction = In, nillable = true)
|
||||
- Объект:
|
||||
|
||||
| Поле | Умолчание | Описание |
|
||||
|------|----------|----------|
|
||||
| `type` | `xs:string` | XDTO-тип параметра |
|
||||
| `nillable` | `true` | Может ли быть null |
|
||||
| `direction` | `In` | Направление: `In`, `Out`, `InOut` |
|
||||
|
||||
Стандартные XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "WebService", "name": "DataExchange",
|
||||
"namespace": "http://www.1c.ru/DataExchange",
|
||||
"operations": {
|
||||
"TestConnection": {
|
||||
"returnType": "xs:boolean",
|
||||
"handler": "ПроверкаПодключения",
|
||||
"parameters": {
|
||||
"ErrorMessage": { "type": "xs:string", "direction": "Out" }
|
||||
}
|
||||
},
|
||||
"GetVersion": "xs:string"
|
||||
}
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
||||
# Свойства объекта и complex properties
|
||||
|
||||
Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString).
|
||||
|
||||
## modify-property
|
||||
|
||||
Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`):
|
||||
```powershell
|
||||
-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150"
|
||||
-Operation modify-property -Value "Hierarchical=true"
|
||||
```
|
||||
|
||||
## Complex properties
|
||||
|
||||
Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
|
||||
|
||||
| Свойство | Объекты | Inline-значение |
|
||||
|----------|---------|-----------------|
|
||||
| Owners | Catalog, ChartOfCharacteristicTypes | `Catalog.XXX` |
|
||||
| RegisterRecords | Document | `AccumulationRegister.XXX` |
|
||||
| BasedOn | Document, Catalog, BP, Task | `Document.XXX` |
|
||||
| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` |
|
||||
|
||||
### add-owner / add-registerRecord / add-basedOn
|
||||
|
||||
Полное имя метаданных `MetaType.Name`:
|
||||
```powershell
|
||||
-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
||||
-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров"
|
||||
-Operation add-basedOn -Value "Document.ЗаказКлиента"
|
||||
```
|
||||
|
||||
### add-inputByString
|
||||
|
||||
Пути полей (префикс `MetaType.Name.` добавляется автоматически):
|
||||
```powershell
|
||||
-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
||||
```
|
||||
|
||||
### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString
|
||||
|
||||
```powershell
|
||||
-Operation remove-owner -Value "Catalog.Контрагенты"
|
||||
-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code"
|
||||
```
|
||||
|
||||
### set-owners / set-registerRecords / set-basedOn / set-inputByString
|
||||
|
||||
Заменяют **весь список** (в отличие от add/remove):
|
||||
```powershell
|
||||
-Operation set-owners -Value "Catalog.Организации ;; Catalog.Контрагенты"
|
||||
-Operation set-registerRecords -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров"
|
||||
-Operation set-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
||||
```
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
name: meta-remove
|
||||
description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
|
||||
argument-hint: <ConfigDir> -Object <Type.Name>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /meta-remove — удаление объекта метаданных
|
||||
|
||||
Безопасно удаляет объект из XML-выгрузки конфигурации. Перед удалением проверяет ссылки на объект в реквизитах, коде и других метаданных. Если ссылки найдены — удаление блокируется.
|
||||
|
||||
## Использование
|
||||
|
||||
```
|
||||
/meta-remove <ConfigDir> -Object <Type.Name>
|
||||
```
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|------------|:------------:|-------------------------------------------------|
|
||||
| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) |
|
||||
| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. |
|
||||
| DryRun | нет | Только показать что будет удалено, без изменений |
|
||||
| KeepFiles | нет | Не удалять файлы, только дерегистрировать |
|
||||
| Force | нет | Удалить несмотря на найденные ссылки |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/meta-remove/scripts/meta-remove.ps1 -ConfigDir "<путь>" -Object "Catalog.Товары"
|
||||
```
|
||||
|
||||
## Что делает
|
||||
|
||||
1. **Находит файлы объекта**: `{TypePlural}/{Name}.xml` и `{TypePlural}/{Name}/`
|
||||
2. **Проверяет ссылки** (блокирует при наличии, если нет `-Force`):
|
||||
- XML-типы в реквизитах других объектов: `CatalogRef.Имя`, `DocumentRef.Имя` и т.д.
|
||||
- BSL-код: `Справочники.Имя`, `Catalogs.Имя`, вызовы общих модулей
|
||||
- Журналы документов, подписки на события, определяемые типы
|
||||
3. **Удаляет из Configuration.xml**: убирает из `<ChildObjects>`
|
||||
4. **Очищает подсистемы**: рекурсивно удаляет из `<Content>`
|
||||
5. **Удаляет файлы**: XML-файл и каталог объекта
|
||||
|
||||
## Поддерживаемые типы
|
||||
|
||||
Catalog, Document, Enum, Constant, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, ExchangePlan, DocumentJournal, Report, DataProcessor, CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService, DefinedType, Role, Subsystem, CommonForm, CommonTemplate, CommonPicture, CommonAttribute, SessionParameter, FunctionalOption, FunctionalOptionsParameter, Sequence, FilterCriterion, SettingsStorage, XDTOPackage, WSReference, StyleItem, Language
|
||||
|
||||
## Вывод (объект без ссылок)
|
||||
|
||||
```
|
||||
=== meta-remove: Catalog.Устаревший ===
|
||||
|
||||
[FOUND] Catalogs/Устаревший.xml
|
||||
[FOUND] Catalogs/Устаревший/ (8 files)
|
||||
|
||||
--- Reference check ---
|
||||
[OK] No references found
|
||||
|
||||
--- Configuration.xml ---
|
||||
[OK] Removed <Catalog>Устаревший</Catalog> from ChildObjects
|
||||
[OK] Configuration.xml saved
|
||||
|
||||
--- Subsystems ---
|
||||
[OK] Removed from subsystem 'Справочники'
|
||||
|
||||
--- Files ---
|
||||
[OK] Deleted directory: Catalogs/Устаревший/
|
||||
[OK] Deleted file: Catalogs/Устаревший.xml
|
||||
|
||||
=== Done: 4 actions performed (1 subsystem references removed) ===
|
||||
```
|
||||
|
||||
## Вывод (объект со ссылками — блокировка)
|
||||
|
||||
```
|
||||
=== meta-remove: Catalog.Валюты ===
|
||||
|
||||
[FOUND] Catalogs/Валюты.xml
|
||||
[FOUND] Catalogs/Валюты/ (4 files)
|
||||
|
||||
--- Reference check ---
|
||||
[WARN] Found 3 reference(s) to Catalog.Валюты:
|
||||
|
||||
Documents/СчетНаОплату.xml
|
||||
pattern: CatalogRef.Валюты
|
||||
InformationRegisters/КурсыВалют.xml
|
||||
pattern: CatalogRef.Валюты
|
||||
CommonModules/РаботаСВалютами/Ext/Module.bsl
|
||||
pattern: Справочники.Валюты
|
||||
|
||||
[ERROR] Cannot remove: object has 3 reference(s).
|
||||
Use -Force to remove anyway, or fix references first.
|
||||
```
|
||||
|
||||
Код возврата: 0 = успешно, 1 = ошибки или найдены ссылки.
|
||||
|
||||
## Проверяемые ссылки
|
||||
|
||||
| Категория | Паттерны поиска |
|
||||
|-----------|----------------|
|
||||
| XML-типы реквизитов | `CatalogRef.Name`, `DocumentRef.Name`, `EnumRef.Name` и др. |
|
||||
| BSL-код (рус.) | `Справочники.Name`, `Документы.Name`, `Перечисления.Name` и др. |
|
||||
| BSL-код (англ.) | `Catalogs.Name`, `Documents.Name`, `Enums.Name` и др. |
|
||||
| Общие модули | `Name.` (вызовы методов), `<Handler>Name.`, `<MethodName>Name.` |
|
||||
|
||||
Ссылки из Configuration.xml, ConfigDumpInfo.xml и подсистем НЕ считаются блокирующими — они очищаются автоматически.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Проверка ссылок + dry run
|
||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -DryRun
|
||||
|
||||
# Удалить объект без ссылок
|
||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший"
|
||||
|
||||
# Принудительно удалить несмотря на ссылки
|
||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -Force
|
||||
|
||||
# Только дерегистрировать (файлы оставить)
|
||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Report.Старый" -KeepFiles
|
||||
|
||||
# Удалить общий модуль
|
||||
... -ConfigDir src -Object "CommonModule.МойМодуль"
|
||||
```
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- **Рефакторинг**: удаление неиспользуемых объектов
|
||||
- **Очистка**: удаление временных/тестовых объектов
|
||||
- **Перенос**: удаление объекта перед пересозданием с другой структурой
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
name: meta-validate
|
||||
description: Валидация объекта метаданных 1С. Используй после создания или модификации объекта конфигурации для проверки корректности
|
||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30] — pipe-separated paths for batch
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /meta-validate — валидация объекта метаданных 1С
|
||||
|
||||
Проверяет XML объекта метаданных из выгрузки конфигурации на структурные ошибки.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ObjectPath | да | — | Путь к XML-файлу или каталогу. Через `\|` для batch |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок (per object) |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml"
|
||||
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Банки|Documents/Заказ"
|
||||
```
|
||||
|
||||
## Поддерживаемые типы (23)
|
||||
|
||||
**Ссылочные:** Catalog, Document, Enum, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task
|
||||
**Регистры:** InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister
|
||||
**Отчёты/Обработки:** Report, DataProcessor
|
||||
**Сервисные:** CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService
|
||||
**Прочие:** Constant, DocumentJournal, DefinedType
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|----|------------------------------------------|--------------|
|
||||
| 1 | XML well-formedness + root structure | ERROR |
|
||||
| 2 | InternalInfo / GeneratedType | ERROR / WARN |
|
||||
| 3 | Properties — Name, Synonym | ERROR / WARN |
|
||||
| 4 | Properties — enum-значения свойств | ERROR |
|
||||
| 5 | StandardAttributes | ERROR / WARN |
|
||||
| 6 | ChildObjects — допустимые элементы | ERROR |
|
||||
| 7 | Attributes/Dimensions/Resources — UUID, Name, Type | ERROR |
|
||||
| 7b | Reserved attribute names | WARN |
|
||||
| 8 | Уникальность имён | ERROR |
|
||||
| 9 | TabularSections — внутренняя структура | ERROR / WARN |
|
||||
| 10 | Кросс-свойства | ERROR / WARN |
|
||||
| 11 | HTTPService/WebService — вложенная структура | ERROR |
|
||||
| 12 | Forbidden properties per type | ERROR |
|
||||
| 13 | Method reference (Handler/MethodName) | ERROR / WARN |
|
||||
| 14 | DocumentJournal Columns | ERROR |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
name: mxl-decompile
|
||||
description: Декомпиляция табличного документа (MXL) в JSON-определение. Используй когда нужно получить редактируемое описание существующего макета
|
||||
argument-hint: <TemplatePath> [OutputPath]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /mxl-decompile — Декомпилятор макета в DSL
|
||||
|
||||
Принимает Template.xml табличного документа 1С и генерирует компактное JSON-определение (DSL). Обратная операция к `/mxl-compile`.
|
||||
|
||||
## Использование
|
||||
|
||||
```
|
||||
/mxl-decompile <TemplatePath> [OutputPath]
|
||||
```
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|--------------|:------------:|-----------------------------------------|
|
||||
| TemplatePath | да | Путь к Template.xml |
|
||||
| OutputPath | нет | Путь для JSON (если не указан — stdout) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
|
||||
```
|
||||
|
||||
## Рабочий процесс
|
||||
|
||||
Декомпиляция существующего макета для анализа или доработки:
|
||||
|
||||
1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml
|
||||
2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили)
|
||||
3. Claude вызывает `/mxl-compile` для генерации нового Template.xml
|
||||
4. Claude вызывает `/mxl-validate` для проверки
|
||||
|
||||
## JSON-схема DSL
|
||||
|
||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool).
|
||||
|
||||
## Генерация имён
|
||||
|
||||
Скрипт автоматически генерирует осмысленные имена:
|
||||
|
||||
- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам
|
||||
- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств
|
||||
|
||||
## Детектирование `rowStyle`
|
||||
|
||||
Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: mxl-validate
|
||||
description: Валидация макета табличного документа (MXL). Используй после создания или модификации макета для проверки корректности
|
||||
argument-hint: <TemplatePath> [-Detailed] [-MaxErrors 20]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /mxl-validate — валидация макета табличного документа (MXL)
|
||||
|
||||
Проверяет Template.xml на структурные ошибки: индексы, ссылки на палитры, диапазоны именованных областей и объединений.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|---------------|:-----:|---------|--------------------------------------------|
|
||||
| TemplatePath | да | — | Путь к макету (директория или Template.xml) |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 20 | Остановиться после N ошибок |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "Catalogs/Номенклатура/Templates/Макет"
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "src/МояОбработка/Templates/ПечатнаяФорма"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|---|---|---|
|
||||
| 1 | `<height>` >= максимальный индекс строки + 1 | ERROR |
|
||||
| 2 | `<vgRows>` <= `<height>` | WARN |
|
||||
| 3 | Индексы форматов ячеек (`<f>`) в пределах палитры форматов | ERROR |
|
||||
| 4 | `<formatIndex>` строк и колонок в пределах палитры | ERROR |
|
||||
| 5 | Индексы колонок в ячейках (`<i>`) в пределах количества колонок (с учётом набора) | ERROR |
|
||||
| 6 | `<columnsID>` строк ссылается на существующий набор колонок | ERROR |
|
||||
| 7 | `<columnsID>` в merge/namedItem ссылается на существующий набор | ERROR |
|
||||
| 8 | Диапазоны именованных областей в пределах границ документа | ERROR |
|
||||
| 9 | Диапазоны объединений в пределах границ документа | ERROR |
|
||||
| 10 | Индексы шрифтов в форматах в пределах палитры шрифтов | ERROR |
|
||||
| 11 | Индексы линий границ в форматах в пределах палитры линий | ERROR |
|
||||
| 12 | `pictureIndex` рисунков ссылается на существующую картинку | ERROR |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: role-validate
|
||||
description: Валидация роли 1С. Используй после создания или модификации роли для проверки корректности
|
||||
argument-hint: <RightsPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
---
|
||||
|
||||
# /role-validate — валидация роли 1С
|
||||
|
||||
Проверяет корректность `Rights.xml` роли: формат XML, namespace, глобальные флаги, типы объектов, имена прав, RLS-ограничения, шаблоны. Опционально проверяет метаданные роли (UUID, имя, синоним).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|--------------|:-----:|---------|-------------------------------------------------|
|
||||
| RightsPath | да | — | Путь к роли (директория или `Rights.xml`) |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Макс. ошибок до остановки (по умолчанию 30) |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/role-validate/scripts/role-validate.ps1 -RightsPath "Roles/МояРоль"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|---|----------|-------------|
|
||||
| 1 | XML well-formed — парсинг без ошибок | ERROR |
|
||||
| 2 | Корневой элемент `<Rights>` с namespace `http://v8.1c.ru/8.2/roles` | ERROR |
|
||||
| 3 | Три глобальных флага: setForNewObjects, setForAttributesByDefault, independentRightsOfChildObjects | ERROR |
|
||||
| 4 | Объекты: name не пуст, тип распознан, права валидны для типа (с подсказкой при опечатке) | ERROR/WARN |
|
||||
| 5 | Вложенные объекты (3+ сегмента): допустимы только View, Edit (или Use для IntegrationServiceChannel) | ERROR |
|
||||
| 6 | RLS `<restrictionByCondition>`: condition не пуст | ERROR |
|
||||
| 7 | Шаблоны `<restrictionTemplate>`: name и condition не пусты | ERROR |
|
||||
| 8 | Метаданные (если MetadataPath): UUID, Name, Synonym | ERROR/WARN |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,299 +0,0 @@
|
||||
---
|
||||
name: skd-compile
|
||||
description: Компиляция схемы компоновки данных 1С (СКД) из компактного JSON-определения. Используй когда нужно создать СКД с нуля
|
||||
argument-hint: "[-DefinitionFile <json> | -Value <json-string>] -OutputPath <Template.xml>"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /skd-compile — генерация СКД из JSON DSL
|
||||
|
||||
Принимает JSON-определение схемы компоновки данных → генерирует Template.xml (DataCompositionSchema).
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `DefinitionFile` | Путь к JSON-файлу с определением СКД (взаимоисключающий с Value) |
|
||||
| `Value` | JSON-строка с определением СКД (взаимоисключающий с DefinitionFile) |
|
||||
| `OutputPath` | Путь к выходному Template.xml |
|
||||
|
||||
```powershell
|
||||
# Из файла
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.ps1 -DefinitionFile "<json>" -OutputPath "<Template.xml>"
|
||||
|
||||
# Из строки (без промежуточного файла)
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.ps1 -Value '<json-string>' -OutputPath "<Template.xml>"
|
||||
```
|
||||
|
||||
## JSON DSL — краткий справочник
|
||||
|
||||
Справочник ниже. Все примеры компилируемы как есть.
|
||||
|
||||
### Корневая структура
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSets": [...],
|
||||
"calculatedFields": [...],
|
||||
"totalFields": [...],
|
||||
"parameters": [...],
|
||||
"templates": [...],
|
||||
"groupTemplates": [...],
|
||||
"dataSetLinks": [...],
|
||||
"settingsVariants": [...]
|
||||
}
|
||||
```
|
||||
|
||||
Умолчания: `dataSources` → авто `ИсточникДанных1/Local`; `settingsVariants` → авто "Основной" с деталями.
|
||||
|
||||
### Наборы данных
|
||||
|
||||
Тип по ключу: `query` → DataSetQuery, `objectName` → DataSetObject, `items` → DataSetUnion.
|
||||
|
||||
```json
|
||||
{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] }
|
||||
```
|
||||
|
||||
Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD.
|
||||
|
||||
### Поля — shorthand и объектная форма
|
||||
|
||||
```
|
||||
"Наименование" — просто имя
|
||||
"Количество: decimal(15,2)" — имя + тип
|
||||
"Организация: CatalogRef.Организации @dimension" — + роль
|
||||
"Служебное: string #noFilter #noOrder" — + ограничения
|
||||
```
|
||||
|
||||
Объектная форма — когда нужен title или другие свойства:
|
||||
```json
|
||||
{ "field": "ОстатокНаНачалоПериода", "title": "Остаток на начало периода" }
|
||||
```
|
||||
`dataPath` автоматически берётся из `field`, если не указан явно.
|
||||
|
||||
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
|
||||
|
||||
**Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые.
|
||||
|
||||
Роли: `@dimension`, `@account`, `@balance`, `@period`.
|
||||
|
||||
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
|
||||
|
||||
### Итоги (shorthand)
|
||||
|
||||
```json
|
||||
"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"]
|
||||
```
|
||||
|
||||
### Параметры (shorthand + @autoDates)
|
||||
|
||||
```json
|
||||
"parameters": [
|
||||
"Период: StandardPeriod = LastMonth @autoDates"
|
||||
]
|
||||
```
|
||||
|
||||
`@autoDates` — автоматически генерирует параметры `ДатаНачала` и `ДатаОкончания` с выражениями `&Период.ДатаНачала` / `&Период.ДатаОкончания` и `availableAsField=false`. Заменяет 5 строк на 1.
|
||||
|
||||
### Фильтры — shorthand
|
||||
|
||||
```json
|
||||
"filter": [
|
||||
"Организация = _ @off @user",
|
||||
"Дата >= 2024-01-01T00:00:00",
|
||||
"Статус filled"
|
||||
]
|
||||
```
|
||||
|
||||
Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`.
|
||||
|
||||
В объектной форме доступны: `viewMode`, `userSettingID`, `userSettingPresentation`.
|
||||
|
||||
Группы фильтров (Or/And/Not):
|
||||
```json
|
||||
{ "group": "Or", "items": [
|
||||
{ "group": "And", "items": [
|
||||
{ "field": "Статус", "op": "=", "value": "Активен" },
|
||||
{ "field": "Сумма", "op": ">", "value": 1000 }
|
||||
]},
|
||||
{ "field": "Количество", "op": "filled" }
|
||||
]}
|
||||
```
|
||||
|
||||
### Параметры данных — shorthand
|
||||
|
||||
```json
|
||||
"dataParameters": [
|
||||
"Период = LastMonth @user",
|
||||
"Организация @off @user"
|
||||
]
|
||||
```
|
||||
|
||||
Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически.
|
||||
|
||||
### Структура — string shorthand
|
||||
|
||||
```json
|
||||
"structure": "Организация > details"
|
||||
"structure": "Организация > Номенклатура > details"
|
||||
```
|
||||
|
||||
`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне.
|
||||
|
||||
Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма.
|
||||
|
||||
### Варианты настроек
|
||||
|
||||
```json
|
||||
"settingsVariants": [{
|
||||
"name": "Основной",
|
||||
"title": "Продажи по организациям",
|
||||
"settings": {
|
||||
"selection": ["Номенклатура", "Количество", "Auto"],
|
||||
"filter": ["Организация = _ @off @user"],
|
||||
"order": ["Количество desc", "Auto"],
|
||||
"conditionalAppearance": [
|
||||
{
|
||||
"filter": ["Просрочено = true"],
|
||||
"appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" },
|
||||
"presentation": "Выделять просроченные",
|
||||
"viewMode": "Normal",
|
||||
"userSettingID": "auto"
|
||||
}
|
||||
],
|
||||
"outputParameters": { "Заголовок": "Мой отчёт" },
|
||||
"dataParameters": ["Период = LastMonth @user"],
|
||||
"structure": "Организация > details"
|
||||
}
|
||||
}]
|
||||
```
|
||||
|
||||
### Условное оформление (conditionalAppearance)
|
||||
|
||||
```json
|
||||
"conditionalAppearance": [
|
||||
{
|
||||
"selection": ["Поле1"],
|
||||
"filter": ["Поле1 notFilled"],
|
||||
"appearance": { "Текст": "Не указано", "ЦветТекста": "style:XXX" },
|
||||
"presentation": "Описание",
|
||||
"viewMode": "Normal",
|
||||
"userSettingID": "auto"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Текст` → LocalStringType, прочее → String.
|
||||
|
||||
### Итоги с привязкой к группировкам
|
||||
|
||||
```json
|
||||
"totalFields": [
|
||||
{ "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["Группа1", "Группа1 Иерархия", "ОбщийИтог"] }
|
||||
]
|
||||
```
|
||||
|
||||
### Шаблоны вывода — компактный DSL
|
||||
|
||||
Вместо raw XML (`template`) — табличное описание через `rows` + именованный стиль `style`:
|
||||
|
||||
```json
|
||||
"templates": [
|
||||
{
|
||||
"name": "Макет1",
|
||||
"style": "header",
|
||||
"widths": [36, 33, 16, 17],
|
||||
"minHeight": 24.75,
|
||||
"rows": [
|
||||
["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на\nконец периода"],
|
||||
["|", "|", "|", "|"],
|
||||
["К1", "К2", "К3", "К4"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Макет2",
|
||||
"style": "data",
|
||||
"widths": [36, 33, 16, 17],
|
||||
"rows": [["{ВидКассы}", "{Валюта}", "{Остаток}", "{ОстатокКонец}"]],
|
||||
"parameters": [
|
||||
{ "name": "ВидКассы", "expression": "Представление(Счет)" },
|
||||
{ "name": "Остаток", "expression": "ОстатокНаНачалоПериода" }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `null` — пустая.
|
||||
|
||||
Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы.
|
||||
|
||||
Пользовательские стили: файл `skd-styles.json` рядом с JSON или в корне проекта. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`.
|
||||
|
||||
Raw XML (`"template": "<...>"`) остаётся как fallback. Детект: если есть `rows` — DSL, иначе — raw.
|
||||
|
||||
### Привязки макетов к группировкам
|
||||
|
||||
```json
|
||||
"groupTemplates": [
|
||||
{ "groupField": "Счет", "templateType": "GroupHeader", "template": "Макет1" },
|
||||
{ "groupField": "Счет", "templateType": "Header", "template": "Макет2" }
|
||||
]
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
### Минимальный
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSets": [{
|
||||
"query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура",
|
||||
"fields": ["Наименование"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### С запросом из внешнего файла (@file)
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSets": [{
|
||||
"query": "@queries/sales.sql",
|
||||
"fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### С ресурсами, параметрами и @autoDates
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSets": [{
|
||||
"query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи",
|
||||
"fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
|
||||
}],
|
||||
"totalFields": ["Количество: Сумма", "Сумма: Сумма"],
|
||||
"parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"],
|
||||
"settingsVariants": [{
|
||||
"name": "Основной",
|
||||
"settings": {
|
||||
"selection": ["Номенклатура", "Количество", "Сумма", "Auto"],
|
||||
"filter": ["Организация = _ @off @user"],
|
||||
"dataParameters": ["Период = LastMonth @user"],
|
||||
"structure": "Организация > details"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/skd-validate <OutputPath> — валидация структуры XML
|
||||
/skd-info <OutputPath> — визуальная сводка
|
||||
/skd-info <OutputPath> -Mode variant -Name 1 — проверка варианта настроек
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,218 +0,0 @@
|
||||
---
|
||||
name: skd-edit
|
||||
description: Точечное редактирование схемы компоновки данных 1С (СКД). Используй когда нужно модифицировать существующую СКД — добавить поля, итоги, фильтры, параметры, изменить текст запроса
|
||||
argument-hint: <TemplatePath> -Operation <op> -Value <value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /skd-edit — точечное редактирование СКД (Template.xml)
|
||||
|
||||
Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) |
|
||||
| `Operation` | Операция (см. список ниже) |
|
||||
| `Value` | Значение операции (shorthand-строка или текст запроса) |
|
||||
| `DataSet` | (опц.) Имя набора данных (умолч. первый) |
|
||||
| `Variant` | (опц.) Имя варианта настроек (умолч. первый) |
|
||||
| `NoSelection` | (опц.) Не добавлять поле в selection варианта |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-edit/scripts/skd-edit.ps1 -TemplatePath "<path>" -Operation <op> -Value "<value>"
|
||||
```
|
||||
|
||||
## Пакетный режим (batch)
|
||||
|
||||
Несколько значений в одном вызове через разделитель `;;`:
|
||||
|
||||
```powershell
|
||||
-Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)"
|
||||
```
|
||||
|
||||
Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`.
|
||||
|
||||
## Операции
|
||||
|
||||
### add-field — добавить поле в набор данных
|
||||
|
||||
Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`.
|
||||
|
||||
```
|
||||
"Цена: decimal(15,2)"
|
||||
"Организация [Орг-ция]: CatalogRef.Организации @dimension"
|
||||
"Служебное: string #noFilter #noOrder"
|
||||
```
|
||||
|
||||
Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск.
|
||||
|
||||
### add-total — добавить итог
|
||||
|
||||
```
|
||||
"Цена: Среднее"
|
||||
"Стоимость: Сумма(Кол * Цена)"
|
||||
```
|
||||
|
||||
### add-calculated-field — добавить вычисляемое поле
|
||||
|
||||
Shorthand: `"Имя [Заголовок]: тип = Выражение"`.
|
||||
|
||||
```
|
||||
"Маржа = Продажа - Закупка"
|
||||
"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100"
|
||||
```
|
||||
|
||||
Также добавляется в selection варианта.
|
||||
|
||||
### add-parameter — добавить параметр
|
||||
|
||||
```
|
||||
"Период: StandardPeriod = LastMonth @autoDates"
|
||||
"Организация: CatalogRef.Организации"
|
||||
```
|
||||
|
||||
`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически.
|
||||
|
||||
### add-filter — добавить фильтр в вариант
|
||||
|
||||
Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`.
|
||||
|
||||
```
|
||||
"Номенклатура = _ @off @user"
|
||||
"Дата >= 2024-01-01T00:00:00"
|
||||
"Статус filled"
|
||||
```
|
||||
|
||||
### add-dataParameter — добавить параметр данных в вариант
|
||||
|
||||
Shorthand: `"Имя [= значение] @флаги"`.
|
||||
|
||||
```
|
||||
"Период = LastMonth @user"
|
||||
"Организация @off @user"
|
||||
```
|
||||
|
||||
### add-order — добавить сортировку
|
||||
|
||||
Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент.
|
||||
|
||||
```
|
||||
"Количество desc"
|
||||
"Auto"
|
||||
```
|
||||
|
||||
### add-selection — добавить элемент выборки
|
||||
|
||||
```
|
||||
"Номенклатура"
|
||||
"Auto"
|
||||
```
|
||||
|
||||
### add-dataSetLink — добавить связь наборов данных
|
||||
|
||||
Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`.
|
||||
|
||||
```
|
||||
"Набор1 > Набор2 on Поле1 = Поле2"
|
||||
"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]"
|
||||
```
|
||||
|
||||
### add-dataSet — добавить набор данных
|
||||
|
||||
Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРОСА"` (авто-имя `НаборДанныхN`).
|
||||
|
||||
```
|
||||
"Доп: ВЫБРАТЬ 1 КАК Тест"
|
||||
"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"
|
||||
"Продажи: @queries/sales.sql"
|
||||
```
|
||||
|
||||
`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`).
|
||||
|
||||
### add-variant — добавить вариант настроек
|
||||
|
||||
Shorthand: `"Имя [Представление]"`. Представление опционально, по умолчанию = имя.
|
||||
|
||||
```
|
||||
"Детальный"
|
||||
"Детальный [Детальный отчёт]"
|
||||
```
|
||||
|
||||
Создаёт вариант с Auto selection + detail group. Дубликат имени — предупреждение, пропуск.
|
||||
|
||||
### add-conditionalAppearance — добавить условное оформление
|
||||
|
||||
Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение).
|
||||
|
||||
```
|
||||
"ЦветТекста = web:Red when Сумма < 0"
|
||||
"ЦветФона = web:LightGreen when Статус = Одобрен for Статус"
|
||||
"МинимальнаяШирина = 50 for Организация"
|
||||
"Формат = ЧДЦ=2 for Цена, Сумма"
|
||||
```
|
||||
|
||||
Типы значений (автодетект): `web:*`/`style:*`/`win:*` → цвет, `true`/`false` → boolean, иначе строка.
|
||||
|
||||
### set-query — заменить текст запроса
|
||||
|
||||
Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD.
|
||||
|
||||
### set-outputParameter — установить параметр вывода
|
||||
|
||||
```
|
||||
"Заголовок = Мой отчёт"
|
||||
"ВыводитьЗаголовок = true"
|
||||
```
|
||||
|
||||
Если параметр уже существует — заменяет значение.
|
||||
|
||||
### set-structure — установить структуру варианта
|
||||
|
||||
Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
|
||||
|
||||
```
|
||||
"Организация > Номенклатура > details"
|
||||
"details"
|
||||
```
|
||||
|
||||
### modify-field — изменить существующее поле
|
||||
|
||||
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
|
||||
|
||||
```
|
||||
"Цена [Цена USD]: decimal(10,4) @dimension"
|
||||
```
|
||||
|
||||
### modify-filter — изменить существующий фильтр
|
||||
|
||||
Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги.
|
||||
|
||||
### modify-dataParameter — изменить параметр данных
|
||||
|
||||
Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги.
|
||||
|
||||
### remove-* и clear-*
|
||||
|
||||
| Операция | Value | Действие |
|
||||
|----------|-------|----------|
|
||||
| `remove-field` | dataPath | Удаляет поле из набора + из selection варианта |
|
||||
| `remove-total` | dataPath | Удаляет итог |
|
||||
| `remove-calculated-field` | dataPath | Удаляет вычисляемое поле + из selection |
|
||||
| `remove-parameter` | name | Удаляет параметр |
|
||||
| `remove-filter` | поле | Удаляет первый фильтр с указанным полем |
|
||||
| `clear-selection` | `*` | Очищает все элементы selection |
|
||||
| `clear-order` | `*` | Очищает все элементы order |
|
||||
| `clear-filter` | `*` | Очищает все элементы filter |
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/skd-validate <TemplatePath> — валидация структуры после редактирования
|
||||
/skd-info <TemplatePath> — визуальная сводка
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,246 +0,0 @@
|
||||
# /skd-info — полная справка по режимам
|
||||
|
||||
Компактное описание — в [SKILL.md](SKILL.md).
|
||||
|
||||
## overview (по умолчанию) — карта схемы
|
||||
|
||||
Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
|
||||
|
||||
```
|
||||
=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
|
||||
|
||||
Sources: ИсточникДанных1 (Local)
|
||||
|
||||
Datasets:
|
||||
[Query] НоменклатураСЦенами 7 fields, query 40 lines
|
||||
Calculated: 1
|
||||
Resources: 1
|
||||
Templates: 1 templates, 1 group bindings
|
||||
Params: (none)
|
||||
|
||||
Variants:
|
||||
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
|
||||
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
|
||||
|
||||
Next:
|
||||
-Mode query query text
|
||||
-Mode fields field tables by dataset
|
||||
-Mode calculated calculated field expressions
|
||||
-Mode resources resource aggregation
|
||||
-Mode variant -Name <N> variant structure (1..2)
|
||||
```
|
||||
|
||||
Для DataSetUnion — дерево наборов + связи:
|
||||
```
|
||||
Datasets:
|
||||
[Union] РасчетНалогаНаИмущество 52 fields
|
||||
├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
|
||||
├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
|
||||
├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
|
||||
Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
|
||||
```
|
||||
|
||||
Параметры разделяются на видимые/скрытые:
|
||||
```
|
||||
Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
|
||||
```
|
||||
|
||||
## query — текст запроса
|
||||
|
||||
`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
|
||||
|
||||
Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей:
|
||||
|
||||
```
|
||||
=== Query: ДанныеТ13 (334 lines, 13 batches) ===
|
||||
Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
|
||||
Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
|
||||
...
|
||||
--- Batch 1 ---
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(1, 1, 1) КАК Период
|
||||
ПОМЕСТИТЬ Представления_Периоды
|
||||
...
|
||||
```
|
||||
|
||||
Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
|
||||
|
||||
## fields — поля наборов данных
|
||||
|
||||
Без `-Name` — карта: имена полей по наборам:
|
||||
```
|
||||
=== Fields map ===
|
||||
СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
|
||||
РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
|
||||
РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
|
||||
```
|
||||
|
||||
С `-Name <поле>` — детали конкретного поля:
|
||||
```
|
||||
=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
|
||||
|
||||
Dataset: СостояниеОС [Query]
|
||||
Format: ДФ=dd.MM.yyyy
|
||||
```
|
||||
|
||||
Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
|
||||
|
||||
## links — связи наборов данных
|
||||
|
||||
```
|
||||
=== Links (4) ===
|
||||
|
||||
РасчетНалогаНаИмущество -> СостояниеОС :
|
||||
Организация -> Организация
|
||||
ОсновноеСредство -> ОсновноеСредство
|
||||
```
|
||||
|
||||
Группирует по парам наборов. Показывает поля связи и параметры.
|
||||
|
||||
## calculated — вычисляемые поля
|
||||
|
||||
Без `-Name` — карта: имена и заголовки:
|
||||
```
|
||||
=== Calculated fields (23) ===
|
||||
ДоляСтоимости "Доля стоимости"
|
||||
КоэффициентКи "Коэффициент Ки"
|
||||
...
|
||||
```
|
||||
|
||||
С `-Name <поле>` — полное выражение:
|
||||
```
|
||||
=== Calculated: ДоляСтоимости ===
|
||||
|
||||
Expression:
|
||||
ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
|
||||
Title: Доля стоимости
|
||||
Restrict: condition
|
||||
```
|
||||
|
||||
## resources — ресурсы (итоги по группировкам)
|
||||
|
||||
Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
|
||||
```
|
||||
=== Resources (51) ===
|
||||
НалоговаяБаза
|
||||
КоэффициентКи *
|
||||
...
|
||||
* = has group-level formulas
|
||||
```
|
||||
|
||||
С `-Name <поле>` — формулы агрегации:
|
||||
```
|
||||
=== Resource: ДатаСостояния ===
|
||||
|
||||
[ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
|
||||
```
|
||||
|
||||
## params — параметры схемы
|
||||
|
||||
```
|
||||
=== Parameters (16) ===
|
||||
Name Type Default Visible Expression
|
||||
Период StandardPeriod LastMonth yes -
|
||||
НачалоПериода DateTime - hidden &Период.ДатаНачала
|
||||
Организация CatalogRef.Организации null yes -
|
||||
```
|
||||
|
||||
## variant — варианты отчёта
|
||||
|
||||
Без `-Name` — список вариантов:
|
||||
```
|
||||
=== Variants (2) ===
|
||||
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
|
||||
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
|
||||
```
|
||||
|
||||
С `-Name <N|имя>` — структура конкретного варианта:
|
||||
```
|
||||
=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
|
||||
|
||||
Structure:
|
||||
Table "Таблица"
|
||||
├── Columns: [ТипЦен Items]
|
||||
│ Selection: Auto, Цена
|
||||
└── Rows: [Номенклатура Items]
|
||||
Selection: Номенклатура, УИД, Auto
|
||||
|
||||
Filter:
|
||||
[ ] Номенклатура InHierarchy [user]
|
||||
[ ] ТипЦен Equal
|
||||
[x] ВАрхиве = false "Исключая скрытые товары"
|
||||
|
||||
DataParams: КлючВарианта="НоменклатураИЦены"
|
||||
Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
|
||||
```
|
||||
|
||||
## templates — привязки шаблонов вывода
|
||||
|
||||
Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
|
||||
|
||||
Без `-Name` — карта привязок:
|
||||
```
|
||||
=== Templates (70 defined: 49 field, 37 group) ===
|
||||
|
||||
Field bindings (49): (all trivial)
|
||||
ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
|
||||
|
||||
Group bindings (37):
|
||||
ВидНалоговойБазы
|
||||
Header -> Макет3 (1 rows, 1 params)
|
||||
СреднегодоваяСтоимость2019
|
||||
Footer -> Макет50 (1 rows) spacer
|
||||
GroupHeader -> Макет40 (3 rows)
|
||||
```
|
||||
|
||||
С `-Name <группировка|поле>` — содержимое шаблонов:
|
||||
```
|
||||
=== Templates: СреднегодоваяСтоимость2019 ===
|
||||
|
||||
Footer -> Макет50 [1 rows, 1 cells]:
|
||||
Row 1: (empty)
|
||||
|
||||
GroupHeader -> Макет40 [3 rows, 78 cells]:
|
||||
Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
|
||||
Row 2: "01.01" | "01.02" | ... | "31.12"
|
||||
Row 3: "1" | "2" | ... | "26"
|
||||
```
|
||||
|
||||
Для field-привязок:
|
||||
```
|
||||
=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
|
||||
[1 rows, 1 cells]
|
||||
Row 1: {ОстаточнаяСтоимостьНа0101}
|
||||
(all params trivial)
|
||||
```
|
||||
|
||||
**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
|
||||
|
||||
## trace — трассировка поля от заголовка до запроса
|
||||
|
||||
Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
|
||||
|
||||
```
|
||||
=== Trace: КоэффициентКи "Коэффициент Ки" ===
|
||||
|
||||
Dataset: (schema-level only, not in dataset fields)
|
||||
|
||||
Calculated:
|
||||
ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
|
||||
Operands:
|
||||
КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
|
||||
КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
|
||||
|
||||
Resource:
|
||||
[ОсновноеСредство] Сумма(КоэффициентКи)
|
||||
```
|
||||
|
||||
Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
|
||||
|
||||
## Что не выводится
|
||||
|
||||
- XML namespace-декларации
|
||||
- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
|
||||
- userSettingID (GUID-ы пользовательских настроек)
|
||||
- Дефолтные periodAdditionBegin/End = 0001-01-01
|
||||
- viewMode
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: skd-validate
|
||||
description: Валидация схемы компоновки данных 1С (СКД). Используй после создания или модификации СКД для проверки корректности
|
||||
argument-hint: <TemplatePath> [-Detailed] [-MaxErrors 20]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /skd-validate — валидация СКД (DataCompositionSchema)
|
||||
|
||||
Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|--------------|:-----:|---------|---------------------------------------------------------|
|
||||
| TemplatePath | да | — | Путь к Template.xml или каталогу макета |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 20 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-validate/scripts/skd-validate.ps1 -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема"
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-validate/scripts/skd-validate.ps1 -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml"
|
||||
```
|
||||
|
||||
## Проверки (~30)
|
||||
|
||||
| Группа | Что проверяется |
|
||||
|--------|-----------------|
|
||||
| **Root** | XML parse, корневой элемент `DataCompositionSchema`, default namespace, ns-префиксы |
|
||||
| **DataSource** | Наличие, name не пуст, type валиден (Local/External), уникальность имён |
|
||||
| **DataSet** | Наличие, xsi:type валиден, name не пуст, уникальность, ссылка на dataSource, query не пуст |
|
||||
| **Fields** | dataPath не пуст, field не пуст, уникальность dataPath в наборе |
|
||||
| **Links** | source/dest ссылаются на существующие наборы, expressions не пусты |
|
||||
| **CalcFields** | dataPath не пуст, expression не пуст, уникальность, коллизии с полями наборов |
|
||||
| **TotalFields** | dataPath не пуст, expression не пуст |
|
||||
| **Parameters** | name не пуст, уникальность |
|
||||
| **Templates** | name не пуст, уникальность |
|
||||
| **GroupTemplates** | template ссылается на существующий template, templateType валиден |
|
||||
| **Variants** | Наличие, name не пуст, settings element присутствует |
|
||||
| **Settings** | selection/filter/order ссылаются на известные поля, comparisonType валиден, structure items типизированы |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,338 +0,0 @@
|
||||
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
[string]$Value,
|
||||
[Parameter(Mandatory)][string]$OutputDir,
|
||||
[string]$Parent,
|
||||
[switch]$NoValidate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- 1. Load JSON ---
|
||||
if ($DefinitionFile -and $Value) {
|
||||
Write-Error "Cannot use both -DefinitionFile and -Value"
|
||||
exit 1
|
||||
}
|
||||
if (-not $DefinitionFile -and -not $Value) {
|
||||
Write-Error "Either -DefinitionFile or -Value is required"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($DefinitionFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
||||
}
|
||||
if (-not (Test-Path $DefinitionFile)) {
|
||||
Write-Error "Definition file not found: $DefinitionFile"
|
||||
exit 1
|
||||
}
|
||||
$json = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
||||
} else {
|
||||
$json = $Value
|
||||
}
|
||||
|
||||
$def = $json | ConvertFrom-Json
|
||||
|
||||
if (-not $def.name) {
|
||||
Write-Error "JSON must have 'name' field"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$objName = "$($def.name)"
|
||||
|
||||
# Resolve OutputDir
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- 2. XML helpers ---
|
||||
$script:xml = New-Object System.Text.StringBuilder 8192
|
||||
|
||||
function X([string]$text) {
|
||||
$script:xml.AppendLine($text) | Out-Null
|
||||
}
|
||||
|
||||
function Esc-Xml([string]$s) {
|
||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||
}
|
||||
|
||||
function Split-CamelCase([string]$name) {
|
||||
if (-not $name) { return $name }
|
||||
$result = [regex]::Replace($name, '([a-z\u0430-\u044F\u0451])([A-Z\u0410-\u042F\u0401])', '$1 $2')
|
||||
if ($result.Length -gt 1) {
|
||||
$result = $result.Substring(0,1) + $result.Substring(1).ToLower()
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
function Emit-MLText([string]$indent, [string]$tag, [string]$text) {
|
||||
if (-not $text) {
|
||||
X "$indent<$tag/>"
|
||||
return
|
||||
}
|
||||
X "$indent<$tag>"
|
||||
X "$indent`t<v8:item>"
|
||||
X "$indent`t`t<v8:lang>ru</v8:lang>"
|
||||
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
|
||||
X "$indent`t</v8:item>"
|
||||
X "$indent</$tag>"
|
||||
}
|
||||
|
||||
function New-Guid-String {
|
||||
return [System.Guid]::NewGuid().ToString()
|
||||
}
|
||||
|
||||
# --- 3. Resolve defaults ---
|
||||
$synonym = if ($def.synonym) { "$($def.synonym)" } else { Split-CamelCase $objName }
|
||||
$comment = if ($def.comment) { "$($def.comment)" } else { "" }
|
||||
$includeHelpInContents = "true"
|
||||
$includeInCI = if ($null -ne $def.includeInCommandInterface) { "$($def.includeInCommandInterface)".ToLower() } else { "true" }
|
||||
$useOneCommand = if ($null -ne $def.useOneCommand) { "$($def.useOneCommand)".ToLower() } else { "false" }
|
||||
$explanation = if ($def.explanation) { "$($def.explanation)" } else { "" }
|
||||
$picture = if ($def.picture) { "$($def.picture)" } else { "" }
|
||||
|
||||
$contentItems = @()
|
||||
if ($def.content) {
|
||||
foreach ($c in $def.content) { $contentItems += "$c" }
|
||||
}
|
||||
|
||||
$children = @()
|
||||
if ($def.children) {
|
||||
foreach ($ch in $def.children) { $children += "$ch" }
|
||||
}
|
||||
|
||||
# --- 4. Build XML ---
|
||||
$uuid = New-Guid-String
|
||||
$indent = "`t`t`t"
|
||||
|
||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
X '<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">'
|
||||
X "`t<Subsystem uuid=`"$uuid`">"
|
||||
X "`t`t<Properties>"
|
||||
|
||||
# Name
|
||||
X "`t`t`t<Name>$(Esc-Xml $objName)</Name>"
|
||||
|
||||
# Synonym
|
||||
Emit-MLText "`t`t`t" "Synonym" $synonym
|
||||
|
||||
# Comment
|
||||
if ($comment) {
|
||||
X "`t`t`t<Comment>$(Esc-Xml $comment)</Comment>"
|
||||
} else {
|
||||
X "`t`t`t<Comment/>"
|
||||
}
|
||||
|
||||
# Boolean properties
|
||||
X "`t`t`t<IncludeHelpInContents>$includeHelpInContents</IncludeHelpInContents>"
|
||||
X "`t`t`t<IncludeInCommandInterface>$includeInCI</IncludeInCommandInterface>"
|
||||
X "`t`t`t<UseOneCommand>$useOneCommand</UseOneCommand>"
|
||||
|
||||
# Explanation
|
||||
Emit-MLText "`t`t`t" "Explanation" $explanation
|
||||
|
||||
# Picture
|
||||
if ($picture) {
|
||||
X "`t`t`t<Picture>"
|
||||
X "`t`t`t`t<xr:Ref>$picture</xr:Ref>"
|
||||
X "`t`t`t`t<xr:LoadTransparent>false</xr:LoadTransparent>"
|
||||
X "`t`t`t</Picture>"
|
||||
} else {
|
||||
X "`t`t`t<Picture/>"
|
||||
}
|
||||
|
||||
# Content
|
||||
if ($contentItems.Count -gt 0) {
|
||||
X "`t`t`t<Content>"
|
||||
foreach ($item in $contentItems) {
|
||||
X "`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml $item)</xr:Item>"
|
||||
}
|
||||
X "`t`t`t</Content>"
|
||||
} else {
|
||||
X "`t`t`t<Content/>"
|
||||
}
|
||||
|
||||
X "`t`t</Properties>"
|
||||
|
||||
# ChildObjects
|
||||
if ($children.Count -gt 0) {
|
||||
X "`t`t<ChildObjects>"
|
||||
foreach ($ch in $children) {
|
||||
X "`t`t`t<Subsystem>$(Esc-Xml $ch)</Subsystem>"
|
||||
}
|
||||
X "`t`t</ChildObjects>"
|
||||
} else {
|
||||
X "`t`t<ChildObjects/>"
|
||||
}
|
||||
|
||||
X "`t</Subsystem>"
|
||||
X '</MetaDataObject>'
|
||||
|
||||
# --- 5. Write files ---
|
||||
|
||||
# Determine target directory
|
||||
if ($Parent) {
|
||||
# Nested subsystem
|
||||
if (-not [System.IO.Path]::IsPathRooted($Parent)) {
|
||||
$Parent = Join-Path (Get-Location).Path $Parent
|
||||
}
|
||||
if (-not (Test-Path $Parent)) {
|
||||
Write-Error "Parent subsystem not found: $Parent"
|
||||
exit 1
|
||||
}
|
||||
$parentDir = [System.IO.Path]::GetDirectoryName($Parent)
|
||||
$parentBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Parent)
|
||||
$subsDir = Join-Path (Join-Path $parentDir $parentBaseName) "Subsystems"
|
||||
} else {
|
||||
# Top-level subsystem
|
||||
$subsDir = Join-Path $OutputDir "Subsystems"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $subsDir)) {
|
||||
New-Item -ItemType Directory -Path $subsDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$targetXml = Join-Path $subsDir "$objName.xml"
|
||||
|
||||
# Write XML
|
||||
$xmlContent = $script:xml.ToString()
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($targetXml, $xmlContent, $utf8Bom)
|
||||
Write-Host "[OK] Created: $targetXml"
|
||||
|
||||
# Create subdirectory if children exist
|
||||
if ($children.Count -gt 0) {
|
||||
$childSubsDir = Join-Path (Join-Path $subsDir $objName) "Subsystems"
|
||||
if (-not (Test-Path $childSubsDir)) {
|
||||
New-Item -ItemType Directory -Path $childSubsDir -Force | Out-Null
|
||||
Write-Host "[OK] Created directory: $childSubsDir"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 6. Register in parent ---
|
||||
$parentXmlPath = $null
|
||||
if ($Parent) {
|
||||
$parentXmlPath = $Parent
|
||||
} else {
|
||||
$configXml = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $configXml) {
|
||||
$parentXmlPath = $configXml
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentXmlPath -and (Test-Path $parentXmlPath)) {
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $true
|
||||
$doc.Load($parentXmlPath)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
# Find ChildObjects
|
||||
$childObjects = $null
|
||||
if ($Parent) {
|
||||
$childObjects = $doc.SelectSingleNode("//md:Subsystem/md:ChildObjects", $ns)
|
||||
} else {
|
||||
$childObjects = $doc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||
}
|
||||
|
||||
if ($childObjects) {
|
||||
# Check for self-closing tag
|
||||
$isSelfClosing = (-not $childObjects.HasChildNodes) -or ($childObjects.IsEmpty)
|
||||
|
||||
# Check if already registered
|
||||
$alreadyExists = $false
|
||||
foreach ($child in $childObjects.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText -eq $objName) {
|
||||
$alreadyExists = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $alreadyExists) {
|
||||
$newEl = $doc.CreateElement("Subsystem", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$newEl.InnerText = $objName
|
||||
|
||||
if ($isSelfClosing) {
|
||||
# Expand self-closing tag
|
||||
$parentIndent = ""
|
||||
$prev = $childObjects.PreviousSibling
|
||||
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
|
||||
if ($prev.Value -match '(\t+)$') { $parentIndent = $Matches[1] }
|
||||
}
|
||||
$childIndent = "$parentIndent`t"
|
||||
$ws1 = $doc.CreateWhitespace("`r`n$childIndent")
|
||||
$ws2 = $doc.CreateWhitespace("`r`n$parentIndent")
|
||||
$childObjects.AppendChild($ws1) | Out-Null
|
||||
$childObjects.AppendChild($newEl) | Out-Null
|
||||
$childObjects.AppendChild($ws2) | Out-Null
|
||||
} else {
|
||||
# Insert before trailing whitespace
|
||||
$childIndent = "`t`t`t"
|
||||
foreach ($child in $childObjects.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
|
||||
if ($child.Value -match '^\r?\n(\t+)') { $childIndent = $Matches[1]; break }
|
||||
}
|
||||
}
|
||||
$trailing = $childObjects.LastChild
|
||||
$ws = $doc.CreateWhitespace("`r`n$childIndent")
|
||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
||||
$childObjects.InsertBefore($ws, $trailing) | Out-Null
|
||||
$childObjects.InsertBefore($newEl, $trailing) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($ws) | Out-Null
|
||||
$childObjects.AppendChild($newEl) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Save parent XML
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$doc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
[System.IO.File]::WriteAllText($parentXmlPath, $text, $utf8Bom)
|
||||
|
||||
Write-Host "[OK] Registered in: $parentXmlPath"
|
||||
} else {
|
||||
Write-Host "[SKIP] Already registered in: $parentXmlPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[WARN] ChildObjects not found in: $parentXmlPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[INFO] No parent XML to register in"
|
||||
}
|
||||
|
||||
# --- 7. Auto-validate ---
|
||||
if (-not $NoValidate) {
|
||||
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\subsystem-validate") "scripts\subsystem-validate.ps1"
|
||||
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
|
||||
if (Test-Path $validateScript) {
|
||||
Write-Host ""
|
||||
Write-Host "--- Running subsystem-validate ---"
|
||||
& powershell.exe -NoProfile -File $validateScript -SubsystemPath $targetXml
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== subsystem-compile summary ==="
|
||||
Write-Host " Name: $objName"
|
||||
Write-Host " UUID: $uuid"
|
||||
Write-Host " Content: $($contentItems.Count) objects"
|
||||
Write-Host " Children: $($children.Count)"
|
||||
Write-Host " File: $targetXml"
|
||||
exit 0
|
||||
@@ -1,288 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def esc_xml(s):
|
||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
def emit_mltext(lines, indent, tag, text):
|
||||
if not text:
|
||||
lines.append(f"{indent}<{tag}/>")
|
||||
return
|
||||
lines.append(f"{indent}<{tag}>")
|
||||
lines.append(f"{indent}\t<v8:item>")
|
||||
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
||||
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
|
||||
lines.append(f"{indent}\t</v8:item>")
|
||||
lines.append(f"{indent}</{tag}>")
|
||||
|
||||
|
||||
def new_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def write_utf8_bom(path, content):
|
||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def split_camel_case(name):
|
||||
if not name:
|
||||
return name
|
||||
result = re.sub(r'([a-z\u0430-\u044f\u0451])([A-Z\u0410-\u042f\u0401])', r'\1 \2', name)
|
||||
if len(result) > 1:
|
||||
result = result[0] + result[1:].lower()
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description='Compile 1C subsystem from JSON definition', allow_abbrev=False)
|
||||
parser.add_argument('-DefinitionFile', type=str, default=None)
|
||||
parser.add_argument('-Value', type=str, default=None)
|
||||
parser.add_argument('-OutputDir', type=str, required=True)
|
||||
parser.add_argument('-Parent', type=str, default=None)
|
||||
parser.add_argument('-NoValidate', action='store_true', default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- 1. Load JSON ---
|
||||
if args.DefinitionFile and args.Value:
|
||||
print("Cannot use both -DefinitionFile and -Value", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not args.DefinitionFile and not args.Value:
|
||||
print("Either -DefinitionFile or -Value is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.DefinitionFile:
|
||||
def_file = args.DefinitionFile
|
||||
if not os.path.isabs(def_file):
|
||||
def_file = os.path.join(os.getcwd(), def_file)
|
||||
if not os.path.exists(def_file):
|
||||
print(f"Definition file not found: {def_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(def_file, 'r', encoding='utf-8-sig') as f:
|
||||
json_text = f.read()
|
||||
else:
|
||||
json_text = args.Value
|
||||
|
||||
defn = json.loads(json_text)
|
||||
|
||||
if not defn.get('name'):
|
||||
print("JSON must have 'name' field", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
obj_name = str(defn['name'])
|
||||
|
||||
# Resolve OutputDir
|
||||
output_dir = args.OutputDir
|
||||
if not os.path.isabs(output_dir):
|
||||
output_dir = os.path.join(os.getcwd(), output_dir)
|
||||
|
||||
# --- 2. Resolve defaults ---
|
||||
synonym = str(defn['synonym']) if defn.get('synonym') else split_camel_case(obj_name)
|
||||
comment = str(defn['comment']) if defn.get('comment') else ''
|
||||
include_help_in_contents = 'true'
|
||||
include_in_ci = str(defn['includeInCommandInterface']).lower() if defn.get('includeInCommandInterface') is not None else 'true'
|
||||
use_one_command = str(defn['useOneCommand']).lower() if defn.get('useOneCommand') is not None else 'false'
|
||||
explanation = str(defn['explanation']) if defn.get('explanation') else ''
|
||||
picture = str(defn['picture']) if defn.get('picture') else ''
|
||||
|
||||
content_items = []
|
||||
if defn.get('content'):
|
||||
for c in defn['content']:
|
||||
content_items.append(str(c))
|
||||
|
||||
children = []
|
||||
if defn.get('children'):
|
||||
for ch in defn['children']:
|
||||
children.append(str(ch))
|
||||
|
||||
# --- 3. Build XML ---
|
||||
uid = new_uuid()
|
||||
lines = []
|
||||
|
||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
lines.append('<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">')
|
||||
lines.append(f'\t<Subsystem uuid="{uid}">')
|
||||
lines.append('\t\t<Properties>')
|
||||
|
||||
# Name
|
||||
lines.append(f'\t\t\t<Name>{esc_xml(obj_name)}</Name>')
|
||||
|
||||
# Synonym
|
||||
emit_mltext(lines, '\t\t\t', 'Synonym', synonym)
|
||||
|
||||
# Comment
|
||||
if comment:
|
||||
lines.append(f'\t\t\t<Comment>{esc_xml(comment)}</Comment>')
|
||||
else:
|
||||
lines.append('\t\t\t<Comment/>')
|
||||
|
||||
# Boolean properties
|
||||
lines.append(f'\t\t\t<IncludeHelpInContents>{include_help_in_contents}</IncludeHelpInContents>')
|
||||
lines.append(f'\t\t\t<IncludeInCommandInterface>{include_in_ci}</IncludeInCommandInterface>')
|
||||
lines.append(f'\t\t\t<UseOneCommand>{use_one_command}</UseOneCommand>')
|
||||
|
||||
# Explanation
|
||||
emit_mltext(lines, '\t\t\t', 'Explanation', explanation)
|
||||
|
||||
# Picture
|
||||
if picture:
|
||||
lines.append('\t\t\t<Picture>')
|
||||
lines.append(f'\t\t\t\t<xr:Ref>{picture}</xr:Ref>')
|
||||
lines.append('\t\t\t\t<xr:LoadTransparent>false</xr:LoadTransparent>')
|
||||
lines.append('\t\t\t</Picture>')
|
||||
else:
|
||||
lines.append('\t\t\t<Picture/>')
|
||||
|
||||
# Content
|
||||
if len(content_items) > 0:
|
||||
lines.append('\t\t\t<Content>')
|
||||
for item in content_items:
|
||||
lines.append(f'\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(item)}</xr:Item>')
|
||||
lines.append('\t\t\t</Content>')
|
||||
else:
|
||||
lines.append('\t\t\t<Content/>')
|
||||
|
||||
lines.append('\t\t</Properties>')
|
||||
|
||||
# ChildObjects
|
||||
if len(children) > 0:
|
||||
lines.append('\t\t<ChildObjects>')
|
||||
for ch in children:
|
||||
lines.append(f'\t\t\t<Subsystem>{esc_xml(ch)}</Subsystem>')
|
||||
lines.append('\t\t</ChildObjects>')
|
||||
else:
|
||||
lines.append('\t\t<ChildObjects/>')
|
||||
|
||||
lines.append('\t</Subsystem>')
|
||||
lines.append('</MetaDataObject>')
|
||||
|
||||
# --- 4. Write files ---
|
||||
parent = args.Parent
|
||||
|
||||
if parent:
|
||||
# Nested subsystem
|
||||
if not os.path.isabs(parent):
|
||||
parent = os.path.join(os.getcwd(), parent)
|
||||
if not os.path.exists(parent):
|
||||
print(f"Parent subsystem not found: {parent}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
parent_dir = os.path.dirname(parent)
|
||||
parent_base_name = os.path.splitext(os.path.basename(parent))[0]
|
||||
subs_dir = os.path.join(parent_dir, parent_base_name, 'Subsystems')
|
||||
else:
|
||||
# Top-level subsystem
|
||||
subs_dir = os.path.join(output_dir, 'Subsystems')
|
||||
|
||||
os.makedirs(subs_dir, exist_ok=True)
|
||||
|
||||
target_xml = os.path.join(subs_dir, f'{obj_name}.xml')
|
||||
|
||||
# Write XML
|
||||
xml_content = '\n'.join(lines) + '\n'
|
||||
write_utf8_bom(target_xml, xml_content)
|
||||
print(f"[OK] Created: {target_xml}")
|
||||
|
||||
# Create subdirectory if children exist
|
||||
if len(children) > 0:
|
||||
child_subs_dir = os.path.join(subs_dir, obj_name, 'Subsystems')
|
||||
if not os.path.exists(child_subs_dir):
|
||||
os.makedirs(child_subs_dir, exist_ok=True)
|
||||
print(f"[OK] Created directory: {child_subs_dir}")
|
||||
|
||||
# --- 5. Register in parent ---
|
||||
parent_xml_path = None
|
||||
if parent:
|
||||
parent_xml_path = parent
|
||||
else:
|
||||
config_xml = os.path.join(output_dir, 'Configuration.xml')
|
||||
if os.path.exists(config_xml):
|
||||
parent_xml_path = config_xml
|
||||
|
||||
if parent_xml_path and os.path.exists(parent_xml_path):
|
||||
with open(parent_xml_path, 'r', encoding='utf-8-sig') as f:
|
||||
raw_text = f.read()
|
||||
|
||||
doc = ET.ElementTree(ET.fromstring(raw_text))
|
||||
root = doc.getroot()
|
||||
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
|
||||
|
||||
# Find ChildObjects
|
||||
child_objects = None
|
||||
if parent:
|
||||
for sub in root.iter(f'{{{md_ns}}}Subsystem'):
|
||||
child_objects = sub.find(f'{{{md_ns}}}ChildObjects')
|
||||
break
|
||||
else:
|
||||
for cfg in root.iter(f'{{{md_ns}}}Configuration'):
|
||||
child_objects = cfg.find(f'{{{md_ns}}}ChildObjects')
|
||||
break
|
||||
|
||||
if child_objects is not None:
|
||||
# Check if already registered
|
||||
already_exists = False
|
||||
for child in child_objects:
|
||||
if child.tag == f'{{{md_ns}}}Subsystem' and child.text == obj_name:
|
||||
already_exists = True
|
||||
break
|
||||
|
||||
if not already_exists:
|
||||
new_el = ET.SubElement(child_objects, f'{{{md_ns}}}Subsystem')
|
||||
new_el.text = obj_name
|
||||
|
||||
# Re-serialize with whitespace preservation via raw text manipulation instead
|
||||
# Since ElementTree doesn't preserve whitespace well, use regex-based insertion
|
||||
# Find </ChildObjects> or <ChildObjects/> and inject
|
||||
pass # Fall through to raw text approach below
|
||||
|
||||
if not already_exists:
|
||||
# Use raw text manipulation to preserve formatting
|
||||
if '<ChildObjects/>' in raw_text:
|
||||
replacement = f'<ChildObjects>\n\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n\t\t</ChildObjects>'
|
||||
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
|
||||
elif '</ChildObjects>' in raw_text:
|
||||
insert_line = f'\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n'
|
||||
raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1)
|
||||
|
||||
write_utf8_bom(parent_xml_path, raw_text)
|
||||
print(f"[OK] Registered in: {parent_xml_path}")
|
||||
else:
|
||||
print(f"[SKIP] Already registered in: {parent_xml_path}")
|
||||
else:
|
||||
print(f"[WARN] ChildObjects not found in: {parent_xml_path}")
|
||||
else:
|
||||
print("[INFO] No parent XML to register in")
|
||||
|
||||
# --- 6. Auto-validate ---
|
||||
if not args.NoValidate:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
validate_script = os.path.normpath(os.path.join(script_dir, '..', '..', 'subsystem-validate', 'scripts', 'subsystem-validate.ps1'))
|
||||
if os.path.exists(validate_script):
|
||||
print()
|
||||
print("--- Running subsystem-validate ---")
|
||||
os.system(f'powershell.exe -NoProfile -File "{validate_script}" -SubsystemPath "{target_xml}"')
|
||||
|
||||
# --- 7. Summary ---
|
||||
print()
|
||||
print("=== subsystem-compile summary ===")
|
||||
print(f" Name: {obj_name}")
|
||||
print(f" UUID: {uid}")
|
||||
print(f" Content: {len(content_items)} objects")
|
||||
print(f" Children: {len(children)}")
|
||||
print(f" File: {target_xml}")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,414 +0,0 @@
|
||||
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SubsystemPath,
|
||||
[string]$DefinitionFile,
|
||||
[ValidateSet("add-content","remove-content","add-child","remove-child","set-property")]
|
||||
[string]$Operation,
|
||||
[string]$Value,
|
||||
[switch]$NoValidate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Mode validation ---
|
||||
if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 }
|
||||
if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 }
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) {
|
||||
$SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath
|
||||
}
|
||||
if (Test-Path $SubsystemPath -PathType Container) {
|
||||
$dirName = Split-Path $SubsystemPath -Leaf
|
||||
$candidate = Join-Path $SubsystemPath "$dirName.xml"
|
||||
$sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml"
|
||||
if (Test-Path $candidate) { $SubsystemPath = $candidate }
|
||||
elseif (Test-Path $sibling) { $SubsystemPath = $sibling }
|
||||
else { Write-Error "No $dirName.xml found in directory or as sibling"; exit 1 }
|
||||
}
|
||||
# File not found — check Dir/Name/Name.xml → Dir/Name.xml
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
$fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath)
|
||||
$pd = Split-Path $SubsystemPath
|
||||
if ($fn -eq (Split-Path $pd -Leaf)) {
|
||||
$c = Join-Path (Split-Path $pd) "$fn.xml"
|
||||
if (Test-Path $c) { $SubsystemPath = $c }
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path $SubsystemPath)) { Write-Error "File not found: $SubsystemPath"; exit 1 }
|
||||
$resolvedPath = (Resolve-Path $SubsystemPath).Path
|
||||
|
||||
# --- Load XML with PreserveWhitespace ---
|
||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$script:xmlDoc.PreserveWhitespace = $true
|
||||
$script:xmlDoc.Load($resolvedPath)
|
||||
|
||||
$script:addCount = 0
|
||||
$script:removeCount = 0
|
||||
$script:modifyCount = 0
|
||||
|
||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||
|
||||
# --- Detect structure ---
|
||||
$root = $script:xmlDoc.DocumentElement
|
||||
$script:mdNs = "http://v8.1c.ru/8.3/MDClasses"
|
||||
$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
$script:v8Ns = "http://v8.1c.ru/8.1/data/core"
|
||||
|
||||
$script:sub = $null
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") {
|
||||
$script:sub = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $script:sub) { Write-Error "No <Subsystem> element found"; exit 1 }
|
||||
|
||||
$script:propsEl = $null
|
||||
$script:childObjsEl = $null
|
||||
foreach ($child in $script:sub.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Properties") { $script:propsEl = $child }
|
||||
if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child }
|
||||
}
|
||||
|
||||
$script:objName = ""
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") {
|
||||
$script:objName = $child.InnerText.Trim(); break
|
||||
}
|
||||
}
|
||||
Info "Subsystem: $($script:objName)"
|
||||
|
||||
# --- XML manipulation helpers (from meta-edit pattern) ---
|
||||
function Import-Fragment([string]$xmlString) {
|
||||
$wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString</_W>"
|
||||
$frag = New-Object System.Xml.XmlDocument
|
||||
$frag.PreserveWhitespace = $true
|
||||
$frag.LoadXml($wrapper)
|
||||
$nodes = @()
|
||||
foreach ($child in $frag.DocumentElement.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element') {
|
||||
$nodes += $script:xmlDoc.ImportNode($child, $true)
|
||||
}
|
||||
}
|
||||
return ,$nodes
|
||||
}
|
||||
|
||||
function Get-ChildIndent($container) {
|
||||
foreach ($child in $container.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
|
||||
if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] }
|
||||
if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] }
|
||||
}
|
||||
}
|
||||
$depth = 0; $current = $container
|
||||
while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode }
|
||||
return "`t" * ($depth + 1)
|
||||
}
|
||||
|
||||
function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) {
|
||||
$ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent")
|
||||
if ($refNode) {
|
||||
$container.InsertBefore($ws, $refNode) | Out-Null
|
||||
$container.InsertBefore($newNode, $ws) | Out-Null
|
||||
} else {
|
||||
$trailing = $container.LastChild
|
||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
||||
$container.InsertBefore($ws, $trailing) | Out-Null
|
||||
$container.InsertBefore($newNode, $trailing) | Out-Null
|
||||
} else {
|
||||
$container.AppendChild($ws) | Out-Null
|
||||
$container.AppendChild($newNode) | Out-Null
|
||||
$parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" }
|
||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent")
|
||||
$container.AppendChild($closeWs) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-NodeWithWhitespace($node) {
|
||||
$parent = $node.ParentNode
|
||||
$prev = $node.PreviousSibling
|
||||
$next = $node.NextSibling
|
||||
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
|
||||
$parent.RemoveChild($prev) | Out-Null
|
||||
} elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) {
|
||||
$parent.RemoveChild($next) | Out-Null
|
||||
}
|
||||
$parent.RemoveChild($node) | Out-Null
|
||||
}
|
||||
|
||||
function Expand-SelfClosingElement($container, $parentIndent) {
|
||||
# If the element is self-closing (empty), add whitespace for children
|
||||
if (-not $container.HasChildNodes -or $container.IsEmpty) {
|
||||
$childIndent = "$parentIndent`t"
|
||||
# The element is self-closing; we need to add something to make it non-empty
|
||||
# Adding a whitespace node will force opening+closing tags
|
||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent")
|
||||
$container.AppendChild($closeWs) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# --- Parse value: string or JSON array ---
|
||||
function Parse-ValueList([string]$val) {
|
||||
$val = $val.Trim()
|
||||
if ($val.StartsWith("[")) {
|
||||
$arr = $val | ConvertFrom-Json
|
||||
$result = @(); foreach ($item in $arr) { $result += "$item" }
|
||||
return ,$result
|
||||
}
|
||||
return @($val)
|
||||
}
|
||||
|
||||
# --- Operations ---
|
||||
function Do-AddContent([string[]]$items) {
|
||||
$contentEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") {
|
||||
$contentEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 }
|
||||
|
||||
# Get existing items for dedup
|
||||
$existing = @()
|
||||
foreach ($child in $contentEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item") {
|
||||
$existing += $child.InnerText.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
# Determine indentation
|
||||
$propsIndent = Get-ChildIndent $script:propsEl
|
||||
$contentIndent = "$propsIndent`t"
|
||||
|
||||
# Expand self-closing if needed
|
||||
if (-not $contentEl.HasChildNodes -or $contentEl.IsEmpty) {
|
||||
Expand-SelfClosingElement $contentEl $propsIndent
|
||||
$contentIndent = "$propsIndent`t"
|
||||
} else {
|
||||
$contentIndent = Get-ChildIndent $contentEl
|
||||
}
|
||||
|
||||
foreach ($item in $items) {
|
||||
if ($item -in $existing) {
|
||||
Warn "Content already contains: $item"
|
||||
continue
|
||||
}
|
||||
$fragXml = "<xr:Item xsi:type=`"xr:MDObjectRef`">$item</xr:Item>"
|
||||
$nodes = Import-Fragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $contentEl $nodes[0] $null $contentIndent
|
||||
$script:addCount++
|
||||
Info "Added content: $item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Do-RemoveContent([string[]]$items) {
|
||||
$contentEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") {
|
||||
$contentEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 }
|
||||
|
||||
foreach ($item in $items) {
|
||||
$found = $false
|
||||
foreach ($child in @($contentEl.ChildNodes)) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item" -and $child.InnerText.Trim() -eq $item) {
|
||||
Remove-NodeWithWhitespace $child
|
||||
$script:removeCount++
|
||||
Info "Removed content: $item"
|
||||
$found = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found) { Warn "Content item not found: $item" }
|
||||
}
|
||||
}
|
||||
|
||||
function Do-AddChild([string]$childName) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
# Dedup check
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) {
|
||||
Warn "ChildObjects already contains: $childName"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$subIndent = Get-ChildIndent $script:sub
|
||||
if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) {
|
||||
Expand-SelfClosingElement $script:childObjsEl $subIndent
|
||||
}
|
||||
$childIndent = Get-ChildIndent $script:childObjsEl
|
||||
|
||||
$newEl = $script:xmlDoc.CreateElement("Subsystem", $script:mdNs)
|
||||
$newEl.InnerText = $childName
|
||||
Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent
|
||||
$script:addCount++
|
||||
Info "Added child subsystem: $childName"
|
||||
}
|
||||
|
||||
function Do-RemoveChild([string]$childName) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
$found = $false
|
||||
foreach ($child in @($script:childObjsEl.ChildNodes)) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) {
|
||||
Remove-NodeWithWhitespace $child
|
||||
$script:removeCount++
|
||||
Info "Removed child subsystem: $childName"
|
||||
$found = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found) { Warn "Child subsystem not found: $childName" }
|
||||
}
|
||||
|
||||
function Do-SetProperty([string]$jsonVal) {
|
||||
$propDef = $jsonVal | ConvertFrom-Json
|
||||
$propName = "$($propDef.name)"
|
||||
$propValue = "$($propDef.value)"
|
||||
|
||||
$propEl = $null
|
||||
foreach ($child in $script:propsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) {
|
||||
$propEl = $child; break
|
||||
}
|
||||
}
|
||||
if (-not $propEl) {
|
||||
Write-Error "Property '$propName' not found in Properties"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$boolProps = @("IncludeInCommandInterface","UseOneCommand","IncludeHelpInContents")
|
||||
if ($propName -in $boolProps) {
|
||||
$propEl.InnerText = $propValue.ToLower()
|
||||
$script:modifyCount++
|
||||
Info "Set $propName = $propValue"
|
||||
return
|
||||
}
|
||||
|
||||
$mlProps = @("Synonym","Explanation")
|
||||
if ($propName -in $mlProps) {
|
||||
if (-not $propValue) {
|
||||
# Clear - make self-closing
|
||||
$propEl.InnerXml = ""
|
||||
$script:modifyCount++
|
||||
Info "Cleared $propName"
|
||||
} else {
|
||||
$indent = Get-ChildIndent $script:propsEl
|
||||
$mlXml = "`r`n$indent`t<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$([System.Security.SecurityElement]::Escape($propValue))</v8:content>`r`n$indent`t</v8:item>`r`n$indent"
|
||||
$propEl.InnerXml = $mlXml
|
||||
$script:modifyCount++
|
||||
Info "Set $propName = `"$propValue`""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($propName -eq "Comment") {
|
||||
if (-not $propValue) { $propEl.InnerXml = "" }
|
||||
else { $propEl.InnerText = $propValue }
|
||||
$script:modifyCount++
|
||||
Info "Set Comment = `"$propValue`""
|
||||
return
|
||||
}
|
||||
|
||||
if ($propName -eq "Picture") {
|
||||
if (-not $propValue) {
|
||||
$propEl.InnerXml = ""
|
||||
} else {
|
||||
$indent = Get-ChildIndent $script:propsEl
|
||||
$picXml = "`r`n$indent`t<xr:Ref>$propValue</xr:Ref>`r`n$indent`t<xr:LoadTransparent>false</xr:LoadTransparent>`r`n$indent"
|
||||
$propEl.InnerXml = $picXml
|
||||
}
|
||||
$script:modifyCount++
|
||||
Info "Set Picture = `"$propValue`""
|
||||
return
|
||||
}
|
||||
|
||||
# Generic text property
|
||||
$propEl.InnerText = $propValue
|
||||
$script:modifyCount++
|
||||
Info "Set $propName = `"$propValue`""
|
||||
}
|
||||
|
||||
# --- Execute operations ---
|
||||
$operations = @()
|
||||
if ($DefinitionFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
||||
}
|
||||
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
||||
$ops = $jsonText | ConvertFrom-Json
|
||||
if ($ops -is [System.Array]) {
|
||||
foreach ($op in $ops) { $operations += $op }
|
||||
} else {
|
||||
$operations += $ops
|
||||
}
|
||||
} else {
|
||||
$operations += @{ operation = $Operation; value = $Value }
|
||||
}
|
||||
|
||||
foreach ($op in $operations) {
|
||||
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
|
||||
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
|
||||
|
||||
switch ($opName) {
|
||||
"add-content" { Do-AddContent (Parse-ValueList $opValue) }
|
||||
"remove-content" { Do-RemoveContent (Parse-ValueList $opValue) }
|
||||
"add-child" { Do-AddChild $opValue }
|
||||
"remove-child" { Do-RemoveChild $opValue }
|
||||
"set-property" { Do-SetProperty $opValue }
|
||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Save ---
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$script:xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||
Info "Saved: $resolvedPath"
|
||||
|
||||
# --- Auto-validate ---
|
||||
if (-not $NoValidate) {
|
||||
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\subsystem-validate") "scripts\subsystem-validate.ps1"
|
||||
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
|
||||
if (Test-Path $validateScript) {
|
||||
Write-Host ""
|
||||
Write-Host "--- Running subsystem-validate ---"
|
||||
& powershell.exe -NoProfile -File $validateScript -SubsystemPath $resolvedPath
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host "=== subsystem-edit summary ==="
|
||||
Write-Host " Subsystem: $($script:objName)"
|
||||
Write-Host " Added: $($script:addCount)"
|
||||
Write-Host " Removed: $($script:removeCount)"
|
||||
Write-Host " Modified: $($script:modifyCount)"
|
||||
exit 0
|
||||
@@ -1,464 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from lxml import etree
|
||||
|
||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
NSMAP_WRAPPER = {
|
||||
None: MD_NS,
|
||||
"xsi": XSI_NS,
|
||||
"v8": V8_NS,
|
||||
"xr": XR_NS,
|
||||
"xs": XS_NS,
|
||||
}
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
|
||||
def info(msg):
|
||||
print(f"[INFO] {msg}")
|
||||
|
||||
|
||||
def warn(msg):
|
||||
print(f"[WARN] {msg}")
|
||||
|
||||
|
||||
def get_child_indent(container):
|
||||
"""Detect indentation of children inside a container element."""
|
||||
if container.text and "\n" in container.text:
|
||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||
if after_nl and not after_nl.strip():
|
||||
return after_nl
|
||||
for child in container:
|
||||
if child.tail and "\n" in child.tail:
|
||||
after_nl = child.tail.rsplit("\n", 1)[-1]
|
||||
if after_nl and not after_nl.strip():
|
||||
return after_nl
|
||||
# Fallback: count depth
|
||||
depth = 0
|
||||
current = container
|
||||
while current is not None:
|
||||
depth += 1
|
||||
current = current.getparent()
|
||||
return "\t" * depth
|
||||
|
||||
|
||||
def insert_before_closing(container, new_el, child_indent):
|
||||
"""Insert new_el before the closing tag of container, with proper indentation."""
|
||||
children = list(container)
|
||||
if len(children) == 0:
|
||||
# Empty element: set text to newline+indent, tail of new_el to newline+parent_indent
|
||||
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
|
||||
container.text = "\r\n" + child_indent
|
||||
new_el.tail = "\r\n" + parent_indent
|
||||
container.append(new_el)
|
||||
else:
|
||||
last = children[-1]
|
||||
new_el.tail = last.tail
|
||||
last.tail = "\r\n" + child_indent
|
||||
container.append(new_el)
|
||||
|
||||
|
||||
def remove_with_indent(el):
|
||||
"""Remove element and clean up surrounding whitespace."""
|
||||
parent = el.getparent()
|
||||
prev = el.getprevious()
|
||||
if prev is not None:
|
||||
# Transfer el.tail to prev.tail
|
||||
if el.tail and el.tail.strip() == "":
|
||||
pass # just drop extra whitespace
|
||||
prev.tail = el.tail if el.tail and el.tail.strip() else (prev.tail or "")
|
||||
# Actually try to keep the prev's tail as the closing indent
|
||||
# Better approach: set prev.tail to what el.tail was (newline+indent of next or closing)
|
||||
if el.tail:
|
||||
prev.tail = el.tail
|
||||
else:
|
||||
# First child: adjust parent.text
|
||||
if el.tail:
|
||||
parent.text = el.tail
|
||||
parent.remove(el)
|
||||
|
||||
|
||||
def expand_self_closing(container, parent_indent):
|
||||
"""If container is self-closing (no children, no text), add closing whitespace."""
|
||||
if len(container) == 0 and not (container.text and container.text.strip()):
|
||||
container.text = "\r\n" + parent_indent
|
||||
|
||||
|
||||
def import_fragment(xml_string, doc_root):
|
||||
"""Parse an XML fragment in the MD namespace context and return elements."""
|
||||
wrapper = (
|
||||
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
|
||||
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
|
||||
)
|
||||
frag = etree.fromstring(wrapper.encode("utf-8"))
|
||||
nodes = []
|
||||
for child in frag:
|
||||
nodes.append(child)
|
||||
return nodes
|
||||
|
||||
|
||||
def parse_value_list(val):
|
||||
"""Parse a string or JSON array into a list of strings."""
|
||||
val = val.strip()
|
||||
if val.startswith("["):
|
||||
arr = json.loads(val)
|
||||
return [str(item) for item in arr]
|
||||
return [val]
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Edit existing 1C subsystem XML", allow_abbrev=False)
|
||||
parser.add_argument("-SubsystemPath", required=True)
|
||||
parser.add_argument("-DefinitionFile", default=None)
|
||||
parser.add_argument("-Operation", default=None, choices=["add-content", "remove-content", "add-child", "remove-child", "set-property"])
|
||||
parser.add_argument("-Value", default=None)
|
||||
parser.add_argument("-NoValidate", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Mode validation ---
|
||||
if args.DefinitionFile and args.Operation:
|
||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not args.DefinitionFile and not args.Operation:
|
||||
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Resolve path ---
|
||||
subsystem_path = args.SubsystemPath
|
||||
if not os.path.isabs(subsystem_path):
|
||||
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
|
||||
|
||||
if os.path.isdir(subsystem_path):
|
||||
dir_name = os.path.basename(subsystem_path)
|
||||
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
|
||||
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
|
||||
if os.path.isfile(candidate):
|
||||
subsystem_path = candidate
|
||||
elif os.path.isfile(sibling):
|
||||
subsystem_path = sibling
|
||||
else:
|
||||
print(f"No {dir_name}.xml found in directory or as sibling", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isfile(subsystem_path):
|
||||
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
|
||||
pd = os.path.dirname(subsystem_path)
|
||||
if fn == os.path.basename(pd):
|
||||
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
|
||||
if os.path.isfile(c):
|
||||
subsystem_path = c
|
||||
|
||||
if not os.path.isfile(subsystem_path):
|
||||
print(f"File not found: {subsystem_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
resolved_path = os.path.abspath(subsystem_path)
|
||||
|
||||
# --- Load XML ---
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
add_count = 0
|
||||
remove_count = 0
|
||||
modify_count = 0
|
||||
|
||||
# --- Detect structure ---
|
||||
sub = None
|
||||
for child in xml_root:
|
||||
if isinstance(child.tag, str) and localname(child) == "Subsystem":
|
||||
sub = child
|
||||
break
|
||||
if sub is None:
|
||||
print("No <Subsystem> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
props_el = None
|
||||
child_objs_el = None
|
||||
for child in sub:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if localname(child) == "Properties":
|
||||
props_el = child
|
||||
if localname(child) == "ChildObjects":
|
||||
child_objs_el = child
|
||||
|
||||
obj_name = ""
|
||||
if props_el is not None:
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "Name":
|
||||
obj_name = (child.text or "").strip()
|
||||
break
|
||||
info(f"Subsystem: {obj_name}")
|
||||
|
||||
# --- Operations ---
|
||||
def do_add_content(items):
|
||||
nonlocal add_count
|
||||
content_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "Content":
|
||||
content_el = child
|
||||
break
|
||||
if content_el is None:
|
||||
print("No <Content> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
existing = set()
|
||||
for child in content_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "Item":
|
||||
existing.add((child.text or "").strip())
|
||||
|
||||
props_indent = get_child_indent(props_el)
|
||||
if len(content_el) == 0 and not (content_el.text and content_el.text.strip()):
|
||||
expand_self_closing(content_el, props_indent)
|
||||
content_indent = get_child_indent(content_el)
|
||||
|
||||
for item in items:
|
||||
if item in existing:
|
||||
warn(f"Content already contains: {item}")
|
||||
continue
|
||||
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{item}</xr:Item>'
|
||||
nodes = import_fragment(frag_xml, xml_root)
|
||||
if nodes:
|
||||
insert_before_closing(content_el, nodes[0], content_indent)
|
||||
add_count += 1
|
||||
info(f"Added content: {item}")
|
||||
|
||||
def do_remove_content(items):
|
||||
nonlocal remove_count
|
||||
content_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "Content":
|
||||
content_el = child
|
||||
break
|
||||
if content_el is None:
|
||||
print("No <Content> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for item in items:
|
||||
found = False
|
||||
for child in list(content_el):
|
||||
if isinstance(child.tag, str) and localname(child) == "Item" and (child.text or "").strip() == item:
|
||||
remove_with_indent(child)
|
||||
remove_count += 1
|
||||
info(f"Removed content: {item}")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
warn(f"Content item not found: {item}")
|
||||
|
||||
def do_add_child(child_name):
|
||||
nonlocal add_count
|
||||
if child_objs_el is None:
|
||||
print("No <ChildObjects> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for child in child_objs_el:
|
||||
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
|
||||
warn(f"ChildObjects already contains: {child_name}")
|
||||
return
|
||||
|
||||
sub_indent = get_child_indent(sub)
|
||||
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
|
||||
expand_self_closing(child_objs_el, sub_indent)
|
||||
ci = get_child_indent(child_objs_el)
|
||||
|
||||
new_el = etree.SubElement(child_objs_el, f"{{{MD_NS}}}Subsystem")
|
||||
# Actually we need to use insert_before_closing pattern
|
||||
child_objs_el.remove(new_el)
|
||||
new_el = etree.Element(f"{{{MD_NS}}}Subsystem")
|
||||
new_el.text = child_name
|
||||
insert_before_closing(child_objs_el, new_el, ci)
|
||||
add_count += 1
|
||||
info(f"Added child subsystem: {child_name}")
|
||||
|
||||
def do_remove_child(child_name):
|
||||
nonlocal remove_count
|
||||
if child_objs_el is None:
|
||||
print("No <ChildObjects> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
found = False
|
||||
for child in list(child_objs_el):
|
||||
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
|
||||
remove_with_indent(child)
|
||||
remove_count += 1
|
||||
info(f"Removed child subsystem: {child_name}")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
warn(f"Child subsystem not found: {child_name}")
|
||||
|
||||
def do_set_property(json_val):
|
||||
nonlocal modify_count
|
||||
prop_def = json.loads(json_val)
|
||||
prop_name = str(prop_def["name"])
|
||||
prop_value = str(prop_def.get("value", ""))
|
||||
|
||||
prop_el = None
|
||||
for child in props_el:
|
||||
if isinstance(child.tag, str) and localname(child) == prop_name:
|
||||
prop_el = child
|
||||
break
|
||||
if prop_el is None:
|
||||
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
bool_props = ["IncludeInCommandInterface", "UseOneCommand", "IncludeHelpInContents"]
|
||||
if prop_name in bool_props:
|
||||
prop_el.text = prop_value.lower()
|
||||
# Clear children
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
modify_count += 1
|
||||
info(f"Set {prop_name} = {prop_value}")
|
||||
return
|
||||
|
||||
ml_props = ["Synonym", "Explanation"]
|
||||
if prop_name in ml_props:
|
||||
if not prop_value:
|
||||
# Clear - make self-closing
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
prop_el.text = None
|
||||
modify_count += 1
|
||||
info(f"Cleared {prop_name}")
|
||||
else:
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
indent = get_child_indent(props_el)
|
||||
|
||||
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
|
||||
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
|
||||
lang_el.text = "ru"
|
||||
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
|
||||
content_el.text = prop_value
|
||||
|
||||
# Set whitespace
|
||||
prop_el.text = "\r\n" + indent + "\t"
|
||||
item_el.text = "\r\n" + indent + "\t\t"
|
||||
lang_el.tail = "\r\n" + indent + "\t\t"
|
||||
content_el.tail = "\r\n" + indent + "\t"
|
||||
item_el.tail = "\r\n" + indent
|
||||
|
||||
modify_count += 1
|
||||
info(f'Set {prop_name} = "{prop_value}"')
|
||||
return
|
||||
|
||||
if prop_name == "Comment":
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
if not prop_value:
|
||||
prop_el.text = None
|
||||
else:
|
||||
prop_el.text = prop_value
|
||||
modify_count += 1
|
||||
info(f'Set Comment = "{prop_value}"')
|
||||
return
|
||||
|
||||
if prop_name == "Picture":
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
if not prop_value:
|
||||
prop_el.text = None
|
||||
else:
|
||||
indent = get_child_indent(props_el)
|
||||
ref_el = etree.SubElement(prop_el, f"{{{XR_NS}}}Ref")
|
||||
ref_el.text = prop_value
|
||||
load_el = etree.SubElement(prop_el, f"{{{XR_NS}}}LoadTransparent")
|
||||
load_el.text = "false"
|
||||
prop_el.text = "\r\n" + indent + "\t"
|
||||
ref_el.tail = "\r\n" + indent + "\t"
|
||||
load_el.tail = "\r\n" + indent
|
||||
modify_count += 1
|
||||
info(f'Set Picture = "{prop_value}"')
|
||||
return
|
||||
|
||||
# Generic text property
|
||||
for ch in list(prop_el):
|
||||
prop_el.remove(ch)
|
||||
prop_el.text = prop_value
|
||||
modify_count += 1
|
||||
info(f'Set {prop_name} = "{prop_value}"')
|
||||
|
||||
# --- Execute operations ---
|
||||
operations = []
|
||||
if args.DefinitionFile:
|
||||
def_file = args.DefinitionFile
|
||||
if not os.path.isabs(def_file):
|
||||
def_file = os.path.join(os.getcwd(), def_file)
|
||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||
ops = json.loads(fh.read())
|
||||
if isinstance(ops, list):
|
||||
operations = ops
|
||||
else:
|
||||
operations = [ops]
|
||||
else:
|
||||
operations = [{"operation": args.Operation, "value": args.Value or ""}]
|
||||
|
||||
for op in operations:
|
||||
op_name = op.get("operation", args.Operation or "")
|
||||
op_value = op.get("value", args.Value or "")
|
||||
|
||||
if op_name == "add-content":
|
||||
do_add_content(parse_value_list(op_value))
|
||||
elif op_name == "remove-content":
|
||||
do_remove_content(parse_value_list(op_value))
|
||||
elif op_name == "add-child":
|
||||
do_add_child(op_value)
|
||||
elif op_name == "remove-child":
|
||||
do_remove_child(op_value)
|
||||
elif op_name == "set-property":
|
||||
do_set_property(op_value)
|
||||
else:
|
||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Save ---
|
||||
save_xml_bom(tree, resolved_path)
|
||||
info(f"Saved: {resolved_path}")
|
||||
|
||||
# --- Auto-validate ---
|
||||
if not args.NoValidate:
|
||||
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "subsystem-validate", "scripts", "subsystem-validate.py"))
|
||||
if os.path.isfile(validate_script):
|
||||
print()
|
||||
print("--- Running subsystem-validate ---")
|
||||
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
|
||||
|
||||
# --- Summary ---
|
||||
print()
|
||||
print("=== subsystem-edit summary ===")
|
||||
print(f" Subsystem: {obj_name}")
|
||||
print(f" Added: {add_count}")
|
||||
print(f" Removed: {remove_count}")
|
||||
print(f" Modified: {modify_count}")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
name: subsystem-validate
|
||||
description: Валидация подсистемы 1С. Используй после создания или модификации подсистемы для проверки корректности
|
||||
argument-hint: <SubsystemPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /subsystem-validate — валидация подсистемы 1С
|
||||
|
||||
Проверяет структурную корректность XML-файла подсистемы из выгрузки конфигурации.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|---------------|:-----:|---------|--------------------------------------------|
|
||||
| SubsystemPath | да | — | Путь к XML-файлу подсистемы |
|
||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".claude/skills/subsystem-validate/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи"
|
||||
powershell.exe -NoProfile -File ".claude/skills/subsystem-validate/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи.xml"
|
||||
```
|
||||
|
||||
## Проверки (13)
|
||||
|
||||
| # | Проверка | Серьёзность |
|
||||
|---|----------|-------------|
|
||||
| 1 | XML well-formedness + root structure (MetaDataObject/Subsystem) | ERROR |
|
||||
| 2 | Properties — 9 обязательных свойств | ERROR |
|
||||
| 3 | Name — непустой, валидный идентификатор | ERROR |
|
||||
| 4 | Synonym — непустой (хотя бы один v8:item) | WARN |
|
||||
| 5 | Булевы свойства — содержат true/false | ERROR |
|
||||
| 6 | Content — формат xr:Item, xsi:type | ERROR |
|
||||
| 7 | Content — нет дубликатов | WARN |
|
||||
| 8 | ChildObjects — элементы непустые | ERROR |
|
||||
| 9 | ChildObjects — нет дубликатов | WARN |
|
||||
| 10 | ChildObjects → файлы существуют | WARN |
|
||||
| 11 | CommandInterface.xml — well-formedness | ERROR |
|
||||
| 12 | Picture — формат ссылки | ERROR |
|
||||
| 13 | UseOneCommand=true → ровно 1 элемент в Content | ERROR |
|
||||
|
||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
||||
@@ -1,224 +0,0 @@
|
||||
# template-add v1.1 — Add template to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias("ProcessorName")]
|
||||
[string]$ObjectName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$TemplateName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")]
|
||||
[string]$TemplateType,
|
||||
|
||||
[string]$Synonym = $TemplateName,
|
||||
|
||||
[string]$SrcDir = "src",
|
||||
|
||||
[switch]$SetMainSKD
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Маппинг типов ---
|
||||
|
||||
$typeMap = @{
|
||||
"HTML" = @{ TemplateType = "HTMLDocument"; Ext = ".html" }
|
||||
"Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" }
|
||||
"SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" }
|
||||
"BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" }
|
||||
"DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" }
|
||||
}
|
||||
|
||||
$tmpl = $typeMap[$TemplateType]
|
||||
|
||||
# --- Проверки ---
|
||||
|
||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
||||
if (-not (Test-Path $rootXmlPath)) {
|
||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$processorDir = Join-Path $SrcDir $ObjectName
|
||||
$templatesDir = Join-Path $processorDir "Templates"
|
||||
$templateMetaPath = Join-Path $templatesDir "$TemplateName.xml"
|
||||
|
||||
if (Test-Path $templateMetaPath) {
|
||||
Write-Error "Макет уже существует: $templateMetaPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Создание каталогов ---
|
||||
|
||||
$templateExtDir = Join-Path (Join-Path $templatesDir $TemplateName) "Ext"
|
||||
New-Item -ItemType Directory -Path $templateExtDir -Force | Out-Null
|
||||
|
||||
# --- Кодировка ---
|
||||
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
# --- 1. Метаданные макета (Templates/<TemplateName>.xml) ---
|
||||
|
||||
$templateUuid = [guid]::NewGuid().ToString()
|
||||
|
||||
$templateMetaXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Template uuid="$templateUuid">
|
||||
<Properties>
|
||||
<Name>$TemplateName</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>$Synonym</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<TemplateType>$($tmpl.TemplateType)</TemplateType>
|
||||
</Properties>
|
||||
</Template>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
[System.IO.File]::WriteAllText($templateMetaPath, $templateMetaXml, $encBom)
|
||||
|
||||
# --- 2. Содержимое макета (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
||||
|
||||
$templateFilePath = Join-Path $templateExtDir "Template$($tmpl.Ext)"
|
||||
|
||||
switch ($TemplateType) {
|
||||
"HTML" {
|
||||
$content = @"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
||||
}
|
||||
"Text" {
|
||||
[System.IO.File]::WriteAllText($templateFilePath, "", $encBom)
|
||||
}
|
||||
"SpreadsheetDocument" {
|
||||
$content = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document" xmlns:ss="http://v8.1c.ru/spreadsheet/document" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
</SpreadsheetDocument>
|
||||
"@
|
||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
||||
}
|
||||
"BinaryData" {
|
||||
[System.IO.File]::WriteAllBytes($templateFilePath, @())
|
||||
}
|
||||
"DataCompositionSchema" {
|
||||
$content = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
||||
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
||||
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
||||
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<dataSource>
|
||||
<name>ИсточникДанных1</name>
|
||||
<dataSourceType>Local</dataSourceType>
|
||||
</dataSource>
|
||||
</DataCompositionSchema>
|
||||
"@
|
||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
||||
}
|
||||
}
|
||||
|
||||
# --- 3. Модификация корневого XML ---
|
||||
|
||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($rootXmlFull.Path)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
||||
if (-not $childObjects) {
|
||||
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Добавить <Template> в конец ChildObjects
|
||||
$templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$templateElem.InnerText = $TemplateName
|
||||
|
||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($templateElem) | Out-Null
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
} else {
|
||||
$lastChild = $childObjects.LastChild
|
||||
# Вставить перед закрывающим whitespace (если есть), или в конец
|
||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
||||
$childObjects.InsertBefore($templateElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($templateElem) | Out-Null
|
||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. MainDataCompositionSchema (для ExternalReport / Report) ---
|
||||
|
||||
$mainDCSUpdated = $false
|
||||
if ($TemplateType -eq "DataCompositionSchema") {
|
||||
# Определяем корневой элемент объекта
|
||||
$reportLikeTypes = @("ExternalReport", "Report")
|
||||
$objectTypeNode = $null
|
||||
$objectTypeName = $null
|
||||
foreach ($rt in $reportLikeTypes) {
|
||||
$node = $xmlDoc.SelectSingleNode("//md:$rt", $nsMgr)
|
||||
if ($node) {
|
||||
$objectTypeNode = $node
|
||||
$objectTypeName = $rt
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($objectTypeNode) {
|
||||
$mainDCS = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:MainDataCompositionSchema", $nsMgr)
|
||||
if ($mainDCS) {
|
||||
$isEmpty = [string]::IsNullOrWhiteSpace($mainDCS.InnerText)
|
||||
if ($isEmpty -or $SetMainSKD) {
|
||||
$objName = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:Name", $nsMgr).InnerText
|
||||
$mainDCS.InnerText = "$objectTypeName.$objName.Template.$TemplateName"
|
||||
$mainDCSUpdated = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Сохранить с BOM
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = $encBom
|
||||
$settings.Indent = $false
|
||||
|
||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
||||
$xmlDoc.Save($writer)
|
||||
$writer.Close()
|
||||
$stream.Close()
|
||||
|
||||
Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)"
|
||||
Write-Host " Метаданные: $templateMetaPath"
|
||||
Write-Host " Содержимое: $templateFilePath"
|
||||
if ($mainDCSUpdated) {
|
||||
Write-Host " MainDataCompositionSchema: $($mainDCS.InnerText)"
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# add-template v1.0 — Add template to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
|
||||
TYPE_MAP = {
|
||||
"HTML": {"TemplateType": "HTMLDocument", "Ext": ".html"},
|
||||
"Text": {"TemplateType": "TextDocument", "Ext": ".txt"},
|
||||
"SpreadsheetDocument": {"TemplateType": "SpreadsheetDocument", "Ext": ".xml"},
|
||||
"BinaryData": {"TemplateType": "BinaryData", "Ext": ".bin"},
|
||||
"DataCompositionSchema": {"TemplateType": "DataCompositionSchema", "Ext": ".xml"},
|
||||
}
|
||||
|
||||
|
||||
def save_xml_with_bom(tree, path):
|
||||
"""Save XML tree to file with UTF-8 BOM."""
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def write_text_with_bom(path, text):
|
||||
"""Write text to file with UTF-8 BOM."""
|
||||
with open(path, "w", encoding="utf-8-sig") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Add template to 1C object", allow_abbrev=False)
|
||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
||||
parser.add_argument("-TemplateName", required=True)
|
||||
parser.add_argument("-TemplateType", required=True,
|
||||
choices=["HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema"])
|
||||
parser.add_argument("-Synonym", default=None)
|
||||
parser.add_argument("-SrcDir", default="src")
|
||||
parser.add_argument("-SetMainSKD", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
object_name = args.ObjectName
|
||||
template_name = args.TemplateName
|
||||
template_type = args.TemplateType
|
||||
synonym = args.Synonym if args.Synonym is not None else template_name
|
||||
src_dir = args.SrcDir
|
||||
set_main_skd = args.SetMainSKD
|
||||
|
||||
tmpl = TYPE_MAP[template_type]
|
||||
|
||||
# --- Checks ---
|
||||
|
||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
||||
if not os.path.exists(root_xml_path):
|
||||
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
processor_dir = os.path.join(src_dir, object_name)
|
||||
templates_dir = os.path.join(processor_dir, "Templates")
|
||||
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
|
||||
|
||||
if os.path.exists(template_meta_path):
|
||||
print(f"Макет уже существует: {template_meta_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Create directories ---
|
||||
|
||||
template_ext_dir = os.path.join(templates_dir, template_name, "Ext")
|
||||
os.makedirs(template_ext_dir, exist_ok=True)
|
||||
|
||||
# --- 1. Template metadata (Templates/<TemplateName>.xml) ---
|
||||
|
||||
template_uuid = str(uuid.uuid4())
|
||||
|
||||
template_meta_xml = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<MetaDataObject 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"'
|
||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
' version="2.17">\n'
|
||||
f'\t<Template uuid="{template_uuid}">\n'
|
||||
'\t\t<Properties>\n'
|
||||
f'\t\t\t<Name>{template_name}</Name>\n'
|
||||
'\t\t\t<Synonym>\n'
|
||||
'\t\t\t\t<v8:item>\n'
|
||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
||||
'\t\t\t\t</v8:item>\n'
|
||||
'\t\t\t</Synonym>\n'
|
||||
'\t\t\t<Comment/>\n'
|
||||
f'\t\t\t<TemplateType>{tmpl["TemplateType"]}</TemplateType>\n'
|
||||
'\t\t</Properties>\n'
|
||||
'\t</Template>\n'
|
||||
'</MetaDataObject>'
|
||||
)
|
||||
|
||||
write_text_with_bom(template_meta_path, template_meta_xml)
|
||||
|
||||
# --- 2. Template content (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
||||
|
||||
template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}")
|
||||
|
||||
if template_type == "HTML":
|
||||
content = (
|
||||
'<!DOCTYPE html>\n'
|
||||
'<html>\n'
|
||||
'<head>\n'
|
||||
'\t<meta charset="UTF-8">\n'
|
||||
'\t<title></title>\n'
|
||||
'</head>\n'
|
||||
'<body>\n'
|
||||
'</body>\n'
|
||||
'</html>'
|
||||
)
|
||||
write_text_with_bom(template_file_path, content)
|
||||
|
||||
elif template_type == "Text":
|
||||
write_text_with_bom(template_file_path, "")
|
||||
|
||||
elif template_type == "SpreadsheetDocument":
|
||||
content = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document"'
|
||||
' xmlns:ss="http://v8.1c.ru/spreadsheet/document"'
|
||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema">\n'
|
||||
'</SpreadsheetDocument>'
|
||||
)
|
||||
write_text_with_bom(template_file_path, content)
|
||||
|
||||
elif template_type == "BinaryData":
|
||||
with open(template_file_path, "wb") as f:
|
||||
pass # empty file
|
||||
|
||||
elif template_type == "DataCompositionSchema":
|
||||
content = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"\n'
|
||||
'\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"\n'
|
||||
'\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"\n'
|
||||
'\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"\n'
|
||||
'\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"\n'
|
||||
'\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"\n'
|
||||
'\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"\n'
|
||||
'\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n'
|
||||
'\t<dataSource>\n'
|
||||
'\t\t<name>ИсточникДанных1</name>\n'
|
||||
'\t\t<dataSourceType>Local</dataSourceType>\n'
|
||||
'\t</dataSource>\n'
|
||||
'</DataCompositionSchema>'
|
||||
)
|
||||
write_text_with_bom(template_file_path, content)
|
||||
|
||||
# --- 3. Modify root XML ---
|
||||
|
||||
root_xml_full = os.path.abspath(root_xml_path)
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(root_xml_full, parser_xml)
|
||||
root = tree.getroot()
|
||||
|
||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
||||
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
||||
if child_objects is None:
|
||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Add <Template> to end of ChildObjects
|
||||
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
|
||||
template_elem.text = template_name
|
||||
# Remove auto-appended element to reinsert with proper whitespace
|
||||
child_objects.remove(template_elem)
|
||||
|
||||
children = list(child_objects)
|
||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||
# Empty ChildObjects (self-closing)
|
||||
child_objects.text = "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = "\n\t\t"
|
||||
else:
|
||||
if len(children) > 0:
|
||||
last_child = children[-1]
|
||||
# last_child.tail is the trailing whitespace before </ChildObjects>
|
||||
old_tail = last_child.tail
|
||||
last_child.tail = "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||
else:
|
||||
# Has text content but no element children
|
||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||
child_objects.append(template_elem)
|
||||
template_elem.tail = "\n\t\t"
|
||||
|
||||
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
|
||||
|
||||
main_dcs_updated = False
|
||||
if template_type == "DataCompositionSchema":
|
||||
report_like_types = ["ExternalReport", "Report"]
|
||||
object_type_node = None
|
||||
object_type_name = None
|
||||
for rt in report_like_types:
|
||||
node = root.find(f".//md:{rt}", NSMAP)
|
||||
if node is not None:
|
||||
object_type_node = node
|
||||
object_type_name = rt
|
||||
break
|
||||
|
||||
if object_type_node is not None:
|
||||
main_dcs = root.find(f".//md:{object_type_name}/md:Properties/md:MainDataCompositionSchema", NSMAP)
|
||||
if main_dcs is not None:
|
||||
is_empty = main_dcs.text is None or main_dcs.text.strip() == ""
|
||||
if is_empty or set_main_skd:
|
||||
obj_name_node = root.find(f".//md:{object_type_name}/md:Properties/md:Name", NSMAP)
|
||||
obj_name = obj_name_node.text if obj_name_node is not None else ""
|
||||
main_dcs.text = f"{object_type_name}.{obj_name}.Template.{template_name}"
|
||||
main_dcs_updated = True
|
||||
|
||||
# Save with BOM
|
||||
save_xml_with_bom(tree, root_xml_full)
|
||||
|
||||
print(f"[OK] Создан макет: {template_name} ({template_type})")
|
||||
print(f" Метаданные: {template_meta_path}")
|
||||
print(f" Содержимое: {template_file_path}")
|
||||
if main_dcs_updated:
|
||||
print(f" MainDataCompositionSchema: {main_dcs.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,374 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// web-test run v1.2 — CLI runner for 1C web client automation
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* CLI runner for 1C web client automation.
|
||||
*
|
||||
* Architecture: `start` launches browser + HTTP server in one process.
|
||||
* `exec`, `shot`, `stop` send requests to the running server.
|
||||
*
|
||||
* Usage:
|
||||
* node src/run.mjs start <url> — launch browser, connect to 1C, serve requests
|
||||
* node src/run.mjs run <url> <file|-> — autonomous: connect, execute script, disconnect
|
||||
* node src/run.mjs exec <file|-> — run script against existing session
|
||||
* node src/run.mjs shot [file] — take screenshot
|
||||
* node src/run.mjs stop — logout + close browser
|
||||
* node src/run.mjs status — check session
|
||||
*/
|
||||
import http from 'http';
|
||||
import * as browser from './browser.mjs';
|
||||
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SESSION_FILE = resolve(__dirname, '..', '.browser-session.json');
|
||||
|
||||
const [,, cmd, ...rawArgs] = process.argv;
|
||||
const flags = { noRecord: rawArgs.includes('--no-record') };
|
||||
const args = rawArgs.filter(a => !a.startsWith('--'));
|
||||
|
||||
switch (cmd) {
|
||||
case 'start': await cmdStart(args[0]); break;
|
||||
case 'run': await cmdRun(args[0], args[1]); break;
|
||||
case 'exec': await cmdExec(args[0], flags); break;
|
||||
case 'shot': await cmdShot(args[0]); break;
|
||||
case 'stop': await cmdStop(); break;
|
||||
case 'status': cmdStatus(); break;
|
||||
default: usage();
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// start: launch browser + HTTP server
|
||||
// ============================================================
|
||||
|
||||
async function cmdStart(url) {
|
||||
if (!url) die('Usage: node src/run.mjs start <url>');
|
||||
|
||||
// Connect to 1C
|
||||
const state = await browser.connect(url);
|
||||
|
||||
// Start HTTP server for exec/shot/stop
|
||||
const httpServer = http.createServer(handleRequest);
|
||||
httpServer.listen(0, '127.0.0.1', () => {
|
||||
const port = httpServer.address().port;
|
||||
const session = {
|
||||
port,
|
||||
url,
|
||||
pid: process.pid,
|
||||
startedAt: new Date().toISOString()
|
||||
};
|
||||
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
||||
out({ ok: true, message: 'Browser ready', port, ...state });
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await browser.disconnect();
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
try {
|
||||
if (req.method === 'POST' && req.url === '/exec') {
|
||||
const code = await readBody(req);
|
||||
const noRecord = req.headers['x-no-record'] === '1';
|
||||
const result = await executeScript(code, { noRecord });
|
||||
json(res, result);
|
||||
|
||||
} else if (req.method === 'GET' && req.url === '/shot') {
|
||||
const png = await browser.screenshot();
|
||||
res.writeHead(200, { 'Content-Type': 'image/png' });
|
||||
res.end(png);
|
||||
|
||||
} else if (req.method === 'POST' && req.url === '/stop') {
|
||||
json(res, { ok: true, message: 'Stopping' });
|
||||
await browser.disconnect();
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
|
||||
} else if (req.method === 'GET' && req.url === '/status') {
|
||||
json(res, { ok: true, connected: browser.isConnected() });
|
||||
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
} catch (e) {
|
||||
json(res, { ok: false, error: e.message }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeScript(code, { noRecord } = {}) {
|
||||
const output = [];
|
||||
const origLog = console.log;
|
||||
const origErr = console.error;
|
||||
console.log = (...a) => output.push(a.map(String).join(' '));
|
||||
console.error = (...a) => output.push('[ERR] ' + a.map(String).join(' '));
|
||||
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
// Build sandbox: all browser.mjs exports + useful Node globals
|
||||
const exports = {};
|
||||
for (const [k, v] of Object.entries(browser)) {
|
||||
if (k !== 'default') exports[k] = v;
|
||||
}
|
||||
exports.writeFileSync = writeFileSync;
|
||||
exports.readFileSync = readFileSync;
|
||||
|
||||
// --no-record: stub recording/narration functions to return safe defaults
|
||||
if (noRecord) {
|
||||
const noop = async () => {};
|
||||
exports.startRecording = noop;
|
||||
exports.stopRecording = async () => ({ file: null, duration: 0, size: 0 });
|
||||
exports.addNarration = async () => ({ file: null, duration: 0, size: 0, captions: 0 });
|
||||
for (const fn of ['showCaption', 'hideCaption']) {
|
||||
exports[fn] = noop;
|
||||
}
|
||||
exports.isRecording = () => false;
|
||||
exports.getCaptions = () => [];
|
||||
}
|
||||
|
||||
// Wrap action functions to auto-detect 1C errors (modal, balloon)
|
||||
// and stop execution immediately with diagnostic info
|
||||
const ACTION_FNS = [
|
||||
'clickElement', 'fillFields', 'fillField', 'selectValue', 'fillTableRow',
|
||||
'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile',
|
||||
'closeForm', 'filterList', 'unfilterList'
|
||||
];
|
||||
for (const name of ACTION_FNS) {
|
||||
if (typeof exports[name] !== 'function') continue;
|
||||
const orig = exports[name];
|
||||
exports[name] = async (...args) => {
|
||||
const result = await orig(...args);
|
||||
const errors = result?.errors;
|
||||
if (errors?.modal || errors?.balloon) {
|
||||
// Screenshot while the error modal is still visible (before fetchErrorStack closes it)
|
||||
let errorShot;
|
||||
try {
|
||||
const png = await exports.screenshot();
|
||||
errorShot = resolve(__dirname, '..', 'error-shot.png');
|
||||
writeFileSync(errorShot, png);
|
||||
} catch {}
|
||||
// Try to fetch call stack for modal errors before throwing
|
||||
let stack = null;
|
||||
if (errors?.modal && typeof exports.fetchErrorStack === 'function') {
|
||||
try {
|
||||
stack = await exports.fetchErrorStack(errors.modal.formNum, errors.modal.hasReport);
|
||||
} catch { /* don't fail if stack fetch fails */ }
|
||||
}
|
||||
const msg = errors.modal?.message || errors.balloon?.message || 'Unknown 1C error';
|
||||
const err = new Error(msg);
|
||||
err.onecError = { step: name, args, errors, formState: result, stack, screenshot: errorShot };
|
||||
throw err;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
||||
const fn = new AsyncFunction(...Object.keys(exports), code);
|
||||
await fn(...Object.values(exports));
|
||||
|
||||
console.log = origLog;
|
||||
console.error = origErr;
|
||||
return { ok: true, output: output.join('\n'), elapsed: elapsed(t0) };
|
||||
} catch (e) {
|
||||
console.log = origLog;
|
||||
console.error = origErr;
|
||||
|
||||
// Auto-stop recording if active (prevents "Already recording" on next exec)
|
||||
if (browser.isRecording()) {
|
||||
try { await browser.stopRecording(); } catch {}
|
||||
}
|
||||
|
||||
// Error screenshot (skip if already taken before fetchErrorStack closed the modal)
|
||||
let shotFile = e.onecError?.screenshot;
|
||||
if (!shotFile) {
|
||||
try {
|
||||
const png = await browser.screenshot();
|
||||
shotFile = resolve(__dirname, '..', 'error-shot.png');
|
||||
writeFileSync(shotFile, png);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const result = { ok: false, error: e.message, output: output.join('\n'), screenshot: shotFile, elapsed: elapsed(t0) };
|
||||
|
||||
// Enrich with 1C error context if available
|
||||
if (e.onecError) {
|
||||
result.step = e.onecError.step;
|
||||
result.stepArgs = e.onecError.args;
|
||||
result.onecErrors = e.onecError.errors;
|
||||
result.formState = e.onecError.formState;
|
||||
if (e.onecError.stack) result.stack = e.onecError.stack;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// run: autonomous connect → execute → disconnect (no server)
|
||||
// ============================================================
|
||||
|
||||
async function cmdRun(url, fileOrDash) {
|
||||
if (!url || !fileOrDash) die('Usage: node src/run.mjs run <url> <file|->');
|
||||
|
||||
const code = fileOrDash === '-'
|
||||
? await readStdin()
|
||||
: readFileSync(resolve(fileOrDash), 'utf-8');
|
||||
|
||||
await browser.connect(url);
|
||||
const result = await executeScript(code);
|
||||
await browser.disconnect();
|
||||
|
||||
out(result);
|
||||
if (!result.ok) process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// exec: send script to running server
|
||||
// ============================================================
|
||||
|
||||
async function cmdExec(fileOrDash, flags = {}) {
|
||||
if (!fileOrDash) die('Usage: node src/run.mjs exec <file|-> [--no-record]');
|
||||
|
||||
let code = fileOrDash === '-'
|
||||
? await readStdin()
|
||||
: readFileSync(resolve(fileOrDash), 'utf-8');
|
||||
|
||||
const sess = loadSession();
|
||||
const headers = {};
|
||||
if (flags.noRecord) headers['x-no-record'] = '1';
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
hostname: '127.0.0.1', port: sess.port, path: '/exec',
|
||||
method: 'POST', timeout: 30 * 60 * 1000, headers,
|
||||
}, res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error(data)); } });
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => { req.destroy(new Error('Exec timeout (10 min)')); });
|
||||
req.write(code);
|
||||
req.end();
|
||||
});
|
||||
out(result);
|
||||
if (!result.ok) process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// shot: take screenshot via server
|
||||
// ============================================================
|
||||
|
||||
async function cmdShot(file) {
|
||||
const sess = loadSession();
|
||||
const resp = await fetch(`http://127.0.0.1:${sess.port}/shot`);
|
||||
if (!resp.ok) {
|
||||
const err = await resp.text();
|
||||
die(`Screenshot failed: ${err}`);
|
||||
}
|
||||
const buf = Buffer.from(await resp.arrayBuffer());
|
||||
const outFile = file || 'shot.png';
|
||||
writeFileSync(outFile, buf);
|
||||
out({ ok: true, file: outFile });
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// stop: send stop to server
|
||||
// ============================================================
|
||||
|
||||
async function cmdStop() {
|
||||
const sess = loadSession();
|
||||
try {
|
||||
const resp = await fetch(`http://127.0.0.1:${sess.port}/stop`, { method: 'POST' });
|
||||
const result = await resp.json();
|
||||
out(result);
|
||||
} catch {
|
||||
// Server may have already exited before responding
|
||||
out({ ok: true, message: 'Stopped' });
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// status: check session
|
||||
// ============================================================
|
||||
|
||||
function cmdStatus() {
|
||||
if (!existsSync(SESSION_FILE)) {
|
||||
out({ ok: false, message: 'No active session' });
|
||||
process.exit(1);
|
||||
}
|
||||
const sess = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
|
||||
out({ ok: true, ...sess });
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// helpers
|
||||
// ============================================================
|
||||
|
||||
function loadSession() {
|
||||
if (!existsSync(SESSION_FILE)) {
|
||||
die('No active session. Run: node src/run.mjs start <url>');
|
||||
}
|
||||
return JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
try { unlinkSync(SESSION_FILE); } catch {}
|
||||
}
|
||||
|
||||
async function readBody(req) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) chunks.push(chunk);
|
||||
return Buffer.concat(chunks).toString('utf-8');
|
||||
}
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) chunks.push(chunk);
|
||||
return Buffer.concat(chunks).toString('utf-8');
|
||||
}
|
||||
|
||||
function elapsed(t0) {
|
||||
return Math.round((Date.now() - t0) / 100) / 10;
|
||||
}
|
||||
|
||||
function json(res, obj, status = 200) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
|
||||
function out(obj) {
|
||||
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
||||
}
|
||||
|
||||
function die(msg) {
|
||||
process.stderr.write(msg + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function usage() {
|
||||
die(`Usage: node src/run.mjs <command> [args]
|
||||
|
||||
Commands:
|
||||
start <url> Launch browser and connect to 1C web client
|
||||
run <url> <file|-> Autonomous: connect, execute script, disconnect
|
||||
exec <file|-> [options] Execute script (file path or - for stdin)
|
||||
shot [file] Take screenshot (default: shot.png)
|
||||
stop Logout and close browser
|
||||
status Check session status
|
||||
|
||||
Options for exec:
|
||||
--no-record Skip video recording (record() becomes no-op)`);
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
# Реальные выгрузки обработок (примеры, не для версионирования)
|
||||
upload/
|
||||
|
||||
# Результаты сборки
|
||||
build/
|
||||
base/
|
||||
*.epf
|
||||
*.log
|
||||
|
||||
# Временные файлы тестов
|
||||
test-tmp/
|
||||
|
||||
# Локальные настройки Claude Code
|
||||
.claude/settings.local.json
|
||||
|
||||
# Инструменты (portable Apache и т.д.)
|
||||
tools/
|
||||
|
||||
# Отладка навыков (eval, trigger-test, run_loop результаты)
|
||||
debug/
|
||||
|
||||
# Python кэш
|
||||
__pycache__/
|
||||
|
||||
# Локальный реестр баз данных 1С
|
||||
.v8-project.json
|
||||
|
||||
# web-test: Node.js зависимости и runtime-артефакты
|
||||
.claude/skills/web-test/scripts/node_modules/
|
||||
.claude/skills/web-test/.browser-session.json
|
||||
|
||||
# Скриншоты и видео (артефакты тестирования web-test)
|
||||
*.png
|
||||
*.mp4
|
||||
|
||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
||||
.agents/
|
||||
.augment/
|
||||
.cline/
|
||||
.codex/
|
||||
.cursor/
|
||||
.gemini/
|
||||
.github/skills/
|
||||
.kilocode/
|
||||
.kiro/
|
||||
.opencode/
|
||||
.roo/
|
||||
.windsurf/
|
||||
@@ -1 +1 @@
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
@@ -1,58 +1,60 @@
|
||||
---
|
||||
name: cf-edit
|
||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
|
||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-edit — редактирование конфигурации 1С
|
||||
|
||||
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Operation` | Операция (см. таблицу) |
|
||||
| `Value` | Значение для операции (batch через `;;`) |
|
||||
| `DefinitionFile` | JSON-файл с массивом операций |
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
| Операция | Формат Value | Описание |
|
||||
|----------|-------------|----------|
|
||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
|
||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||
|
||||
Подробнее: `reference.md` в каталоге навыка.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Изменить версию и поставщика
|
||||
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||
|
||||
# Добавить объекты
|
||||
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||
|
||||
# Удалить объект
|
||||
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||
|
||||
# Роли по умолчанию
|
||||
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
|
||||
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||
```
|
||||
---
|
||||
name: cf-edit
|
||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
|
||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-edit — редактирование конфигурации 1С
|
||||
|
||||
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Operation` | Операция (см. таблицу) |
|
||||
| `Value` | Значение для операции (batch через `;;`) |
|
||||
| `DefinitionFile` | JSON-файл с массивом операций |
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cf-edit/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
| Операция | Формат Value | Описание |
|
||||
|----------|-------------|----------|
|
||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
|
||||
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
|
||||
|
||||
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Изменить версию и поставщика
|
||||
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||
|
||||
# Добавить объекты
|
||||
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||
|
||||
# Удалить объект
|
||||
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||
|
||||
# Роли по умолчанию
|
||||
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
|
||||
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||
```
|
||||
@@ -0,0 +1,150 @@
|
||||
# cf-edit — справочник операций
|
||||
|
||||
## modify-property
|
||||
|
||||
Свойства для редактирования:
|
||||
|
||||
### Скалярные
|
||||
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
|
||||
|
||||
### LocalString (многоязычные)
|
||||
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
|
||||
|
||||
### Enum
|
||||
| Свойство | Допустимые значения |
|
||||
|----------|---------------------|
|
||||
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
|
||||
| `ConfigurationExtensionCompatibilityMode` | то же |
|
||||
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
||||
| `ScriptVariant` | `Russian`, `English` |
|
||||
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
|
||||
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
||||
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
|
||||
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
||||
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
||||
|
||||
### Ref
|
||||
`DefaultLanguage` — значение вида `Language.Русский`
|
||||
|
||||
### Формат batch
|
||||
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
|
||||
|
||||
## add-childObject / remove-childObject
|
||||
|
||||
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
||||
|
||||
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
|
||||
|
||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||
|
||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||
|
||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||
|
||||
`set-defaultRoles` полностью заменяет список ролей.
|
||||
|
||||
## set-panels
|
||||
|
||||
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
|
||||
|
||||
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
|
||||
|
||||
**Запись** — одна из:
|
||||
- Строка-алиас (одна панель в этом слоте)
|
||||
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
|
||||
|
||||
**Алиасы панелей:**
|
||||
|
||||
| Алиас | Панель |
|
||||
|-------|--------|
|
||||
| `sections` | Панель разделов |
|
||||
| `open` | Панель открытых |
|
||||
| `favorites` | Панель избранного |
|
||||
| `history` | Панель истории |
|
||||
| `functions` | Панель функций текущего раздела |
|
||||
|
||||
**Семантика:**
|
||||
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
|
||||
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
|
||||
|
||||
**Пример** (DefinitionFile):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"operation": "set-panels",
|
||||
"value": {
|
||||
"top": ["open"],
|
||||
"left": ["sections"],
|
||||
"right": [{ "group": ["favorites", "history"] }],
|
||||
"bottom": ["functions"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
|
||||
|
||||
## set-home-page
|
||||
|
||||
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
|
||||
|
||||
`value` — объект:
|
||||
|
||||
| Ключ | Канонич. (XML) | Описание |
|
||||
|------|----------------|----------|
|
||||
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
|
||||
| `left` | `LeftColumn` | массив записей форм |
|
||||
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
|
||||
|
||||
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
|
||||
|
||||
**Запись формы** — одна из:
|
||||
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
|
||||
- Объект `{form, height?, visibility?, roles?}`
|
||||
|
||||
| Поле | Канонич. | Дефолт | Описание |
|
||||
|------|----------|--------|----------|
|
||||
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
|
||||
| `height` | `Height` | `10` | Высота |
|
||||
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
|
||||
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
|
||||
|
||||
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
|
||||
|
||||
**Пример:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"operation": "set-home-page",
|
||||
"value": {
|
||||
"template": "TwoColumnsVariableWidth",
|
||||
"left": [
|
||||
"CommonForm.НачалоРаботы",
|
||||
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
|
||||
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
|
||||
{
|
||||
"form": "CommonForm.РабочийСтолОператора",
|
||||
"visibility": false,
|
||||
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## DefinitionFile (JSON)
|
||||
|
||||
```json
|
||||
[
|
||||
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
|
||||
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
|
||||
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
|
||||
]
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,50 +1,54 @@
|
||||
---
|
||||
name: cf-info
|
||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||
argument-hint: <ConfigPath> [-Mode overview|brief|full]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-info — Структура конфигурации 1С
|
||||
|
||||
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
|
||||
```
|
||||
|
||||
## Три режима
|
||||
|
||||
| Режим | Что показывает |
|
||||
|---|---|
|
||||
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
|
||||
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
|
||||
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор пустой конфигурации
|
||||
... -ConfigPath upload/cfempty
|
||||
|
||||
# Краткая сводка реальной конфигурации
|
||||
... -ConfigPath upload/acc_8.3.24 -Mode brief
|
||||
|
||||
# Полная информация
|
||||
... -ConfigPath upload/acc_8.3.24 -Mode full
|
||||
|
||||
# С пагинацией
|
||||
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
|
||||
```
|
||||
---
|
||||
name: cf-info
|
||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-info — Структура конфигурации 1С
|
||||
|
||||
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
||||
```
|
||||
|
||||
## Три режима
|
||||
|
||||
| Режим | Что показывает |
|
||||
|---|---|
|
||||
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
|
||||
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
|
||||
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор пустой конфигурации
|
||||
... -ConfigPath src
|
||||
|
||||
# Краткая сводка реальной конфигурации
|
||||
... -ConfigPath src -Mode brief
|
||||
|
||||
# Полная информация
|
||||
... -ConfigPath src -Mode full
|
||||
|
||||
# С пагинацией
|
||||
... -ConfigPath src -Mode full -Limit 50 -Offset 100
|
||||
|
||||
# Drill-down: только начальная страница (раскладка форм с ролями)
|
||||
... -ConfigPath src -Section home-page
|
||||
```
|
||||
+655
-387
File diff suppressed because it is too large
Load Diff
+238
-6
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from lxml import etree
|
||||
@@ -13,8 +14,9 @@ sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
# --- Argument parsing ---
|
||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
|
||||
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
||||
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
||||
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
|
||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||
@@ -93,7 +95,7 @@ def get_prop_ml(prop_name):
|
||||
type_order = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
@@ -109,6 +111,7 @@ type_ru_names = {
|
||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||
"Bot": "Боты",
|
||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||
@@ -125,6 +128,173 @@ type_ru_names = {
|
||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
||||
}
|
||||
|
||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||
PANEL_NAMES = {
|
||||
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых",
|
||||
"b553047f-c9aa-4157-978d-448ecad24248": "Разделов",
|
||||
"13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного",
|
||||
"c933ac92-92cd-459d-81cc-e0c8a83ced99": "История",
|
||||
"b2735bd3-d822-4430-ba59-c9e869693b24": "Функций",
|
||||
}
|
||||
CAI_NS = "http://v8.1c.ru/8.2/managed-application/core"
|
||||
|
||||
def get_panels_layout():
|
||||
cfg_dir = os.path.dirname(config_path)
|
||||
cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml")
|
||||
if not os.path.isfile(cai_path):
|
||||
return None
|
||||
try:
|
||||
cai_tree = etree.parse(cai_path)
|
||||
except Exception:
|
||||
return None
|
||||
cai_root = cai_tree.getroot()
|
||||
layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []}
|
||||
for side in ("top", "left", "right", "bottom"):
|
||||
for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"):
|
||||
slot = []
|
||||
for u in side_el.iter(f"{{{CAI_NS}}}uuid"):
|
||||
key = (u.text or "").strip()
|
||||
slot.append(PANEL_NAMES.get(key, f"?{key}"))
|
||||
if slot:
|
||||
layout[side].append(slot)
|
||||
for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"):
|
||||
key = pd.get("id", "")
|
||||
layout["declared"].append(PANEL_NAMES.get(key, f"?{key}"))
|
||||
return layout
|
||||
|
||||
def format_layout_slots(slots):
|
||||
if not slots:
|
||||
return ""
|
||||
parts = []
|
||||
for slot in slots:
|
||||
if len(slot) == 1:
|
||||
parts.append(slot[0])
|
||||
else:
|
||||
parts.append("Стек(" + ", ".join(slot) + ")")
|
||||
return " | ".join(parts)
|
||||
|
||||
panel_layout = get_panels_layout()
|
||||
|
||||
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
||||
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
||||
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
|
||||
def get_home_page_layout():
|
||||
cfg_dir = os.path.dirname(config_path)
|
||||
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
|
||||
if not os.path.isfile(hp_path):
|
||||
return None
|
||||
try:
|
||||
hp_tree = etree.parse(hp_path)
|
||||
except Exception:
|
||||
return None
|
||||
hp_root = hp_tree.getroot()
|
||||
result = {"template": "", "left": [], "right": []}
|
||||
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
|
||||
if tn is not None and tn.text:
|
||||
result["template"] = tn.text.strip()
|
||||
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
||||
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
|
||||
if col is None:
|
||||
continue
|
||||
items = []
|
||||
for it in col.findall(f"{{{HP_NS}}}Item"):
|
||||
f = it.find(f"{{{HP_NS}}}Form")
|
||||
h = it.find(f"{{{HP_NS}}}Height")
|
||||
vis = it.find(f"{{{HP_NS}}}Visibility")
|
||||
common = True
|
||||
roles = []
|
||||
if vis is not None:
|
||||
cn = vis.find(f"{{{XR_NS_HP}}}Common")
|
||||
if cn is not None and cn.text:
|
||||
common = cn.text.strip() == "true"
|
||||
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
|
||||
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
|
||||
items.append({
|
||||
"form": (f.text or "").strip() if f is not None else "",
|
||||
"height": int((h.text or "10").strip()) if h is not None else 10,
|
||||
"common": common,
|
||||
"roles": roles,
|
||||
})
|
||||
result[key] = items
|
||||
return result
|
||||
|
||||
home_page = get_home_page_layout()
|
||||
|
||||
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
|
||||
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
|
||||
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
|
||||
# vendor block separately); only computed when g==0.
|
||||
def read_support_state(bin_path):
|
||||
try:
|
||||
if not os.path.isfile(bin_path):
|
||||
return {"state": "absent"}
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return {"state": "removed"}
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return None
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return {"state": "removed"}
|
||||
vendors = []
|
||||
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
|
||||
vendors.append({
|
||||
"version": m.group(1).replace('""', '"'),
|
||||
"vendor": m.group(2).replace('""', '"'),
|
||||
"name": m.group(3).replace('""', '"'),
|
||||
})
|
||||
counts = None
|
||||
if g == 0:
|
||||
counts = [0, 0, 0]
|
||||
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
|
||||
counts[int(m.group(1))] += 1
|
||||
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_support_lines():
|
||||
config_dir = os.path.dirname(config_path)
|
||||
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
|
||||
st = read_support_state(bin_path)
|
||||
res = []
|
||||
if not st or st["state"] == "absent":
|
||||
if cfg_ext_purpose:
|
||||
res.append("Поддержка: расширение (CFE), правки свободны")
|
||||
else:
|
||||
res.append("Поддержка: не на поддержке (своя конфигурация)")
|
||||
return res
|
||||
if st["state"] == "removed":
|
||||
res.append("Поддержка: снята с поддержки полностью")
|
||||
return res
|
||||
res.append("Поддержка: на поддержке")
|
||||
if st["g"] == 0:
|
||||
res.append(" Возможность изменения: включена")
|
||||
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
|
||||
else:
|
||||
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
|
||||
res.append(f" Конфигураций поставщика: {st['k']}")
|
||||
if st["k"] > 1:
|
||||
for v in st["vendors"]:
|
||||
res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}")
|
||||
return res
|
||||
|
||||
def format_home_page_item(it, detailed):
|
||||
badges = [f"h={it['height']}"]
|
||||
if not it["common"]:
|
||||
badges.append("скрыта")
|
||||
if it["roles"]:
|
||||
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
|
||||
tail = f" ({', '.join(badges)})" if badges else ""
|
||||
return f" {it['form']}{tail}"
|
||||
|
||||
# --- Count objects in ChildObjects ---
|
||||
object_counts = OrderedDict()
|
||||
total_objects = 0
|
||||
@@ -146,6 +316,7 @@ cfg_version = get_prop_text("Version")
|
||||
cfg_vendor = get_prop_text("Vendor")
|
||||
cfg_compat = get_prop_text("CompatibilityMode")
|
||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||
cfg_script = get_prop_text("ScriptVariant")
|
||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||
@@ -159,14 +330,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
|
||||
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
||||
|
||||
# --- BRIEF mode ---
|
||||
if args.Mode == "brief":
|
||||
if args.Mode == "brief" and not args.Section:
|
||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
||||
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
||||
|
||||
# --- OVERVIEW mode ---
|
||||
if args.Mode == "overview":
|
||||
if args.Mode == "overview" and not args.Section:
|
||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||
@@ -178,6 +349,8 @@ if args.Mode == "overview":
|
||||
out(f"Поставщик: {cfg_vendor}")
|
||||
if cfg_version:
|
||||
out(f"Версия: {cfg_version}")
|
||||
for ln in get_support_lines():
|
||||
out(ln)
|
||||
out(f"Совместимость: {cfg_compat}")
|
||||
out(f"Режим запуска: {cfg_default_run}")
|
||||
out(f"Язык скриптов: {cfg_script}")
|
||||
@@ -187,6 +360,20 @@ if args.Mode == "overview":
|
||||
out(f"Интерфейс: {cfg_intf_compat}")
|
||||
out()
|
||||
|
||||
if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")):
|
||||
out("--- Раскладка панелей ---")
|
||||
for s in ("top", "left", "right", "bottom"):
|
||||
if panel_layout[s]:
|
||||
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
|
||||
out()
|
||||
|
||||
# Home page (brief summary)
|
||||
if home_page:
|
||||
out("--- Начальная страница ---")
|
||||
out(f" Шаблон: {home_page['template']}")
|
||||
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
||||
out()
|
||||
|
||||
# Object counts table
|
||||
out(f"--- Состав ({total_objects} объектов) ---")
|
||||
out()
|
||||
@@ -207,7 +394,30 @@ if args.Mode == "overview":
|
||||
out(f" {padded} {count}")
|
||||
|
||||
# --- FULL mode ---
|
||||
if args.Mode == "full":
|
||||
# --- Drill-down: -Section home-page ---
|
||||
if args.Section == "home-page":
|
||||
if not home_page:
|
||||
out("Файл Ext/HomePageWorkArea.xml не найден")
|
||||
else:
|
||||
out(f"=== Начальная страница: {cfg_name} ===")
|
||||
out()
|
||||
out(f"Шаблон: {home_page['template']}")
|
||||
out()
|
||||
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
||||
items = home_page[col_key]
|
||||
if not items:
|
||||
out(f"{col_lbl}: —")
|
||||
out()
|
||||
continue
|
||||
out(f"{col_lbl} ({len(items)}):")
|
||||
for it in items:
|
||||
out(format_home_page_item(it, True))
|
||||
for r in it["roles"]:
|
||||
rval = "true" if r["value"] else "false"
|
||||
out(f" {r['name']}: {rval}")
|
||||
out()
|
||||
|
||||
if args.Mode == "full" and not args.Section:
|
||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||
@@ -229,6 +439,8 @@ if args.Mode == "full":
|
||||
out(f"Поставщик: {cfg_vendor}")
|
||||
if cfg_version:
|
||||
out(f"Версия: {cfg_version}")
|
||||
for ln in get_support_lines():
|
||||
out(ln)
|
||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||
if cfg_update_addr:
|
||||
out(f"Каталог обн.: {cfg_update_addr}")
|
||||
@@ -283,6 +495,26 @@ if args.Mode == "full":
|
||||
out(f"Обычн.формы в управл.: {use_of}")
|
||||
out()
|
||||
|
||||
# --- Section: Panel layout ---
|
||||
if panel_layout:
|
||||
out("--- Раскладка панелей ---")
|
||||
for s in ("top", "left", "right", "bottom"):
|
||||
slots = panel_layout[s]
|
||||
if slots:
|
||||
out(f" {s.ljust(7)} {format_layout_slots(slots)}")
|
||||
else:
|
||||
out(f" {s.ljust(7)} —")
|
||||
if panel_layout["declared"]:
|
||||
out(f" объявлено: {', '.join(panel_layout['declared'])}")
|
||||
out()
|
||||
|
||||
# --- Section: Home page (brief summary) ---
|
||||
if home_page:
|
||||
out("--- Начальная страница ---")
|
||||
out(f" Шаблон: {home_page['template']}")
|
||||
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
||||
out()
|
||||
|
||||
# --- Section: Storages & default forms ---
|
||||
out("--- Хранилища и формы по умолчанию ---")
|
||||
storage_props = [
|
||||
@@ -1,58 +1,49 @@
|
||||
---
|
||||
name: cf-init
|
||||
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
|
||||
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-init — Создание пустой конфигурации 1С
|
||||
|
||||
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `Name` | Имя конфигурации (обязат.) |
|
||||
| `Synonym` | Синоним (= Name если не указан) |
|
||||
| `OutputDir` | Каталог для создания (default: `src`) |
|
||||
| `Version` | Версия конфигурации |
|
||||
| `Vendor` | Поставщик |
|
||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<OutputDir>/
|
||||
├── Configuration.xml # Корневой файл — все свойства
|
||||
└── Languages/
|
||||
└── Русский.xml # Язык по умолчанию
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Базовая конфигурация
|
||||
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
||||
|
||||
# С версией и поставщиком
|
||||
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||
|
||||
# Другой режим совместимости
|
||||
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cf-init TestConfig -OutputDir test-tmp/cf
|
||||
/cf-info test-tmp/cf — проверить созданное
|
||||
/cf-validate test-tmp/cf — валидировать
|
||||
```
|
||||
---
|
||||
name: cf-init
|
||||
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
|
||||
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-init — Создание пустой конфигурации 1С
|
||||
|
||||
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `Name` | Имя конфигурации (обязат.) |
|
||||
| `Synonym` | Синоним (= Name если не указан) |
|
||||
| `OutputDir` | Каталог для создания (default: `src`) |
|
||||
| `Version` | Версия конфигурации |
|
||||
| `Vendor` | Поставщик |
|
||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cf-init/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Базовая конфигурация
|
||||
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
||||
|
||||
# С версией и поставщиком
|
||||
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||
|
||||
# Другой режим совместимости
|
||||
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cf-init TestConfig -OutputDir test-tmp/cf
|
||||
/cf-info test-tmp/cf — проверить созданное
|
||||
/cf-validate test-tmp/cf — валидировать
|
||||
```
|
||||
+254
-215
@@ -1,215 +1,254 @@
|
||||
# cf-init v1.0 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$OutputDir = "src",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
$uuidCfg = [guid]::NewGuid().ToString()
|
||||
$uuidLang = [guid]::NewGuid().ToString()
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Mobile functionalities ---
|
||||
$mobileFuncs = @(
|
||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
|
||||
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
|
||||
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
|
||||
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
|
||||
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
|
||||
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
|
||||
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
|
||||
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
|
||||
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
|
||||
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
|
||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||
)
|
||||
|
||||
$mobileXml = ""
|
||||
foreach ($mf in $mobileFuncs) {
|
||||
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
|
||||
}
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<NamePrefix/>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles/>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<UpdateCatalogAddress/>
|
||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||
<AdditionalFullTextSearchDictionaries/>
|
||||
<CommonSettingsStorage/>
|
||||
<ReportsUserSettingsStorage/>
|
||||
<ReportsVariantsStorage/>
|
||||
<FormDataSettingsStorage/>
|
||||
<DynamicListsUserSettingsStorage/>
|
||||
<URLExternalDataStorage/>
|
||||
<Content/>
|
||||
<DefaultReportForm/>
|
||||
<DefaultReportVariantForm/>
|
||||
<DefaultReportSettingsForm/>
|
||||
<DefaultReportAppearanceTemplate/>
|
||||
<DefaultDynamicListSettingsForm/>
|
||||
<DefaultSearchForm/>
|
||||
<DefaultDataHistoryChangeHistoryForm/>
|
||||
<DefaultDataHistoryVersionDataForm/>
|
||||
<DefaultDataHistoryVersionDifferencesForm/>
|
||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
||||
<RequiredMobileApplicationPermissions/>
|
||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||
</UsedMobileApplicationFunctionalities>
|
||||
<StandaloneConfigurationRestrictionRoles/>
|
||||
<MobileApplicationURLs/>
|
||||
<AllowedIncomingShareRequestTypes/>
|
||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
||||
<DefaultInterface/>
|
||||
<DefaultStyle/>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<DataLockControlMode>Managed</DataLockControlMode>
|
||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||
<DefaultConstantsForm/>
|
||||
</Properties>
|
||||
<ChildObjects>
|
||||
<Language>Русский</Language>
|
||||
</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml ---
|
||||
$langXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Language uuid="$uuidLang">
|
||||
<Properties>
|
||||
<Name>Русский</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Русский</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
|
||||
# --- Output ---
|
||||
Write-Host "[OK] Создана конфигурация: $Name"
|
||||
Write-Host " Каталог: $OutputDir"
|
||||
Write-Host " Configuration.xml: $cfgFile"
|
||||
Write-Host " Languages: $langFile"
|
||||
# cf-init v1.3 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$OutputDir = "src",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24",
|
||||
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||
# совместимости она не зависит: 8.3.24 пишет 2.17, 8.3.27 — 2.20. Дефолт консервативный —
|
||||
# 2.17 читается всеми поддерживаемыми платформами.
|
||||
[ValidateSet("2.17", "2.20", "2.21")]
|
||||
[string]$FormatVersion = "2.17"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
$uuidCfg = [guid]::NewGuid().ToString()
|
||||
$uuidLang = [guid]::NewGuid().ToString()
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Mobile functionalities ---
|
||||
$mobileFuncs = @(
|
||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
|
||||
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
|
||||
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
|
||||
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
|
||||
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
|
||||
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
|
||||
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
|
||||
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
|
||||
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
|
||||
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
|
||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||
)
|
||||
|
||||
$mobileXml = ""
|
||||
foreach ($mf in $mobileFuncs) {
|
||||
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
|
||||
}
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<NamePrefix/>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles/>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<UpdateCatalogAddress/>
|
||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||
<AdditionalFullTextSearchDictionaries/>
|
||||
<CommonSettingsStorage/>
|
||||
<ReportsUserSettingsStorage/>
|
||||
<ReportsVariantsStorage/>
|
||||
<FormDataSettingsStorage/>
|
||||
<DynamicListsUserSettingsStorage/>
|
||||
<URLExternalDataStorage/>
|
||||
<Content/>
|
||||
<DefaultReportForm/>
|
||||
<DefaultReportVariantForm/>
|
||||
<DefaultReportSettingsForm/>
|
||||
<DefaultReportAppearanceTemplate/>
|
||||
<DefaultDynamicListSettingsForm/>
|
||||
<DefaultSearchForm/>
|
||||
<DefaultDataHistoryChangeHistoryForm/>
|
||||
<DefaultDataHistoryVersionDataForm/>
|
||||
<DefaultDataHistoryVersionDifferencesForm/>
|
||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
||||
<RequiredMobileApplicationPermissions/>
|
||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||
</UsedMobileApplicationFunctionalities>
|
||||
<StandaloneConfigurationRestrictionRoles/>
|
||||
<MobileApplicationURLs/>
|
||||
<AllowedIncomingShareRequestTypes/>
|
||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
||||
<DefaultInterface/>
|
||||
<DefaultStyle/>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<DataLockControlMode>Managed</DataLockControlMode>
|
||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||
<DefaultConstantsForm/>
|
||||
</Properties>
|
||||
<ChildObjects>
|
||||
<Language>Русский</Language>
|
||||
</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml ---
|
||||
$langXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||
<Language uuid="$uuidLang">
|
||||
<Properties>
|
||||
<Name>Русский</Name>
|
||||
<Synonym>
|
||||
<v8:item>
|
||||
<v8:lang>ru</v8:lang>
|
||||
<v8:content>Русский</v8:content>
|
||||
</v8:item>
|
||||
</Synonym>
|
||||
<Comment/>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||
# via panelDef but not placed by default. Without this file the web client renders
|
||||
# section icons without labels (icon-only mode).
|
||||
$openPanelInst = [guid]::NewGuid().ToString()
|
||||
$sectionsPanelInst = [guid]::NewGuid().ToString()
|
||||
$caiXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||
<top>
|
||||
<panel id="$openPanelInst">
|
||||
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||
</panel>
|
||||
</top>
|
||||
<left>
|
||||
<panel id="$sectionsPanelInst">
|
||||
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||
</panel>
|
||||
</left>
|
||||
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||
</ClientApplicationInterface>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
$extDir = Join-Path $OutputDir "Ext"
|
||||
if (-not (Test-Path $extDir)) {
|
||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
||||
|
||||
# --- Output ---
|
||||
Write-Host "[OK] Создана конфигурация: $Name"
|
||||
Write-Host " Каталог: $OutputDir"
|
||||
Write-Host " Configuration.xml: $cfgFile"
|
||||
Write-Host " Languages: $langFile"
|
||||
Write-Host " Ext/CAI: $caiFile"
|
||||
+37
-4
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-init v1.0 — Create empty 1C configuration scaffold
|
||||
# cf-init v1.3 — Create empty 1C configuration scaffold
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Generates minimal XML source files for a 1C configuration."""
|
||||
import sys, os, argparse, uuid
|
||||
@@ -24,6 +24,9 @@ def main():
|
||||
parser.add_argument('-Version', dest='Version', default='')
|
||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
|
||||
# 8.3.24 пишет 2.17, 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми платформами.
|
||||
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', choices=['2.17', '2.20', '2.21'])
|
||||
args = parser.parse_args()
|
||||
|
||||
name = args.Name
|
||||
@@ -96,7 +99,7 @@ def main():
|
||||
\t\t\t</xr:ContainedObject>\n"""
|
||||
|
||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||
\t<Configuration uuid="{uuid_cfg}">
|
||||
\t\t<InternalInfo>
|
||||
{contained_objects}\t\t</InternalInfo>
|
||||
@@ -155,7 +158,7 @@ def main():
|
||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||
\t\t\t<DefaultConstantsForm/>
|
||||
@@ -168,7 +171,7 @@ def main():
|
||||
|
||||
# --- Languages/Русский.xml ---
|
||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||
\t<Language uuid="{uuid_lang}">
|
||||
\t\t<Properties>
|
||||
\t\t\t<Name>Русский</Name>
|
||||
@@ -184,20 +187,50 @@ def main():
|
||||
\t</Language>
|
||||
</MetaDataObject>'''
|
||||
|
||||
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||
# via panelDef but not placed by default. Without this file the web client renders
|
||||
# section icons without labels (icon-only mode).
|
||||
open_panel_inst = new_uuid()
|
||||
sections_panel_inst = new_uuid()
|
||||
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||
\t<top>
|
||||
\t\t<panel id="{open_panel_inst}">
|
||||
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||
\t\t</panel>
|
||||
\t</top>
|
||||
\t<left>
|
||||
\t\t<panel id="{sections_panel_inst}">
|
||||
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||
\t\t</panel>
|
||||
\t</left>
|
||||
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||
</ClientApplicationInterface>'''
|
||||
|
||||
# --- Create directories ---
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
lang_dir = os.path.join(output_dir, "Languages")
|
||||
os.makedirs(lang_dir, exist_ok=True)
|
||||
ext_dir = os.path.join(output_dir, "Ext")
|
||||
os.makedirs(ext_dir, exist_ok=True)
|
||||
|
||||
# --- Write files ---
|
||||
write_utf8_bom(cfg_file, cfg_xml)
|
||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||
write_utf8_bom(lang_file, lang_xml)
|
||||
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||
write_utf8_bom(cai_file, cai_xml)
|
||||
|
||||
print(f"[OK] Создана конфигурация: {name}")
|
||||
print(f" Каталог: {output_dir}")
|
||||
print(f" Configuration.xml: {cfg_file}")
|
||||
print(f" Languages: {lang_file}")
|
||||
print(f" Ext/CAI: {cai_file}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: cf-validate
|
||||
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
||||
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cf-validate — валидация конфигурации 1С
|
||||
|
||||
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|------------|:-----:|---------|-------------------------------------------------|
|
||||
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||
```
|
||||
+612
-544
File diff suppressed because it is too large
Load Diff
+67
-9
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-validate v1.1 — Validate 1C configuration XML structure
|
||||
# cf-validate v1.4 — Validate 1C configuration XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||
import sys, os, argparse, re
|
||||
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
|
||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||
]
|
||||
|
||||
# 44 types in canonical order
|
||||
# 45 types in canonical order
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||
'Bot': 'Bots',
|
||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||
@@ -82,7 +83,7 @@ VALID_ENUM_VALUES = {
|
||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
||||
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
||||
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||
],
|
||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||
'ScriptVariant': ['Russian', 'English'],
|
||||
@@ -90,7 +91,10 @@ VALID_ENUM_VALUES = {
|
||||
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
||||
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
||||
'InterfaceCompatibilityMode': [
|
||||
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
||||
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
||||
],
|
||||
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
||||
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
||||
'CompatibilityMode': [
|
||||
@@ -100,7 +104,7 @@ VALID_ENUM_VALUES = {
|
||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
||||
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
||||
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -162,7 +166,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Validate 1C configuration XML structure', allow_abbrev=False
|
||||
)
|
||||
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
|
||||
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
|
||||
parser.add_argument('-Detailed', action='store_true')
|
||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||
@@ -228,8 +232,8 @@ def main():
|
||||
version = root.get('version', '')
|
||||
if not version:
|
||||
r.warn('1. Missing version attribute on MetaDataObject')
|
||||
elif version not in ('2.17', '2.20'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||
elif version not in ('2.17', '2.20', '2.21'):
|
||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||
|
||||
# Must have Configuration child
|
||||
cfg_node = None
|
||||
@@ -534,6 +538,60 @@ def main():
|
||||
else:
|
||||
pass # no ChildObjects
|
||||
|
||||
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
||||
def test_form_ref(ref):
|
||||
if not ref:
|
||||
return True
|
||||
if GUID_PATTERN.match(ref):
|
||||
return True
|
||||
parts = ref.split('.')
|
||||
if len(parts) == 2 and parts[0] == 'CommonForm':
|
||||
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
|
||||
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
|
||||
return os.path.isfile(p) or os.path.isfile(p_ext)
|
||||
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
|
||||
d = CHILD_TYPE_DIR_MAP[parts[0]]
|
||||
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
|
||||
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
|
||||
return os.path.isfile(p) or os.path.isfile(p_ext)
|
||||
return False
|
||||
|
||||
form_refs_checked = 0
|
||||
form_ref_errors = []
|
||||
|
||||
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
|
||||
if os.path.isfile(hp_path):
|
||||
try:
|
||||
hp_tree = etree.parse(hp_path)
|
||||
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
||||
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
|
||||
ref = (f.text or '').strip()
|
||||
if not ref:
|
||||
continue
|
||||
form_refs_checked += 1
|
||||
if not test_form_ref(ref):
|
||||
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
|
||||
except Exception as e:
|
||||
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
|
||||
|
||||
if props_node is not None:
|
||||
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
|
||||
for pn in form_props:
|
||||
node = props_node.find(f'md:{pn}', NS)
|
||||
if node is not None and node.text and node.text.strip():
|
||||
ref = node.text.strip()
|
||||
form_refs_checked += 1
|
||||
if not test_form_ref(ref):
|
||||
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
|
||||
|
||||
if form_refs_checked == 0:
|
||||
r.ok('9. Form references: none to check')
|
||||
elif not form_ref_errors:
|
||||
r.ok(f'9. Form references: {form_refs_checked} verified')
|
||||
else:
|
||||
for err in form_ref_errors:
|
||||
r.error(f'9. {err}')
|
||||
|
||||
# --- Final output ---
|
||||
r.finalize(out_file)
|
||||
sys.exit(1 if r.errors > 0 else 0)
|
||||
@@ -1,101 +1,101 @@
|
||||
---
|
||||
name: cfe-borrow
|
||||
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-borrow — Заимствование объектов из конфигурации
|
||||
|
||||
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||
| `BorrowMainAttribute` | Используй при добавлении нового реквизита на заимствованную форму. `Form` (по умолч.) — реквизиты с формы, `All` — все реквизиты объекта |
|
||||
|
||||
## Формат -Object
|
||||
|
||||
- `Catalog.Контрагенты` — справочник
|
||||
- `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 родительского объекта
|
||||
|
||||
### Заимствование основного реквизита формы (-BorrowMainAttribute)
|
||||
|
||||
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
|
||||
|
||||
**Два режима**:
|
||||
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
|
||||
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
|
||||
|
||||
**Типовой сценарий** (добавление реквизита + вывод на форму):
|
||||
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
|
||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||
|
||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Заимствовать один объект
|
||||
... -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.ВидыОплат"
|
||||
|
||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
|
||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <ExtensionPath>
|
||||
```
|
||||
|
||||
---
|
||||
name: cfe-borrow
|
||||
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-borrow — Заимствование объектов из конфигурации
|
||||
|
||||
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||
|
||||
## Формат -Object
|
||||
|
||||
- `Catalog.Контрагенты` — справочник
|
||||
- `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 родительского объекта
|
||||
|
||||
### Заимствование основного реквизита формы (-BorrowMainAttribute)
|
||||
|
||||
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
|
||||
|
||||
**Два режима**:
|
||||
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
|
||||
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
|
||||
|
||||
**Типовой сценарий** (добавление реквизита + вывод на форму):
|
||||
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
|
||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||
|
||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cfe-borrow/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Заимствовать один объект
|
||||
... -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.ВидыОплат"
|
||||
|
||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||
|
||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <ExtensionPath>
|
||||
```
|
||||
|
||||
+1866
-1753
File diff suppressed because it is too large
Load Diff
+262
-117
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||
|
||||
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||
# platform rejects the form with "Неверный путь к данным" on load.
|
||||
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
|
||||
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
|
||||
|
||||
|
||||
def strip_form_bindings(xml, keep_objekt):
|
||||
"""Strip data-binding tags whose root attribute isn't borrowed.
|
||||
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
|
||||
for tag in FORM_BINDING_DATA_TAGS:
|
||||
if keep_objekt:
|
||||
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
|
||||
else:
|
||||
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||
for tag in FORM_BINDING_PICTURE_TAGS:
|
||||
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||
return xml
|
||||
|
||||
|
||||
def decode_numeric_entities(s):
|
||||
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
||||
back to literal so PS↔PY output matches. Named entities (& < ...) are left intact."""
|
||||
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
|
||||
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
|
||||
return s
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
@@ -254,6 +284,22 @@ XMLNS_DECL = (
|
||||
)
|
||||
|
||||
|
||||
def detect_format_version(d):
|
||||
while d:
|
||||
cfg_path = os.path.join(d, "Configuration.xml")
|
||||
if os.path.isfile(cfg_path):
|
||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||
head = f.read(2000)
|
||||
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||
if m:
|
||||
return m.group(1)
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return "2.17"
|
||||
|
||||
|
||||
def get_child_indent(container):
|
||||
if container.text and "\n" in container.text:
|
||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||
@@ -303,11 +349,49 @@ def expand_self_closing(container, parent_indent):
|
||||
container.text = "\r\n" + parent_indent
|
||||
|
||||
|
||||
def _detect_xml_style(path):
|
||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||
try:
|
||||
raw = open(path, "rb").read()
|
||||
except OSError:
|
||||
return None
|
||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
body = raw[3:] if bom else raw
|
||||
crlf = b"\r\n" in body
|
||||
m = re.search(rb'encoding="([^"]+)"', body[:200])
|
||||
enc = m.group(1).decode("ascii") if m else "utf-8"
|
||||
final_nl = body.endswith(b"\n")
|
||||
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
|
||||
|
||||
|
||||
def _finalize_xml_bytes(xml_bytes, style):
|
||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
||||
enc_decl = style["enc"] if style else "utf-8"
|
||||
xml_bytes = xml_bytes.replace(
|
||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||
want_final_nl = style["final_nl"] if style else True
|
||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||
if want_final_nl:
|
||||
xml_bytes += b"\n"
|
||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||
if style and style["crlf"]:
|
||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||
return xml_bytes
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
style = _detect_xml_style(path)
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
||||
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
if style is None or style["bom"]:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
@@ -363,6 +447,8 @@ def main():
|
||||
cfg_resolved = os.path.abspath(cfg_path)
|
||||
cfg_dir = os.path.dirname(cfg_resolved)
|
||||
|
||||
format_version = detect_format_version(ext_dir)
|
||||
|
||||
# --- 2. Load extension Configuration.xml ---
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(ext_resolved, xml_parser)
|
||||
@@ -442,6 +528,13 @@ def main():
|
||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||
if prop_node is not None:
|
||||
src_props[prop_name] = (prop_node.text or "").strip()
|
||||
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||
if type_name == "DefinedType":
|
||||
type_node = props_node.find(f"{{{MD_NS}}}Type")
|
||||
if type_node is not None:
|
||||
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||
|
||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||
|
||||
@@ -499,7 +592,7 @@ def main():
|
||||
|
||||
lines = []
|
||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">')
|
||||
lines.append(f'<MetaDataObject {XMLNS_DECL} version="{format_version}">')
|
||||
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
|
||||
lines.append(internal_info_xml)
|
||||
lines.append("\t\t<Properties>")
|
||||
@@ -513,6 +606,10 @@ def main():
|
||||
prop_val = source_props.get(prop_name, "false")
|
||||
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
||||
|
||||
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||
if type_name == "DefinedType" and "__TypeXml" in source_props:
|
||||
lines.append(f"\t\t\t{source_props['__TypeXml']}")
|
||||
|
||||
lines.append("\t\t</Properties>")
|
||||
|
||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||
@@ -624,7 +721,26 @@ def main():
|
||||
first_level = {}
|
||||
deep_paths = []
|
||||
|
||||
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
||||
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||
for tag in FORM_BINDING_DATA_TAGS:
|
||||
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', 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
|
||||
seg2 = segments[2] if len(segments) >= 3 else None
|
||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||
|
||||
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
|
||||
path = m.group(1)
|
||||
segments = path.split(".")
|
||||
seg0 = segments[0]
|
||||
@@ -635,22 +751,14 @@ def main():
|
||||
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
|
||||
seg2 = segments[2] if len(segments) >= 3 else None
|
||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||
|
||||
# Deduplicate deep paths
|
||||
seen = set()
|
||||
unique_deep = []
|
||||
for dp in deep_paths:
|
||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_deep.append(dp)
|
||||
@@ -921,26 +1029,40 @@ def main():
|
||||
# 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
|
||||
# Read existing object XML (needed for dedup + enrichment)
|
||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||
obj_content = fh.read()
|
||||
|
||||
# Inject extra properties after ExtendedConfigurationObject
|
||||
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||
existing_child_names = set()
|
||||
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
|
||||
if m_co:
|
||||
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
|
||||
existing_child_names.add(nm)
|
||||
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
|
||||
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
|
||||
|
||||
# Generate full object XML with attributes and TS
|
||||
content_parts = []
|
||||
for attr in insert_attrs:
|
||||
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
|
||||
for ts in insert_ts:
|
||||
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
|
||||
adopted_content = "\n".join(content_parts).rstrip()
|
||||
|
||||
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||
if extra_props:
|
||||
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
||||
obj_props_block = m_props.group(1) if m_props else ""
|
||||
props_xml = ""
|
||||
for p_name, p_val in extra_props.items():
|
||||
if f"<{p_name}>" in obj_props_block:
|
||||
continue
|
||||
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
||||
if props_xml:
|
||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
||||
|
||||
# Replace empty ChildObjects with adopted content
|
||||
if adopted_content:
|
||||
@@ -992,79 +1114,93 @@ def main():
|
||||
|
||||
# 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"]] = []
|
||||
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||
deep_by_attr = {}
|
||||
for dp in deep_paths:
|
||||
if dp["ObjectAttr"] in ts_names:
|
||||
continue
|
||||
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
||||
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
||||
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
||||
|
||||
if deep_by_attr:
|
||||
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
|
||||
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
|
||||
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
||||
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
|
||||
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||
|
||||
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']}")
|
||||
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||
ts_deep_by_col = {}
|
||||
for dp in deep_paths:
|
||||
if dp["ObjectAttr"] not in ts_names:
|
||||
continue
|
||||
if not dp.get("SubSubAttr"):
|
||||
continue
|
||||
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
||||
continue
|
||||
k = (dp["ObjectAttr"], dp["SubAttr"])
|
||||
ts_deep_by_col.setdefault(k, [])
|
||||
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
||||
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
||||
if ts_deep_by_col:
|
||||
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
||||
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
||||
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
||||
if not ts_info:
|
||||
continue
|
||||
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
||||
if not col_info:
|
||||
continue
|
||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
||||
if not cat_match:
|
||||
continue
|
||||
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||
|
||||
info(" Main attribute borrowing complete")
|
||||
|
||||
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
|
||||
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
|
||||
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
|
||||
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
|
||||
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}")
|
||||
|
||||
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"])
|
||||
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']}")
|
||||
|
||||
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||
|
||||
@@ -1080,11 +1216,25 @@ def main():
|
||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||
src_form_content = fh.read()
|
||||
|
||||
# 3. Generate form metadata XML
|
||||
new_form_uuid = new_guid()
|
||||
# 3. Generate form metadata XML.
|
||||
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||
# (regenerating it would churn the form's identity on every rerun).
|
||||
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
|
||||
new_form_uuid = ""
|
||||
if os.path.isfile(existing_wrapper):
|
||||
try:
|
||||
existing_root = etree.parse(existing_wrapper).getroot()
|
||||
for c in existing_root:
|
||||
if isinstance(c.tag, str) and localname(c) == "Form":
|
||||
new_form_uuid = c.get("uuid", "") or ""
|
||||
break
|
||||
except Exception:
|
||||
new_form_uuid = ""
|
||||
if not new_form_uuid:
|
||||
new_form_uuid = new_guid()
|
||||
form_meta_lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
f'<MetaDataObject {XMLNS_DECL} version="2.17">',
|
||||
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
||||
f'\t<Form uuid="{new_form_uuid}">',
|
||||
'\t\t<InternalInfo/>',
|
||||
'\t\t<Properties>',
|
||||
@@ -1111,7 +1261,10 @@ def main():
|
||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||
src_form_el = src_form_tree.getroot()
|
||||
|
||||
form_version = src_form_el.get("version", "2.17")
|
||||
# Borrowed form uses the extension's format version (not the source form's) — keeps the
|
||||
# extension uniform; otherwise the platform rejects the import on a version mismatch
|
||||
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
|
||||
form_version = format_version
|
||||
|
||||
src_auto_cmd = None
|
||||
form_props = []
|
||||
@@ -1129,25 +1282,21 @@ def main():
|
||||
continue
|
||||
if not reached_visual:
|
||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||
form_props.append(etree.tostring(fc, encoding="unicode"))
|
||||
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
||||
|
||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||
|
||||
# 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")
|
||||
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
||||
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
||||
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||
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
|
||||
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)
|
||||
# Strip data-binding tags whose root attribute isn't borrowed
|
||||
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||
|
||||
# ChildItems: copy full tree, clean up base-config references
|
||||
child_items_xml = ""
|
||||
@@ -1158,20 +1307,12 @@ def main():
|
||||
break
|
||||
|
||||
if src_child_items is not None:
|
||||
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
|
||||
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
||||
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 / 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 data-binding tags whose root attribute isn't borrowed
|
||||
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||
# 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)
|
||||
@@ -1408,12 +1549,16 @@ def main():
|
||||
save_text_bom(form_xml_file, "".join(parts))
|
||||
info(f" Created: {form_xml_file}")
|
||||
|
||||
# 6. Create empty Module.bsl
|
||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||
# not clobber user code added to the form module).
|
||||
module_dir = os.path.join(form_xml_dir, "Form")
|
||||
os.makedirs(module_dir, exist_ok=True)
|
||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||
save_text_bom(module_bsl_file, "")
|
||||
info(f" Created: {module_bsl_file}")
|
||||
if os.path.isfile(module_bsl_file):
|
||||
info(" Preserved existing Module.bsl")
|
||||
else:
|
||||
save_text_bom(module_bsl_file, "")
|
||||
info(f" Created: {module_bsl_file}")
|
||||
|
||||
# 7. Register form in parent object ChildObjects
|
||||
register_form_in_object(type_name, obj_name, form_name)
|
||||
@@ -1,70 +1,57 @@
|
||||
---
|
||||
name: cfe-diff
|
||||
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-diff — Анализ расширения конфигурации
|
||||
|
||||
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
|
||||
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
```
|
||||
|
||||
## Mode A — обзор расширения
|
||||
|
||||
Для каждого объекта показывает:
|
||||
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
||||
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
||||
|
||||
Пример вывода:
|
||||
```
|
||||
[BORROWED] Catalog.Валюты
|
||||
&ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — 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 — проверка переноса
|
||||
|
||||
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
||||
|
||||
Статусы:
|
||||
- `[TRANSFERRED]` — код найден в конфигурации
|
||||
- `[NOT_TRANSFERRED]` — код не найден
|
||||
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор — что изменено в расширении
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
|
||||
# Проверка переноса — все ли #Вставка перенесены
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
||||
```
|
||||
---
|
||||
name: cfe-diff
|
||||
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-diff — Анализ расширения конфигурации
|
||||
|
||||
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
|
||||
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cfe-diff/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
```
|
||||
|
||||
## Mode A — обзор расширения
|
||||
|
||||
Для каждого объекта показывает:
|
||||
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
||||
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
||||
|
||||
Для каждой формы заимствованного объекта показывается:
|
||||
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
||||
- callType-события формы и элементов
|
||||
- callType на командах
|
||||
|
||||
## Mode B — проверка переноса
|
||||
|
||||
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
||||
|
||||
Статусы:
|
||||
- `[TRANSFERRED]` — код найден в конфигурации
|
||||
- `[NOT_TRANSFERRED]` — код не найден
|
||||
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор — что изменено в расширении
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||
|
||||
# Проверка переноса — все ли #Вставка перенесены
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
||||
```
|
||||
+471
-471
@@ -1,471 +1,471 @@
|
||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ConfigPath,
|
||||
|
||||
[ValidateSet("A","B")]
|
||||
[string]$Mode = "A"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve paths ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
|
||||
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
|
||||
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
|
||||
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
|
||||
|
||||
# --- Type -> directory mapping ---
|
||||
$childTypeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
||||
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
||||
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
||||
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
||||
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
||||
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
$extDoc = New-Object System.Xml.XmlDocument
|
||||
$extDoc.PreserveWhitespace = $false
|
||||
$extDoc.Load($extCfg)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
||||
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
||||
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
||||
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
||||
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||
|
||||
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
||||
Write-Host " NamePrefix: $namePrefix"
|
||||
Write-Host ""
|
||||
|
||||
# --- Collect ChildObjects ---
|
||||
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||
if (-not $childObjNode) {
|
||||
Write-Host "[WARN] No ChildObjects in extension"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$objects = @()
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Language") { continue }
|
||||
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
||||
}
|
||||
|
||||
if ($objects.Count -eq 0) {
|
||||
Write-Host "No objects (besides Language) in extension."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Helper: check if object is borrowed ---
|
||||
function Get-ObjectInfo {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $false
|
||||
$doc.Load($objFile)
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$objEl = $null
|
||||
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
||||
|
||||
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
||||
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
||||
|
||||
$info = @{
|
||||
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
||||
File = $objFile
|
||||
Exists = $true
|
||||
Type = $objType
|
||||
Name = $objName
|
||||
DirName = $dirName
|
||||
ObjElement = $objEl
|
||||
ObjNs = $objNs
|
||||
}
|
||||
return $info
|
||||
}
|
||||
|
||||
# --- Helper: find .bsl files for object ---
|
||||
function Get-BslFiles {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
||||
|
||||
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
||||
|
||||
$bslFiles = @()
|
||||
$extDir = Join-Path $objDir "Ext"
|
||||
if (Test-Path $extDir) {
|
||||
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($item in $items) { $bslFiles += $item.FullName }
|
||||
}
|
||||
|
||||
# Forms
|
||||
$formsDir = Join-Path $objDir "Forms"
|
||||
if (Test-Path $formsDir) {
|
||||
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
||||
}
|
||||
|
||||
return $bslFiles
|
||||
}
|
||||
|
||||
# --- Helper: parse interceptors from .bsl ---
|
||||
function Get-Interceptors {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$interceptors = @()
|
||||
$i = 0
|
||||
while ($i -lt $lines.Count) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
|
||||
$type = $Matches[1]
|
||||
$method = $Matches[2]
|
||||
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
|
||||
}
|
||||
$i++
|
||||
}
|
||||
return $interceptors
|
||||
}
|
||||
|
||||
# --- Helper: extract #Вставка blocks from .bsl ---
|
||||
function Get-InsertionBlocks {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$blocks = @()
|
||||
$inBlock = $false
|
||||
$blockLines = @()
|
||||
$startLine = 0
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -eq "#Вставка") {
|
||||
$inBlock = $true
|
||||
$blockLines = @()
|
||||
$startLine = $i + 1
|
||||
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
|
||||
$inBlock = $false
|
||||
$blocks += @{
|
||||
StartLine = $startLine
|
||||
EndLine = $i + 1
|
||||
Code = ($blockLines -join "`n").Trim()
|
||||
File = $bslPath
|
||||
}
|
||||
} elseif ($inBlock) {
|
||||
$blockLines += $lines[$i]
|
||||
}
|
||||
}
|
||||
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
|
||||
# ============================================================
|
||||
if ($Mode -eq "A") {
|
||||
$borrowedList = @()
|
||||
$ownList = @()
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
||||
continue
|
||||
}
|
||||
if (-not $info.Exists) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($info.Borrowed) {
|
||||
$borrowedList += $obj
|
||||
|
||||
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Find .bsl files and interceptors
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
if ($interceptors.Count -gt 0) {
|
||||
foreach ($ic in $interceptors) {
|
||||
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host " $relPath (no interceptors)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check for own attributes/forms in ChildObjects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$ownAttrs = 0
|
||||
$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)
|
||||
if ($cProps) {
|
||||
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
||||
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
||||
$borrowedItems++
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $ownAttrs++ }
|
||||
"TabularSection" { $ownTS++ }
|
||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
||||
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
||||
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
||||
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
||||
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 {
|
||||
$ownList += $obj
|
||||
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Brief info for own objects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$attrs = 0; $forms = 0; $ts = 0
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $attrs++ }
|
||||
"TabularSection" { $ts++ }
|
||||
"Form" { $forms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
||||
if ($ts -gt 0) { $parts += "$ts TS" }
|
||||
if ($forms -gt 0) { $parts += "$forms forms" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " $($parts -join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE B: Transfer check
|
||||
# ============================================================
|
||||
if ($Mode -eq "B") {
|
||||
$transferred = 0
|
||||
$notTransferred = 0
|
||||
$needsReview = 0
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
||||
|
||||
# Find .bsl files with &ИзменениеИКонтроль
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
||||
|
||||
if ($macInterceptors.Count -eq 0) { continue }
|
||||
|
||||
foreach ($ic in $macInterceptors) {
|
||||
$methodName = $ic.Method
|
||||
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
|
||||
# Find #Вставка blocks in this file
|
||||
$insertBlocks = Get-InsertionBlocks $bsl
|
||||
|
||||
if ($insertBlocks.Count -eq 0) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
# Find corresponding module in config
|
||||
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
||||
$dirName = $childTypeDirMap[$obj.Type]
|
||||
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
||||
|
||||
if (-not (Test-Path $configBsl)) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$allTransferred = $true
|
||||
foreach ($block in $insertBlocks) {
|
||||
$code = $block.Code
|
||||
if (-not $code) { continue }
|
||||
|
||||
# Normalize whitespace for comparison
|
||||
$codeNorm = $code -replace '\s+', ' '
|
||||
$configNorm = $configContent -replace '\s+', ' '
|
||||
|
||||
if ($configNorm.Contains($codeNorm)) {
|
||||
# Found in config
|
||||
} else {
|
||||
$allTransferred = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($allTransferred) {
|
||||
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
|
||||
$transferred++
|
||||
} else {
|
||||
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
|
||||
$notTransferred++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
|
||||
}
|
||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ExtensionPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ConfigPath,
|
||||
|
||||
[ValidateSet("A","B")]
|
||||
[string]$Mode = "A"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve paths ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
|
||||
}
|
||||
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
|
||||
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
|
||||
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
|
||||
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
|
||||
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
|
||||
|
||||
# --- Type -> directory mapping ---
|
||||
$childTypeDirMap = @{
|
||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
||||
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
||||
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
||||
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
||||
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
||||
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
$extDoc = New-Object System.Xml.XmlDocument
|
||||
$extDoc.PreserveWhitespace = $false
|
||||
$extDoc.Load($extCfg)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
||||
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
||||
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
||||
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
||||
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||
|
||||
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
||||
Write-Host " NamePrefix: $namePrefix"
|
||||
Write-Host ""
|
||||
|
||||
# --- Collect ChildObjects ---
|
||||
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||
if (-not $childObjNode) {
|
||||
Write-Host "[WARN] No ChildObjects in extension"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$objects = @()
|
||||
foreach ($child in $childObjNode.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq "Language") { continue }
|
||||
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
||||
}
|
||||
|
||||
if ($objects.Count -eq 0) {
|
||||
Write-Host "No objects (besides Language) in extension."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Helper: check if object is borrowed ---
|
||||
function Get-ObjectInfo {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $false
|
||||
$doc.Load($objFile)
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$objEl = $null
|
||||
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
||||
|
||||
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
||||
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
||||
|
||||
$info = @{
|
||||
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
||||
File = $objFile
|
||||
Exists = $true
|
||||
Type = $objType
|
||||
Name = $objName
|
||||
DirName = $dirName
|
||||
ObjElement = $objEl
|
||||
ObjNs = $objNs
|
||||
}
|
||||
return $info
|
||||
}
|
||||
|
||||
# --- Helper: find .bsl files for object ---
|
||||
function Get-BslFiles {
|
||||
param([string]$objType, [string]$objName)
|
||||
|
||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
||||
$dirName = $childTypeDirMap[$objType]
|
||||
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
||||
|
||||
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
||||
|
||||
$bslFiles = @()
|
||||
$extDir = Join-Path $objDir "Ext"
|
||||
if (Test-Path $extDir) {
|
||||
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($item in $items) { $bslFiles += $item.FullName }
|
||||
}
|
||||
|
||||
# Forms
|
||||
$formsDir = Join-Path $objDir "Forms"
|
||||
if (Test-Path $formsDir) {
|
||||
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
||||
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
||||
}
|
||||
|
||||
return $bslFiles
|
||||
}
|
||||
|
||||
# --- Helper: parse interceptors from .bsl ---
|
||||
function Get-Interceptors {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$interceptors = @()
|
||||
$i = 0
|
||||
while ($i -lt $lines.Count) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
|
||||
$type = $Matches[1]
|
||||
$method = $Matches[2]
|
||||
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
|
||||
}
|
||||
$i++
|
||||
}
|
||||
return $interceptors
|
||||
}
|
||||
|
||||
# --- Helper: extract #Вставка blocks from .bsl ---
|
||||
function Get-InsertionBlocks {
|
||||
param([string]$bslPath)
|
||||
|
||||
if (-not (Test-Path $bslPath)) { return @() }
|
||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||
$blocks = @()
|
||||
$inBlock = $false
|
||||
$blockLines = @()
|
||||
$startLine = 0
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i].Trim()
|
||||
if ($line -eq "#Вставка") {
|
||||
$inBlock = $true
|
||||
$blockLines = @()
|
||||
$startLine = $i + 1
|
||||
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
|
||||
$inBlock = $false
|
||||
$blocks += @{
|
||||
StartLine = $startLine
|
||||
EndLine = $i + 1
|
||||
Code = ($blockLines -join "`n").Trim()
|
||||
File = $bslPath
|
||||
}
|
||||
} elseif ($inBlock) {
|
||||
$blockLines += $lines[$i]
|
||||
}
|
||||
}
|
||||
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
|
||||
# ============================================================
|
||||
if ($Mode -eq "A") {
|
||||
$borrowedList = @()
|
||||
$ownList = @()
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
||||
continue
|
||||
}
|
||||
if (-not $info.Exists) {
|
||||
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($info.Borrowed) {
|
||||
$borrowedList += $obj
|
||||
|
||||
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Find .bsl files and interceptors
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
if ($interceptors.Count -gt 0) {
|
||||
foreach ($ic in $interceptors) {
|
||||
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
|
||||
}
|
||||
} else {
|
||||
Write-Host " $relPath (no interceptors)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check for own attributes/forms in ChildObjects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$ownAttrs = 0
|
||||
$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)
|
||||
if ($cProps) {
|
||||
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
||||
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
||||
$borrowedItems++
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $ownAttrs++ }
|
||||
"TabularSection" { $ownTS++ }
|
||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
||||
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
||||
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
||||
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
||||
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 {
|
||||
$ownList += $obj
|
||||
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
||||
|
||||
# Brief info for own objects
|
||||
if ($info.ObjElement) {
|
||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||
if ($childObj) {
|
||||
$attrs = 0; $forms = 0; $ts = 0
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $attrs++ }
|
||||
"TabularSection" { $ts++ }
|
||||
"Form" { $forms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
||||
if ($ts -gt 0) { $parts += "$ts TS" }
|
||||
if ($forms -gt 0) { $parts += "$forms forms" }
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " $($parts -join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE B: Transfer check
|
||||
# ============================================================
|
||||
if ($Mode -eq "B") {
|
||||
$transferred = 0
|
||||
$notTransferred = 0
|
||||
$needsReview = 0
|
||||
|
||||
foreach ($obj in $objects) {
|
||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
||||
|
||||
# Find .bsl files with &ИзменениеИКонтроль
|
||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||
foreach ($bsl in $bslFiles) {
|
||||
$interceptors = Get-Interceptors $bsl
|
||||
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
||||
|
||||
if ($macInterceptors.Count -eq 0) { continue }
|
||||
|
||||
foreach ($ic in $macInterceptors) {
|
||||
$methodName = $ic.Method
|
||||
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||
|
||||
# Find #Вставка blocks in this file
|
||||
$insertBlocks = Get-InsertionBlocks $bsl
|
||||
|
||||
if ($insertBlocks.Count -eq 0) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
# Find corresponding module in config
|
||||
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
||||
$dirName = $childTypeDirMap[$obj.Type]
|
||||
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
||||
|
||||
if (-not (Test-Path $configBsl)) {
|
||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
||||
$needsReview++
|
||||
continue
|
||||
}
|
||||
|
||||
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
||||
|
||||
$allTransferred = $true
|
||||
foreach ($block in $insertBlocks) {
|
||||
$code = $block.Code
|
||||
if (-not $code) { continue }
|
||||
|
||||
# Normalize whitespace for comparison
|
||||
$codeNorm = $code -replace '\s+', ' '
|
||||
$configNorm = $configContent -replace '\s+', ' '
|
||||
|
||||
if ($configNorm.Contains($codeNorm)) {
|
||||
# Found in config
|
||||
} else {
|
||||
$allTransferred = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($allTransferred) {
|
||||
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
|
||||
$transferred++
|
||||
} else {
|
||||
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
|
||||
$notTransferred++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
|
||||
}
|
||||
@@ -1,82 +1,71 @@
|
||||
---
|
||||
name: cfe-init
|
||||
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
|
||||
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-init — Создание расширения конфигурации 1С
|
||||
|
||||
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
|
||||
|
||||
## Подготовка
|
||||
|
||||
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `Name` | Имя расширения (обязат.) | — |
|
||||
| `Synonym` | Синоним | = Name |
|
||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||
| `OutputDir` | Каталог для создания | `src` |
|
||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||
| `Version` | Версия расширения | — |
|
||||
| `Vendor` | Поставщик | — |
|
||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||
| `NoRole` | Без основной роли | false |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
|
||||
```
|
||||
|
||||
## Что создаётся
|
||||
|
||||
```
|
||||
<OutputDir>/
|
||||
├── Configuration.xml # Свойства расширения
|
||||
├── Languages/
|
||||
│ └── Русский.xml # Язык (заимствованный)
|
||||
└── Roles/ # Если не -NoRole
|
||||
└── <Prefix>ОсновнаяРоль.xml
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
||||
|
||||
# Расширение-исправление с явным режимом совместимости
|
||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
||||
|
||||
# Расширение-доработка с версией
|
||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
||||
|
||||
# Без роли, с явным префиксом
|
||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <OutputDir>
|
||||
```
|
||||
|
||||
---
|
||||
name: cfe-init
|
||||
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
|
||||
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-init — Создание расширения конфигурации 1С
|
||||
|
||||
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
|
||||
|
||||
## Подготовка
|
||||
|
||||
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `Name` | Имя расширения (обязат.) | — |
|
||||
| `Synonym` | Синоним | = Name |
|
||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||
| `OutputDir` | Каталог для создания | `src` |
|
||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||
| `Version` | Версия расширения | — |
|
||||
| `Vendor` | Поставщик | — |
|
||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||
| `NoRole` | Без основной роли | false |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cfe-init/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
||||
|
||||
# Расширение-исправление с явным режимом совместимости
|
||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
||||
|
||||
# Расширение-доработка с версией
|
||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
||||
|
||||
# Без роли, с явным префиксом
|
||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <OutputDir>
|
||||
```
|
||||
|
||||
+279
-261
@@ -1,261 +1,279 @@
|
||||
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$NamePrefix,
|
||||
[string]$OutputDir = "src",
|
||||
[ValidateSet("Patch","Customization","AddOn")]
|
||||
[string]$Purpose = "Customization",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24",
|
||||
[string]$ConfigPath,
|
||||
[switch]$NoRole
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Default NamePrefix ---
|
||||
if (-not $NamePrefix) {
|
||||
$NamePrefix = "${Name}_"
|
||||
}
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $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()
|
||||
$uuidRole = [guid]::NewGuid().ToString()
|
||||
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Role name ---
|
||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||
|
||||
# --- DefaultRoles XML ---
|
||||
$defaultRolesXml = ""
|
||||
if (-not $NoRole) {
|
||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- ChildObjects ---
|
||||
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
|
||||
if (-not $NoRole) {
|
||||
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
|
||||
}
|
||||
$childObjectsXml += "`r`n`t`t"
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
</Properties>
|
||||
<ChildObjects>$childObjectsXml</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml (adopted format) ---
|
||||
$langXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Language uuid="$uuidLang">
|
||||
<InternalInfo/>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>Русский</Name>
|
||||
<Comment/>
|
||||
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Role XML ---
|
||||
$roleXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<Role uuid="$uuidRole">
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||
<Synonym/>
|
||||
<Comment/>
|
||||
</Properties>
|
||||
</Role>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
|
||||
# --- Role ---
|
||||
if (-not $NoRole) {
|
||||
$roleDir = Join-Path $OutputDir "Roles"
|
||||
if (-not (Test-Path $roleDir)) {
|
||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||
}
|
||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
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) {
|
||||
Write-Host " Role: $roleFile"
|
||||
}
|
||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Name,
|
||||
[string]$Synonym = $Name,
|
||||
[string]$NamePrefix,
|
||||
[string]$OutputDir = "src",
|
||||
[ValidateSet("Patch","Customization","AddOn")]
|
||||
[string]$Purpose = "Customization",
|
||||
[string]$Version,
|
||||
[string]$Vendor,
|
||||
[string]$CompatibilityMode = "Version8_3_24",
|
||||
[string]$ConfigPath,
|
||||
[switch]$NoRole
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Default NamePrefix ---
|
||||
if (-not $NamePrefix) {
|
||||
$NamePrefix = "${Name}_"
|
||||
}
|
||||
|
||||
# --- Resolve output dir ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Check existing ---
|
||||
$cfgFile = Join-Path $OutputDir "Configuration.xml"
|
||||
if (Test-Path $cfgFile) {
|
||||
Write-Error "Configuration.xml already exists: $cfgFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||
$formatVersion = "2.17"
|
||||
|
||||
# --- 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 and InterfaceCompatibilityMode 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")
|
||||
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
||||
if ($fmtVer) {
|
||||
$formatVersion = $fmtVer
|
||||
Write-Host "[INFO] Base config format version: $formatVersion"
|
||||
}
|
||||
$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"
|
||||
}
|
||||
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
|
||||
if ($ifcNode -and $ifcNode.InnerText) {
|
||||
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
|
||||
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
|
||||
} else {
|
||||
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
|
||||
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
|
||||
}
|
||||
} else {
|
||||
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
|
||||
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()
|
||||
$uuidRole = [guid]::NewGuid().ToString()
|
||||
|
||||
# 7 ContainedObject ObjectIds
|
||||
$co1 = [guid]::NewGuid().ToString()
|
||||
$co2 = [guid]::NewGuid().ToString()
|
||||
$co3 = [guid]::NewGuid().ToString()
|
||||
$co4 = [guid]::NewGuid().ToString()
|
||||
$co5 = [guid]::NewGuid().ToString()
|
||||
$co6 = [guid]::NewGuid().ToString()
|
||||
$co7 = [guid]::NewGuid().ToString()
|
||||
|
||||
# --- Synonym XML ---
|
||||
$synonymXml = ""
|
||||
if ($Synonym) {
|
||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- Optional properties ---
|
||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
||||
|
||||
# --- Role name ---
|
||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||
|
||||
# --- DefaultRoles XML ---
|
||||
$defaultRolesXml = ""
|
||||
if (-not $NoRole) {
|
||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
||||
}
|
||||
|
||||
# --- ChildObjects ---
|
||||
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
|
||||
if (-not $NoRole) {
|
||||
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
|
||||
}
|
||||
$childObjectsXml += "`r`n`t`t"
|
||||
|
||||
# --- Configuration.xml ---
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||
<Configuration uuid="$uuidCfg">
|
||||
<InternalInfo>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
|
||||
<xr:ObjectId>$co1</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
|
||||
<xr:ObjectId>$co2</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
|
||||
<xr:ObjectId>$co3</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
|
||||
<xr:ObjectId>$co4</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
|
||||
<xr:ObjectId>$co5</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
|
||||
<xr:ObjectId>$co6</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
<xr:ContainedObject>
|
||||
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
|
||||
<xr:ObjectId>$co7</xr:ObjectId>
|
||||
</xr:ContainedObject>
|
||||
</InternalInfo>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
||||
<Synonym>$synonymXml</Synonym>
|
||||
<Comment/>
|
||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||
<UsePurposes>
|
||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||
</UsePurposes>
|
||||
<ScriptVariant>Russian</ScriptVariant>
|
||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
||||
<Vendor>$vendorXml</Vendor>
|
||||
<Version>$versionXml</Version>
|
||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||
<BriefInformation/>
|
||||
<DetailedInformation/>
|
||||
<Copyright/>
|
||||
<VendorInformationAddress/>
|
||||
<ConfigurationInformationAddress/>
|
||||
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
|
||||
</Properties>
|
||||
<ChildObjects>$childObjectsXml</ChildObjects>
|
||||
</Configuration>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Languages/Русский.xml (adopted format) ---
|
||||
$langXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||
<Language uuid="$uuidLang">
|
||||
<InternalInfo/>
|
||||
<Properties>
|
||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||
<Name>Русский</Name>
|
||||
<Comment/>
|
||||
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
|
||||
<LanguageCode>ru</LanguageCode>
|
||||
</Properties>
|
||||
</Language>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Role XML ---
|
||||
$roleXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||
<Role uuid="$uuidRole">
|
||||
<Properties>
|
||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||
<Synonym/>
|
||||
<Comment/>
|
||||
</Properties>
|
||||
</Role>
|
||||
</MetaDataObject>
|
||||
"@
|
||||
|
||||
# --- Create directories ---
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
$langDir = Join-Path $OutputDir "Languages"
|
||||
if (-not (Test-Path $langDir)) {
|
||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Write files with UTF-8 BOM ---
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||
$langFile = Join-Path $langDir "Русский.xml"
|
||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||
|
||||
# --- Role ---
|
||||
if (-not $NoRole) {
|
||||
$roleDir = Join-Path $OutputDir "Roles"
|
||||
if (-not (Test-Path $roleDir)) {
|
||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||
}
|
||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
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) {
|
||||
Write-Host " Role: $roleFile"
|
||||
}
|
||||
+23
-6
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
|
||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||
import sys, os, argparse, uuid
|
||||
@@ -50,6 +50,10 @@ def main():
|
||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||
format_version = "2.17"
|
||||
|
||||
# --- Resolve ConfigPath ---
|
||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||
if args.ConfigPath:
|
||||
@@ -84,10 +88,14 @@ def main():
|
||||
else:
|
||||
print(f"[WARN] Base config language not found: {base_lang_file}")
|
||||
|
||||
# Read CompatibilityMode from base config
|
||||
# Read CompatibilityMode and InterfaceCompatibilityMode from base config
|
||||
try:
|
||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||
base_cfg_root = base_cfg_tree.getroot()
|
||||
fmt_ver = base_cfg_root.get("version")
|
||||
if fmt_ver:
|
||||
format_version = fmt_ver
|
||||
print(f"[INFO] Base config format version: {format_version}")
|
||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||
if compat_node is not None and compat_node.text:
|
||||
@@ -95,9 +103,18 @@ def main():
|
||||
print(f"[INFO] Base config CompatibilityMode: {compat}")
|
||||
else:
|
||||
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
|
||||
ifc_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:InterfaceCompatibilityMode', ns)
|
||||
if ifc_node is not None and ifc_node.text:
|
||||
ifc_mode = ifc_node.text.strip()
|
||||
print(f"[INFO] Base config InterfaceCompatibilityMode: {ifc_mode}")
|
||||
else:
|
||||
ifc_mode = "TaxiEnableVersion8_2"
|
||||
print(f"[WARN] InterfaceCompatibilityMode not found in base config, using default: {ifc_mode}")
|
||||
except Exception:
|
||||
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
|
||||
ifc_mode = "TaxiEnableVersion8_2"
|
||||
else:
|
||||
ifc_mode = "TaxiEnableVersion8_2"
|
||||
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
|
||||
|
||||
# --- Generate UUIDs ---
|
||||
@@ -146,7 +163,7 @@ def main():
|
||||
\t\t\t</xr:ContainedObject>\n"""
|
||||
|
||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||
\t<Configuration uuid="{uuid_cfg}">
|
||||
\t\t<InternalInfo>
|
||||
{contained_objects}\t\t</InternalInfo>
|
||||
@@ -173,7 +190,7 @@ def main():
|
||||
\t\t\t<Copyright/>
|
||||
\t\t\t<VendorInformationAddress/>
|
||||
\t\t\t<ConfigurationInformationAddress/>
|
||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||
\t\t\t<InterfaceCompatibilityMode>{ifc_mode}</InterfaceCompatibilityMode>
|
||||
\t\t</Properties>
|
||||
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
|
||||
\t</Configuration>
|
||||
@@ -181,7 +198,7 @@ def main():
|
||||
|
||||
# --- Languages/Русский.xml (adopted format) ---
|
||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||
\t<Language uuid="{uuid_lang}">
|
||||
\t\t<InternalInfo/>
|
||||
\t\t<Properties>
|
||||
@@ -196,7 +213,7 @@ def main():
|
||||
|
||||
# --- Role XML ---
|
||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||
<MetaDataObject 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" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||
\t<Role uuid="{uuid_role}">
|
||||
\t\t<Properties>
|
||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: cfe-patch-method
|
||||
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
|
||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-patch-method — Генерация перехватчика метода
|
||||
|
||||
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
|
||||
|
||||
## Предусловие
|
||||
|
||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
|
||||
|
||||
### Авто-определение ConfigPath
|
||||
|
||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||
1. Прочитай `.v8-project.json` из корня проекта
|
||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||
4. Если `configSrc` нет — спроси у пользователя
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание | По умолчанию |
|
||||
|----------|----------|--------------|
|
||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
|
||||
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
|
||||
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
|
||||
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
|
||||
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
|
||||
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
|
||||
|
||||
## Формат ModulePath
|
||||
|
||||
| ModulePath | Файл |
|
||||
|------------|------|
|
||||
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
|
||||
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
|
||||
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
|
||||
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
|
||||
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
|
||||
|
||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||
|
||||
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
|
||||
|
||||
## Типы перехвата
|
||||
|
||||
| InterceptorType | Декоратор | Назначение | Применим к |
|
||||
|-----------------|-----------|------------|------------|
|
||||
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
|
||||
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
|
||||
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
|
||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
|
||||
|
||||
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
|
||||
|
||||
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
|
||||
|
||||
- **Добавляешь код** → оберни его `#Вставка` … `#КонецВставки`.
|
||||
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление` … `#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
|
||||
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
|
||||
|
||||
Пример:
|
||||
```bsl
|
||||
&ИзменениеИКонтроль("ПриЗаписи")
|
||||
Процедура Расш_ПриЗаписи(Отказ)
|
||||
СуммаДокумента = РассчитатьСумму();
|
||||
#Вставка
|
||||
// доработка: округляем
|
||||
СуммаДокумента = Окр(СуммаДокумента, 2);
|
||||
#КонецВставки
|
||||
#Удаление
|
||||
Записать();
|
||||
#КонецУдаления
|
||||
#Вставка
|
||||
ЗаписатьСПроверкой(Отказ);
|
||||
#КонецВставки
|
||||
КонецПроцедуры
|
||||
```
|
||||
|
||||
Правила:
|
||||
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||
|
||||
## Актуализация
|
||||
|
||||
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
|
||||
|
||||
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
|
||||
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
|
||||
|
||||
Статусы в выводе:
|
||||
|
||||
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
|
||||
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
|
||||
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
|
||||
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
|
||||
|
||||
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cfe-patch-method/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Код перед записью
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||
|
||||
# Перехват После на форме
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
||||
|
||||
# Замена функции (ПродолжитьВызов)
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||
|
||||
# ИзменениеИКонтроль — копия тела для правки маркерами
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
||||
|
||||
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||
|
||||
# Проверить все контролируемые методы расширения на дрейф
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
|
||||
|
||||
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
|
||||
```
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/cfe-validate <ExtensionPath>
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: cfe-validate
|
||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|---------------|:-----:|---------|-------------------------------------------------|
|
||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
||||
powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
||||
```
|
||||
+950
-938
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user