mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-26 21:19:42 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8672f96e8d |
@@ -1,67 +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-тип и имя объекта через точку.
|
|
||||||
|
|
||||||
**Важно про `add-childObject`**: операция регистрирует в `<ChildObjects>` Configuration.xml только объект, **файл которого уже существует на диске** (например `Catalogs/Товары.xml`). Если файла нет — скрипт падает с exit 1 и подсказкой. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его в Configuration.xml за один вызов.
|
|
||||||
|
|
||||||
Когда `add-childObject` всё-таки нужен: откатили Configuration.xml (или перезаписали из выгрузки БД), а файлы объектов остались — нужно восстановить ссылки в `<ChildObjects>`.
|
|
||||||
|
|
||||||
При добавлении объект вставляется в каноническую позицию:
|
|
||||||
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,563 +0,0 @@
|
|||||||
# cf-edit v1.1 — 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
|
|
||||||
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
|
||||||
|
|
||||||
# --- 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Type → on-disk directory name (plural) ---
|
|
||||||
$script:typeToDir = @{
|
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
|
||||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
|
||||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
|
||||||
"Constant"="Constants"; "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents"
|
|
||||||
"DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences"; "DocumentJournal"="DocumentJournals"; "Enum"="Enums"
|
|
||||||
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
|
||||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
|
||||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
|
||||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check that the referenced object actually exists on disk.
|
|
||||||
# cf-edit add-childObject is a low-level operation for rare scenarios
|
|
||||||
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
|
|
||||||
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
|
|
||||||
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
|
|
||||||
# unnecessary and error-prone.
|
|
||||||
$typeDir = $script:typeToDir[$typeName]
|
|
||||||
$objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml"
|
|
||||||
if (-not (Test-Path $objFile)) {
|
|
||||||
$hintSkill = switch ($typeName) {
|
|
||||||
"Subsystem" { "subsystem-compile" }
|
|
||||||
"Role" { "role-compile" }
|
|
||||||
default { "meta-compile" }
|
|
||||||
}
|
|
||||||
Write-Error @"
|
|
||||||
Object file not found: $typeDir/$objNameVal.xml
|
|
||||||
cf-edit add-childObject only references objects that already exist on disk.
|
|
||||||
To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
|
||||||
/$hintSkill with {"type":"$typeName","name":"$objNameVal"}
|
|
||||||
"@
|
|
||||||
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,554 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# cf-edit v1.1 — 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",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Type → on-disk directory name (plural)
|
|
||||||
TYPE_TO_DIR = {
|
|
||||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
|
||||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
|
||||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
|
||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
|
||||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
|
||||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
|
||||||
"Constant": "Constants", "CommonForm": "CommonForms", "Catalog": "Catalogs", "Document": "Documents",
|
|
||||||
"DocumentNumerator": "DocumentNumerators", "Sequence": "Sequences", "DocumentJournal": "DocumentJournals", "Enum": "Enums",
|
|
||||||
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
|
||||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
|
|
||||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
|
|
||||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
|
|
||||||
}
|
|
||||||
|
|
||||||
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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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)
|
|
||||||
config_dir = os.path.dirname(resolved_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)
|
|
||||||
|
|
||||||
# Check that the referenced object actually exists on disk.
|
|
||||||
# cf-edit add-childObject is a low-level operation for rare scenarios
|
|
||||||
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
|
|
||||||
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
|
|
||||||
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
|
|
||||||
# unnecessary and error-prone.
|
|
||||||
type_dir = TYPE_TO_DIR.get(type_name)
|
|
||||||
obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml")
|
|
||||||
if not os.path.exists(obj_file):
|
|
||||||
hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile")
|
|
||||||
print(
|
|
||||||
f"Object file not found: {type_dir}/{obj_name_val}.xml\n"
|
|
||||||
f"cf-edit add-childObject only references objects that already exist on disk.\n"
|
|
||||||
f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n"
|
|
||||||
f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}',
|
|
||||||
file=sys.stderr
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# 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,49 +0,0 @@
|
|||||||
---
|
|
||||||
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 "МояКонфигурация"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```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 — валидировать
|
|
||||||
```
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
# cf-init v1.1 — 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>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="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"
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# cf-init v1.1 — 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
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
|
||||||
|
|
||||||
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 main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
|
|
||||||
parser.add_argument('-Name', dest='Name', required=True)
|
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
|
||||||
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
|
|
||||||
parser.add_argument('-Version', dest='Version', default='')
|
|
||||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
|
||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
name = args.Name
|
|
||||||
synonym = args.Synonym if args.Synonym else name
|
|
||||||
output_dir = args.OutputDir
|
|
||||||
version = args.Version
|
|
||||||
vendor = args.Vendor
|
|
||||||
compat = args.CompatibilityMode
|
|
||||||
|
|
||||||
# --- Resolve output dir ---
|
|
||||||
if not os.path.isabs(output_dir):
|
|
||||||
output_dir = os.path.join(os.getcwd(), output_dir)
|
|
||||||
|
|
||||||
# --- Check existing ---
|
|
||||||
cfg_file = os.path.join(output_dir, "Configuration.xml")
|
|
||||||
if os.path.exists(cfg_file):
|
|
||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Generate UUIDs ---
|
|
||||||
uuid_cfg = new_uuid()
|
|
||||||
uuid_lang = new_uuid()
|
|
||||||
co = [new_uuid() for _ in range(7)]
|
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
|
||||||
mobile_funcs = [
|
|
||||||
("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"),
|
|
||||||
]
|
|
||||||
|
|
||||||
mobile_xml = ""
|
|
||||||
for func_name, func_use in mobile_funcs:
|
|
||||||
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
|
|
||||||
|
|
||||||
# --- Synonym XML ---
|
|
||||||
synonym_xml = ""
|
|
||||||
if synonym:
|
|
||||||
synonym_xml = f"\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>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
|
||||||
version_xml = esc_xml(version) if version else ""
|
|
||||||
|
|
||||||
class_ids = [
|
|
||||||
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
|
||||||
"9fcd25a0-4822-11d4-9414-008048da11f9",
|
|
||||||
"e3687481-0a87-462c-a166-9f34594f9bba",
|
|
||||||
"9de14907-ec23-4a07-96f0-85521cb6b53b",
|
|
||||||
"51f2d5d8-ea4d-4064-8892-82951750031e",
|
|
||||||
"e68182ea-4237-4383-967f-90c1e3370bc7",
|
|
||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
|
||||||
]
|
|
||||||
|
|
||||||
contained_objects = ""
|
|
||||||
for i in range(7):
|
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
|
||||||
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
|
||||||
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
|
|
||||||
\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">
|
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
|
||||||
\t\t<InternalInfo>
|
|
||||||
{contained_objects}\t\t</InternalInfo>
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<NamePrefix/>
|
|
||||||
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
|
||||||
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
|
||||||
\t\t\t<UsePurposes>
|
|
||||||
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
|
||||||
\t\t\t</UsePurposes>
|
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
|
||||||
\t\t\t<DefaultRoles/>
|
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
|
||||||
\t\t\t<UpdateCatalogAddress/>
|
|
||||||
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
|
||||||
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
|
||||||
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
|
||||||
\t\t\t<AdditionalFullTextSearchDictionaries/>
|
|
||||||
\t\t\t<CommonSettingsStorage/>
|
|
||||||
\t\t\t<ReportsUserSettingsStorage/>
|
|
||||||
\t\t\t<ReportsVariantsStorage/>
|
|
||||||
\t\t\t<FormDataSettingsStorage/>
|
|
||||||
\t\t\t<DynamicListsUserSettingsStorage/>
|
|
||||||
\t\t\t<URLExternalDataStorage/>
|
|
||||||
\t\t\t<Content/>
|
|
||||||
\t\t\t<DefaultReportForm/>
|
|
||||||
\t\t\t<DefaultReportVariantForm/>
|
|
||||||
\t\t\t<DefaultReportSettingsForm/>
|
|
||||||
\t\t\t<DefaultReportAppearanceTemplate/>
|
|
||||||
\t\t\t<DefaultDynamicListSettingsForm/>
|
|
||||||
\t\t\t<DefaultSearchForm/>
|
|
||||||
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
|
||||||
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
|
||||||
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
|
||||||
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
|
|
||||||
\t\t\t<RequiredMobileApplicationPermissions/>
|
|
||||||
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
|
||||||
\t\t\t</UsedMobileApplicationFunctionalities>
|
|
||||||
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
|
||||||
\t\t\t<MobileApplicationURLs/>
|
|
||||||
\t\t\t<AllowedIncomingShareRequestTypes/>
|
|
||||||
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
|
||||||
\t\t\t<DefaultInterface/>
|
|
||||||
\t\t\t<DefaultStyle/>
|
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
|
||||||
\t\t\t<BriefInformation/>
|
|
||||||
\t\t\t<DetailedInformation/>
|
|
||||||
\t\t\t<Copyright/>
|
|
||||||
\t\t\t<VendorInformationAddress/>
|
|
||||||
\t\t\t<ConfigurationInformationAddress/>
|
|
||||||
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
|
|
||||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
|
||||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
|
||||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
|
||||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
|
||||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
|
||||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
|
||||||
\t\t\t<DefaultConstantsForm/>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t\t<ChildObjects>
|
|
||||||
\t\t\t<Language>Русский</Language>
|
|
||||||
\t\t</ChildObjects>
|
|
||||||
\t</Configuration>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
# --- 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">
|
|
||||||
\t<Language uuid="{uuid_lang}">
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>Русский</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>Русский</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<LanguageCode>ru</LanguageCode>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t</Language>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
lang_dir = os.path.join(output_dir, "Languages")
|
|
||||||
os.makedirs(lang_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)
|
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
|
||||||
print(f" Каталог: {output_dir}")
|
|
||||||
print(f" Configuration.xml: {cfg_file}")
|
|
||||||
print(f" Languages: {lang_file}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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,209 +0,0 @@
|
|||||||
# cfe-patch-method v1.1 — 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"
|
|
||||||
"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
|
|
||||||
"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
|
|
||||||
"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
|
|
||||||
"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
|
||||||
"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
|
|
||||||
"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
|
|
||||||
"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
|
|
||||||
"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="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,247 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# cfe-patch-method v1.1 — 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",
|
|
||||||
"Catalogs": "Catalogs",
|
|
||||||
"Documents": "Documents",
|
|
||||||
"Enums": "Enums",
|
|
||||||
"CommonModules": "CommonModules",
|
|
||||||
"Reports": "Reports",
|
|
||||||
"DataProcessors": "DataProcessors",
|
|
||||||
"ExchangePlans": "ExchangePlans",
|
|
||||||
"ChartsOfAccounts": "ChartsOfAccounts",
|
|
||||||
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
|
||||||
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
|
|
||||||
"BusinessProcesses": "BusinessProcesses",
|
|
||||||
"Tasks": "Tasks",
|
|
||||||
"InformationRegisters": "InformationRegisters",
|
|
||||||
"AccumulationRegisters": "AccumulationRegisters",
|
|
||||||
"AccountingRegisters": "AccountingRegisters",
|
|
||||||
"CalculationRegisters": "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,29 +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 | нет | — | Подробный вывод (все проверки, включая успешные) |
|
|
||||||
| 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"
|
|
||||||
```
|
|
||||||
@@ -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,279 +0,0 @@
|
|||||||
# db-load-xml v1.3 — 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,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$StrictLog
|
|
||||||
)
|
|
||||||
|
|
||||||
$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
|
|
||||||
|
|
||||||
# --- Read log ---
|
|
||||||
$logContent = $null
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Scan log for silent rejections ---
|
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
|
||||||
$fatalLogPatterns = @(
|
|
||||||
'Неверное свойство объекта метаданных',
|
|
||||||
'не входит в состав объекта метаданных',
|
|
||||||
'Неизвестное имя типа',
|
|
||||||
'Неизвестный объект метаданных',
|
|
||||||
'Ни один из документов не является регистратором для регистра',
|
|
||||||
'Неверное значение перечисления',
|
|
||||||
'не может быть приведен к типу'
|
|
||||||
)
|
|
||||||
$silentFailures = @()
|
|
||||||
if ($logContent) {
|
|
||||||
foreach ($line in ($logContent -split "`r?`n")) {
|
|
||||||
foreach ($pat in $fatalLogPatterns) {
|
|
||||||
if ($line -match [regex]::Escape($pat)) {
|
|
||||||
$silentFailures += $line.Trim()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
|
||||||
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
|
||||||
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($silentFailures.Count -gt 0) {
|
|
||||||
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
|
||||||
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
|
|
||||||
Write-Host $msg -ForegroundColor Yellow
|
|
||||||
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
|
|
||||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-xml v1.3 — 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")
|
|
||||||
parser.add_argument(
|
|
||||||
"-StrictLog",
|
|
||||||
action="store_true",
|
|
||||||
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
|
||||||
)
|
|
||||||
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
|
|
||||||
|
|
||||||
# --- Read log ---
|
|
||||||
log_content = ""
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
except Exception:
|
|
||||||
log_content = ""
|
|
||||||
|
|
||||||
# --- Scan log for silent rejections ---
|
|
||||||
# Platform often writes load-time rejections into /Out but exits with code 0.
|
|
||||||
# These patterns flag cases where metadata was dropped or rejected silently.
|
|
||||||
fatal_log_patterns = [
|
|
||||||
"Неверное свойство объекта метаданных",
|
|
||||||
"не входит в состав объекта метаданных",
|
|
||||||
"Неизвестное имя типа",
|
|
||||||
"Неизвестный объект метаданных",
|
|
||||||
"Ни один из документов не является регистратором для регистра",
|
|
||||||
"Неверное значение перечисления",
|
|
||||||
"не может быть приведен к типу",
|
|
||||||
]
|
|
||||||
silent_failures = []
|
|
||||||
if log_content:
|
|
||||||
for line in log_content.splitlines():
|
|
||||||
for pat in fatal_log_patterns:
|
|
||||||
if pat in line:
|
|
||||||
silent_failures.append(line.strip())
|
|
||||||
break
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
# Default: mirror platform's verdict via exit code. Log content (including any
|
|
||||||
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
|
|
||||||
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Load completed successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
|
|
||||||
if silent_failures:
|
|
||||||
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
|
||||||
print(
|
|
||||||
f"[warning] log contains {len(silent_failures)} rejection(s) — "
|
|
||||||
f"platform loaded config but dropped properties/refs{suffix}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
for f in silent_failures:
|
|
||||||
print(f" {f}", file=sys.stderr)
|
|
||||||
if args.StrictLog and exit_code == 0:
|
|
||||||
exit_code = 1
|
|
||||||
|
|
||||||
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,226 +0,0 @@
|
|||||||
# epf-add-form v1.1 — 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"
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$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="$formatVersion">
|
|
||||||
<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="$formatVersion">
|
|
||||||
<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,272 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-form v1.1 — Add managed form to 1C external data processor
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
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 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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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
|
|
||||||
|
|
||||||
format_version = detect_format_version(os.path.abspath(src_dir))
|
|
||||||
|
|
||||||
# --- 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"'
|
|
||||||
f' version="{format_version}">\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"'
|
|
||||||
f' version="{format_version}">\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,41 +0,0 @@
|
|||||||
---
|
|
||||||
name: epf-init
|
|
||||||
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
|
|
||||||
argument-hint: <Name> [Synonym]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /epf-init — Создание новой обработки
|
|
||||||
|
|
||||||
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/epf-init <Name> [Synonym] [SrcDir]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|-----------|:------------:|--------------|-------------------------------------|
|
|
||||||
| Name | да | — | Имя обработки (латиница/кириллица) |
|
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дальнейшие шаги
|
|
||||||
|
|
||||||
- Добавить форму: `/epf-add-form`
|
|
||||||
- Добавить макет: `/template-add`
|
|
||||||
- Добавить справку: `/help-add`
|
|
||||||
- Собрать EPF: `/epf-build`
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# epf-init v1.0 — Init 1C external data processor scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Name,
|
|
||||||
|
|
||||||
[string]$Synonym = $Name,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid3 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$xml = @"
|
|
||||||
<?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">
|
|
||||||
<ExternalDataProcessor uuid="$uuid1">
|
|
||||||
<InternalInfo>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
|
|
||||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
|
|
||||||
<xr:TypeId>$uuid3</xr:TypeId>
|
|
||||||
<xr:ValueId>$uuid4</xr:ValueId>
|
|
||||||
</xr:GeneratedType>
|
|
||||||
</InternalInfo>
|
|
||||||
<Properties>
|
|
||||||
<Name>$Name</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<DefaultForm/>
|
|
||||||
<AuxiliaryForm/>
|
|
||||||
</Properties>
|
|
||||||
<ChildObjects/>
|
|
||||||
</ExternalDataProcessor>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
|
||||||
$processorDir = Join-Path $SrcDir $Name
|
|
||||||
|
|
||||||
if (Test-Path $rootFile) {
|
|
||||||
Write-Error "Файл уже существует: $rootFile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $SrcDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$extDir = Join-Path $processorDir "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
|
||||||
|
|
||||||
Write-Host "[OK] Создана обработка: $rootFile"
|
|
||||||
Write-Host " Каталог: $processorDir"
|
|
||||||
Write-Host " Модуль: $modulePath"
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-init v1.0 — Init 1C external data processor scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
"""Generates minimal XML source files for a 1C external data processor."""
|
|
||||||
import sys, os, argparse, uuid
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
|
||||||
|
|
||||||
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 main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Init 1C external data processor scaffold', allow_abbrev=False)
|
|
||||||
parser.add_argument('-Name', dest='Name', required=True)
|
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
name = args.Name
|
|
||||||
synonym = args.Synonym if args.Synonym else name
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
uuid1 = new_uuid()
|
|
||||||
uuid2 = new_uuid()
|
|
||||||
uuid3 = new_uuid()
|
|
||||||
uuid4 = new_uuid()
|
|
||||||
|
|
||||||
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">
|
|
||||||
\t<ExternalDataProcessor uuid="{uuid1}">
|
|
||||||
\t\t<InternalInfo>
|
|
||||||
\t\t\t<xr:ContainedObject>
|
|
||||||
\t\t\t\t<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
|
|
||||||
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
|
|
||||||
\t\t\t</xr:ContainedObject>
|
|
||||||
\t\t\t<xr:GeneratedType name="ExternalDataProcessorObject.{name}" category="Object">
|
|
||||||
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
|
|
||||||
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
|
|
||||||
\t\t\t</xr:GeneratedType>
|
|
||||||
\t\t</InternalInfo>
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<DefaultForm/>
|
|
||||||
\t\t\t<AuxiliaryForm/>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t\t<ChildObjects/>
|
|
||||||
\t</ExternalDataProcessor>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
root_file = os.path.join(src_dir, f"{name}.xml")
|
|
||||||
processor_dir = os.path.join(src_dir, name)
|
|
||||||
|
|
||||||
if os.path.exists(root_file):
|
|
||||||
print(f"Файл уже существует: {root_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
os.makedirs(src_dir, exist_ok=True)
|
|
||||||
ext_dir = os.path.join(processor_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
module_bsl = """\
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти"""
|
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
|
||||||
write_utf8_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
print(f"[OK] Создана обработка: {root_file}")
|
|
||||||
print(f" Каталог: {processor_dir}")
|
|
||||||
print(f" Модуль: {module_path}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
name: erf-init
|
|
||||||
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
|
|
||||||
argument-hint: <Name> [Synonym] [--with-skd]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /erf-init — Создание нового отчёта
|
|
||||||
|
|
||||||
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|-----------|:------------:|--------------|---------------------------------------|
|
|
||||||
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
|
||||||
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дальнейшие шаги
|
|
||||||
|
|
||||||
- Добавить форму: `/form-add`
|
|
||||||
- Добавить макет: `/template-add`
|
|
||||||
- Добавить справку: `/help-add`
|
|
||||||
- Собрать ERF: `/erf-build`
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
# erf-init v1.0 — Init 1C external report scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Name,
|
|
||||||
|
|
||||||
[string]$Synonym = $Name,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src",
|
|
||||||
|
|
||||||
[switch]$WithSKD
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid3 = [guid]::NewGuid().ToString()
|
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# --- Формируем Properties ---
|
|
||||||
|
|
||||||
$mainDCSValue = ""
|
|
||||||
$childObjectsContent = ""
|
|
||||||
|
|
||||||
if ($WithSKD) {
|
|
||||||
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
$childObjectsContent = @"
|
|
||||||
|
|
||||||
<Template>ОсновнаяСхемаКомпоновкиДанных</Template>
|
|
||||||
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
$mainDCSElement = if ($mainDCSValue) {
|
|
||||||
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
|
|
||||||
} else {
|
|
||||||
"<MainDataCompositionSchema/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$childObjectsXml = if ($childObjectsContent) {
|
|
||||||
"<ChildObjects>$childObjectsContent</ChildObjects>"
|
|
||||||
} else {
|
|
||||||
"<ChildObjects/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$xml = @"
|
|
||||||
<?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">
|
|
||||||
<ExternalReport uuid="$uuid1">
|
|
||||||
<InternalInfo>
|
|
||||||
<xr:ContainedObject>
|
|
||||||
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
|
|
||||||
<xr:ObjectId>$uuid2</xr:ObjectId>
|
|
||||||
</xr:ContainedObject>
|
|
||||||
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
|
|
||||||
<xr:TypeId>$uuid3</xr:TypeId>
|
|
||||||
<xr:ValueId>$uuid4</xr:ValueId>
|
|
||||||
</xr:GeneratedType>
|
|
||||||
</InternalInfo>
|
|
||||||
<Properties>
|
|
||||||
<Name>$Name</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<DefaultForm/>
|
|
||||||
<AuxiliaryForm/>
|
|
||||||
$mainDCSElement
|
|
||||||
<DefaultSettingsForm/>
|
|
||||||
<AuxiliarySettingsForm/>
|
|
||||||
<DefaultVariantForm/>
|
|
||||||
<VariantsStorage/>
|
|
||||||
<SettingsStorage/>
|
|
||||||
</Properties>
|
|
||||||
$childObjectsXml
|
|
||||||
</ExternalReport>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$rootFile = Join-Path $SrcDir "$Name.xml"
|
|
||||||
$reportDir = Join-Path $SrcDir $Name
|
|
||||||
|
|
||||||
if (Test-Path $rootFile) {
|
|
||||||
Write-Error "Файл уже существует: $rootFile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $SrcDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$extDir = Join-Path $reportDir "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
|
||||||
|
|
||||||
Write-Host "[OK] Создан отчёт: $rootFile"
|
|
||||||
Write-Host " Каталог: $reportDir"
|
|
||||||
Write-Host " Модуль: $modulePath"
|
|
||||||
|
|
||||||
# --- СКД-макет (если --WithSKD) ---
|
|
||||||
|
|
||||||
if ($WithSKD) {
|
|
||||||
$templatesDir = Join-Path $reportDir "Templates"
|
|
||||||
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
$skdMetaPath = Join-Path $templatesDir "$skdName.xml"
|
|
||||||
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
|
|
||||||
|
|
||||||
$skdUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$skdMetaXml = @"
|
|
||||||
<?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="$skdUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$skdName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>Основная схема компоновки данных</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<TemplateType>DataCompositionSchema</TemplateType>
|
|
||||||
</Properties>
|
|
||||||
</Template>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
|
|
||||||
|
|
||||||
$skdContent = @"
|
|
||||||
<?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>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
|
||||||
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
|
|
||||||
|
|
||||||
Write-Host " СКД: $skdMetaPath"
|
|
||||||
Write-Host " Тело: $skdFilePath"
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# erf-init v1.0 — Init 1C external report scaffold
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
"""Generates minimal XML source files for a 1C external report."""
|
|
||||||
import sys, os, argparse, uuid
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
|
||||||
|
|
||||||
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 main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Init 1C external report scaffold', allow_abbrev=False)
|
|
||||||
parser.add_argument('-Name', dest='Name', required=True)
|
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
|
||||||
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
name = args.Name
|
|
||||||
synonym = args.Synonym if args.Synonym else name
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
uuid1 = new_uuid()
|
|
||||||
uuid2 = new_uuid()
|
|
||||||
uuid3 = new_uuid()
|
|
||||||
uuid4 = new_uuid()
|
|
||||||
|
|
||||||
# --- Properties ---
|
|
||||||
main_dcs_value = ""
|
|
||||||
child_objects_content = ""
|
|
||||||
|
|
||||||
if args.WithSKD:
|
|
||||||
main_dcs_value = f"ExternalReport.{name}.Template.ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
child_objects_content = f"\n\t\t\t<Template>ОсновнаяСхемаКомпоновкиДанных</Template>\n"
|
|
||||||
|
|
||||||
main_dcs_element = f"<MainDataCompositionSchema>{main_dcs_value}</MainDataCompositionSchema>" if main_dcs_value else "<MainDataCompositionSchema/>"
|
|
||||||
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
|
|
||||||
|
|
||||||
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">
|
|
||||||
\t<ExternalReport uuid="{uuid1}">
|
|
||||||
\t\t<InternalInfo>
|
|
||||||
\t\t\t<xr:ContainedObject>
|
|
||||||
\t\t\t\t<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
|
|
||||||
\t\t\t\t<xr:ObjectId>{uuid2}</xr:ObjectId>
|
|
||||||
\t\t\t</xr:ContainedObject>
|
|
||||||
\t\t\t<xr:GeneratedType name="ExternalReportObject.{name}" category="Object">
|
|
||||||
\t\t\t\t<xr:TypeId>{uuid3}</xr:TypeId>
|
|
||||||
\t\t\t\t<xr:ValueId>{uuid4}</xr:ValueId>
|
|
||||||
\t\t\t</xr:GeneratedType>
|
|
||||||
\t\t</InternalInfo>
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<DefaultForm/>
|
|
||||||
\t\t\t<AuxiliaryForm/>
|
|
||||||
\t\t\t{main_dcs_element}
|
|
||||||
\t\t\t<DefaultSettingsForm/>
|
|
||||||
\t\t\t<AuxiliarySettingsForm/>
|
|
||||||
\t\t\t<DefaultVariantForm/>
|
|
||||||
\t\t\t<VariantsStorage/>
|
|
||||||
\t\t\t<SettingsStorage/>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t\t{child_objects_xml}
|
|
||||||
\t</ExternalReport>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
root_file = os.path.join(src_dir, f"{name}.xml")
|
|
||||||
report_dir = os.path.join(src_dir, name)
|
|
||||||
|
|
||||||
if os.path.exists(root_file):
|
|
||||||
print(f"Файл уже существует: {root_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
os.makedirs(src_dir, exist_ok=True)
|
|
||||||
ext_dir = os.path.join(report_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
|
||||||
module_bsl = """\
|
|
||||||
#Область ОписаниеПеременных
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ПрограммныйИнтерфейс
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти"""
|
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
|
||||||
write_utf8_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
print(f"[OK] Создан отчёт: {root_file}")
|
|
||||||
print(f" Каталог: {report_dir}")
|
|
||||||
print(f" Модуль: {module_path}")
|
|
||||||
|
|
||||||
# --- СКД-макет ---
|
|
||||||
if args.WithSKD:
|
|
||||||
templates_dir = os.path.join(report_dir, "Templates")
|
|
||||||
skd_name = "ОсновнаяСхемаКомпоновкиДанных"
|
|
||||||
skd_meta_path = os.path.join(templates_dir, f"{skd_name}.xml")
|
|
||||||
skd_ext_dir = os.path.join(templates_dir, skd_name, "Ext")
|
|
||||||
os.makedirs(skd_ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
skd_uuid = new_uuid()
|
|
||||||
|
|
||||||
skd_meta_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">
|
|
||||||
\t<Template uuid="{skd_uuid}">
|
|
||||||
\t\t<Properties>
|
|
||||||
\t\t\t<Name>{skd_name}</Name>
|
|
||||||
\t\t\t<Synonym>
|
|
||||||
\t\t\t\t<v8:item>
|
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
|
||||||
\t\t\t\t\t<v8:content>Основная схема компоновки данных</v8:content>
|
|
||||||
\t\t\t\t</v8:item>
|
|
||||||
\t\t\t</Synonym>
|
|
||||||
\t\t\t<Comment/>
|
|
||||||
\t\t\t<TemplateType>DataCompositionSchema</TemplateType>
|
|
||||||
\t\t</Properties>
|
|
||||||
\t</Template>
|
|
||||||
</MetaDataObject>'''
|
|
||||||
|
|
||||||
write_utf8_bom(skd_meta_path, skd_meta_xml)
|
|
||||||
|
|
||||||
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
|
||||||
\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
|
||||||
\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
|
||||||
\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
|
||||||
\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"
|
|
||||||
\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
|
||||||
\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
\t<dataSource>
|
|
||||||
\t\t<name>ИсточникДанных1</name>
|
|
||||||
\t\t<dataSourceType>Local</dataSourceType>
|
|
||||||
\t</dataSource>
|
|
||||||
</DataCompositionSchema>'''
|
|
||||||
|
|
||||||
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
|
|
||||||
write_utf8_bom(skd_file_path, skd_content)
|
|
||||||
|
|
||||||
print(f" СКД: {skd_meta_path}")
|
|
||||||
print(f" Тело: {skd_file_path}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
name: form-add
|
|
||||||
description: Добавить управляемую форму к объекту конфигурации 1С
|
|
||||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /form-add — Добавление формы к объекту конфигурации
|
|
||||||
|
|
||||||
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|-------------|:------------:|--------------|----------------------------------------------|
|
|
||||||
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
|
|
||||||
| FormName | да | — | Имя формы (ФормаДокумента) |
|
|
||||||
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
|
|
||||||
| Synonym | нет | = FormName | Синоним формы |
|
|
||||||
| --set-default | нет | авто | Установить как форму по умолчанию |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Purpose — назначение формы
|
|
||||||
|
|
||||||
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|
|
||||||
|---------|-------------------------|-------------------|---------------------|
|
|
||||||
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
|
|
||||||
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
|
|
||||||
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
|
|
||||||
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```
|
|
||||||
# Форма документа
|
|
||||||
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
|
|
||||||
|
|
||||||
# Форма списка каталога
|
|
||||||
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
|
|
||||||
|
|
||||||
# Форма записи регистра сведений
|
|
||||||
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
|
|
||||||
|
|
||||||
# Форма выбора с синонимом
|
|
||||||
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
|
|
||||||
|
|
||||||
# Установить как форму по умолчанию
|
|
||||||
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. `/form-add` — создать каркас формы
|
|
||||||
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
|
|
||||||
3. `/form-validate` — проверить корректность
|
|
||||||
4. `/form-info` — проанализировать результат
|
|
||||||
@@ -1,484 +0,0 @@
|
|||||||
# form-add v1.3 — Add managed form to 1C config object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ObjectPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$Synonym = $FormName,
|
|
||||||
|
|
||||||
[string]$Purpose = "Object",
|
|
||||||
|
|
||||||
[switch]$SetDefault
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# --- Detect XML format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Фаза 1: Определение типа объекта ---
|
|
||||||
|
|
||||||
# Resolve ObjectPath (directory → .xml)
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) {
|
|
||||||
$ObjectPath = Join-Path (Get-Location).Path $ObjectPath
|
|
||||||
}
|
|
||||||
if (Test-Path $ObjectPath -PathType Container) {
|
|
||||||
$dirName = Split-Path $ObjectPath -Leaf
|
|
||||||
$candidate = Join-Path $ObjectPath "$dirName.xml"
|
|
||||||
$sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml"
|
|
||||||
if (Test-Path $candidate) { $ObjectPath = $candidate }
|
|
||||||
elseif (Test-Path $sibling) { $ObjectPath = $sibling }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $ObjectPath)) {
|
|
||||||
Write-Error "Файл объекта не найден: $ObjectPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$objectXmlFull = Resolve-Path $ObjectPath
|
|
||||||
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($objectXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
|
||||||
|
|
||||||
# Определяем тип объекта по корневому тегу внутри MetaDataObject
|
|
||||||
$metaDataObject = $xmlDoc.SelectSingleNode("//md:MetaDataObject", $nsMgr)
|
|
||||||
if (-not $metaDataObject) {
|
|
||||||
# Пробуем без namespace (fallback)
|
|
||||||
$metaDataObject = $xmlDoc.DocumentElement
|
|
||||||
}
|
|
||||||
|
|
||||||
$supportedTypes = @(
|
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
|
||||||
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
|
||||||
"ExchangePlan", "BusinessProcess", "Task"
|
|
||||||
)
|
|
||||||
|
|
||||||
$objectType = $null
|
|
||||||
$objectNode = $null
|
|
||||||
foreach ($t in $supportedTypes) {
|
|
||||||
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
|
|
||||||
if ($node) {
|
|
||||||
$objectType = $t
|
|
||||||
$objectNode = $node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $objectType) {
|
|
||||||
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Имя объекта из Properties/Name
|
|
||||||
$objectName = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:Name", $nsMgr).InnerText
|
|
||||||
if (-not $objectName) {
|
|
||||||
Write-Error "Не удалось определить имя объекта из Properties/Name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "=== form-add ==="
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Object: $objectType.$objectName"
|
|
||||||
|
|
||||||
# --- Фаза 2: Валидация Purpose ---
|
|
||||||
|
|
||||||
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
|
|
||||||
# Нормализация
|
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" { }
|
|
||||||
"List" { }
|
|
||||||
"Choice" { }
|
|
||||||
"Record" { }
|
|
||||||
default {
|
|
||||||
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
|
|
||||||
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
|
|
||||||
|
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" {
|
|
||||||
# допустимо для всех типов
|
|
||||||
}
|
|
||||||
"List" {
|
|
||||||
if ($objectType -eq "DataProcessor") {
|
|
||||||
Write-Error "Purpose=List недопустим для DataProcessor"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"Choice" {
|
|
||||||
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
|
|
||||||
Write-Error "Purpose=Choice недопустим для $objectType"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"Record" {
|
|
||||||
if ($objectType -ne "InformationRegister") {
|
|
||||||
Write-Error "Purpose=Record допустим только для InformationRegister"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Фаза 3: Создание файлов ---
|
|
||||||
|
|
||||||
$objectDir = [System.IO.Path]::ChangeExtension($objectXmlFull.Path, $null).TrimEnd('.')
|
|
||||||
$formsDir = Join-Path $objectDir "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)
|
|
||||||
|
|
||||||
# --- 3a. Метаданные формы ---
|
|
||||||
|
|
||||||
$formUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# ExtendedPresentation — only for DataProcessor, Report, ExternalDataProcessor, ExternalReport forms
|
|
||||||
$extPresentationLine = ""
|
|
||||||
if ($objectType -in $processorLikeTypes) {
|
|
||||||
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$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="$($script:formatVersion)">
|
|
||||||
<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>$extPresentationLine
|
|
||||||
</Properties>
|
|
||||||
</Form>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
|
||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
|
||||||
|
|
||||||
$formNsDecl = '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"'
|
|
||||||
|
|
||||||
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|
||||||
# Динамический список
|
|
||||||
# MainTable: тип.имя
|
|
||||||
$mainTable = "$objectType.$objectName"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<Events>
|
|
||||||
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
|
||||||
</Events>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="Список" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:DynamicList</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
<Settings xsi:type="DynamicList">
|
|
||||||
<MainTable>$mainTable</MainTable>
|
|
||||||
</Settings>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
} elseif ($Purpose -eq "Record") {
|
|
||||||
# Запись регистра сведений
|
|
||||||
$mainAttrName = "Запись"
|
|
||||||
$mainAttrType = "InformationRegisterRecordManager.$objectName"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<Events>
|
|
||||||
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
|
||||||
</Events>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="$mainAttrName" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
<SavedData>true</SavedData>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
} else {
|
|
||||||
# Object — форма объекта
|
|
||||||
$mainAttrName = "Объект"
|
|
||||||
|
|
||||||
# Маппинг типа объекта на тип реквизита
|
|
||||||
$attrTypeMap = @{
|
|
||||||
"Document" = "DocumentObject"
|
|
||||||
"Catalog" = "CatalogObject"
|
|
||||||
"DataProcessor" = "DataProcessorObject"
|
|
||||||
"Report" = "ReportObject"
|
|
||||||
"ExternalDataProcessor" = "ExternalDataProcessorObject"
|
|
||||||
"ExternalReport" = "ExternalReportObject"
|
|
||||||
"ChartOfAccounts" = "ChartOfAccountsObject"
|
|
||||||
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
|
|
||||||
"ExchangePlan" = "ExchangePlanObject"
|
|
||||||
"BusinessProcess" = "BusinessProcessObject"
|
|
||||||
"Task" = "TaskObject"
|
|
||||||
"InformationRegister" = "InformationRegisterRecordManager"
|
|
||||||
}
|
|
||||||
|
|
||||||
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<Events>
|
|
||||||
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
|
||||||
</Events>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="$mainAttrName" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
<SavedData>true</SavedData>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $formXmlPath) {
|
|
||||||
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
|
||||||
} else {
|
|
||||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
|
||||||
|
|
||||||
$modulePath = Join-Path $formModuleDir "Module.bsl"
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОбработчикиСобытийФормы
|
|
||||||
|
|
||||||
&НаСервере
|
|
||||||
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
|
|
||||||
|
|
||||||
КонецПроцедуры
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиСобытийЭлементовФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиКомандФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиОповещений
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
if (Test-Path $modulePath) {
|
|
||||||
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
|
|
||||||
} else {
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Фаза 4: Регистрация в родительском объекте ---
|
|
||||||
|
|
||||||
$childObjects = $xmlDoc.SelectSingleNode("//md:${objectType}/md:ChildObjects", $nsMgr)
|
|
||||||
if (-not $childObjects) {
|
|
||||||
Write-Error "Не найден элемент ChildObjects в $ObjectPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавить <Form>$FormName</Form>
|
|
||||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$formElem.InnerText = $FormName
|
|
||||||
|
|
||||||
# Ищем первый <Template> для вставки перед ним
|
|
||||||
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
|
|
||||||
# Ищем первую <TabularSection> для вставки перед ней (если нет Template)
|
|
||||||
$firstTabular = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr)
|
|
||||||
|
|
||||||
# Определяем точку вставки: перед Template, перед TabularSection, или в конец
|
|
||||||
$insertBefore = $null
|
|
||||||
if ($firstTemplate) {
|
|
||||||
$insertBefore = $firstTemplate
|
|
||||||
} elseif ($firstTabular) {
|
|
||||||
$insertBefore = $firstTabular
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($insertBefore) {
|
|
||||||
# Вставить перед найденным элементом, с переносом строки
|
|
||||||
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
|
|
||||||
$childObjects.InsertBefore($whitespace, $formElem) | Out-Null
|
|
||||||
# Переставляем: whitespace перед formElem — неправильный порядок
|
|
||||||
# Правильно: formElem, затем whitespace перед insertBefore
|
|
||||||
# InsertBefore возвращает вставленный узел, порядок: ... formElem whitespace insertBefore ...
|
|
||||||
# На самом деле нам нужно: ... \n\t\t\tformElem \n\t\t\tinsertBefore
|
|
||||||
# Удалим и вставим правильно
|
|
||||||
$childObjects.RemoveChild($whitespace) | Out-Null
|
|
||||||
$childObjects.RemoveChild($formElem) | Out-Null
|
|
||||||
|
|
||||||
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
|
|
||||||
# Whitespace нужен ДО formElem (перенос строки + отступ)
|
|
||||||
# Но перед insertBefore уже должен быть whitespace от предыдущего элемента
|
|
||||||
# Нам нужно добавить whitespace ПОСЛЕ formElem (перед insertBefore)
|
|
||||||
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
|
|
||||||
} else {
|
|
||||||
# Добавить в конец 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
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- SetDefault ---
|
|
||||||
|
|
||||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
|
||||||
$isFirstFormForPurpose = $false
|
|
||||||
$defaultPropName = $null
|
|
||||||
$defaultValue = "$objectType.$objectName.Form.$FormName"
|
|
||||||
|
|
||||||
# Определяем имя свойства для DefaultForm
|
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" {
|
|
||||||
if ($objectType -in $processorLikeTypes) {
|
|
||||||
$defaultPropName = "DefaultForm"
|
|
||||||
} else {
|
|
||||||
$defaultPropName = "DefaultObjectForm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"List" { $defaultPropName = "DefaultListForm" }
|
|
||||||
"Choice" { $defaultPropName = "DefaultChoiceForm" }
|
|
||||||
"Record" { $defaultPropName = "DefaultRecordForm" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем, установлено ли уже значение
|
|
||||||
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
|
|
||||||
if ($defaultNode) {
|
|
||||||
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultUpdated = $false
|
|
||||||
if ($SetDefault -or $isFirstFormForPurpose) {
|
|
||||||
if ($defaultNode) {
|
|
||||||
$defaultNode.InnerText = $defaultValue
|
|
||||||
$defaultUpdated = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
# --- Фаза 5: Вывод ---
|
|
||||||
|
|
||||||
# Относительные пути для вывода
|
|
||||||
$basePath = Split-Path $objectXmlFull.Path -Parent
|
|
||||||
# Определяем корень (ищем родительский каталог типа Documents, Catalogs и т.д.)
|
|
||||||
$relFormMeta = $formMetaPath.Replace($basePath, "").TrimStart("\", "/")
|
|
||||||
$relFormXml = $formXmlPath.Replace($basePath, "").TrimStart("\", "/")
|
|
||||||
$relModule = $modulePath.Replace($basePath, "").TrimStart("\", "/")
|
|
||||||
|
|
||||||
$objFileName = [System.IO.Path]::GetFileName($ObjectPath)
|
|
||||||
$objDirName = Split-Path $ObjectPath -Parent
|
|
||||||
$objBaseName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath)
|
|
||||||
|
|
||||||
Write-Host "Created:"
|
|
||||||
Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
|
|
||||||
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
|
|
||||||
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
|
|
||||||
if ($defaultUpdated) {
|
|
||||||
Write-Host "${defaultPropName}: $defaultValue"
|
|
||||||
}
|
|
||||||
Write-Host ""
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# form-add v1.3 — Add managed form to 1C config object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {
|
|
||||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
|
||||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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 config object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectPath", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-Synonym", default=None)
|
|
||||||
parser.add_argument("-Purpose", default="Object")
|
|
||||||
parser.add_argument("-SetDefault", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
|
||||||
form_name = args.FormName
|
|
||||||
synonym = args.Synonym if args.Synonym is not None else form_name
|
|
||||||
purpose = args.Purpose
|
|
||||||
set_default = args.SetDefault
|
|
||||||
|
|
||||||
# --- Phase 1: Determine object type ---
|
|
||||||
|
|
||||||
# Resolve ObjectPath (directory → .xml)
|
|
||||||
if not os.path.isabs(object_path):
|
|
||||||
object_path = os.path.join(os.getcwd(), object_path)
|
|
||||||
if os.path.isdir(object_path):
|
|
||||||
dir_name = os.path.basename(object_path.rstrip("/\\"))
|
|
||||||
candidate = os.path.join(object_path, dir_name + ".xml")
|
|
||||||
sibling = os.path.join(os.path.dirname(object_path.rstrip("/\\")), dir_name + ".xml")
|
|
||||||
if os.path.isfile(candidate):
|
|
||||||
object_path = candidate
|
|
||||||
elif os.path.isfile(sibling):
|
|
||||||
object_path = sibling
|
|
||||||
if not os.path.isfile(object_path):
|
|
||||||
print(f"Файл объекта не найден: {object_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
object_xml_full = os.path.abspath(object_path)
|
|
||||||
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(object_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
supported_types = [
|
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
|
||||||
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
|
||||||
"ExchangePlan", "BusinessProcess", "Task",
|
|
||||||
]
|
|
||||||
|
|
||||||
object_type = None
|
|
||||||
object_node = None
|
|
||||||
for t in supported_types:
|
|
||||||
node = root.find(f".//md:{t}", NSMAP)
|
|
||||||
if node is not None:
|
|
||||||
object_type = t
|
|
||||||
object_node = node
|
|
||||||
break
|
|
||||||
|
|
||||||
if object_type is None:
|
|
||||||
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Object name from Properties/Name
|
|
||||||
name_node = root.find(f".//md:{object_type}/md:Properties/md:Name", NSMAP)
|
|
||||||
if name_node is None or not name_node.text:
|
|
||||||
print("Не удалось определить имя объекта из Properties/Name", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
object_name = name_node.text
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=== form-add ===")
|
|
||||||
print()
|
|
||||||
print(f"Object: {object_type}.{object_name}")
|
|
||||||
|
|
||||||
# --- Phase 2: Validate Purpose ---
|
|
||||||
|
|
||||||
# Normalize: capitalize first letter, lowercase rest
|
|
||||||
purpose = purpose[0].upper() + purpose[1:].lower()
|
|
||||||
|
|
||||||
valid_purposes = ["Object", "List", "Choice", "Record"]
|
|
||||||
if purpose not in valid_purposes:
|
|
||||||
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
|
||||||
"ExchangePlan", "BusinessProcess", "Task"]
|
|
||||||
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
|
|
||||||
|
|
||||||
if purpose == "List":
|
|
||||||
if object_type == "DataProcessor":
|
|
||||||
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif purpose == "Choice":
|
|
||||||
if object_type in processor_like_types or object_type == "InformationRegister":
|
|
||||||
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif purpose == "Record":
|
|
||||||
if object_type != "InformationRegister":
|
|
||||||
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Phase 3: Create files ---
|
|
||||||
|
|
||||||
object_dir = os.path.splitext(object_xml_full)[0]
|
|
||||||
forms_dir = os.path.join(object_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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# --- 3a. Form metadata ---
|
|
||||||
|
|
||||||
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"'
|
|
||||||
f' version="{format_version}">\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' if object_type in processor_like_types else '')
|
|
||||||
+ '\t\t</Properties>\n'
|
|
||||||
'\t</Form>\n'
|
|
||||||
'</MetaDataObject>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
|
||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
|
||||||
|
|
||||||
form_ns_decl = (
|
|
||||||
'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"'
|
|
||||||
)
|
|
||||||
|
|
||||||
if purpose in ("List", "Choice"):
|
|
||||||
# Dynamic list
|
|
||||||
main_table = f"{object_type}.{object_name}"
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\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<Events>\n'
|
|
||||||
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
|
||||||
'\t</Events>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t\t<Settings xsi:type="DynamicList">\n'
|
|
||||||
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
|
||||||
'\t\t\t</Settings>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
elif purpose == "Record":
|
|
||||||
# Information register record
|
|
||||||
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
|
|
||||||
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\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<Events>\n'
|
|
||||||
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
|
||||||
'\t</Events>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t\t<SavedData>true</SavedData>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Object — object form
|
|
||||||
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
|
|
||||||
|
|
||||||
attr_type_map = {
|
|
||||||
"Document": "DocumentObject",
|
|
||||||
"Catalog": "CatalogObject",
|
|
||||||
"DataProcessor": "DataProcessorObject",
|
|
||||||
"Report": "ReportObject",
|
|
||||||
"ExternalDataProcessor": "ExternalDataProcessorObject",
|
|
||||||
"ExternalReport": "ExternalReportObject",
|
|
||||||
"ChartOfAccounts": "ChartOfAccountsObject",
|
|
||||||
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
|
|
||||||
"ExchangePlan": "ExchangePlanObject",
|
|
||||||
"BusinessProcess": "BusinessProcessObject",
|
|
||||||
"Task": "TaskObject",
|
|
||||||
"InformationRegister": "InformationRegisterRecordManager",
|
|
||||||
}
|
|
||||||
|
|
||||||
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\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<Events>\n'
|
|
||||||
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
|
||||||
'\t</Events>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t\t<SavedData>true</SavedData>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(form_xml_path):
|
|
||||||
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
|
||||||
else:
|
|
||||||
write_text_with_bom(form_xml_path, form_xml)
|
|
||||||
|
|
||||||
# --- 3c. 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'
|
|
||||||
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
|
|
||||||
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
|
|
||||||
'\n'
|
|
||||||
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\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'
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(module_path):
|
|
||||||
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
|
|
||||||
else:
|
|
||||||
write_text_with_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
# --- Phase 4: Register in parent object ---
|
|
||||||
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
child_objects = root.find(f".//md:{object_type}/md:ChildObjects", NSMAP)
|
|
||||||
if child_objects is None:
|
|
||||||
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Add <Form>$FormName</Form>
|
|
||||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
|
||||||
form_elem.text = form_name
|
|
||||||
|
|
||||||
# Find first <Template> to insert before it
|
|
||||||
first_template = child_objects.find("md:Template", NSMAP)
|
|
||||||
# Find first <TabularSection> to insert before it (if no Template)
|
|
||||||
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
|
||||||
|
|
||||||
# Determine insertion point: before Template, before TabularSection, or at end
|
|
||||||
insert_before = None
|
|
||||||
if first_template is not None:
|
|
||||||
insert_before = first_template
|
|
||||||
elif first_tabular is not None:
|
|
||||||
insert_before = first_tabular
|
|
||||||
|
|
||||||
if insert_before is not None:
|
|
||||||
# Insert before the found element
|
|
||||||
idx = list(child_objects).index(insert_before)
|
|
||||||
child_objects.insert(idx, form_elem)
|
|
||||||
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
|
||||||
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"
|
|
||||||
|
|
||||||
# --- SetDefault ---
|
|
||||||
|
|
||||||
is_first_form_for_purpose = False
|
|
||||||
default_prop_name = None
|
|
||||||
default_value = f"{object_type}.{object_name}.Form.{form_name}"
|
|
||||||
|
|
||||||
# Determine property name for DefaultForm
|
|
||||||
if purpose == "Object":
|
|
||||||
if object_type in processor_like_types:
|
|
||||||
default_prop_name = "DefaultForm"
|
|
||||||
else:
|
|
||||||
default_prop_name = "DefaultObjectForm"
|
|
||||||
elif purpose == "List":
|
|
||||||
default_prop_name = "DefaultListForm"
|
|
||||||
elif purpose == "Choice":
|
|
||||||
default_prop_name = "DefaultChoiceForm"
|
|
||||||
elif purpose == "Record":
|
|
||||||
default_prop_name = "DefaultRecordForm"
|
|
||||||
|
|
||||||
# Check if value is already set
|
|
||||||
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
|
|
||||||
if default_node is not None:
|
|
||||||
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
|
|
||||||
|
|
||||||
default_updated = False
|
|
||||||
if set_default or is_first_form_for_purpose:
|
|
||||||
if default_node is not None:
|
|
||||||
default_node.text = default_value
|
|
||||||
default_updated = True
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, object_xml_full)
|
|
||||||
|
|
||||||
# --- Phase 5: Output ---
|
|
||||||
|
|
||||||
obj_dir_name = os.path.dirname(object_path)
|
|
||||||
obj_base_name = os.path.splitext(os.path.basename(object_path))[0]
|
|
||||||
|
|
||||||
print("Created:")
|
|
||||||
print(f" Metadata: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}.xml")
|
|
||||||
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
|
||||||
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
|
||||||
print()
|
|
||||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
|
||||||
if default_updated:
|
|
||||||
print(f"{default_prop_name}: {default_value}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
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,87 +0,0 @@
|
|||||||
# form-remove v1.1 — Remove form from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[Alias("ProcessorName")]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$formsDir = Join-Path $processorDir "Forms"
|
|
||||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
|
||||||
$formDir = Join-Path $formsDir $FormName
|
|
||||||
|
|
||||||
if (-not (Test-Path $formMetaPath)) {
|
|
||||||
Write-Error "Метаданные формы не найдены: $formMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Удаление файлов ---
|
|
||||||
|
|
||||||
if (Test-Path $formDir) {
|
|
||||||
Remove-Item -Path $formDir -Recurse -Force
|
|
||||||
Write-Host "[OK] Удалён каталог: $formDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
Remove-Item -Path $formMetaPath -Force
|
|
||||||
Write-Host "[OK] Удалён файл: $formMetaPath"
|
|
||||||
|
|
||||||
# --- Модификация корневого 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")
|
|
||||||
|
|
||||||
# Удалить <Form>FormName</Form> из ChildObjects
|
|
||||||
$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr)
|
|
||||||
foreach ($node in $formNodes) {
|
|
||||||
if ($node.InnerText -eq $FormName) {
|
|
||||||
$parent = $node.ParentNode
|
|
||||||
# Удалить предшествующий whitespace
|
|
||||||
$prev = $node.PreviousSibling
|
|
||||||
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$parent.RemoveChild($prev) | Out-Null
|
|
||||||
}
|
|
||||||
$parent.RemoveChild($node) | Out-Null
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Очистить DefaultForm если указывала на эту форму
|
|
||||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
|
||||||
if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") {
|
|
||||||
$defaultForm.InnerText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$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] Форма $FormName удалена из $rootXmlPath"
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# remove-form v1.0 — Remove form from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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="Remove form from 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
form_name = args.FormName
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- 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)
|
|
||||||
forms_dir = os.path.join(processor_dir, "Forms")
|
|
||||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
|
||||||
form_dir = os.path.join(forms_dir, form_name)
|
|
||||||
|
|
||||||
if not os.path.exists(form_meta_path):
|
|
||||||
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Delete files ---
|
|
||||||
|
|
||||||
if os.path.isdir(form_dir):
|
|
||||||
shutil.rmtree(form_dir)
|
|
||||||
print(f"[OK] Удалён каталог: {form_dir}")
|
|
||||||
|
|
||||||
os.remove(form_meta_path)
|
|
||||||
print(f"[OK] Удалён файл: {form_meta_path}")
|
|
||||||
|
|
||||||
# --- 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()
|
|
||||||
|
|
||||||
# Remove <Form>FormName</Form> from ChildObjects
|
|
||||||
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
|
|
||||||
if node.text and node.text.strip() == form_name:
|
|
||||||
parent = node.getparent()
|
|
||||||
prev = node.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
# Whitespace is in prev.tail
|
|
||||||
if prev.tail and prev.tail.strip() == "":
|
|
||||||
prev.tail = ""
|
|
||||||
else:
|
|
||||||
# First child — whitespace is in parent.text
|
|
||||||
if parent.text and parent.text.strip() == "":
|
|
||||||
parent.text = ""
|
|
||||||
parent.remove(node)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Clear DefaultForm if it pointed to removed form
|
|
||||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
|
||||||
if default_form is not None and default_form.text:
|
|
||||||
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
|
|
||||||
default_form.text = ""
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# help-add v1.3 — 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"
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$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="$formatVersion">
|
|
||||||
<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,166 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-help v1.3 — Add built-in help to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
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 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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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
|
|
||||||
|
|
||||||
format_version = detect_format_version(os.path.abspath(src_dir))
|
|
||||||
|
|
||||||
# --- 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"'
|
|
||||||
f' version="{format_version}">\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,519 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# interface-edit v1.3 — Edit 1C CommandInterface.xml
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
CI_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|
||||||
|
|
||||||
SECTION_ORDER = ["CommandsVisibility", "CommandsPlacement", "CommandsOrder", "SubsystemsOrder", "GroupsOrder"]
|
|
||||||
|
|
||||||
|
|
||||||
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 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 import_ci_fragment(xml_string):
|
|
||||||
wrapper = (
|
|
||||||
f'<_W xmlns="{CI_NS}" xmlns:xr="{XR_NS}" '
|
|
||||||
f'xmlns:xsi="{XSI_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):
|
|
||||||
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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
TYPE_NORM_MAP = {
|
|
||||||
'Catalogs': 'Catalog', 'Documents': 'Document', 'Enums': 'Enum',
|
|
||||||
'Constants': 'Constant', 'Reports': 'Report', 'DataProcessors': 'DataProcessor',
|
|
||||||
'InformationRegisters': 'InformationRegister', 'AccumulationRegisters': 'AccumulationRegister',
|
|
||||||
'AccountingRegisters': 'AccountingRegister', 'CalculationRegisters': 'CalculationRegister',
|
|
||||||
'ChartsOfAccounts': 'ChartOfAccounts', 'ChartsOfCharacteristicTypes': 'ChartOfCharacteristicTypes',
|
|
||||||
'ChartsOfCalculationTypes': 'ChartOfCalculationTypes',
|
|
||||||
'BusinessProcesses': 'BusinessProcess', 'Tasks': 'Task',
|
|
||||||
'ExchangePlans': 'ExchangePlan', 'DocumentJournals': 'DocumentJournal',
|
|
||||||
'CommonModules': 'CommonModule', 'CommonCommands': 'CommonCommand',
|
|
||||||
'CommonForms': 'CommonForm', 'CommonPictures': 'CommonPicture',
|
|
||||||
'CommonTemplates': 'CommonTemplate', 'CommonAttributes': 'CommonAttribute',
|
|
||||||
'CommandGroups': 'CommandGroup', 'Roles': 'Role',
|
|
||||||
'Subsystems': 'Subsystem', 'StyleItems': 'StyleItem',
|
|
||||||
# Russian singular
|
|
||||||
'Справочник': 'Catalog', 'Документ': 'Document', 'Перечисление': 'Enum',
|
|
||||||
'Константа': 'Constant', 'Отчёт': 'Report', 'Отчет': 'Report', 'Обработка': 'DataProcessor',
|
|
||||||
'РегистрСведений': 'InformationRegister', 'РегистрНакопления': 'AccumulationRegister',
|
|
||||||
'РегистрБухгалтерии': 'AccountingRegister',
|
|
||||||
'ПланСчетов': 'ChartOfAccounts', 'ПланВидовХарактеристик': 'ChartOfCharacteristicTypes',
|
|
||||||
'БизнесПроцесс': 'BusinessProcess', 'Задача': 'Task',
|
|
||||||
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
|
|
||||||
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
|
|
||||||
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
|
|
||||||
# Russian plural
|
|
||||||
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
|
|
||||||
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
|
|
||||||
'РегистрыСведений': 'InformationRegister', 'РегистрыНакопления': 'AccumulationRegister',
|
|
||||||
'РегистрыБухгалтерии': 'AccountingRegister',
|
|
||||||
'ПланыСчетов': 'ChartOfAccounts', 'ПланыВидовХарактеристик': 'ChartOfCharacteristicTypes',
|
|
||||||
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
|
|
||||||
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
|
|
||||||
'Подсистемы': 'Subsystem',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_cmd_name(name):
|
|
||||||
if not name or '.' not in name:
|
|
||||||
return name
|
|
||||||
dot_idx = name.index('.')
|
|
||||||
first = name[:dot_idx]
|
|
||||||
rest = name[dot_idx:]
|
|
||||||
if first in TYPE_NORM_MAP:
|
|
||||||
normalized = TYPE_NORM_MAP[first] + rest
|
|
||||||
if normalized != name:
|
|
||||||
print(f'[NORM] Command: {name} -> {normalized}')
|
|
||||||
return normalized
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def find_command_by_name(section, cmd_name):
|
|
||||||
for child in section:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Command":
|
|
||||||
if child.get("name") == cmd_name:
|
|
||||||
return child
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Edit 1C CommandInterface.xml", allow_abbrev=False)
|
|
||||||
parser.add_argument("-CIPath", required=True)
|
|
||||||
parser.add_argument("-DefinitionFile", default=None)
|
|
||||||
parser.add_argument("-Operation", default=None, choices=["hide", "show", "place", "order", "subsystem-order", "group-order"])
|
|
||||||
parser.add_argument("-Value", default=None)
|
|
||||||
parser.add_argument("-CreateIfMissing", action="store_true")
|
|
||||||
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)
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
ci_dir = os.path.dirname(os.path.abspath(args.CIPath))
|
|
||||||
format_version = detect_format_version(ci_dir)
|
|
||||||
|
|
||||||
# --- Resolve path ---
|
|
||||||
ci_path = args.CIPath
|
|
||||||
if not os.path.isabs(ci_path):
|
|
||||||
ci_path = os.path.join(os.getcwd(), ci_path)
|
|
||||||
resolved_path = ci_path
|
|
||||||
|
|
||||||
# --- Create if missing ---
|
|
||||||
if not os.path.isfile(ci_path):
|
|
||||||
if args.CreateIfMissing:
|
|
||||||
parent_dir = os.path.dirname(ci_path)
|
|
||||||
if parent_dir and not os.path.isdir(parent_dir):
|
|
||||||
os.makedirs(parent_dir, exist_ok=True)
|
|
||||||
empty_ci = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<CommandInterface xmlns="{CI_NS}"\n'
|
|
||||||
f'\txmlns:xr="{XR_NS}"\n'
|
|
||||||
f'\txmlns:xs="{XS_NS}"\n'
|
|
||||||
f'\txmlns:xsi="{XSI_NS}"\n'
|
|
||||||
f'\tversion="{format_version}">\n'
|
|
||||||
f'</CommandInterface>'
|
|
||||||
)
|
|
||||||
with open(ci_path, "w", encoding="utf-8-sig") as fh:
|
|
||||||
fh.write(empty_ci)
|
|
||||||
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
|
|
||||||
else:
|
|
||||||
print(f"File not found: {ci_path} (use -CreateIfMissing to create)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
resolved_path = os.path.abspath(ci_path)
|
|
||||||
|
|
||||||
# --- Load XML ---
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(resolved_path, xml_parser)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
add_count = 0
|
|
||||||
remove_count = 0
|
|
||||||
modify_count = 0
|
|
||||||
|
|
||||||
if localname(root) != "CommandInterface":
|
|
||||||
print(f"Expected <CommandInterface> root element, got <{localname(root)}>", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def ensure_section(section_name):
|
|
||||||
# Find existing
|
|
||||||
for child in root:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == section_name:
|
|
||||||
return child
|
|
||||||
|
|
||||||
# Create new section
|
|
||||||
new_section = etree.Element(f"{{{CI_NS}}}{section_name}")
|
|
||||||
|
|
||||||
my_idx = SECTION_ORDER.index(section_name) if section_name in SECTION_ORDER else -1
|
|
||||||
ref_node = None
|
|
||||||
for child in root:
|
|
||||||
if not isinstance(child.tag, str):
|
|
||||||
continue
|
|
||||||
child_idx = SECTION_ORDER.index(localname(child)) if localname(child) in SECTION_ORDER else -1
|
|
||||||
if child_idx > my_idx:
|
|
||||||
ref_node = child
|
|
||||||
break
|
|
||||||
|
|
||||||
root_indent = get_child_indent(root)
|
|
||||||
new_section.text = "\r\n" + root_indent
|
|
||||||
|
|
||||||
if ref_node is not None:
|
|
||||||
# Insert before ref_node
|
|
||||||
idx = list(root).index(ref_node)
|
|
||||||
new_section.tail = "\r\n" + root_indent
|
|
||||||
root.insert(idx, new_section)
|
|
||||||
else:
|
|
||||||
insert_before_closing(root, new_section, root_indent)
|
|
||||||
|
|
||||||
return new_section
|
|
||||||
|
|
||||||
def do_hide(commands):
|
|
||||||
nonlocal add_count, modify_count
|
|
||||||
commands = [normalize_cmd_name(c) for c in commands]
|
|
||||||
section = ensure_section("CommandsVisibility")
|
|
||||||
section_indent = get_child_indent(section)
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
existing = find_command_by_name(section, cmd)
|
|
||||||
if existing is not None:
|
|
||||||
common_el = None
|
|
||||||
for vis in existing:
|
|
||||||
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
|
|
||||||
for c in vis:
|
|
||||||
if isinstance(c.tag, str) and localname(c) == "Common":
|
|
||||||
common_el = c
|
|
||||||
break
|
|
||||||
if common_el is not None and (common_el.text or "").strip() == "false":
|
|
||||||
warn(f"Already hidden: {cmd}")
|
|
||||||
continue
|
|
||||||
if common_el is not None:
|
|
||||||
common_el.text = "false"
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Changed to hidden: {cmd}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>false</xr:Common></Visibility></Command>'
|
|
||||||
nodes = import_ci_fragment(frag_xml)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(section, nodes[0], section_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Hidden: {cmd}")
|
|
||||||
|
|
||||||
def do_show(commands):
|
|
||||||
nonlocal add_count, modify_count
|
|
||||||
commands = [normalize_cmd_name(c) for c in commands]
|
|
||||||
section = None
|
|
||||||
for child in root:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "CommandsVisibility":
|
|
||||||
section = child
|
|
||||||
break
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
if section is None:
|
|
||||||
section = ensure_section("CommandsVisibility")
|
|
||||||
|
|
||||||
existing = find_command_by_name(section, cmd)
|
|
||||||
if existing is not None:
|
|
||||||
common_el = None
|
|
||||||
for vis in existing:
|
|
||||||
if isinstance(vis.tag, str) and localname(vis) == "Visibility":
|
|
||||||
for c in vis:
|
|
||||||
if isinstance(c.tag, str) and localname(c) == "Common":
|
|
||||||
common_el = c
|
|
||||||
break
|
|
||||||
if common_el is not None and (common_el.text or "").strip() == "true":
|
|
||||||
warn(f"Already shown: {cmd}")
|
|
||||||
continue
|
|
||||||
if common_el is not None and (common_el.text or "").strip() == "false":
|
|
||||||
common_el.text = "true"
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Changed to shown: {cmd}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
section_indent = get_child_indent(section)
|
|
||||||
frag_xml = f'<Command name="{cmd}"><Visibility><xr:Common>true</xr:Common></Visibility></Command>'
|
|
||||||
nodes = import_ci_fragment(frag_xml)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(section, nodes[0], section_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Shown: {cmd}")
|
|
||||||
|
|
||||||
def do_place(json_val):
|
|
||||||
nonlocal add_count, modify_count
|
|
||||||
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
|
|
||||||
cmd_name = normalize_cmd_name(str(defn["command"]))
|
|
||||||
group_name = str(defn["group"])
|
|
||||||
if not cmd_name or not group_name:
|
|
||||||
print("place requires {command, group}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
section = ensure_section("CommandsPlacement")
|
|
||||||
section_indent = get_child_indent(section)
|
|
||||||
|
|
||||||
existing = find_command_by_name(section, cmd_name)
|
|
||||||
if existing is not None:
|
|
||||||
for child in existing:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "CommandGroup":
|
|
||||||
child.text = group_name
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Updated placement: {cmd_name} -> {group_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup><Placement>Auto</Placement></Command>'
|
|
||||||
nodes = import_ci_fragment(frag_xml)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(section, nodes[0], section_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Placed: {cmd_name} -> {group_name}")
|
|
||||||
|
|
||||||
def do_order(json_val):
|
|
||||||
nonlocal add_count, remove_count
|
|
||||||
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
|
|
||||||
group_name = str(defn["group"])
|
|
||||||
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
|
|
||||||
if not group_name or not commands:
|
|
||||||
print("order requires {group, commands:[...]}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
section = ensure_section("CommandsOrder")
|
|
||||||
section_indent = get_child_indent(section)
|
|
||||||
|
|
||||||
# Remove existing entries for this group
|
|
||||||
to_remove = []
|
|
||||||
for child in section:
|
|
||||||
if not isinstance(child.tag, str) or localname(child) != "Command":
|
|
||||||
continue
|
|
||||||
for gc in child:
|
|
||||||
if isinstance(gc.tag, str) and localname(gc) == "CommandGroup" and (gc.text or "").strip() == group_name:
|
|
||||||
to_remove.append(child)
|
|
||||||
break
|
|
||||||
for node in to_remove:
|
|
||||||
remove_with_indent(node)
|
|
||||||
remove_count += 1
|
|
||||||
|
|
||||||
# Add new entries
|
|
||||||
for cmd_name in commands:
|
|
||||||
frag_xml = f'<Command name="{cmd_name}"><CommandGroup>{group_name}</CommandGroup></Command>'
|
|
||||||
nodes = import_ci_fragment(frag_xml)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(section, nodes[0], section_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Set order for {group_name} : {len(commands)} commands")
|
|
||||||
|
|
||||||
def do_subsystem_order(json_val):
|
|
||||||
nonlocal add_count, remove_count
|
|
||||||
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
|
|
||||||
subsystems = [str(s) for s in parsed]
|
|
||||||
if not subsystems:
|
|
||||||
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
section = ensure_section("SubsystemsOrder")
|
|
||||||
section_indent = get_child_indent(section)
|
|
||||||
|
|
||||||
# Clear existing
|
|
||||||
for child in list(section):
|
|
||||||
if isinstance(child.tag, str):
|
|
||||||
remove_with_indent(child)
|
|
||||||
remove_count += 1
|
|
||||||
|
|
||||||
# Add new entries
|
|
||||||
for sub in subsystems:
|
|
||||||
new_el = etree.Element(f"{{{CI_NS}}}Subsystem")
|
|
||||||
new_el.text = sub
|
|
||||||
insert_before_closing(section, new_el, section_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Set subsystem order: {len(subsystems)} entries")
|
|
||||||
|
|
||||||
def do_group_order(json_val):
|
|
||||||
nonlocal add_count, remove_count
|
|
||||||
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
|
|
||||||
groups = [str(g) for g in parsed]
|
|
||||||
if not groups:
|
|
||||||
print("group-order requires array of group names", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
section = ensure_section("GroupsOrder")
|
|
||||||
section_indent = get_child_indent(section)
|
|
||||||
|
|
||||||
# Clear existing
|
|
||||||
for child in list(section):
|
|
||||||
if isinstance(child.tag, str):
|
|
||||||
remove_with_indent(child)
|
|
||||||
remove_count += 1
|
|
||||||
|
|
||||||
# Add new entries
|
|
||||||
for grp in groups:
|
|
||||||
new_el = etree.Element(f"{{{CI_NS}}}Group")
|
|
||||||
new_el.text = grp
|
|
||||||
insert_before_closing(section, new_el, section_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Set group order: {len(groups)} entries")
|
|
||||||
|
|
||||||
# --- 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 == "hide":
|
|
||||||
do_hide(parse_value_list(op_value))
|
|
||||||
elif op_name == "show":
|
|
||||||
do_show(parse_value_list(op_value))
|
|
||||||
elif op_name == "place":
|
|
||||||
do_place(op_value)
|
|
||||||
elif op_name == "order":
|
|
||||||
do_order(op_value)
|
|
||||||
elif op_name == "subsystem-order":
|
|
||||||
do_subsystem_order(op_value)
|
|
||||||
elif op_name == "group-order":
|
|
||||||
do_group_order(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__), "..", "..", "interface-validate", "scripts", "interface-validate.py"))
|
|
||||||
if os.path.isfile(validate_script):
|
|
||||||
print()
|
|
||||||
print("--- Running interface-validate ---")
|
|
||||||
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
print()
|
|
||||||
print("=== interface-edit summary ===")
|
|
||||||
print(f" Added: {add_count}")
|
|
||||||
print(f" Removed: {remove_count}")
|
|
||||||
print(f" Modified: {modify_count}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,119 +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.Валюты" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
|
||||||
|
|
||||||
## Catalog
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
|
|
||||||
| `limitLevelCount` | `false` | LimitLevelCount |
|
|
||||||
| `levelCount` | `2` | LevelCount |
|
|
||||||
| `foldersOnTop` | `true` | FoldersOnTop |
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `codeType` | `String` | CodeType |
|
|
||||||
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
|
|
||||||
| `codeSeries` | `WholeCatalog` | CodeSeries |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
|
|
||||||
| `subordinationUse` | `ToItems` | SubordinationUse |
|
|
||||||
| `quickChoice` | `true` | QuickChoice |
|
|
||||||
| `choiceMode` | `BothWays` | ChoiceMode |
|
|
||||||
| `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` | `DontUse` | DependenceOnCalculationTypes |
|
|
||||||
| `actionPeriodUse` | `false` | ActionPeriodUse |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **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,495 +0,0 @@
|
|||||||
# meta-remove v1.1 — Remove metadata object from 1C configuration dump
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ConfigDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Object,
|
|
||||||
|
|
||||||
[switch]$DryRun,
|
|
||||||
|
|
||||||
[switch]$KeepFiles,
|
|
||||||
|
|
||||||
[switch]$Force
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Type → plural directory mapping ---
|
|
||||||
|
|
||||||
$typePluralMap = @{
|
|
||||||
"Catalog" = "Catalogs"
|
|
||||||
"Document" = "Documents"
|
|
||||||
"Enum" = "Enums"
|
|
||||||
"Constant" = "Constants"
|
|
||||||
"InformationRegister" = "InformationRegisters"
|
|
||||||
"AccumulationRegister" = "AccumulationRegisters"
|
|
||||||
"AccountingRegister" = "AccountingRegisters"
|
|
||||||
"CalculationRegister" = "CalculationRegisters"
|
|
||||||
"ChartOfAccounts" = "ChartsOfAccounts"
|
|
||||||
"ChartOfCharacteristicTypes" = "ChartsOfCharacteristicTypes"
|
|
||||||
"ChartOfCalculationTypes" = "ChartsOfCalculationTypes"
|
|
||||||
"BusinessProcess" = "BusinessProcesses"
|
|
||||||
"Task" = "Tasks"
|
|
||||||
"ExchangePlan" = "ExchangePlans"
|
|
||||||
"DocumentJournal" = "DocumentJournals"
|
|
||||||
"Report" = "Reports"
|
|
||||||
"DataProcessor" = "DataProcessors"
|
|
||||||
"CommonModule" = "CommonModules"
|
|
||||||
"ScheduledJob" = "ScheduledJobs"
|
|
||||||
"EventSubscription" = "EventSubscriptions"
|
|
||||||
"HTTPService" = "HTTPServices"
|
|
||||||
"WebService" = "WebServices"
|
|
||||||
"DefinedType" = "DefinedTypes"
|
|
||||||
"Role" = "Roles"
|
|
||||||
"Subsystem" = "Subsystems"
|
|
||||||
"CommonForm" = "CommonForms"
|
|
||||||
"CommonTemplate" = "CommonTemplates"
|
|
||||||
"CommonPicture" = "CommonPictures"
|
|
||||||
"CommonAttribute" = "CommonAttributes"
|
|
||||||
"SessionParameter" = "SessionParameters"
|
|
||||||
"FunctionalOption" = "FunctionalOptions"
|
|
||||||
"FunctionalOptionsParameter" = "FunctionalOptionsParameters"
|
|
||||||
"Sequence" = "Sequences"
|
|
||||||
"FilterCriterion" = "FilterCriteria"
|
|
||||||
"SettingsStorage" = "SettingsStorages"
|
|
||||||
"XDTOPackage" = "XDTOPackages"
|
|
||||||
"WSReference" = "WSReferences"
|
|
||||||
"StyleItem" = "StyleItems"
|
|
||||||
"Language" = "Languages"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Resolve paths ---
|
|
||||||
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ConfigDir)) {
|
|
||||||
$ConfigDir = Join-Path (Get-Location).Path $ConfigDir
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $ConfigDir -PathType Container)) {
|
|
||||||
Write-Host "[ERROR] Config directory not found: $ConfigDir"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$configXml = Join-Path $ConfigDir "Configuration.xml"
|
|
||||||
if (-not (Test-Path $configXml)) {
|
|
||||||
Write-Host "[ERROR] Configuration.xml not found in: $ConfigDir"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Parse object spec ---
|
|
||||||
|
|
||||||
$parts = $Object -split "\.", 2
|
|
||||||
if ($parts.Count -ne 2 -or -not $parts[0] -or -not $parts[1]) {
|
|
||||||
Write-Host "[ERROR] Invalid object format '$Object'. Expected: Type.Name (e.g. Catalog.Товары)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$objType = $parts[0]
|
|
||||||
$objName = $parts[1]
|
|
||||||
|
|
||||||
if (-not $typePluralMap.ContainsKey($objType)) {
|
|
||||||
Write-Host "[ERROR] Unknown type '$objType'. Supported: $($typePluralMap.Keys -join ', ')"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$typePlural = $typePluralMap[$objType]
|
|
||||||
|
|
||||||
Write-Host "=== meta-remove: ${objType}.${objName} ==="
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
if ($DryRun) {
|
|
||||||
Write-Host "[DRY-RUN] No changes will be made"
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
|
|
||||||
$actions = 0
|
|
||||||
$errors = 0
|
|
||||||
|
|
||||||
# --- 1. Find object files ---
|
|
||||||
|
|
||||||
$typeDir = Join-Path $ConfigDir $typePlural
|
|
||||||
$objXml = Join-Path $typeDir "$objName.xml"
|
|
||||||
$objDir = Join-Path $typeDir $objName
|
|
||||||
|
|
||||||
$hasXml = Test-Path $objXml
|
|
||||||
$hasDir = Test-Path $objDir -PathType Container
|
|
||||||
|
|
||||||
if (-not $hasXml -and -not $hasDir) {
|
|
||||||
# Check if registered in Configuration.xml before proceeding
|
|
||||||
$cfgCheckDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$cfgCheckDoc.PreserveWhitespace = $true
|
|
||||||
$cfgCheckDoc.Load($configXml)
|
|
||||||
$cfgCheckNs = New-Object System.Xml.XmlNamespaceManager($cfgCheckDoc.NameTable)
|
|
||||||
$cfgCheckNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$cfgCheckNode = $cfgCheckDoc.DocumentElement.SelectSingleNode("md:Configuration/md:ChildObjects", $cfgCheckNs)
|
|
||||||
$registeredInCfg = $false
|
|
||||||
if ($cfgCheckNode) {
|
|
||||||
foreach ($child in @($cfgCheckNode.ChildNodes)) {
|
|
||||||
if ($child.NodeType -ne 'Element') { continue }
|
|
||||||
if ($child.LocalName -eq $objType -and $child.InnerText.Trim() -eq $objName) {
|
|
||||||
$registeredInCfg = $true; break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $registeredInCfg) {
|
|
||||||
Write-Host "[ERROR] Object not found: $typePlural/$objName.xml and not registered in Configuration.xml"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
Write-Host "[WARN] Object files not found: $typePlural/$objName.xml"
|
|
||||||
Write-Host " Proceeding with deregistration only..."
|
|
||||||
} else {
|
|
||||||
if ($hasXml) { Write-Host "[FOUND] $typePlural/$objName.xml" }
|
|
||||||
if ($hasDir) {
|
|
||||||
$fileCount = @(Get-ChildItem $objDir -Recurse -File).Count
|
|
||||||
Write-Host "[FOUND] $typePlural/$objName/ ($fileCount files)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 2. Reference check ---
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "--- Reference check ---"
|
|
||||||
|
|
||||||
# Build search patterns based on object type
|
|
||||||
|
|
||||||
# Type → reference type name (used in XML <v8:Type> elements)
|
|
||||||
$typeRefNames = @{
|
|
||||||
"Catalog" = @("CatalogRef","CatalogObject")
|
|
||||||
"Document" = @("DocumentRef","DocumentObject")
|
|
||||||
"Enum" = @("EnumRef")
|
|
||||||
"ExchangePlan" = @("ExchangePlanRef","ExchangePlanObject")
|
|
||||||
"ChartOfAccounts" = @("ChartOfAccountsRef","ChartOfAccountsObject")
|
|
||||||
"ChartOfCharacteristicTypes" = @("ChartOfCharacteristicTypesRef","ChartOfCharacteristicTypesObject")
|
|
||||||
"ChartOfCalculationTypes" = @("ChartOfCalculationTypesRef","ChartOfCalculationTypesObject")
|
|
||||||
"BusinessProcess" = @("BusinessProcessRef","BusinessProcessObject")
|
|
||||||
"Task" = @("TaskRef","TaskObject")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Type → Russian manager name (used in BSL code: Справочники.Товары)
|
|
||||||
$typeRuManager = @{
|
|
||||||
"Catalog" = "Справочники"
|
|
||||||
"Document" = "Документы"
|
|
||||||
"Enum" = "Перечисления"
|
|
||||||
"Constant" = "Константы"
|
|
||||||
"InformationRegister" = "РегистрыСведений"
|
|
||||||
"AccumulationRegister" = "РегистрыНакопления"
|
|
||||||
"AccountingRegister" = "РегистрыБухгалтерии"
|
|
||||||
"CalculationRegister" = "РегистрыРасчета"
|
|
||||||
"ChartOfAccounts" = "ПланыСчетов"
|
|
||||||
"ChartOfCharacteristicTypes" = "ПланыВидовХарактеристик"
|
|
||||||
"ChartOfCalculationTypes" = "ПланыВидовРасчета"
|
|
||||||
"BusinessProcess" = "БизнесПроцессы"
|
|
||||||
"Task" = "Задачи"
|
|
||||||
"ExchangePlan" = "ПланыОбмена"
|
|
||||||
"Report" = "Отчеты"
|
|
||||||
"DataProcessor" = "Обработки"
|
|
||||||
"DocumentJournal" = "ЖурналыДокументов"
|
|
||||||
"CommonModule" = $null
|
|
||||||
}
|
|
||||||
|
|
||||||
$searchPatterns = @()
|
|
||||||
|
|
||||||
# 1) XML type references: CatalogRef.Name, CatalogObject.Name
|
|
||||||
if ($typeRefNames.ContainsKey($objType)) {
|
|
||||||
foreach ($refName in $typeRefNames[$objType]) {
|
|
||||||
$searchPatterns += "$refName.$objName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2) BSL code references: Справочники.Name, Catalogs.Name
|
|
||||||
$ruMgr = $typeRuManager[$objType]
|
|
||||||
if ($ruMgr) {
|
|
||||||
$searchPatterns += "$ruMgr.$objName"
|
|
||||||
}
|
|
||||||
# English manager = plural directory name
|
|
||||||
$searchPatterns += "$typePlural.$objName"
|
|
||||||
|
|
||||||
# 3) CommonModule: method calls in BSL (ModuleName.)
|
|
||||||
if ($objType -eq "CommonModule") {
|
|
||||||
$searchPatterns += "$objName."
|
|
||||||
}
|
|
||||||
|
|
||||||
# 4) ScheduledJob/EventSubscription handler references
|
|
||||||
if ($objType -eq "CommonModule") {
|
|
||||||
$searchPatterns += "<Handler>$objName."
|
|
||||||
$searchPatterns += "<MethodName>$objName."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Exclude object's own files from search
|
|
||||||
$excludeDirs = @()
|
|
||||||
if ($hasDir) { $excludeDirs += $objDir }
|
|
||||||
$excludeFile = ""
|
|
||||||
if ($hasXml) { $excludeFile = $objXml }
|
|
||||||
|
|
||||||
# Search all XML and BSL files
|
|
||||||
$references = @()
|
|
||||||
$searchExtensions = @("*.xml", "*.bsl")
|
|
||||||
|
|
||||||
foreach ($ext in $searchExtensions) {
|
|
||||||
$files = @(Get-ChildItem $ConfigDir -Filter $ext -Recurse -File -ErrorAction SilentlyContinue)
|
|
||||||
foreach ($file in $files) {
|
|
||||||
# Skip own files
|
|
||||||
if ($excludeFile -and $file.FullName -eq $excludeFile) { continue }
|
|
||||||
if ($excludeDirs.Count -gt 0) {
|
|
||||||
$skip = $false
|
|
||||||
foreach ($ed in $excludeDirs) {
|
|
||||||
if ($file.FullName.StartsWith($ed)) { $skip = $true; break }
|
|
||||||
}
|
|
||||||
if ($skip) { continue }
|
|
||||||
}
|
|
||||||
# Skip auto-cleaned files (Configuration.xml, ConfigDumpInfo.xml, Subsystems)
|
|
||||||
$relPath = $file.FullName.Substring($ConfigDir.Length + 1)
|
|
||||||
if ($relPath -eq "Configuration.xml" -or $relPath -eq "ConfigDumpInfo.xml" -or $relPath.StartsWith("Subsystems")) { continue }
|
|
||||||
|
|
||||||
$content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
|
|
||||||
foreach ($pat in $searchPatterns) {
|
|
||||||
if ($content.Contains($pat)) {
|
|
||||||
$references += @{ File = $relPath; Pattern = $pat }
|
|
||||||
break # one match per file is enough
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Also check for Type.Name references (subsystem content, doc journal, etc.) — but NOT in own files
|
|
||||||
$typeNameRef = "${objType}.${objName}"
|
|
||||||
$files = @(Get-ChildItem $ConfigDir -Filter "*.xml" -Recurse -File -ErrorAction SilentlyContinue)
|
|
||||||
foreach ($file in $files) {
|
|
||||||
if ($excludeFile -and $file.FullName -eq $excludeFile) { continue }
|
|
||||||
if ($excludeDirs.Count -gt 0) {
|
|
||||||
$skip = $false
|
|
||||||
foreach ($ed in $excludeDirs) {
|
|
||||||
if ($file.FullName.StartsWith($ed)) { $skip = $true; break }
|
|
||||||
}
|
|
||||||
if ($skip) { continue }
|
|
||||||
}
|
|
||||||
# Skip Configuration.xml and Subsystems — they will be cleaned automatically
|
|
||||||
$relPath = $file.FullName.Substring($ConfigDir.Length + 1)
|
|
||||||
if ($relPath -eq "Configuration.xml") { continue }
|
|
||||||
if ($relPath -eq "ConfigDumpInfo.xml") { continue }
|
|
||||||
if ($relPath.StartsWith("Subsystems")) { continue }
|
|
||||||
|
|
||||||
$content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
|
|
||||||
if ($content.Contains($typeNameRef)) {
|
|
||||||
# Check it's not already in references
|
|
||||||
$alreadyFound = $false
|
|
||||||
foreach ($r in $references) {
|
|
||||||
if ($r.File -eq $relPath) { $alreadyFound = $true; break }
|
|
||||||
}
|
|
||||||
if (-not $alreadyFound) {
|
|
||||||
$references += @{ File = $relPath; Pattern = $typeNameRef }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($references.Count -gt 0) {
|
|
||||||
Write-Host "[WARN] Found $($references.Count) reference(s) to ${objType}.${objName}:"
|
|
||||||
Write-Host ""
|
|
||||||
$shown = 0
|
|
||||||
foreach ($ref in $references) {
|
|
||||||
Write-Host " $($ref.File)"
|
|
||||||
Write-Host " pattern: $($ref.Pattern)"
|
|
||||||
$shown++
|
|
||||||
if ($shown -ge 20) {
|
|
||||||
$remaining = $references.Count - $shown
|
|
||||||
if ($remaining -gt 0) {
|
|
||||||
Write-Host " ... and $remaining more"
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
if (-not $Force) {
|
|
||||||
Write-Host "[ERROR] Cannot remove: object has $($references.Count) reference(s)."
|
|
||||||
Write-Host " Use -Force to remove anyway, or fix references first."
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
Write-Host "[WARN] -Force specified, proceeding despite references"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[OK] No references found"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Remove from Configuration.xml ChildObjects ---
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "--- Configuration.xml ---"
|
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($configXml)
|
|
||||||
|
|
||||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
|
||||||
|
|
||||||
$cfgNode = $xmlDoc.DocumentElement.SelectSingleNode("md:Configuration", $ns)
|
|
||||||
if (-not $cfgNode) {
|
|
||||||
Write-Host "[ERROR] Configuration element not found in Configuration.xml"
|
|
||||||
$errors++
|
|
||||||
} else {
|
|
||||||
$childObjects = $cfgNode.SelectSingleNode("md:ChildObjects", $ns)
|
|
||||||
if ($childObjects) {
|
|
||||||
$found = $false
|
|
||||||
foreach ($child in @($childObjects.ChildNodes)) {
|
|
||||||
if ($child.NodeType -ne 'Element') { continue }
|
|
||||||
if ($child.LocalName -eq $objType -and $child.InnerText.Trim() -eq $objName) {
|
|
||||||
$found = $true
|
|
||||||
if (-not $DryRun) {
|
|
||||||
# Remove preceding whitespace if present
|
|
||||||
$prev = $child.PreviousSibling
|
|
||||||
if ($prev -and $prev.NodeType -eq 'Whitespace') {
|
|
||||||
$childObjects.RemoveChild($prev) | Out-Null
|
|
||||||
}
|
|
||||||
$childObjects.RemoveChild($child) | Out-Null
|
|
||||||
}
|
|
||||||
Write-Host "[OK] Removed <$objType>$objName</$objType> from ChildObjects"
|
|
||||||
$actions++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $found) {
|
|
||||||
Write-Host "[WARN] <$objType>$objName</$objType> not found in ChildObjects"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save Configuration.xml
|
|
||||||
if ($actions -gt 0 -and -not $DryRun) {
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding $true
|
|
||||||
$sw = New-Object System.IO.StreamWriter($configXml, $false, $enc)
|
|
||||||
$xmlDoc.Save($sw)
|
|
||||||
$sw.Close()
|
|
||||||
Write-Host "[OK] Configuration.xml saved"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Remove from subsystem Content ---
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "--- Subsystems ---"
|
|
||||||
|
|
||||||
$subsystemsDir = Join-Path $ConfigDir "Subsystems"
|
|
||||||
$subsystemsFound = 0
|
|
||||||
$subsystemsCleaned = 0
|
|
||||||
|
|
||||||
function Remove-FromSubsystems {
|
|
||||||
param([string]$dir)
|
|
||||||
|
|
||||||
$xmlFiles = @(Get-ChildItem $dir -Filter "*.xml" -File -ErrorAction SilentlyContinue)
|
|
||||||
foreach ($xmlFile in $xmlFiles) {
|
|
||||||
$ssDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$ssDoc.PreserveWhitespace = $true
|
|
||||||
try { $ssDoc.Load($xmlFile.FullName) } catch { continue }
|
|
||||||
|
|
||||||
$ssNs = New-Object System.Xml.XmlNamespaceManager($ssDoc.NameTable)
|
|
||||||
$ssNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$ssNs.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
|
||||||
|
|
||||||
$ssNode = $ssDoc.DocumentElement.SelectSingleNode("md:Subsystem", $ssNs)
|
|
||||||
if (-not $ssNode) { continue }
|
|
||||||
|
|
||||||
$propsNode = $ssNode.SelectSingleNode("md:Properties", $ssNs)
|
|
||||||
if (-not $propsNode) { continue }
|
|
||||||
|
|
||||||
$contentNode = $propsNode.SelectSingleNode("md:Content", $ssNs)
|
|
||||||
if (-not $contentNode) { continue }
|
|
||||||
|
|
||||||
$ssNameNode = $propsNode.SelectSingleNode("md:Name", $ssNs)
|
|
||||||
$ssName = if ($ssNameNode) { $ssNameNode.InnerText } else { $xmlFile.BaseName }
|
|
||||||
|
|
||||||
# Content items are <v8:Value>Type.Name</v8:Value>
|
|
||||||
$targetRef = "${objType}.${objName}"
|
|
||||||
$modified = $false
|
|
||||||
|
|
||||||
foreach ($item in @($contentNode.ChildNodes)) {
|
|
||||||
if ($item.NodeType -ne 'Element') { continue }
|
|
||||||
$val = $item.InnerText.Trim()
|
|
||||||
# Content format: "Subsystem.X" or "Catalog.X" etc.
|
|
||||||
if ($val -eq $targetRef) {
|
|
||||||
$script:subsystemsFound++
|
|
||||||
if (-not $DryRun) {
|
|
||||||
$prev = $item.PreviousSibling
|
|
||||||
if ($prev -and $prev.NodeType -eq 'Whitespace') {
|
|
||||||
$contentNode.RemoveChild($prev) | Out-Null
|
|
||||||
}
|
|
||||||
$contentNode.RemoveChild($item) | Out-Null
|
|
||||||
$modified = $true
|
|
||||||
}
|
|
||||||
Write-Host "[OK] Removed from subsystem '$ssName'"
|
|
||||||
$script:subsystemsCleaned++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($modified -and -not $DryRun) {
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding $true
|
|
||||||
$sw = New-Object System.IO.StreamWriter($xmlFile.FullName, $false, $enc)
|
|
||||||
$ssDoc.Save($sw)
|
|
||||||
$sw.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Recurse into child subsystems
|
|
||||||
$childDir = Join-Path $dir ($xmlFile.BaseName)
|
|
||||||
$childSubsystems = Join-Path $childDir "Subsystems"
|
|
||||||
if (Test-Path $childSubsystems -PathType Container) {
|
|
||||||
Remove-FromSubsystems -dir $childSubsystems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $subsystemsDir -PathType Container) {
|
|
||||||
Remove-FromSubsystems -dir $subsystemsDir
|
|
||||||
if ($subsystemsCleaned -eq 0) {
|
|
||||||
Write-Host "[OK] Not referenced in any subsystem"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[OK] No Subsystems directory"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Delete object files ---
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "--- Files ---"
|
|
||||||
|
|
||||||
if (-not $KeepFiles) {
|
|
||||||
if ($hasDir -and -not $DryRun) {
|
|
||||||
Remove-Item $objDir -Recurse -Force
|
|
||||||
Write-Host "[OK] Deleted directory: $typePlural/$objName/"
|
|
||||||
$actions++
|
|
||||||
} elseif ($hasDir) {
|
|
||||||
Write-Host "[DRY] Would delete directory: $typePlural/$objName/"
|
|
||||||
$actions++
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasXml -and -not $DryRun) {
|
|
||||||
Remove-Item $objXml -Force
|
|
||||||
Write-Host "[OK] Deleted file: $typePlural/$objName.xml"
|
|
||||||
$actions++
|
|
||||||
} elseif ($hasXml) {
|
|
||||||
Write-Host "[DRY] Would delete file: $typePlural/$objName.xml"
|
|
||||||
$actions++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $hasXml -and -not $hasDir) {
|
|
||||||
Write-Host "[OK] No files to delete"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[SKIP] File deletion skipped (-KeepFiles)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
$totalActions = $actions + $subsystemsCleaned
|
|
||||||
if ($DryRun) {
|
|
||||||
Write-Host "=== Dry run complete: $totalActions actions would be performed ==="
|
|
||||||
} else {
|
|
||||||
Write-Host "=== Done: $totalActions actions performed ($subsystemsCleaned subsystem references removed) ==="
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($errors -gt 0) {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
exit 0
|
|
||||||
@@ -1,485 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# meta-remove v1.1 — Remove metadata object from 1C configuration dump
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import shutil
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
# --- Type -> plural directory mapping ---
|
|
||||||
|
|
||||||
TYPE_PLURAL_MAP = {
|
|
||||||
"Catalog": "Catalogs",
|
|
||||||
"Document": "Documents",
|
|
||||||
"Enum": "Enums",
|
|
||||||
"Constant": "Constants",
|
|
||||||
"InformationRegister": "InformationRegisters",
|
|
||||||
"AccumulationRegister": "AccumulationRegisters",
|
|
||||||
"AccountingRegister": "AccountingRegisters",
|
|
||||||
"CalculationRegister": "CalculationRegisters",
|
|
||||||
"ChartOfAccounts": "ChartsOfAccounts",
|
|
||||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
|
||||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
|
||||||
"BusinessProcess": "BusinessProcesses",
|
|
||||||
"Task": "Tasks",
|
|
||||||
"ExchangePlan": "ExchangePlans",
|
|
||||||
"DocumentJournal": "DocumentJournals",
|
|
||||||
"Report": "Reports",
|
|
||||||
"DataProcessor": "DataProcessors",
|
|
||||||
"CommonModule": "CommonModules",
|
|
||||||
"ScheduledJob": "ScheduledJobs",
|
|
||||||
"EventSubscription": "EventSubscriptions",
|
|
||||||
"HTTPService": "HTTPServices",
|
|
||||||
"WebService": "WebServices",
|
|
||||||
"DefinedType": "DefinedTypes",
|
|
||||||
"Role": "Roles",
|
|
||||||
"Subsystem": "Subsystems",
|
|
||||||
"CommonForm": "CommonForms",
|
|
||||||
"CommonTemplate": "CommonTemplates",
|
|
||||||
"CommonPicture": "CommonPictures",
|
|
||||||
"CommonAttribute": "CommonAttributes",
|
|
||||||
"SessionParameter": "SessionParameters",
|
|
||||||
"FunctionalOption": "FunctionalOptions",
|
|
||||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
|
|
||||||
"Sequence": "Sequences",
|
|
||||||
"FilterCriterion": "FilterCriteria",
|
|
||||||
"SettingsStorage": "SettingsStorages",
|
|
||||||
"XDTOPackage": "XDTOPackages",
|
|
||||||
"WSReference": "WSReferences",
|
|
||||||
"StyleItem": "StyleItems",
|
|
||||||
"Language": "Languages",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Type -> reference type names (used in XML <v8:Type> elements)
|
|
||||||
TYPE_REF_NAMES = {
|
|
||||||
"Catalog": ["CatalogRef", "CatalogObject"],
|
|
||||||
"Document": ["DocumentRef", "DocumentObject"],
|
|
||||||
"Enum": ["EnumRef"],
|
|
||||||
"ExchangePlan": ["ExchangePlanRef", "ExchangePlanObject"],
|
|
||||||
"ChartOfAccounts": ["ChartOfAccountsRef", "ChartOfAccountsObject"],
|
|
||||||
"ChartOfCharacteristicTypes": ["ChartOfCharacteristicTypesRef", "ChartOfCharacteristicTypesObject"],
|
|
||||||
"ChartOfCalculationTypes": ["ChartOfCalculationTypesRef", "ChartOfCalculationTypesObject"],
|
|
||||||
"BusinessProcess": ["BusinessProcessRef", "BusinessProcessObject"],
|
|
||||||
"Task": ["TaskRef", "TaskObject"],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Type -> Russian manager name (used in BSL code)
|
|
||||||
TYPE_RU_MANAGER = {
|
|
||||||
"Catalog": "\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438",
|
|
||||||
"Document": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
|
|
||||||
"Enum": "\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f",
|
|
||||||
"Constant": "\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b",
|
|
||||||
"InformationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439",
|
|
||||||
"AccumulationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f",
|
|
||||||
"AccountingRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438",
|
|
||||||
"CalculationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
|
|
||||||
"ChartOfAccounts": "\u041f\u043b\u0430\u043d\u044b\u0421\u0447\u0435\u0442\u043e\u0432",
|
|
||||||
"ChartOfCharacteristicTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a",
|
|
||||||
"ChartOfCalculationTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
|
|
||||||
"BusinessProcess": "\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u044b",
|
|
||||||
"Task": "\u0417\u0430\u0434\u0430\u0447\u0438",
|
|
||||||
"ExchangePlan": "\u041f\u043b\u0430\u043d\u044b\u041e\u0431\u043c\u0435\u043d\u0430",
|
|
||||||
"Report": "\u041e\u0442\u0447\u0435\u0442\u044b",
|
|
||||||
"DataProcessor": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438",
|
|
||||||
"DocumentJournal": "\u0416\u0443\u0440\u043d\u0430\u043b\u044b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
|
|
||||||
"CommonModule": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
|
||||||
|
|
||||||
NSMAP = {"md": MD_NS, "v8": V8_NS}
|
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
|
||||||
return etree.QName(el.tag).localname
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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="Remove metadata object from 1C configuration dump", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ConfigDir", required=True)
|
|
||||||
parser.add_argument("-Object", required=True)
|
|
||||||
parser.add_argument("-DryRun", action="store_true")
|
|
||||||
parser.add_argument("-KeepFiles", action="store_true")
|
|
||||||
parser.add_argument("-Force", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
config_dir = args.ConfigDir
|
|
||||||
if not os.path.isabs(config_dir):
|
|
||||||
config_dir = os.path.join(os.getcwd(), config_dir)
|
|
||||||
|
|
||||||
if not os.path.isdir(config_dir):
|
|
||||||
print(f"[ERROR] Config directory not found: {config_dir}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
config_xml = os.path.join(config_dir, "Configuration.xml")
|
|
||||||
if not os.path.isfile(config_xml):
|
|
||||||
print(f"[ERROR] Configuration.xml not found in: {config_dir}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Parse object spec ---
|
|
||||||
parts = args.Object.split(".", 1)
|
|
||||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
||||||
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b)")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
obj_type = parts[0]
|
|
||||||
obj_name = parts[1]
|
|
||||||
|
|
||||||
if obj_type not in TYPE_PLURAL_MAP:
|
|
||||||
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
type_plural = TYPE_PLURAL_MAP[obj_type]
|
|
||||||
|
|
||||||
print(f"=== meta-remove: {obj_type}.{obj_name} ===")
|
|
||||||
print()
|
|
||||||
|
|
||||||
if args.DryRun:
|
|
||||||
print("[DRY-RUN] No changes will be made")
|
|
||||||
print()
|
|
||||||
|
|
||||||
actions = 0
|
|
||||||
errors = 0
|
|
||||||
|
|
||||||
# --- 1. Find object files ---
|
|
||||||
type_dir = os.path.join(config_dir, type_plural)
|
|
||||||
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
|
|
||||||
obj_dir = os.path.join(type_dir, obj_name)
|
|
||||||
|
|
||||||
has_xml = os.path.isfile(obj_xml)
|
|
||||||
has_dir = os.path.isdir(obj_dir)
|
|
||||||
|
|
||||||
if not has_xml and not has_dir:
|
|
||||||
# Check if registered in Configuration.xml before proceeding
|
|
||||||
cfg_check_tree = etree.parse(config_xml, etree.XMLParser(remove_blank_text=False))
|
|
||||||
cfg_check_root = cfg_check_tree.getroot()
|
|
||||||
child_objects = cfg_check_root.find(f"{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
|
|
||||||
registered_in_cfg = False
|
|
||||||
if child_objects is not None:
|
|
||||||
for child in child_objects:
|
|
||||||
if isinstance(child.tag, str) and etree.QName(child.tag).localname == obj_type and (child.text or "").strip() == obj_name:
|
|
||||||
registered_in_cfg = True
|
|
||||||
break
|
|
||||||
if not registered_in_cfg:
|
|
||||||
print(f"[ERROR] Object not found: {type_plural}/{obj_name}.xml and not registered in Configuration.xml")
|
|
||||||
sys.exit(1)
|
|
||||||
print(f"[WARN] Object files not found: {type_plural}/{obj_name}.xml")
|
|
||||||
print(" Proceeding with deregistration only...")
|
|
||||||
else:
|
|
||||||
if has_xml:
|
|
||||||
print(f"[FOUND] {type_plural}/{obj_name}.xml")
|
|
||||||
if has_dir:
|
|
||||||
file_count = sum(len(files) for _, _, files in os.walk(obj_dir))
|
|
||||||
print(f"[FOUND] {type_plural}/{obj_name}/ ({file_count} files)")
|
|
||||||
|
|
||||||
# --- 2. Reference check ---
|
|
||||||
print()
|
|
||||||
print("--- Reference check ---")
|
|
||||||
|
|
||||||
search_patterns = []
|
|
||||||
|
|
||||||
# 1) XML type references
|
|
||||||
if obj_type in TYPE_REF_NAMES:
|
|
||||||
for ref_name in TYPE_REF_NAMES[obj_type]:
|
|
||||||
search_patterns.append(f"{ref_name}.{obj_name}")
|
|
||||||
|
|
||||||
# 2) BSL code references
|
|
||||||
ru_mgr = TYPE_RU_MANAGER.get(obj_type)
|
|
||||||
if ru_mgr:
|
|
||||||
search_patterns.append(f"{ru_mgr}.{obj_name}")
|
|
||||||
search_patterns.append(f"{type_plural}.{obj_name}")
|
|
||||||
|
|
||||||
# 3) CommonModule: method calls
|
|
||||||
if obj_type == "CommonModule":
|
|
||||||
search_patterns.append(f"{obj_name}.")
|
|
||||||
|
|
||||||
# 4) ScheduledJob/EventSubscription handler references
|
|
||||||
if obj_type == "CommonModule":
|
|
||||||
search_patterns.append(f"<Handler>{obj_name}.")
|
|
||||||
search_patterns.append(f"<MethodName>{obj_name}.")
|
|
||||||
|
|
||||||
# Exclude object's own files
|
|
||||||
exclude_dirs = []
|
|
||||||
if has_dir:
|
|
||||||
exclude_dirs.append(obj_dir)
|
|
||||||
exclude_file = obj_xml if has_xml else ""
|
|
||||||
|
|
||||||
# Search all XML and BSL files
|
|
||||||
references = []
|
|
||||||
search_extensions = (".xml", ".bsl")
|
|
||||||
|
|
||||||
for root_path, dirs, files in os.walk(config_dir):
|
|
||||||
for fname in files:
|
|
||||||
ext = os.path.splitext(fname)[1].lower()
|
|
||||||
if ext not in search_extensions:
|
|
||||||
continue
|
|
||||||
full_path = os.path.join(root_path, fname)
|
|
||||||
|
|
||||||
# Skip own files
|
|
||||||
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
|
|
||||||
continue
|
|
||||||
skip = False
|
|
||||||
for ed in exclude_dirs:
|
|
||||||
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
|
|
||||||
skip = True
|
|
||||||
break
|
|
||||||
if skip:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get relative path
|
|
||||||
rel_path = os.path.relpath(full_path, config_dir)
|
|
||||||
rel_path_fwd = rel_path.replace("\\", "/")
|
|
||||||
|
|
||||||
# Skip auto-cleaned files
|
|
||||||
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(full_path, "r", encoding="utf-8-sig") as fh:
|
|
||||||
content = fh.read()
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for pat in search_patterns:
|
|
||||||
if pat in content:
|
|
||||||
references.append({"File": rel_path, "Pattern": pat})
|
|
||||||
break
|
|
||||||
|
|
||||||
# Also check Type.Name references
|
|
||||||
type_name_ref = f"{obj_type}.{obj_name}"
|
|
||||||
already_found_files = {r["File"] for r in references}
|
|
||||||
|
|
||||||
for root_path, dirs, files in os.walk(config_dir):
|
|
||||||
for fname in files:
|
|
||||||
if not fname.lower().endswith(".xml"):
|
|
||||||
continue
|
|
||||||
full_path = os.path.join(root_path, fname)
|
|
||||||
|
|
||||||
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
|
|
||||||
continue
|
|
||||||
skip = False
|
|
||||||
for ed in exclude_dirs:
|
|
||||||
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
|
|
||||||
skip = True
|
|
||||||
break
|
|
||||||
if skip:
|
|
||||||
continue
|
|
||||||
|
|
||||||
rel_path = os.path.relpath(full_path, config_dir)
|
|
||||||
rel_path_fwd = rel_path.replace("\\", "/")
|
|
||||||
|
|
||||||
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if rel_path in already_found_files:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(full_path, "r", encoding="utf-8-sig") as fh:
|
|
||||||
content = fh.read()
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if type_name_ref in content:
|
|
||||||
references.append({"File": rel_path, "Pattern": type_name_ref})
|
|
||||||
|
|
||||||
if references:
|
|
||||||
print(f"[WARN] Found {len(references)} reference(s) to {obj_type}.{obj_name}:")
|
|
||||||
print()
|
|
||||||
shown = 0
|
|
||||||
for ref in references:
|
|
||||||
print(f" {ref['File']}")
|
|
||||||
print(f" pattern: {ref['Pattern']}")
|
|
||||||
shown += 1
|
|
||||||
if shown >= 20:
|
|
||||||
remaining = len(references) - shown
|
|
||||||
if remaining > 0:
|
|
||||||
print(f" ... and {remaining} more")
|
|
||||||
break
|
|
||||||
print()
|
|
||||||
|
|
||||||
if not args.Force:
|
|
||||||
print(f"[ERROR] Cannot remove: object has {len(references)} reference(s).")
|
|
||||||
print(" Use -Force to remove anyway, or fix references first.")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("[WARN] -Force specified, proceeding despite references")
|
|
||||||
else:
|
|
||||||
print("[OK] No references found")
|
|
||||||
|
|
||||||
# --- 3. Remove from Configuration.xml ChildObjects ---
|
|
||||||
print()
|
|
||||||
print("--- Configuration.xml ---")
|
|
||||||
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(config_xml, xml_parser)
|
|
||||||
xml_root = tree.getroot()
|
|
||||||
|
|
||||||
cfg_node = xml_root.find(f"{{{MD_NS}}}Configuration")
|
|
||||||
if cfg_node is None:
|
|
||||||
print("[ERROR] Configuration element not found in Configuration.xml")
|
|
||||||
errors += 1
|
|
||||||
else:
|
|
||||||
child_objects = cfg_node.find(f"{{{MD_NS}}}ChildObjects")
|
|
||||||
if child_objects is not None:
|
|
||||||
found = False
|
|
||||||
for child in list(child_objects):
|
|
||||||
if not isinstance(child.tag, str):
|
|
||||||
continue
|
|
||||||
if localname(child) == obj_type and (child.text or "").strip() == obj_name:
|
|
||||||
found = True
|
|
||||||
if not args.DryRun:
|
|
||||||
# Remove preceding whitespace (tail of previous sibling or text of parent)
|
|
||||||
prev = child.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
if prev.tail and prev.tail.strip() == "":
|
|
||||||
prev.tail = prev.tail.rsplit("\n", 1)[0] + "\n" if "\n" in prev.tail else ""
|
|
||||||
if not prev.tail.strip():
|
|
||||||
# Keep just the last newline+indent before the next element
|
|
||||||
pass
|
|
||||||
child_objects.remove(child)
|
|
||||||
print(f"[OK] Removed <{obj_type}>{obj_name}</{obj_type}> from ChildObjects")
|
|
||||||
actions += 1
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
print(f"[WARN] <{obj_type}>{obj_name}</{obj_type}> not found in ChildObjects")
|
|
||||||
|
|
||||||
# Save Configuration.xml
|
|
||||||
if actions > 0 and not args.DryRun:
|
|
||||||
save_xml_bom(tree, config_xml)
|
|
||||||
print("[OK] Configuration.xml saved")
|
|
||||||
|
|
||||||
# --- 4. Remove from subsystem Content ---
|
|
||||||
print()
|
|
||||||
print("--- Subsystems ---")
|
|
||||||
|
|
||||||
subsystems_dir = os.path.join(config_dir, "Subsystems")
|
|
||||||
subsystems_found = 0
|
|
||||||
subsystems_cleaned = 0
|
|
||||||
|
|
||||||
def remove_from_subsystems(dir_path):
|
|
||||||
nonlocal subsystems_found, subsystems_cleaned
|
|
||||||
|
|
||||||
if not os.path.isdir(dir_path):
|
|
||||||
return
|
|
||||||
|
|
||||||
for fname in os.listdir(dir_path):
|
|
||||||
if not fname.lower().endswith(".xml"):
|
|
||||||
continue
|
|
||||||
xml_file = os.path.join(dir_path, fname)
|
|
||||||
if not os.path.isfile(xml_file):
|
|
||||||
continue
|
|
||||||
|
|
||||||
ss_parser = etree.XMLParser(remove_blank_text=False)
|
|
||||||
try:
|
|
||||||
ss_tree = etree.parse(xml_file, ss_parser)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ss_root = ss_tree.getroot()
|
|
||||||
ss_node = None
|
|
||||||
for child in ss_root:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Subsystem":
|
|
||||||
ss_node = child
|
|
||||||
break
|
|
||||||
if ss_node is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
props_node = ss_node.find(f"{{{MD_NS}}}Properties")
|
|
||||||
if props_node is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
content_node = props_node.find(f"{{{MD_NS}}}Content")
|
|
||||||
if content_node is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ss_name_node = props_node.find(f"{{{MD_NS}}}Name")
|
|
||||||
ss_name = ss_name_node.text if ss_name_node is not None and ss_name_node.text else os.path.splitext(fname)[0]
|
|
||||||
|
|
||||||
target_ref = f"{obj_type}.{obj_name}"
|
|
||||||
modified = False
|
|
||||||
|
|
||||||
for item in list(content_node):
|
|
||||||
if not isinstance(item.tag, str):
|
|
||||||
continue
|
|
||||||
val = (item.text or "").strip()
|
|
||||||
if val == target_ref:
|
|
||||||
subsystems_found += 1
|
|
||||||
if not args.DryRun:
|
|
||||||
content_node.remove(item)
|
|
||||||
modified = True
|
|
||||||
print(f"[OK] Removed from subsystem '{ss_name}'")
|
|
||||||
subsystems_cleaned += 1
|
|
||||||
|
|
||||||
if modified and not args.DryRun:
|
|
||||||
save_xml_bom(ss_tree, xml_file)
|
|
||||||
|
|
||||||
# Recurse into child subsystems
|
|
||||||
base_name = os.path.splitext(fname)[0]
|
|
||||||
child_dir = os.path.join(dir_path, base_name, "Subsystems")
|
|
||||||
if os.path.isdir(child_dir):
|
|
||||||
remove_from_subsystems(child_dir)
|
|
||||||
|
|
||||||
if os.path.isdir(subsystems_dir):
|
|
||||||
remove_from_subsystems(subsystems_dir)
|
|
||||||
if subsystems_cleaned == 0:
|
|
||||||
print("[OK] Not referenced in any subsystem")
|
|
||||||
else:
|
|
||||||
print("[OK] No Subsystems directory")
|
|
||||||
|
|
||||||
# --- 5. Delete object files ---
|
|
||||||
print()
|
|
||||||
print("--- Files ---")
|
|
||||||
|
|
||||||
if not args.KeepFiles:
|
|
||||||
if has_dir and not args.DryRun:
|
|
||||||
shutil.rmtree(obj_dir)
|
|
||||||
print(f"[OK] Deleted directory: {type_plural}/{obj_name}/")
|
|
||||||
actions += 1
|
|
||||||
elif has_dir:
|
|
||||||
print(f"[DRY] Would delete directory: {type_plural}/{obj_name}/")
|
|
||||||
actions += 1
|
|
||||||
|
|
||||||
if has_xml and not args.DryRun:
|
|
||||||
os.remove(obj_xml)
|
|
||||||
print(f"[OK] Deleted file: {type_plural}/{obj_name}.xml")
|
|
||||||
actions += 1
|
|
||||||
elif has_xml:
|
|
||||||
print(f"[DRY] Would delete file: {type_plural}/{obj_name}.xml")
|
|
||||||
actions += 1
|
|
||||||
|
|
||||||
if not has_xml and not has_dir:
|
|
||||||
print("[OK] No files to delete")
|
|
||||||
else:
|
|
||||||
print("[SKIP] File deletion skipped (-KeepFiles)")
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
print()
|
|
||||||
total_actions = actions + subsystems_cleaned
|
|
||||||
if args.DryRun:
|
|
||||||
print(f"=== Dry run complete: {total_actions} actions would be performed ===")
|
|
||||||
else:
|
|
||||||
print(f"=== Done: {total_actions} actions performed ({subsystems_cleaned} subsystem references removed) ===")
|
|
||||||
|
|
||||||
if errors > 0:
|
|
||||||
sys.exit(1)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
name: mxl-compile
|
|
||||||
description: Компиляция табличного документа (MXL) из JSON-определения. Используй когда нужно создать макет печатной формы
|
|
||||||
argument-hint: <JsonPath> <OutputPath>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /mxl-compile — Компилятор макета из DSL
|
|
||||||
|
|
||||||
Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace).
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
```
|
|
||||||
/mxl-compile <JsonPath> <OutputPath>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|------------|:------------:|------------------------------------|
|
|
||||||
| JsonPath | да | Путь к JSON-определению макета |
|
|
||||||
| OutputPath | да | Путь для генерации Template.xml |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.ps1 -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Рабочий процесс
|
|
||||||
|
|
||||||
1. Claude пишет JSON-определение (Write tool) → файл `.json`
|
|
||||||
2. Claude вызывает `/mxl-compile` для генерации Template.xml
|
|
||||||
3. Claude вызывает `/mxl-validate` для проверки корректности
|
|
||||||
4. Claude вызывает `/mxl-info` для верификации структуры
|
|
||||||
|
|
||||||
**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров.
|
|
||||||
|
|
||||||
## JSON-схема DSL
|
|
||||||
|
|
||||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON).
|
|
||||||
|
|
||||||
Краткая структура:
|
|
||||||
|
|
||||||
```
|
|
||||||
{ columns, page, defaultWidth, columnWidths,
|
|
||||||
fonts: { name: { face, size, bold, italic, underline, strikeout } },
|
|
||||||
styles: { name: { font, align, valign, border, borderWidth, wrap, format } },
|
|
||||||
areas: [{ name, rows: [{ height, rowStyle, cells: [
|
|
||||||
{ col, span, rowspan, style, param, detail, text, template }
|
|
||||||
]}]}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключевые правила:
|
|
||||||
- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"`
|
|
||||||
- `col` — 1-based позиция колонки
|
|
||||||
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине)
|
|
||||||
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template
|
|
||||||
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
|
|
||||||
@@ -1,733 +0,0 @@
|
|||||||
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$JsonPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$OutputPath
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
|
|
||||||
if (-not (Test-Path $JsonPath)) {
|
|
||||||
Write-Error "File not found: $JsonPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
|
||||||
$def = $json | ConvertFrom-Json
|
|
||||||
|
|
||||||
if (-not $def.columns) {
|
|
||||||
Write-Error "Required field 'columns' is missing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if (-not $def.areas) {
|
|
||||||
Write-Error "Required field 'areas' is missing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalColumns = [int]$def.columns
|
|
||||||
$defaultWidth = if ($def.defaultWidth) { [int]$def.defaultWidth } else { 10 }
|
|
||||||
|
|
||||||
# --- 2. Build font palette ---
|
|
||||||
|
|
||||||
$fontMap = [ordered]@{} # name -> 0-based index
|
|
||||||
$fontEntries = @() # array of hashtables
|
|
||||||
|
|
||||||
function Add-Font {
|
|
||||||
param([string]$name, $fontDef)
|
|
||||||
$face = if ($fontDef.face) { $fontDef.face } else { "Arial" }
|
|
||||||
$size = if ($fontDef.size) { [int]$fontDef.size } else { 10 }
|
|
||||||
$bold = if ($fontDef.bold -eq $true) { "true" } else { "false" }
|
|
||||||
$italic = if ($fontDef.italic -eq $true) { "true" } else { "false" }
|
|
||||||
$underline = if ($fontDef.underline -eq $true) { "true" } else { "false" }
|
|
||||||
$strikeout = if ($fontDef.strikeout -eq $true) { "true" } else { "false" }
|
|
||||||
|
|
||||||
$idx = $script:fontEntries.Count
|
|
||||||
$script:fontMap[$name] = $idx
|
|
||||||
$script:fontEntries += @{
|
|
||||||
Face = $face
|
|
||||||
Size = $size
|
|
||||||
Bold = $bold
|
|
||||||
Italic = $italic
|
|
||||||
Underline = $underline
|
|
||||||
Strikeout = $strikeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add user-defined fonts
|
|
||||||
$hasDefault = $false
|
|
||||||
if ($def.fonts) {
|
|
||||||
foreach ($prop in $def.fonts.PSObject.Properties) {
|
|
||||||
if ($prop.Name -eq "default") { $hasDefault = $true }
|
|
||||||
Add-Font -name $prop.Name -fontDef $prop.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure default font exists
|
|
||||||
if (-not $hasDefault) {
|
|
||||||
$defaultDef = New-Object PSObject -Property @{ face = "Arial"; size = 10 }
|
|
||||||
Add-Font -name "default" -fontDef $defaultDef
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Determine line palette ---
|
|
||||||
|
|
||||||
$hasThinBorders = $false
|
|
||||||
$hasThickBorders = $false
|
|
||||||
|
|
||||||
# Scan styles for border usage
|
|
||||||
if ($def.styles) {
|
|
||||||
foreach ($prop in $def.styles.PSObject.Properties) {
|
|
||||||
$s = $prop.Value
|
|
||||||
if ($s.border -and $s.border -ne "none") {
|
|
||||||
if ($s.borderWidth -eq "thick") {
|
|
||||||
$hasThickBorders = $true
|
|
||||||
} else {
|
|
||||||
$hasThinBorders = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$thinLineIndex = -1
|
|
||||||
$thickLineIndex = -1
|
|
||||||
$lineCount = 0
|
|
||||||
if ($hasThinBorders) {
|
|
||||||
$thinLineIndex = $lineCount; $lineCount++
|
|
||||||
}
|
|
||||||
if ($hasThickBorders) {
|
|
||||||
$thickLineIndex = $lineCount; $lineCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Parse column width specs ---
|
|
||||||
|
|
||||||
function Parse-ColumnSpec {
|
|
||||||
param([string]$spec)
|
|
||||||
$cols = @()
|
|
||||||
foreach ($part in $spec -split ',') {
|
|
||||||
$part = $part.Trim()
|
|
||||||
if ($part -match '^(\d+)-(\d+)$') {
|
|
||||||
$from = [int]$Matches[1]
|
|
||||||
$to = [int]$Matches[2]
|
|
||||||
for ($i = $from; $i -le $to; $i++) { $cols += $i }
|
|
||||||
} else {
|
|
||||||
$cols += [int]$part
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $cols
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4a. Auto-calculate defaultWidth from page format ---
|
|
||||||
|
|
||||||
$pageTargets = @{
|
|
||||||
"A4-landscape" = 780
|
|
||||||
"A4-portrait" = 540
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($def.page) {
|
|
||||||
$pageName = "$($def.page)"
|
|
||||||
$targetWidth = $null
|
|
||||||
|
|
||||||
if ($pageName -match '^\d+$') {
|
|
||||||
$targetWidth = [int]$pageName
|
|
||||||
} elseif ($pageTargets.ContainsKey($pageName)) {
|
|
||||||
$targetWidth = $pageTargets[$pageName]
|
|
||||||
} else {
|
|
||||||
Write-Warning "Unknown page format '$pageName'. Known: $($pageTargets.Keys -join ', '), or a number."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($targetWidth) {
|
|
||||||
$totalUnits = 0.0
|
|
||||||
$absoluteSum = 0
|
|
||||||
$specifiedCols = @{}
|
|
||||||
|
|
||||||
if ($def.columnWidths) {
|
|
||||||
foreach ($prop in $def.columnWidths.PSObject.Properties) {
|
|
||||||
$val = "$($prop.Value)"
|
|
||||||
$cols = Parse-ColumnSpec $prop.Name
|
|
||||||
foreach ($c in $cols) {
|
|
||||||
$specifiedCols[[int]$c] = $true
|
|
||||||
if ($val -match '^([0-9.]+)x$') {
|
|
||||||
$totalUnits += [double]$Matches[1]
|
|
||||||
} else {
|
|
||||||
$absoluteSum += [int]$val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($c = 1; $c -le $totalColumns; $c++) {
|
|
||||||
if (-not $specifiedCols.ContainsKey($c)) {
|
|
||||||
$totalUnits += 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($totalUnits -gt 0) {
|
|
||||||
$defaultWidth = [int][math]::Round(($targetWidth - $absoluteSum) / $totalUnits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build column width map: 1-based col -> width
|
|
||||||
$colWidthMap = @{}
|
|
||||||
if ($def.columnWidths) {
|
|
||||||
foreach ($prop in $def.columnWidths.PSObject.Properties) {
|
|
||||||
$val = "$($prop.Value)"
|
|
||||||
if ($val -match '^([0-9.]+)x$') {
|
|
||||||
$width = [int][math]::Round([double]$Matches[1] * $defaultWidth)
|
|
||||||
} else {
|
|
||||||
$width = [int]$val
|
|
||||||
}
|
|
||||||
$columns = Parse-ColumnSpec $prop.Name
|
|
||||||
foreach ($c in $columns) {
|
|
||||||
$colWidthMap[$c] = $width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Style resolver ---
|
|
||||||
|
|
||||||
function Resolve-Style {
|
|
||||||
param([string]$styleName, [string]$fillType)
|
|
||||||
|
|
||||||
$fontIdx = $fontMap["default"]
|
|
||||||
$lb = -1; $tb = -1; $rb = -1; $bb = -1
|
|
||||||
$ha = ""; $va = ""; $nf = ""
|
|
||||||
$wrap = $false
|
|
||||||
|
|
||||||
if ($styleName -and $def.styles) {
|
|
||||||
$style = $def.styles.$styleName
|
|
||||||
if ($style) {
|
|
||||||
# Font
|
|
||||||
if ($style.font -and $fontMap.Contains($style.font)) {
|
|
||||||
$fontIdx = $fontMap[$style.font]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Borders
|
|
||||||
if ($style.border -and $style.border -ne "none") {
|
|
||||||
$lineIdx = if ($style.borderWidth -eq "thick") { $thickLineIndex } else { $thinLineIndex }
|
|
||||||
foreach ($side in ($style.border -split ',')) {
|
|
||||||
switch ($side.Trim()) {
|
|
||||||
"all" { $lb = $lineIdx; $tb = $lineIdx; $rb = $lineIdx; $bb = $lineIdx }
|
|
||||||
"left" { $lb = $lineIdx }
|
|
||||||
"top" { $tb = $lineIdx }
|
|
||||||
"right" { $rb = $lineIdx }
|
|
||||||
"bottom" { $bb = $lineIdx }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alignment
|
|
||||||
if ($style.align) {
|
|
||||||
switch ($style.align) {
|
|
||||||
"left" { $ha = "Left" }
|
|
||||||
"center" { $ha = "Center" }
|
|
||||||
"right" { $ha = "Right" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($style.valign) {
|
|
||||||
switch ($style.valign) {
|
|
||||||
"top" { $va = "Top" }
|
|
||||||
"center" { $va = "Center" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wrap
|
|
||||||
if ($style.wrap -eq $true) { $wrap = $true }
|
|
||||||
|
|
||||||
# Number format
|
|
||||||
if ($style.format) { $nf = $style.format }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @{
|
|
||||||
FontIdx = $fontIdx
|
|
||||||
LB = $lb; TB = $tb; RB = $rb; BB = $bb
|
|
||||||
HA = $ha; VA = $va
|
|
||||||
Wrap = $wrap
|
|
||||||
FillType = $fillType
|
|
||||||
NumberFormat = $nf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Format palette builder ---
|
|
||||||
|
|
||||||
$formatRegistry = [ordered]@{} # key -> hashtable with properties
|
|
||||||
$formatOrder = @() # ordered keys for index assignment
|
|
||||||
|
|
||||||
function Get-FormatKey {
|
|
||||||
param(
|
|
||||||
[int]$fontIdx = -1,
|
|
||||||
[int]$lb = -1, [int]$tb = -1, [int]$rb = -1, [int]$bb = -1,
|
|
||||||
[string]$ha = "", [string]$va = "",
|
|
||||||
[bool]$wrap = $false,
|
|
||||||
[string]$fillType = "",
|
|
||||||
[string]$numberFormat = "",
|
|
||||||
[int]$width = -1,
|
|
||||||
[int]$height = -1
|
|
||||||
)
|
|
||||||
return "f=$fontIdx|lb=$lb|tb=$tb|rb=$rb|bb=$bb|ha=$ha|va=$va|wr=$wrap|ft=$fillType|nf=$numberFormat|w=$width|h=$height"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Register-Format {
|
|
||||||
param([string]$key, [hashtable]$props)
|
|
||||||
if (-not $script:formatRegistry.Contains($key)) {
|
|
||||||
$script:formatRegistry[$key] = $props
|
|
||||||
$script:formatOrder += $key
|
|
||||||
}
|
|
||||||
# Return 1-based index
|
|
||||||
$idx = 0
|
|
||||||
foreach ($k in $script:formatRegistry.Keys) {
|
|
||||||
$idx++
|
|
||||||
if ($k -eq $key) { return $idx }
|
|
||||||
}
|
|
||||||
return $idx
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6a. Default width format
|
|
||||||
$defaultFormatKey = Get-FormatKey -width $defaultWidth
|
|
||||||
$defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth }
|
|
||||||
|
|
||||||
# 6b. Column width formats
|
|
||||||
$colFormatMap = @{} # 1-based col -> format index
|
|
||||||
foreach ($col in ($colWidthMap.Keys | Sort-Object)) {
|
|
||||||
$w = $colWidthMap[$col]
|
|
||||||
$key = Get-FormatKey -width $w
|
|
||||||
$idx = Register-Format -key $key -props @{ Width = $w }
|
|
||||||
$colFormatMap[[int]$col] = $idx
|
|
||||||
}
|
|
||||||
|
|
||||||
# 6c. Scan areas for row heights and cell formats
|
|
||||||
# We need to do two passes: first collect all formats, then generate XML
|
|
||||||
|
|
||||||
# Helper: escape XML special characters
|
|
||||||
function Esc-Xml {
|
|
||||||
param([string]$s)
|
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: determine fillType from cell content
|
|
||||||
function Get-FillType {
|
|
||||||
param($cell)
|
|
||||||
if ($cell.param) { return "Parameter" }
|
|
||||||
if ($cell.template) { return "Template" }
|
|
||||||
if ($cell.text) { return "Text" }
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: register a cell format and return its index
|
|
||||||
function Register-CellFormat {
|
|
||||||
param($styleName, [string]$fillType)
|
|
||||||
$resolved = Resolve-Style -styleName $styleName -fillType $fillType
|
|
||||||
$key = Get-FormatKey -fontIdx $resolved.FontIdx `
|
|
||||||
-lb $resolved.LB -tb $resolved.TB -rb $resolved.RB -bb $resolved.BB `
|
|
||||||
-ha $resolved.HA -va $resolved.VA `
|
|
||||||
-wrap $resolved.Wrap -fillType $resolved.FillType `
|
|
||||||
-numberFormat $resolved.NumberFormat
|
|
||||||
$props = @{
|
|
||||||
FontIdx = $resolved.FontIdx
|
|
||||||
LB = $resolved.LB; TB = $resolved.TB
|
|
||||||
RB = $resolved.RB; BB = $resolved.BB
|
|
||||||
HA = $resolved.HA; VA = $resolved.VA
|
|
||||||
Wrap = $resolved.Wrap
|
|
||||||
FillType = $resolved.FillType
|
|
||||||
NumberFormat = $resolved.NumberFormat
|
|
||||||
}
|
|
||||||
return Register-Format -key $key -props $props
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pre-register all formats from areas
|
|
||||||
foreach ($area in $def.areas) {
|
|
||||||
foreach ($row in $area.rows) {
|
|
||||||
# Skip empty row placeholder
|
|
||||||
if ($row.empty) { continue }
|
|
||||||
|
|
||||||
# Row height format
|
|
||||||
if ($row.height) {
|
|
||||||
$hKey = Get-FormatKey -height ([int]$row.height)
|
|
||||||
Register-Format -key $hKey -props @{ Height = [int]$row.height } | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# rowStyle gap-fill format (no content → no fillType)
|
|
||||||
if ($row.rowStyle) {
|
|
||||||
Register-CellFormat -styleName $row.rowStyle -fillType "" | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Explicit cell formats
|
|
||||||
if ($row.cells) {
|
|
||||||
foreach ($cell in $row.cells) {
|
|
||||||
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
|
|
||||||
$ft = Get-FillType $cell
|
|
||||||
Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Generate XML ---
|
|
||||||
|
|
||||||
$xml = New-Object System.Text.StringBuilder 4096
|
|
||||||
|
|
||||||
function X {
|
|
||||||
param([string]$text)
|
|
||||||
$script:xml.AppendLine($text) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7a. Header
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" 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">'
|
|
||||||
|
|
||||||
# 7b. Language settings
|
|
||||||
X "`t<languageSettings>"
|
|
||||||
X "`t`t<currentLanguage>ru</currentLanguage>"
|
|
||||||
X "`t`t<defaultLanguage>ru</defaultLanguage>"
|
|
||||||
X "`t`t<languageInfo>"
|
|
||||||
X "`t`t`t<id>ru</id>"
|
|
||||||
X "`t`t`t<code>Русский</code>"
|
|
||||||
X "`t`t`t<description>Русский</description>"
|
|
||||||
X "`t`t</languageInfo>"
|
|
||||||
X "`t</languageSettings>"
|
|
||||||
|
|
||||||
# 7c. Columns
|
|
||||||
X "`t<columns>"
|
|
||||||
X "`t`t<size>$totalColumns</size>"
|
|
||||||
|
|
||||||
# Emit columnsItem for columns with non-default widths
|
|
||||||
foreach ($col in ($colFormatMap.Keys | Sort-Object)) {
|
|
||||||
$fmtIdx = $colFormatMap[$col]
|
|
||||||
$colIdx = $col - 1 # Convert to 0-based
|
|
||||||
X "`t`t<columnsItem>"
|
|
||||||
X "`t`t`t<index>$colIdx</index>"
|
|
||||||
X "`t`t`t<column>"
|
|
||||||
X "`t`t`t`t<formatIndex>$fmtIdx</formatIndex>"
|
|
||||||
X "`t`t`t</column>"
|
|
||||||
X "`t`t</columnsItem>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t</columns>"
|
|
||||||
|
|
||||||
# 7d. Rows — main generation loop
|
|
||||||
$globalRow = 0
|
|
||||||
$merges = @()
|
|
||||||
$namedItems = @()
|
|
||||||
$totalRowCount = 0
|
|
||||||
|
|
||||||
foreach ($area in $def.areas) {
|
|
||||||
$areaStartRow = $globalRow
|
|
||||||
$areaName = $area.name
|
|
||||||
$activeRowspans = @() # @{ColStart=1-based; ColEnd=1-based; EndLocalRow=int}
|
|
||||||
$localRow = 0
|
|
||||||
|
|
||||||
foreach ($row in $area.rows) {
|
|
||||||
# Empty row placeholder: emit N empty rows
|
|
||||||
if ($row.empty) {
|
|
||||||
$count = [int]$row.empty
|
|
||||||
for ($ei = 0; $ei -lt $count; $ei++) {
|
|
||||||
X "`t<rowsItem>"
|
|
||||||
X "`t`t<index>$globalRow</index>"
|
|
||||||
X "`t`t<row>"
|
|
||||||
X "`t`t`t<empty>true</empty>"
|
|
||||||
X "`t`t</row>"
|
|
||||||
X "`t</rowsItem>"
|
|
||||||
$globalRow++; $localRow++
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build set of columns occupied by rowspans from previous rows
|
|
||||||
$rowspanOccupied = @{} # 1-based col -> $true
|
|
||||||
foreach ($rs in $activeRowspans) {
|
|
||||||
if ($localRow -gt $rs.StartLocalRow -and $localRow -le $rs.EndLocalRow) {
|
|
||||||
for ($c = $rs.ColStart; $c -le $rs.ColEnd; $c++) {
|
|
||||||
$rowspanOccupied[$c] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rowHasContent = $false
|
|
||||||
$rowCells = @() # array of { Col(0-based), FormatIdx, Content }
|
|
||||||
|
|
||||||
# Determine row height format
|
|
||||||
$rowFormatIdx = 0
|
|
||||||
if ($row.height) {
|
|
||||||
$hKey = Get-FormatKey -height ([int]$row.height)
|
|
||||||
# Find format index for this key
|
|
||||||
$rIdx = 0
|
|
||||||
foreach ($k in $formatRegistry.Keys) {
|
|
||||||
$rIdx++
|
|
||||||
if ($k -eq $hKey) { $rowFormatIdx = $rIdx; break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($row.cells -and $row.cells.Count -gt 0) {
|
|
||||||
$rowHasContent = $true
|
|
||||||
|
|
||||||
# Build set of occupied columns (1-based): explicit cells + rowspan from above
|
|
||||||
$occupiedCols = @{}
|
|
||||||
foreach ($rsk in $rowspanOccupied.Keys) { $occupiedCols[$rsk] = $true }
|
|
||||||
foreach ($cell in $row.cells) {
|
|
||||||
$colStart = [int]$cell.col
|
|
||||||
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
|
|
||||||
for ($c = $colStart; $c -lt ($colStart + $colSpan); $c++) {
|
|
||||||
$occupiedCols[$c] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate explicit cells
|
|
||||||
foreach ($cell in $row.cells) {
|
|
||||||
$colStart = [int]$cell.col
|
|
||||||
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
|
|
||||||
$rowspan = if ($cell.rowspan) { [int]$cell.rowspan } else { 1 }
|
|
||||||
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
|
|
||||||
$ft = Get-FillType $cell
|
|
||||||
$fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft
|
|
||||||
|
|
||||||
$cellInfo = @{
|
|
||||||
Col = $colStart - 1 # 0-based
|
|
||||||
FormatIdx = $fmtIdx
|
|
||||||
Param = $cell.param
|
|
||||||
Detail = $cell.detail
|
|
||||||
Text = $cell.text
|
|
||||||
Template = $cell.template
|
|
||||||
}
|
|
||||||
$rowCells += $cellInfo
|
|
||||||
|
|
||||||
# Track rowspan for subsequent rows
|
|
||||||
if ($rowspan -gt 1) {
|
|
||||||
$activeRowspans += @{
|
|
||||||
ColStart = $colStart
|
|
||||||
ColEnd = $colStart + $colSpan - 1
|
|
||||||
StartLocalRow = $localRow
|
|
||||||
EndLocalRow = $localRow + $rowspan - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect merge (horizontal, vertical, or both)
|
|
||||||
if ($colSpan -gt 1 -or $rowspan -gt 1) {
|
|
||||||
$merge = @{ R = $globalRow; C = $colStart - 1; W = $colSpan - 1 }
|
|
||||||
if ($rowspan -gt 1) { $merge.H = $rowspan - 1 }
|
|
||||||
$merges += $merge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate gap-fill cells for rowStyle
|
|
||||||
if ($row.rowStyle) {
|
|
||||||
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
|
|
||||||
for ($c = 1; $c -le $totalColumns; $c++) {
|
|
||||||
if (-not $occupiedCols.ContainsKey($c)) {
|
|
||||||
$rowCells += @{
|
|
||||||
Col = $c - 1 # 0-based
|
|
||||||
FormatIdx = $gapFmtIdx
|
|
||||||
Param = $null
|
|
||||||
Detail = $null
|
|
||||||
Text = $null
|
|
||||||
Template = $null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sort cells by column
|
|
||||||
$rowCells = $rowCells | Sort-Object { $_.Col }
|
|
||||||
|
|
||||||
} elseif ($row.rowStyle) {
|
|
||||||
# Row with only rowStyle, no explicit cells — fill non-rowspan columns
|
|
||||||
$rowHasContent = $true
|
|
||||||
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
|
|
||||||
for ($c = 1; $c -le $totalColumns; $c++) {
|
|
||||||
if ($rowspanOccupied.ContainsKey($c)) { continue }
|
|
||||||
$rowCells += @{
|
|
||||||
Col = $c - 1
|
|
||||||
FormatIdx = $gapFmtIdx
|
|
||||||
Param = $null
|
|
||||||
Detail = $null
|
|
||||||
Text = $null
|
|
||||||
Template = $null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Emit rowsItem
|
|
||||||
X "`t<rowsItem>"
|
|
||||||
X "`t`t<index>$globalRow</index>"
|
|
||||||
X "`t`t<row>"
|
|
||||||
|
|
||||||
if ($rowFormatIdx -gt 0) {
|
|
||||||
X "`t`t`t<formatIndex>$rowFormatIdx</formatIndex>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $rowHasContent) {
|
|
||||||
X "`t`t`t<empty>true</empty>"
|
|
||||||
} else {
|
|
||||||
foreach ($cellInfo in $rowCells) {
|
|
||||||
X "`t`t`t<c>"
|
|
||||||
X "`t`t`t`t<i>$($cellInfo.Col)</i>"
|
|
||||||
X "`t`t`t`t<c>"
|
|
||||||
X "`t`t`t`t`t<f>$($cellInfo.FormatIdx)</f>"
|
|
||||||
|
|
||||||
if ($cellInfo.Param) {
|
|
||||||
X "`t`t`t`t`t<parameter>$($cellInfo.Param)</parameter>"
|
|
||||||
if ($cellInfo.Detail) {
|
|
||||||
X "`t`t`t`t`t<detailParameter>$($cellInfo.Detail)</detailParameter>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cellInfo.Text) {
|
|
||||||
X "`t`t`t`t`t<tl>"
|
|
||||||
X "`t`t`t`t`t`t<v8:item>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Text)</v8:content>"
|
|
||||||
X "`t`t`t`t`t`t</v8:item>"
|
|
||||||
X "`t`t`t`t`t</tl>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cellInfo.Template) {
|
|
||||||
X "`t`t`t`t`t<tl>"
|
|
||||||
X "`t`t`t`t`t`t<v8:item>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Template)</v8:content>"
|
|
||||||
X "`t`t`t`t`t`t</v8:item>"
|
|
||||||
X "`t`t`t`t`t</tl>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t`t`t`t</c>"
|
|
||||||
X "`t`t`t</c>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t`t</row>"
|
|
||||||
X "`t</rowsItem>"
|
|
||||||
|
|
||||||
$localRow++
|
|
||||||
$globalRow++
|
|
||||||
}
|
|
||||||
|
|
||||||
$areaEndRow = $globalRow - 1
|
|
||||||
$namedItems += @{
|
|
||||||
Name = $areaName
|
|
||||||
BeginRow = $areaStartRow
|
|
||||||
EndRow = $areaEndRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalRowCount = $globalRow
|
|
||||||
|
|
||||||
# 7e. Scalar metadata
|
|
||||||
X "`t<templateMode>true</templateMode>"
|
|
||||||
X "`t<defaultFormatIndex>$defaultFormatIndex</defaultFormatIndex>"
|
|
||||||
X "`t<height>$totalRowCount</height>"
|
|
||||||
X "`t<vgRows>$totalRowCount</vgRows>"
|
|
||||||
|
|
||||||
# 7f. Merges
|
|
||||||
foreach ($m in $merges) {
|
|
||||||
X "`t<merge>"
|
|
||||||
X "`t`t<r>$($m.R)</r>"
|
|
||||||
X "`t`t<c>$($m.C)</c>"
|
|
||||||
if ($m.H) { X "`t`t<h>$($m.H)</h>" }
|
|
||||||
X "`t`t<w>$($m.W)</w>"
|
|
||||||
X "`t</merge>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7g. Named items
|
|
||||||
foreach ($ni in $namedItems) {
|
|
||||||
X "`t<namedItem xsi:type=`"NamedItemCells`">"
|
|
||||||
X "`t`t<name>$($ni.Name)</name>"
|
|
||||||
X "`t`t<area>"
|
|
||||||
X "`t`t`t<type>Rows</type>"
|
|
||||||
X "`t`t`t<beginRow>$($ni.BeginRow)</beginRow>"
|
|
||||||
X "`t`t`t<endRow>$($ni.EndRow)</endRow>"
|
|
||||||
X "`t`t`t<beginColumn>-1</beginColumn>"
|
|
||||||
X "`t`t`t<endColumn>-1</endColumn>"
|
|
||||||
X "`t`t</area>"
|
|
||||||
X "`t</namedItem>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7h. Line palette
|
|
||||||
if ($hasThinBorders) {
|
|
||||||
X "`t<line width=`"1`" gap=`"false`">"
|
|
||||||
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
|
|
||||||
X "`t</line>"
|
|
||||||
}
|
|
||||||
if ($hasThickBorders) {
|
|
||||||
X "`t<line width=`"2`" gap=`"false`">"
|
|
||||||
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
|
|
||||||
X "`t</line>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7i. Font palette
|
|
||||||
foreach ($fe in $fontEntries) {
|
|
||||||
X "`t<font faceName=`"$($fe.Face)`" height=`"$($fe.Size)`" bold=`"$($fe.Bold)`" italic=`"$($fe.Italic)`" underline=`"$($fe.Underline)`" strikeout=`"$($fe.Strikeout)`" kind=`"Absolute`" scale=`"100`"/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7j. Format palette
|
|
||||||
foreach ($key in $formatRegistry.Keys) {
|
|
||||||
$fmt = $formatRegistry[$key]
|
|
||||||
X "`t<format>"
|
|
||||||
|
|
||||||
if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) {
|
|
||||||
X "`t`t<font>$($fmt.FontIdx)</font>"
|
|
||||||
}
|
|
||||||
if ($fmt.LB -ne $null -and $fmt.LB -ge 0) {
|
|
||||||
X "`t`t<leftBorder>$($fmt.LB)</leftBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.TB -ne $null -and $fmt.TB -ge 0) {
|
|
||||||
X "`t`t<topBorder>$($fmt.TB)</topBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.RB -ne $null -and $fmt.RB -ge 0) {
|
|
||||||
X "`t`t<rightBorder>$($fmt.RB)</rightBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.BB -ne $null -and $fmt.BB -ge 0) {
|
|
||||||
X "`t`t<bottomBorder>$($fmt.BB)</bottomBorder>"
|
|
||||||
}
|
|
||||||
if ($fmt.Width) {
|
|
||||||
X "`t`t<width>$($fmt.Width)</width>"
|
|
||||||
}
|
|
||||||
if ($fmt.Height) {
|
|
||||||
X "`t`t<height>$($fmt.Height)</height>"
|
|
||||||
}
|
|
||||||
if ($fmt.HA) {
|
|
||||||
X "`t`t<horizontalAlignment>$($fmt.HA)</horizontalAlignment>"
|
|
||||||
}
|
|
||||||
if ($fmt.VA) {
|
|
||||||
X "`t`t<verticalAlignment>$($fmt.VA)</verticalAlignment>"
|
|
||||||
}
|
|
||||||
if ($fmt.Wrap -eq $true) {
|
|
||||||
X "`t`t<textPlacement>Wrap</textPlacement>"
|
|
||||||
}
|
|
||||||
if ($fmt.FillType) {
|
|
||||||
X "`t`t<fillType>$($fmt.FillType)</fillType>"
|
|
||||||
}
|
|
||||||
if ($fmt.NumberFormat) {
|
|
||||||
X "`t`t<format>"
|
|
||||||
X "`t`t`t<v8:item>"
|
|
||||||
X "`t`t`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "`t`t`t`t<v8:content>$(Esc-Xml $fmt.NumberFormat)</v8:content>"
|
|
||||||
X "`t`t`t</v8:item>"
|
|
||||||
X "`t`t</format>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t</format>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7k. Close document
|
|
||||||
X '</document>'
|
|
||||||
|
|
||||||
# --- 8. Write output ---
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
|
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString(), $enc)
|
|
||||||
|
|
||||||
# --- 9. Summary ---
|
|
||||||
|
|
||||||
Write-Host "[OK] Compiled: $OutputPath"
|
|
||||||
if ($def.page) {
|
|
||||||
Write-Host " Page: $pageName -> target $targetWidth, defaultWidth=$defaultWidth"
|
|
||||||
}
|
|
||||||
Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns"
|
|
||||||
Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)"
|
|
||||||
Write-Host " Merges: $($merges.Count)"
|
|
||||||
@@ -1,636 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
||||||
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False)
|
|
||||||
parser.add_argument('-JsonPath', type=str, required=True)
|
|
||||||
parser.add_argument('-OutputPath', type=str, required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
json_path = args.JsonPath
|
|
||||||
if not os.path.exists(json_path):
|
|
||||||
print(f"File not found: {json_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
defn = json.load(f)
|
|
||||||
|
|
||||||
if not defn.get('columns'):
|
|
||||||
print("Required field 'columns' is missing", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not defn.get('areas'):
|
|
||||||
print("Required field 'areas' is missing", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
total_columns = int(defn['columns'])
|
|
||||||
default_width = int(defn['defaultWidth']) if defn.get('defaultWidth') else 10
|
|
||||||
|
|
||||||
# --- 2. Build font palette ---
|
|
||||||
font_map = {} # name -> 0-based index
|
|
||||||
font_entries = [] # list of dicts
|
|
||||||
|
|
||||||
def add_font(name, font_def):
|
|
||||||
face = font_def.get('face', 'Arial') if font_def else 'Arial'
|
|
||||||
size = int(font_def.get('size', 10)) if font_def else 10
|
|
||||||
bold = 'true' if font_def and font_def.get('bold') is True else 'false'
|
|
||||||
italic = 'true' if font_def and font_def.get('italic') is True else 'false'
|
|
||||||
underline = 'true' if font_def and font_def.get('underline') is True else 'false'
|
|
||||||
strikeout = 'true' if font_def and font_def.get('strikeout') is True else 'false'
|
|
||||||
|
|
||||||
idx = len(font_entries)
|
|
||||||
font_map[name] = idx
|
|
||||||
font_entries.append({
|
|
||||||
'Face': face,
|
|
||||||
'Size': size,
|
|
||||||
'Bold': bold,
|
|
||||||
'Italic': italic,
|
|
||||||
'Underline': underline,
|
|
||||||
'Strikeout': strikeout,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Add user-defined fonts
|
|
||||||
has_default = False
|
|
||||||
if defn.get('fonts'):
|
|
||||||
for fname, fdef in defn['fonts'].items():
|
|
||||||
if fname == 'default':
|
|
||||||
has_default = True
|
|
||||||
add_font(fname, fdef)
|
|
||||||
|
|
||||||
# Ensure default font exists
|
|
||||||
if not has_default:
|
|
||||||
add_font('default', {'face': 'Arial', 'size': 10})
|
|
||||||
|
|
||||||
# --- 3. Determine line palette ---
|
|
||||||
has_thin_borders = False
|
|
||||||
has_thick_borders = False
|
|
||||||
|
|
||||||
if defn.get('styles'):
|
|
||||||
for sname, sval in defn['styles'].items():
|
|
||||||
if sval.get('border') and sval['border'] != 'none':
|
|
||||||
if sval.get('borderWidth') == 'thick':
|
|
||||||
has_thick_borders = True
|
|
||||||
else:
|
|
||||||
has_thin_borders = True
|
|
||||||
|
|
||||||
thin_line_index = -1
|
|
||||||
thick_line_index = -1
|
|
||||||
line_count = 0
|
|
||||||
if has_thin_borders:
|
|
||||||
thin_line_index = line_count
|
|
||||||
line_count += 1
|
|
||||||
if has_thick_borders:
|
|
||||||
thick_line_index = line_count
|
|
||||||
line_count += 1
|
|
||||||
|
|
||||||
# --- 4. Parse column width specs ---
|
|
||||||
def parse_column_spec(spec):
|
|
||||||
cols = []
|
|
||||||
for part in spec.split(','):
|
|
||||||
part = part.strip()
|
|
||||||
m = re.match(r'^(\d+)-(\d+)$', part)
|
|
||||||
if m:
|
|
||||||
from_col = int(m.group(1))
|
|
||||||
to_col = int(m.group(2))
|
|
||||||
for i in range(from_col, to_col + 1):
|
|
||||||
cols.append(i)
|
|
||||||
else:
|
|
||||||
cols.append(int(part))
|
|
||||||
return cols
|
|
||||||
|
|
||||||
# --- 4a. Auto-calculate defaultWidth from page format ---
|
|
||||||
page_targets = {
|
|
||||||
'A4-landscape': 780,
|
|
||||||
'A4-portrait': 540,
|
|
||||||
}
|
|
||||||
|
|
||||||
page_name = None
|
|
||||||
target_width = None
|
|
||||||
if defn.get('page'):
|
|
||||||
page_name = str(defn['page'])
|
|
||||||
|
|
||||||
if re.match(r'^\d+$', page_name):
|
|
||||||
target_width = int(page_name)
|
|
||||||
elif page_name in page_targets:
|
|
||||||
target_width = page_targets[page_name]
|
|
||||||
else:
|
|
||||||
print(f"WARNING: Unknown page format '{page_name}'. Known: {', '.join(page_targets.keys())}, or a number.", file=sys.stderr)
|
|
||||||
|
|
||||||
if target_width:
|
|
||||||
total_units = 0.0
|
|
||||||
absolute_sum = 0
|
|
||||||
specified_cols = {}
|
|
||||||
|
|
||||||
if defn.get('columnWidths'):
|
|
||||||
for prop_name, prop_value in defn['columnWidths'].items():
|
|
||||||
val = str(prop_value)
|
|
||||||
cols = parse_column_spec(prop_name)
|
|
||||||
for c in cols:
|
|
||||||
specified_cols[int(c)] = True
|
|
||||||
m = re.match(r'^([0-9.]+)x$', val)
|
|
||||||
if m:
|
|
||||||
total_units += float(m.group(1))
|
|
||||||
else:
|
|
||||||
absolute_sum += int(val)
|
|
||||||
|
|
||||||
for c in range(1, total_columns + 1):
|
|
||||||
if c not in specified_cols:
|
|
||||||
total_units += 1.0
|
|
||||||
|
|
||||||
if total_units > 0:
|
|
||||||
default_width = round((target_width - absolute_sum) / total_units)
|
|
||||||
|
|
||||||
# Build column width map: 1-based col -> width
|
|
||||||
col_width_map = {}
|
|
||||||
if defn.get('columnWidths'):
|
|
||||||
for prop_name, prop_value in defn['columnWidths'].items():
|
|
||||||
val = str(prop_value)
|
|
||||||
m = re.match(r'^([0-9.]+)x$', val)
|
|
||||||
if m:
|
|
||||||
width = round(float(m.group(1)) * default_width)
|
|
||||||
else:
|
|
||||||
width = int(val)
|
|
||||||
columns = parse_column_spec(prop_name)
|
|
||||||
for c in columns:
|
|
||||||
col_width_map[c] = width
|
|
||||||
|
|
||||||
# --- 5. Style resolver ---
|
|
||||||
def resolve_style(style_name, fill_type):
|
|
||||||
font_idx = font_map.get('default', 0)
|
|
||||||
lb = -1; tb = -1; rb = -1; bb = -1
|
|
||||||
ha = ''; va = ''; nf = ''
|
|
||||||
wrap = False
|
|
||||||
|
|
||||||
if style_name and defn.get('styles'):
|
|
||||||
style = defn['styles'].get(style_name)
|
|
||||||
if style:
|
|
||||||
# Font
|
|
||||||
if style.get('font') and style['font'] in font_map:
|
|
||||||
font_idx = font_map[style['font']]
|
|
||||||
|
|
||||||
# Borders
|
|
||||||
if style.get('border') and style['border'] != 'none':
|
|
||||||
line_idx = thick_line_index if style.get('borderWidth') == 'thick' else thin_line_index
|
|
||||||
for side in style['border'].split(','):
|
|
||||||
side = side.strip()
|
|
||||||
if side == 'all':
|
|
||||||
lb = line_idx; tb = line_idx; rb = line_idx; bb = line_idx
|
|
||||||
elif side == 'left':
|
|
||||||
lb = line_idx
|
|
||||||
elif side == 'top':
|
|
||||||
tb = line_idx
|
|
||||||
elif side == 'right':
|
|
||||||
rb = line_idx
|
|
||||||
elif side == 'bottom':
|
|
||||||
bb = line_idx
|
|
||||||
|
|
||||||
# Alignment
|
|
||||||
if style.get('align'):
|
|
||||||
align_map = {'left': 'Left', 'center': 'Center', 'right': 'Right'}
|
|
||||||
ha = align_map.get(style['align'], '')
|
|
||||||
if style.get('valign'):
|
|
||||||
valign_map = {'top': 'Top', 'center': 'Center'}
|
|
||||||
va = valign_map.get(style['valign'], '')
|
|
||||||
|
|
||||||
# Wrap
|
|
||||||
if style.get('wrap') is True:
|
|
||||||
wrap = True
|
|
||||||
|
|
||||||
# Number format
|
|
||||||
if style.get('format'):
|
|
||||||
nf = style['format']
|
|
||||||
|
|
||||||
return {
|
|
||||||
'FontIdx': font_idx,
|
|
||||||
'LB': lb, 'TB': tb, 'RB': rb, 'BB': bb,
|
|
||||||
'HA': ha, 'VA': va,
|
|
||||||
'Wrap': wrap,
|
|
||||||
'FillType': fill_type,
|
|
||||||
'NumberFormat': nf,
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Format palette builder ---
|
|
||||||
format_registry = {} # key -> props
|
|
||||||
format_order = [] # ordered keys for index assignment
|
|
||||||
|
|
||||||
def get_format_key(font_idx=-1, lb=-1, tb=-1, rb=-1, bb=-1, ha='', va='',
|
|
||||||
wrap=False, fill_type='', number_format='', width=-1, height=-1):
|
|
||||||
return f'f={font_idx}|lb={lb}|tb={tb}|rb={rb}|bb={bb}|ha={ha}|va={va}|wr={wrap}|ft={fill_type}|nf={number_format}|w={width}|h={height}'
|
|
||||||
|
|
||||||
def register_format(key, props):
|
|
||||||
if key not in format_registry:
|
|
||||||
format_registry[key] = props
|
|
||||||
format_order.append(key)
|
|
||||||
# Return 1-based index
|
|
||||||
return format_order.index(key) + 1
|
|
||||||
|
|
||||||
# 6a. Default width format
|
|
||||||
default_format_key = get_format_key(width=default_width)
|
|
||||||
default_format_index = register_format(default_format_key, {'Width': default_width})
|
|
||||||
|
|
||||||
# 6b. Column width formats
|
|
||||||
col_format_map = {} # 1-based col -> format index
|
|
||||||
for col in sorted(col_width_map):
|
|
||||||
w = col_width_map[col]
|
|
||||||
key = get_format_key(width=w)
|
|
||||||
idx = register_format(key, {'Width': w})
|
|
||||||
col_format_map[int(col)] = idx
|
|
||||||
|
|
||||||
# 6c. Helper: determine fillType from cell content
|
|
||||||
def get_fill_type(cell):
|
|
||||||
if cell.get('param'):
|
|
||||||
return 'Parameter'
|
|
||||||
if cell.get('template'):
|
|
||||||
return 'Template'
|
|
||||||
if cell.get('text'):
|
|
||||||
return 'Text'
|
|
||||||
return ''
|
|
||||||
|
|
||||||
# Helper: register a cell format and return its index
|
|
||||||
def register_cell_format(style_name, fill_type):
|
|
||||||
resolved = resolve_style(style_name, fill_type)
|
|
||||||
key = get_format_key(
|
|
||||||
font_idx=resolved['FontIdx'],
|
|
||||||
lb=resolved['LB'], tb=resolved['TB'], rb=resolved['RB'], bb=resolved['BB'],
|
|
||||||
ha=resolved['HA'], va=resolved['VA'],
|
|
||||||
wrap=resolved['Wrap'], fill_type=resolved['FillType'],
|
|
||||||
number_format=resolved['NumberFormat'])
|
|
||||||
props = {
|
|
||||||
'FontIdx': resolved['FontIdx'],
|
|
||||||
'LB': resolved['LB'], 'TB': resolved['TB'],
|
|
||||||
'RB': resolved['RB'], 'BB': resolved['BB'],
|
|
||||||
'HA': resolved['HA'], 'VA': resolved['VA'],
|
|
||||||
'Wrap': resolved['Wrap'],
|
|
||||||
'FillType': resolved['FillType'],
|
|
||||||
'NumberFormat': resolved['NumberFormat'],
|
|
||||||
}
|
|
||||||
return register_format(key, props)
|
|
||||||
|
|
||||||
# Pre-register all formats from areas
|
|
||||||
for area in defn['areas']:
|
|
||||||
for row in area.get('rows', []):
|
|
||||||
# Skip list-of-values shorthand rows (treated as empty rows like PS1)
|
|
||||||
if isinstance(row, list):
|
|
||||||
continue
|
|
||||||
# Skip empty row placeholder
|
|
||||||
if row.get('empty'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Row height format
|
|
||||||
if row.get('height'):
|
|
||||||
h_key = get_format_key(height=int(row['height']))
|
|
||||||
register_format(h_key, {'Height': int(row['height'])})
|
|
||||||
|
|
||||||
# rowStyle gap-fill format
|
|
||||||
if row.get('rowStyle'):
|
|
||||||
register_cell_format(row['rowStyle'], '')
|
|
||||||
|
|
||||||
# Explicit cell formats
|
|
||||||
if row.get('cells'):
|
|
||||||
for cell in row['cells']:
|
|
||||||
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
|
|
||||||
ft = get_fill_type(cell)
|
|
||||||
register_cell_format(cell_style, ft)
|
|
||||||
|
|
||||||
# --- 7. Generate XML ---
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# 7a. Header
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" 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">')
|
|
||||||
|
|
||||||
# 7b. Language settings
|
|
||||||
lines.append('\t<languageSettings>')
|
|
||||||
lines.append('\t\t<currentLanguage>ru</currentLanguage>')
|
|
||||||
lines.append('\t\t<defaultLanguage>ru</defaultLanguage>')
|
|
||||||
lines.append('\t\t<languageInfo>')
|
|
||||||
lines.append('\t\t\t<id>ru</id>')
|
|
||||||
lines.append('\t\t\t<code>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</code>')
|
|
||||||
lines.append('\t\t\t<description>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</description>')
|
|
||||||
lines.append('\t\t</languageInfo>')
|
|
||||||
lines.append('\t</languageSettings>')
|
|
||||||
|
|
||||||
# 7c. Columns
|
|
||||||
lines.append('\t<columns>')
|
|
||||||
lines.append(f'\t\t<size>{total_columns}</size>')
|
|
||||||
|
|
||||||
# Emit columnsItem for columns with non-default widths
|
|
||||||
for col in sorted(col_format_map.keys()):
|
|
||||||
fmt_idx = col_format_map[col]
|
|
||||||
col_idx = col - 1 # Convert to 0-based
|
|
||||||
lines.append('\t\t<columnsItem>')
|
|
||||||
lines.append(f'\t\t\t<index>{col_idx}</index>')
|
|
||||||
lines.append('\t\t\t<column>')
|
|
||||||
lines.append(f'\t\t\t\t<formatIndex>{fmt_idx}</formatIndex>')
|
|
||||||
lines.append('\t\t\t</column>')
|
|
||||||
lines.append('\t\t</columnsItem>')
|
|
||||||
|
|
||||||
lines.append('\t</columns>')
|
|
||||||
|
|
||||||
# 7d. Rows -- main generation loop
|
|
||||||
global_row = 0
|
|
||||||
merges = []
|
|
||||||
named_items = []
|
|
||||||
active_rowspans = [] # list of {ColStart, ColEnd, StartLocalRow, EndLocalRow}
|
|
||||||
|
|
||||||
for area in defn['areas']:
|
|
||||||
area_start_row = global_row
|
|
||||||
area_name = area.get('name', '')
|
|
||||||
active_rowspans = []
|
|
||||||
local_row = 0
|
|
||||||
|
|
||||||
for row in area.get('rows', []):
|
|
||||||
# List-of-values shorthand: treat as row with no properties (like PS1)
|
|
||||||
if isinstance(row, list):
|
|
||||||
row = {}
|
|
||||||
# Empty row placeholder: emit N empty rows
|
|
||||||
if row.get('empty'):
|
|
||||||
count = int(row['empty'])
|
|
||||||
for ei in range(count):
|
|
||||||
lines.append('\t<rowsItem>')
|
|
||||||
lines.append(f'\t\t<index>{global_row}</index>')
|
|
||||||
lines.append('\t\t<row>')
|
|
||||||
lines.append('\t\t\t<empty>true</empty>')
|
|
||||||
lines.append('\t\t</row>')
|
|
||||||
lines.append('\t</rowsItem>')
|
|
||||||
global_row += 1
|
|
||||||
local_row += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Build set of columns occupied by rowspans from previous rows
|
|
||||||
rowspan_occupied = {}
|
|
||||||
for rs in active_rowspans:
|
|
||||||
if local_row > rs['StartLocalRow'] and local_row <= rs['EndLocalRow']:
|
|
||||||
for c in range(rs['ColStart'], rs['ColEnd'] + 1):
|
|
||||||
rowspan_occupied[c] = True
|
|
||||||
|
|
||||||
row_has_content = False
|
|
||||||
row_cells = []
|
|
||||||
|
|
||||||
# Determine row height format
|
|
||||||
row_format_idx = 0
|
|
||||||
if row.get('height'):
|
|
||||||
h_key = get_format_key(height=int(row['height']))
|
|
||||||
if h_key in format_registry:
|
|
||||||
row_format_idx = format_order.index(h_key) + 1
|
|
||||||
|
|
||||||
if row.get('cells') and len(row['cells']) > 0:
|
|
||||||
row_has_content = True
|
|
||||||
|
|
||||||
# Build set of occupied columns (1-based)
|
|
||||||
occupied_cols = dict(rowspan_occupied)
|
|
||||||
for cell in row['cells']:
|
|
||||||
col_start = int(cell['col'])
|
|
||||||
col_span = int(cell.get('span', 1))
|
|
||||||
for c in range(col_start, col_start + col_span):
|
|
||||||
occupied_cols[c] = True
|
|
||||||
|
|
||||||
# Generate explicit cells
|
|
||||||
for cell in row['cells']:
|
|
||||||
col_start = int(cell['col'])
|
|
||||||
col_span = int(cell.get('span', 1))
|
|
||||||
rowspan = int(cell.get('rowspan', 1))
|
|
||||||
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
|
|
||||||
ft = get_fill_type(cell)
|
|
||||||
fmt_idx = register_cell_format(cell_style, ft)
|
|
||||||
|
|
||||||
cell_info = {
|
|
||||||
'Col': col_start - 1, # 0-based
|
|
||||||
'FormatIdx': fmt_idx,
|
|
||||||
'Param': cell.get('param'),
|
|
||||||
'Detail': cell.get('detail'),
|
|
||||||
'Text': cell.get('text'),
|
|
||||||
'Template': cell.get('template'),
|
|
||||||
}
|
|
||||||
row_cells.append(cell_info)
|
|
||||||
|
|
||||||
# Track rowspan for subsequent rows
|
|
||||||
if rowspan > 1:
|
|
||||||
active_rowspans.append({
|
|
||||||
'ColStart': col_start,
|
|
||||||
'ColEnd': col_start + col_span - 1,
|
|
||||||
'StartLocalRow': local_row,
|
|
||||||
'EndLocalRow': local_row + rowspan - 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Collect merge
|
|
||||||
if col_span > 1 or rowspan > 1:
|
|
||||||
merge = {'R': global_row, 'C': col_start - 1, 'W': col_span - 1}
|
|
||||||
if rowspan > 1:
|
|
||||||
merge['H'] = rowspan - 1
|
|
||||||
merges.append(merge)
|
|
||||||
|
|
||||||
# Generate gap-fill cells for rowStyle
|
|
||||||
if row.get('rowStyle'):
|
|
||||||
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
|
|
||||||
for c in range(1, total_columns + 1):
|
|
||||||
if c not in occupied_cols:
|
|
||||||
row_cells.append({
|
|
||||||
'Col': c - 1,
|
|
||||||
'FormatIdx': gap_fmt_idx,
|
|
||||||
'Param': None,
|
|
||||||
'Detail': None,
|
|
||||||
'Text': None,
|
|
||||||
'Template': None,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort cells by column
|
|
||||||
row_cells.sort(key=lambda x: x['Col'])
|
|
||||||
|
|
||||||
elif row.get('rowStyle'):
|
|
||||||
# Row with only rowStyle, no explicit cells
|
|
||||||
row_has_content = True
|
|
||||||
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
|
|
||||||
for c in range(1, total_columns + 1):
|
|
||||||
if c in rowspan_occupied:
|
|
||||||
continue
|
|
||||||
row_cells.append({
|
|
||||||
'Col': c - 1,
|
|
||||||
'FormatIdx': gap_fmt_idx,
|
|
||||||
'Param': None,
|
|
||||||
'Detail': None,
|
|
||||||
'Text': None,
|
|
||||||
'Template': None,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Emit rowsItem
|
|
||||||
lines.append('\t<rowsItem>')
|
|
||||||
lines.append(f'\t\t<index>{global_row}</index>')
|
|
||||||
lines.append('\t\t<row>')
|
|
||||||
|
|
||||||
if row_format_idx > 0:
|
|
||||||
lines.append(f'\t\t\t<formatIndex>{row_format_idx}</formatIndex>')
|
|
||||||
|
|
||||||
if not row_has_content:
|
|
||||||
lines.append('\t\t\t<empty>true</empty>')
|
|
||||||
else:
|
|
||||||
for cell_info in row_cells:
|
|
||||||
lines.append('\t\t\t<c>')
|
|
||||||
lines.append(f'\t\t\t\t<i>{cell_info["Col"]}</i>')
|
|
||||||
lines.append('\t\t\t\t<c>')
|
|
||||||
lines.append(f'\t\t\t\t\t<f>{cell_info["FormatIdx"]}</f>')
|
|
||||||
|
|
||||||
if cell_info['Param']:
|
|
||||||
lines.append(f'\t\t\t\t\t<parameter>{cell_info["Param"]}</parameter>')
|
|
||||||
if cell_info['Detail']:
|
|
||||||
lines.append(f'\t\t\t\t\t<detailParameter>{cell_info["Detail"]}</detailParameter>')
|
|
||||||
|
|
||||||
if cell_info['Text']:
|
|
||||||
lines.append('\t\t\t\t\t<tl>')
|
|
||||||
lines.append('\t\t\t\t\t\t<v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Text"])}</v8:content>')
|
|
||||||
lines.append('\t\t\t\t\t\t</v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t</tl>')
|
|
||||||
|
|
||||||
if cell_info['Template']:
|
|
||||||
lines.append('\t\t\t\t\t<tl>')
|
|
||||||
lines.append('\t\t\t\t\t\t<v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Template"])}</v8:content>')
|
|
||||||
lines.append('\t\t\t\t\t\t</v8:item>')
|
|
||||||
lines.append('\t\t\t\t\t</tl>')
|
|
||||||
|
|
||||||
lines.append('\t\t\t\t</c>')
|
|
||||||
lines.append('\t\t\t</c>')
|
|
||||||
|
|
||||||
lines.append('\t\t</row>')
|
|
||||||
lines.append('\t</rowsItem>')
|
|
||||||
|
|
||||||
local_row += 1
|
|
||||||
global_row += 1
|
|
||||||
|
|
||||||
area_end_row = global_row - 1
|
|
||||||
named_items.append({
|
|
||||||
'Name': area_name,
|
|
||||||
'BeginRow': area_start_row,
|
|
||||||
'EndRow': area_end_row,
|
|
||||||
})
|
|
||||||
|
|
||||||
total_row_count = global_row
|
|
||||||
|
|
||||||
# 7e. Scalar metadata
|
|
||||||
lines.append(f'\t<templateMode>true</templateMode>')
|
|
||||||
lines.append(f'\t<defaultFormatIndex>{default_format_index}</defaultFormatIndex>')
|
|
||||||
lines.append(f'\t<height>{total_row_count}</height>')
|
|
||||||
lines.append(f'\t<vgRows>{total_row_count}</vgRows>')
|
|
||||||
|
|
||||||
# 7f. Merges
|
|
||||||
for m in merges:
|
|
||||||
lines.append('\t<merge>')
|
|
||||||
lines.append(f'\t\t<r>{m["R"]}</r>')
|
|
||||||
lines.append(f'\t\t<c>{m["C"]}</c>')
|
|
||||||
if m.get('H'):
|
|
||||||
lines.append(f'\t\t<h>{m["H"]}</h>')
|
|
||||||
lines.append(f'\t\t<w>{m["W"]}</w>')
|
|
||||||
lines.append('\t</merge>')
|
|
||||||
|
|
||||||
# 7g. Named items
|
|
||||||
for ni in named_items:
|
|
||||||
lines.append('\t<namedItem xsi:type="NamedItemCells">')
|
|
||||||
lines.append(f'\t\t<name>{ni["Name"]}</name>')
|
|
||||||
lines.append('\t\t<area>')
|
|
||||||
lines.append('\t\t\t<type>Rows</type>')
|
|
||||||
lines.append(f'\t\t\t<beginRow>{ni["BeginRow"]}</beginRow>')
|
|
||||||
lines.append(f'\t\t\t<endRow>{ni["EndRow"]}</endRow>')
|
|
||||||
lines.append('\t\t\t<beginColumn>-1</beginColumn>')
|
|
||||||
lines.append('\t\t\t<endColumn>-1</endColumn>')
|
|
||||||
lines.append('\t\t</area>')
|
|
||||||
lines.append('\t</namedItem>')
|
|
||||||
|
|
||||||
# 7h. Line palette
|
|
||||||
if has_thin_borders:
|
|
||||||
lines.append('\t<line width="1" gap="false">')
|
|
||||||
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
|
|
||||||
lines.append('\t</line>')
|
|
||||||
if has_thick_borders:
|
|
||||||
lines.append('\t<line width="2" gap="false">')
|
|
||||||
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
|
|
||||||
lines.append('\t</line>')
|
|
||||||
|
|
||||||
# 7i. Font palette
|
|
||||||
for fe in font_entries:
|
|
||||||
lines.append(f'\t<font faceName="{fe["Face"]}" height="{fe["Size"]}" bold="{fe["Bold"]}" italic="{fe["Italic"]}" underline="{fe["Underline"]}" strikeout="{fe["Strikeout"]}" kind="Absolute" scale="100"/>')
|
|
||||||
|
|
||||||
# 7j. Format palette
|
|
||||||
for key in format_order:
|
|
||||||
fmt = format_registry[key]
|
|
||||||
lines.append('\t<format>')
|
|
||||||
|
|
||||||
if fmt.get('FontIdx') is not None and fmt.get('FontIdx', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<font>{fmt["FontIdx"]}</font>')
|
|
||||||
if fmt.get('LB') is not None and fmt.get('LB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<leftBorder>{fmt["LB"]}</leftBorder>')
|
|
||||||
if fmt.get('TB') is not None and fmt.get('TB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<topBorder>{fmt["TB"]}</topBorder>')
|
|
||||||
if fmt.get('RB') is not None and fmt.get('RB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<rightBorder>{fmt["RB"]}</rightBorder>')
|
|
||||||
if fmt.get('BB') is not None and fmt.get('BB', -1) >= 0:
|
|
||||||
lines.append(f'\t\t<bottomBorder>{fmt["BB"]}</bottomBorder>')
|
|
||||||
if fmt.get('Width'):
|
|
||||||
lines.append(f'\t\t<width>{fmt["Width"]}</width>')
|
|
||||||
if fmt.get('Height'):
|
|
||||||
lines.append(f'\t\t<height>{fmt["Height"]}</height>')
|
|
||||||
if fmt.get('HA'):
|
|
||||||
lines.append(f'\t\t<horizontalAlignment>{fmt["HA"]}</horizontalAlignment>')
|
|
||||||
if fmt.get('VA'):
|
|
||||||
lines.append(f'\t\t<verticalAlignment>{fmt["VA"]}</verticalAlignment>')
|
|
||||||
if fmt.get('Wrap') is True:
|
|
||||||
lines.append('\t\t<textPlacement>Wrap</textPlacement>')
|
|
||||||
if fmt.get('FillType'):
|
|
||||||
lines.append(f'\t\t<fillType>{fmt["FillType"]}</fillType>')
|
|
||||||
if fmt.get('NumberFormat'):
|
|
||||||
lines.append('\t\t<format>')
|
|
||||||
lines.append('\t\t\t<v8:item>')
|
|
||||||
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f'\t\t\t\t<v8:content>{esc_xml(fmt["NumberFormat"])}</v8:content>')
|
|
||||||
lines.append('\t\t\t</v8:item>')
|
|
||||||
lines.append('\t\t</format>')
|
|
||||||
|
|
||||||
lines.append('\t</format>')
|
|
||||||
|
|
||||||
# 7k. Close document
|
|
||||||
lines.append('</document>')
|
|
||||||
|
|
||||||
# --- 8. Write output ---
|
|
||||||
out_path = args.OutputPath
|
|
||||||
if not os.path.isabs(out_path):
|
|
||||||
out_path = os.path.join(os.getcwd(), out_path)
|
|
||||||
|
|
||||||
out_dir = os.path.dirname(out_path)
|
|
||||||
if out_dir and not os.path.exists(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
content = '\n'.join(lines) + '\n'
|
|
||||||
write_utf8_bom(out_path, content)
|
|
||||||
|
|
||||||
# --- 9. Summary ---
|
|
||||||
print(f"[OK] Compiled: {args.OutputPath}")
|
|
||||||
if defn.get('page'):
|
|
||||||
print(f" Page: {page_name} -> target {target_width}, defaultWidth={default_width}")
|
|
||||||
print(f" Areas: {len(named_items)}, Rows: {total_row_count}, Columns: {total_columns}")
|
|
||||||
print(f" Fonts: {len(font_entries)}, Lines: {line_count}, Formats: {len(format_registry)}")
|
|
||||||
print(f" Merges: {len(merges)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -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,645 +0,0 @@
|
|||||||
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$TemplatePath,
|
|
||||||
|
|
||||||
[string]$OutputPath
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load and parse XML ---
|
|
||||||
|
|
||||||
if (-not (Test-Path $TemplatePath)) {
|
|
||||||
Write-Error "File not found: $TemplatePath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $false
|
|
||||||
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)
|
|
||||||
|
|
||||||
$root = $xmlDoc.DocumentElement
|
|
||||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$ns.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet")
|
|
||||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
|
||||||
$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui")
|
|
||||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
|
||||||
|
|
||||||
# --- 2. Extract font palette ---
|
|
||||||
|
|
||||||
$rawFonts = @()
|
|
||||||
foreach ($fNode in $root.SelectNodes("d:font", $ns)) {
|
|
||||||
$rawFonts += @{
|
|
||||||
Face = $fNode.GetAttribute("faceName")
|
|
||||||
Size = [int]$fNode.GetAttribute("height")
|
|
||||||
Bold = $fNode.GetAttribute("bold") -eq "true"
|
|
||||||
Italic = $fNode.GetAttribute("italic") -eq "true"
|
|
||||||
Underline = $fNode.GetAttribute("underline") -eq "true"
|
|
||||||
Strikeout = $fNode.GetAttribute("strikeout") -eq "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Extract line palette ---
|
|
||||||
|
|
||||||
$rawLines = @()
|
|
||||||
foreach ($lNode in $root.SelectNodes("d:line", $ns)) {
|
|
||||||
$rawLines += @{ Width = [int]$lNode.GetAttribute("width") }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Extract format palette ---
|
|
||||||
|
|
||||||
$rawFormats = @()
|
|
||||||
foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) {
|
|
||||||
$fmt = @{
|
|
||||||
FontIdx = -1
|
|
||||||
LB = -1; TB = -1; RB = -1; BB = -1
|
|
||||||
Width = 0; Height = 0
|
|
||||||
HA = ""; VA = ""
|
|
||||||
Wrap = $false; FillType = ""; DataFormat = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:font", $ns)
|
|
||||||
if ($n) { $fmt.FontIdx = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:leftBorder", $ns)
|
|
||||||
if ($n) { $fmt.LB = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:topBorder", $ns)
|
|
||||||
if ($n) { $fmt.TB = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:rightBorder", $ns)
|
|
||||||
if ($n) { $fmt.RB = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns)
|
|
||||||
if ($n) { $fmt.BB = [int]$n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:width", $ns)
|
|
||||||
if ($n) { $fmt.Width = [int]$n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:height", $ns)
|
|
||||||
if ($n) { $fmt.Height = [int]$n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns)
|
|
||||||
if ($n) { $fmt.HA = $n.InnerText }
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns)
|
|
||||||
if ($n) { $fmt.VA = $n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:textPlacement", $ns)
|
|
||||||
if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:fillType", $ns)
|
|
||||||
if ($n) { $fmt.FillType = $n.InnerText }
|
|
||||||
|
|
||||||
$n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns)
|
|
||||||
if ($n) { $fmt.DataFormat = $n.InnerText }
|
|
||||||
|
|
||||||
$rawFormats += $fmt
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-Format {
|
|
||||||
param([int]$idx)
|
|
||||||
if ($idx -le 0 -or $idx -gt $rawFormats.Count) { return $null }
|
|
||||||
return $rawFormats[$idx - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Extract columns and default width ---
|
|
||||||
|
|
||||||
$colNode = $root.SelectSingleNode("d:columns", $ns)
|
|
||||||
$totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText
|
|
||||||
|
|
||||||
$colFormatIndices = @{}
|
|
||||||
foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) {
|
|
||||||
$colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText
|
|
||||||
$fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText
|
|
||||||
$colFormatIndices[$colIdx] = $fmtIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultFmtIdx = 0
|
|
||||||
$n = $root.SelectSingleNode("d:defaultFormatIndex", $ns)
|
|
||||||
if ($n) { $defaultFmtIdx = [int]$n.InnerText }
|
|
||||||
|
|
||||||
$defaultWidth = 10
|
|
||||||
if ($defaultFmtIdx -gt 0) {
|
|
||||||
$defFmt = Get-Format $defaultFmtIdx
|
|
||||||
if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build column width map (1-based col → width), only non-default
|
|
||||||
$colWidthMap = [ordered]@{}
|
|
||||||
foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) {
|
|
||||||
$fmt = Get-Format $colFormatIndices[$col0]
|
|
||||||
if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) {
|
|
||||||
$col1 = [string]($col0 + 1)
|
|
||||||
$colWidthMap.Add($col1, $fmt.Width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Extract merges ---
|
|
||||||
|
|
||||||
$mergeMap = @{}
|
|
||||||
foreach ($mNode in $root.SelectNodes("d:merge", $ns)) {
|
|
||||||
$r = [int]$mNode.SelectSingleNode("d:r", $ns).InnerText
|
|
||||||
$c = [int]$mNode.SelectSingleNode("d:c", $ns).InnerText
|
|
||||||
$w = [int]$mNode.SelectSingleNode("d:w", $ns).InnerText
|
|
||||||
$hNode = $mNode.SelectSingleNode("d:h", $ns)
|
|
||||||
$h = if ($hNode) { [int]$hNode.InnerText } else { 0 }
|
|
||||||
$mergeMap["$r,$c"] = @{ W = $w; H = $h }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Extract named items ---
|
|
||||||
|
|
||||||
$namedAreas = @()
|
|
||||||
foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) {
|
|
||||||
$xsiType = $niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
|
||||||
if ($xsiType -ne "NamedItemCells") { continue }
|
|
||||||
|
|
||||||
$areaNode = $niNode.SelectSingleNode("d:area", $ns)
|
|
||||||
$areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText
|
|
||||||
if ($areaType -ne "Rows") { continue }
|
|
||||||
|
|
||||||
$namedAreas += @{
|
|
||||||
Name = $niNode.SelectSingleNode("d:name", $ns).InnerText
|
|
||||||
BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText
|
|
||||||
EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 8. Extract rows ---
|
|
||||||
|
|
||||||
$rowData = @{}
|
|
||||||
foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) {
|
|
||||||
$rowIdx = [int]$riNode.SelectSingleNode("d:index", $ns).InnerText
|
|
||||||
$rowNode = $riNode.SelectSingleNode("d:row", $ns)
|
|
||||||
|
|
||||||
$indexTo = $rowIdx
|
|
||||||
$itNode = $riNode.SelectSingleNode("d:indexTo", $ns)
|
|
||||||
if ($itNode) { $indexTo = [int]$itNode.InnerText }
|
|
||||||
|
|
||||||
$rowFmtIdx = 0
|
|
||||||
$fmtNode = $rowNode.SelectSingleNode("d:formatIndex", $ns)
|
|
||||||
if ($fmtNode) { $rowFmtIdx = [int]$fmtNode.InnerText }
|
|
||||||
|
|
||||||
$isEmpty = $false
|
|
||||||
$emptyNode = $rowNode.SelectSingleNode("d:empty", $ns)
|
|
||||||
if ($emptyNode -and $emptyNode.InnerText -eq "true") { $isEmpty = $true }
|
|
||||||
|
|
||||||
$cells = @()
|
|
||||||
if (-not $isEmpty) {
|
|
||||||
$col = -1
|
|
||||||
foreach ($cGroup in $rowNode.SelectNodes("d:c", $ns)) {
|
|
||||||
$iNode = $cGroup.SelectSingleNode("d:i", $ns)
|
|
||||||
if ($iNode) { $col = [int]$iNode.InnerText }
|
|
||||||
else { $col++ }
|
|
||||||
|
|
||||||
$cContent = $cGroup.SelectSingleNode("d:c", $ns)
|
|
||||||
if (-not $cContent) { continue }
|
|
||||||
|
|
||||||
$cellFmtIdx = 0
|
|
||||||
$fNode = $cContent.SelectSingleNode("d:f", $ns)
|
|
||||||
if ($fNode) { $cellFmtIdx = [int]$fNode.InnerText }
|
|
||||||
|
|
||||||
$param = $null
|
|
||||||
$pNode = $cContent.SelectSingleNode("d:parameter", $ns)
|
|
||||||
if ($pNode) { $param = $pNode.InnerText }
|
|
||||||
|
|
||||||
$detail = $null
|
|
||||||
$dNode = $cContent.SelectSingleNode("d:detailParameter", $ns)
|
|
||||||
if ($dNode) { $detail = $dNode.InnerText }
|
|
||||||
|
|
||||||
$text = $null
|
|
||||||
$tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns)
|
|
||||||
if ($tNode) { $text = $tNode.InnerText }
|
|
||||||
|
|
||||||
$cells += @{
|
|
||||||
Col = $col
|
|
||||||
FormatIdx = $cellFmtIdx
|
|
||||||
Param = $param
|
|
||||||
Detail = $detail
|
|
||||||
Text = $text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($r = $rowIdx; $r -le $indexTo; $r++) {
|
|
||||||
$rowData[$r] = @{
|
|
||||||
FormatIdx = $rowFmtIdx
|
|
||||||
Cells = $cells
|
|
||||||
Empty = $isEmpty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 9. Build style key (ignoring fillType) ---
|
|
||||||
|
|
||||||
function Get-BorderDesc {
|
|
||||||
param($fmt)
|
|
||||||
if (-not $fmt) { return @{ Border = "none"; Thick = $false } }
|
|
||||||
|
|
||||||
$lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0
|
|
||||||
$rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0
|
|
||||||
|
|
||||||
if (-not $lb -and -not $tb -and -not $rb -and -not $bb) {
|
|
||||||
return @{ Border = "none"; Thick = $false }
|
|
||||||
}
|
|
||||||
|
|
||||||
$thick = $false
|
|
||||||
foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) {
|
|
||||||
if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) {
|
|
||||||
$thick = $true; break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lb -and $tb -and $rb -and $bb) {
|
|
||||||
return @{ Border = "all"; Thick = $thick }
|
|
||||||
}
|
|
||||||
|
|
||||||
$sides = @()
|
|
||||||
if ($tb) { $sides += "top" }
|
|
||||||
if ($bb) { $sides += "bottom" }
|
|
||||||
if ($lb) { $sides += "left" }
|
|
||||||
if ($rb) { $sides += "right" }
|
|
||||||
|
|
||||||
return @{ Border = ($sides -join ","); Thick = $thick }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-StyleKey {
|
|
||||||
param($fmt)
|
|
||||||
if (-not $fmt) { return "empty" }
|
|
||||||
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
|
|
||||||
$bd = Get-BorderDesc $fmt
|
|
||||||
return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 10. Name fonts ---
|
|
||||||
|
|
||||||
$fontNames = @{}
|
|
||||||
$fontDefs = [ordered]@{}
|
|
||||||
|
|
||||||
if ($rawFonts.Count -gt 0) {
|
|
||||||
$fontNames[0] = "default"
|
|
||||||
$fontDefs["default"] = $rawFonts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-FontKey {
|
|
||||||
param($f)
|
|
||||||
return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$fontKeyMap = @{}
|
|
||||||
$fontKeyMap[(Get-FontKey $rawFonts[0])] = "default"
|
|
||||||
|
|
||||||
for ($i = 1; $i -lt $rawFonts.Count; $i++) {
|
|
||||||
$f = $rawFonts[$i]
|
|
||||||
$df = $rawFonts[0]
|
|
||||||
|
|
||||||
# Dedup: if identical font already named, reuse
|
|
||||||
$fKey = Get-FontKey $f
|
|
||||||
if ($fontKeyMap.ContainsKey($fKey)) {
|
|
||||||
$fontNames[$i] = $fontKeyMap[$fKey]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = $null
|
|
||||||
|
|
||||||
if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) {
|
|
||||||
if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) {
|
|
||||||
$name = "bold"
|
|
||||||
} elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) {
|
|
||||||
$name = "italic"
|
|
||||||
} elseif ($f.Underline -and -not $df.Underline -and -not $f.Bold -and -not $f.Italic) {
|
|
||||||
$name = "underline"
|
|
||||||
}
|
|
||||||
} elseif ($f.Face -eq $df.Face -and $f.Size -gt $df.Size -and $f.Bold) {
|
|
||||||
$name = "header"
|
|
||||||
} elseif ($f.Face -eq $df.Face -and $f.Size -lt $df.Size) {
|
|
||||||
$name = "small"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $name) {
|
|
||||||
$parts = @()
|
|
||||||
if ($f.Face -and $f.Face -ne $df.Face) { $parts += $f.Face.ToLower() }
|
|
||||||
$parts += "$($f.Size)"
|
|
||||||
if ($f.Bold) { $parts += "bold" }
|
|
||||||
if ($f.Italic) { $parts += "italic" }
|
|
||||||
if ($f.Underline) { $parts += "underline" }
|
|
||||||
if ($f.Strikeout) { $parts += "strikeout" }
|
|
||||||
$name = $parts -join "-"
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseName = $name; $suffix = 2
|
|
||||||
while ($fontDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ }
|
|
||||||
|
|
||||||
$fontNames[$i] = $name
|
|
||||||
$fontDefs[$name] = $f
|
|
||||||
$fontKeyMap[$fKey] = $name
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 11. Collect and name styles ---
|
|
||||||
|
|
||||||
$styleKeys = [ordered]@{}
|
|
||||||
$formatToStyleKey = @{}
|
|
||||||
|
|
||||||
foreach ($r in $rowData.Values) {
|
|
||||||
foreach ($cell in $r.Cells) {
|
|
||||||
$fmt = Get-Format $cell.FormatIdx
|
|
||||||
if (-not $fmt) { continue }
|
|
||||||
$key = Get-StyleKey $fmt
|
|
||||||
if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt }
|
|
||||||
$formatToStyleKey[$cell.FormatIdx] = $key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Name-Style {
|
|
||||||
param($fmt)
|
|
||||||
if (-not $fmt) { return "default" }
|
|
||||||
$parts = @()
|
|
||||||
|
|
||||||
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
|
|
||||||
if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") {
|
|
||||||
$parts += $fontNames[$fi]
|
|
||||||
}
|
|
||||||
|
|
||||||
$bd = Get-BorderDesc $fmt
|
|
||||||
if ($bd.Border -ne "none") {
|
|
||||||
if ($bd.Border -eq "all") { $parts += "bordered" }
|
|
||||||
else { $parts += "border-$($bd.Border)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fmt.HA -eq "Center") { $parts += "center" }
|
|
||||||
elseif ($fmt.HA -eq "Right") { $parts += "right" }
|
|
||||||
if ($fmt.VA -eq "Center") { $parts += "vcenter" }
|
|
||||||
elseif ($fmt.VA -eq "Top") { $parts += "vtop" }
|
|
||||||
if ($fmt.Wrap) { $parts += "wrap" }
|
|
||||||
if ($fmt.DataFormat) { $parts += "fmt" }
|
|
||||||
|
|
||||||
if ($parts.Count -eq 0) { return "default" }
|
|
||||||
return ($parts -join "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
$styleNames = [ordered]@{}
|
|
||||||
$styleDefs = [ordered]@{}
|
|
||||||
|
|
||||||
foreach ($key in $styleKeys.Keys) {
|
|
||||||
$fmt = $styleKeys[$key]
|
|
||||||
$name = Name-Style $fmt
|
|
||||||
|
|
||||||
$baseName = $name; $suffix = 2
|
|
||||||
while ($styleDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ }
|
|
||||||
|
|
||||||
$styleNames[$key] = $name
|
|
||||||
|
|
||||||
$sDef = [ordered]@{}
|
|
||||||
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
|
|
||||||
if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") {
|
|
||||||
$sDef["font"] = $fontNames[$fi]
|
|
||||||
}
|
|
||||||
if ($fmt.HA) {
|
|
||||||
$a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } }
|
|
||||||
if ($a) { $sDef["align"] = $a }
|
|
||||||
}
|
|
||||||
if ($fmt.VA) {
|
|
||||||
$a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } }
|
|
||||||
if ($a) { $sDef["valign"] = $a }
|
|
||||||
}
|
|
||||||
$bd = Get-BorderDesc $fmt
|
|
||||||
if ($bd.Border -ne "none") {
|
|
||||||
$sDef["border"] = $bd.Border
|
|
||||||
if ($bd.Thick) { $sDef["borderWidth"] = "thick" }
|
|
||||||
}
|
|
||||||
if ($fmt.Wrap) { $sDef["wrap"] = $true }
|
|
||||||
if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat }
|
|
||||||
|
|
||||||
$styleDefs[$name] = $sDef
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-StyleName {
|
|
||||||
param([int]$fmtIdx)
|
|
||||||
$key = $formatToStyleKey[$fmtIdx]
|
|
||||||
if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] }
|
|
||||||
return "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 12. Build areas ---
|
|
||||||
|
|
||||||
$dslAreas = @()
|
|
||||||
|
|
||||||
foreach ($area in $namedAreas) {
|
|
||||||
$areaRows = @()
|
|
||||||
|
|
||||||
for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) {
|
|
||||||
$rd = $rowData[$globalRow]
|
|
||||||
|
|
||||||
if (-not $rd -or $rd.Empty) {
|
|
||||||
$areaRows += [ordered]@{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$dslRow = [ordered]@{}
|
|
||||||
|
|
||||||
# Row height
|
|
||||||
if ($rd.FormatIdx -gt 0) {
|
|
||||||
$rowFmt = Get-Format $rd.FormatIdx
|
|
||||||
if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Separate content cells from gap-fill cells
|
|
||||||
$contentCells = @()
|
|
||||||
$gapCells = @()
|
|
||||||
|
|
||||||
foreach ($cell in $rd.Cells) {
|
|
||||||
$hasContent = $cell.Param -or $cell.Text
|
|
||||||
$hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)")
|
|
||||||
|
|
||||||
if ($hasContent -or $hasMerge) {
|
|
||||||
$contentCells += $cell
|
|
||||||
} else {
|
|
||||||
$gapCells += $cell
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect rowStyle
|
|
||||||
$rowStyleName = $null
|
|
||||||
$rowStyleKey = $null
|
|
||||||
|
|
||||||
if ($gapCells.Count -gt 0) {
|
|
||||||
$gapKeys = @{}
|
|
||||||
foreach ($gc in $gapCells) {
|
|
||||||
$fmt = Get-Format $gc.FormatIdx
|
|
||||||
$gapKeys[(Get-StyleKey $fmt)] = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gapKeys.Count -eq 1) {
|
|
||||||
$rowStyleKey = @($gapKeys.Keys)[0]
|
|
||||||
if ($styleNames.Contains($rowStyleKey)) {
|
|
||||||
$rowStyleName = $styleNames[$rowStyleKey]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName }
|
|
||||||
|
|
||||||
# Build cell list
|
|
||||||
$dslCells = @()
|
|
||||||
|
|
||||||
foreach ($cell in ($contentCells | Sort-Object { $_.Col })) {
|
|
||||||
$dslCell = [ordered]@{ col = $cell.Col + 1 }
|
|
||||||
|
|
||||||
# Span/rowspan from merge
|
|
||||||
$mk = "$globalRow,$($cell.Col)"
|
|
||||||
if ($mergeMap.ContainsKey($mk)) {
|
|
||||||
$m = $mergeMap[$mk]
|
|
||||||
if ($m.W -gt 0) { $dslCell["span"] = $m.W + 1 }
|
|
||||||
if ($m.H -gt 0) { $dslCell["rowspan"] = $m.H + 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Style
|
|
||||||
$cellFmt = Get-Format $cell.FormatIdx
|
|
||||||
$cellStyleKey = Get-StyleKey $cellFmt
|
|
||||||
|
|
||||||
if ($rowStyleKey -and $cellStyleKey -eq $rowStyleKey) {
|
|
||||||
# Inherits rowStyle
|
|
||||||
} else {
|
|
||||||
$sn = Get-StyleName $cell.FormatIdx
|
|
||||||
if ($sn -ne "default" -or -not $rowStyleName) {
|
|
||||||
$dslCell["style"] = $sn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Content
|
|
||||||
$fillType = if ($cellFmt) { $cellFmt.FillType } else { "" }
|
|
||||||
|
|
||||||
if ($cell.Param) {
|
|
||||||
$dslCell["param"] = $cell.Param
|
|
||||||
if ($cell.Detail) { $dslCell["detail"] = $cell.Detail }
|
|
||||||
} elseif ($fillType -eq "Template" -and $cell.Text) {
|
|
||||||
$dslCell["template"] = $cell.Text
|
|
||||||
} elseif ($cell.Text) {
|
|
||||||
$dslCell["text"] = $cell.Text
|
|
||||||
}
|
|
||||||
|
|
||||||
$dslCells += $dslCell
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells }
|
|
||||||
$areaRows += $dslRow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Compress consecutive empty rows ({}) into { empty = N }
|
|
||||||
$compressedRows = @()
|
|
||||||
$emptyRun = 0
|
|
||||||
foreach ($r in $areaRows) {
|
|
||||||
if ($r.Count -eq 0) {
|
|
||||||
$emptyRun++
|
|
||||||
} else {
|
|
||||||
if ($emptyRun -gt 0) {
|
|
||||||
if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} }
|
|
||||||
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
|
|
||||||
$emptyRun = 0
|
|
||||||
}
|
|
||||||
$compressedRows += $r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($emptyRun -gt 0) {
|
|
||||||
if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} }
|
|
||||||
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
|
|
||||||
}
|
|
||||||
|
|
||||||
$dslAreas += [ordered]@{
|
|
||||||
name = $area.Name
|
|
||||||
rows = [array]$compressedRows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 13. Compress columnWidths ---
|
|
||||||
|
|
||||||
$compressedWidths = [ordered]@{}
|
|
||||||
if ($colWidthMap.Count -gt 0) {
|
|
||||||
$grouped = $colWidthMap.Keys | Group-Object { $colWidthMap[$_] }
|
|
||||||
foreach ($g in $grouped) {
|
|
||||||
$width = [int]$g.Name
|
|
||||||
$cols = @($g.Group | Sort-Object { [int]$_ })
|
|
||||||
|
|
||||||
$ranges = @()
|
|
||||||
$rangeStart = $cols[0]; $rangePrev = $cols[0]
|
|
||||||
|
|
||||||
for ($i = 1; $i -lt $cols.Count; $i++) {
|
|
||||||
if ([int]$cols[$i] -eq [int]$rangePrev + 1) {
|
|
||||||
$rangePrev = $cols[$i]
|
|
||||||
} else {
|
|
||||||
if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" }
|
|
||||||
else { $ranges += "$rangeStart-$rangePrev" }
|
|
||||||
$rangeStart = $cols[$i]; $rangePrev = $cols[$i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" }
|
|
||||||
else { $ranges += "$rangeStart-$rangePrev" }
|
|
||||||
|
|
||||||
foreach ($range in $ranges) { $compressedWidths[$range] = $width }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 14. Build fonts output ---
|
|
||||||
|
|
||||||
$fontsOut = [ordered]@{}
|
|
||||||
foreach ($name in $fontDefs.Keys) {
|
|
||||||
$f = $fontDefs[$name]
|
|
||||||
$fOut = [ordered]@{ face = $f.Face; size = $f.Size }
|
|
||||||
if ($f.Bold) { $fOut["bold"] = $true }
|
|
||||||
if ($f.Italic) { $fOut["italic"] = $true }
|
|
||||||
if ($f.Underline) { $fOut["underline"] = $true }
|
|
||||||
if ($f.Strikeout) { $fOut["strikeout"] = $true }
|
|
||||||
$fontsOut[$name] = $fOut
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 15. Assemble result ---
|
|
||||||
|
|
||||||
$result = [ordered]@{
|
|
||||||
columns = $totalColumns
|
|
||||||
defaultWidth = $defaultWidth
|
|
||||||
}
|
|
||||||
if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths }
|
|
||||||
# Remove empty "default" style
|
|
||||||
if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) {
|
|
||||||
$styleDefs.Remove("default")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove unused styles
|
|
||||||
$usedStyles = @{}
|
|
||||||
foreach ($a in $dslAreas) {
|
|
||||||
foreach ($r in $a.rows) {
|
|
||||||
if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true }
|
|
||||||
if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) })
|
|
||||||
foreach ($s in $toRemove) { $styleDefs.Remove($s)
|
|
||||||
}
|
|
||||||
|
|
||||||
$result["fonts"] = $fontsOut
|
|
||||||
$result["styles"] = $styleDefs
|
|
||||||
$result["areas"] = [array]$dslAreas
|
|
||||||
|
|
||||||
# --- 16. Convert to JSON and fix Unicode ---
|
|
||||||
|
|
||||||
$json = $result | ConvertTo-Json -Depth 10
|
|
||||||
|
|
||||||
# PS 5.1 escapes non-ASCII as \uXXXX — unescape back to UTF-8
|
|
||||||
$json = [regex]::Replace($json, '\\u([0-9A-Fa-f]{4})', {
|
|
||||||
param($m)
|
|
||||||
[char][int]("0x" + $m.Groups[1].Value)
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 17. Output ---
|
|
||||||
|
|
||||||
if ($OutputPath) {
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
||||||
[System.IO.File]::WriteAllText(
|
|
||||||
(Join-Path (Get-Location) $OutputPath),
|
|
||||||
$json,
|
|
||||||
$enc
|
|
||||||
)
|
|
||||||
Write-Host "[OK] Decompiled: $OutputPath"
|
|
||||||
} else {
|
|
||||||
Write-Output $json
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host " Areas: $($namedAreas.Count), Rows: $($rowData.Count), Columns: $totalColumns" -ForegroundColor DarkGray
|
|
||||||
Write-Host " Fonts: $($fontDefs.Count), Styles: $($styleDefs.Count), Merges: $($mergeMap.Count)" -ForegroundColor DarkGray
|
|
||||||
@@ -1,705 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from collections import OrderedDict
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
# --- Namespace map ---
|
|
||||||
|
|
||||||
NSMAP = {
|
|
||||||
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
|
|
||||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
|
||||||
"v8ui": "http://v8.1c.ru/8.1/data/ui",
|
|
||||||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
||||||
}
|
|
||||||
|
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
|
|
||||||
|
|
||||||
def find(node, xpath):
|
|
||||||
return node.find(xpath, NSMAP)
|
|
||||||
|
|
||||||
|
|
||||||
def findall(node, xpath):
|
|
||||||
return node.findall(xpath, NSMAP)
|
|
||||||
|
|
||||||
|
|
||||||
def text_of(node):
|
|
||||||
if node is not None and node.text:
|
|
||||||
return node.text
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def int_of(node, default=0):
|
|
||||||
if node is not None and node.text:
|
|
||||||
return int(node.text)
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
# --- Main ---
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
|
|
||||||
parser.add_argument("-TemplatePath", required=True, help="Path to Template.xml")
|
|
||||||
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
template_path = args.TemplatePath
|
|
||||||
output_path = args.OutputPath
|
|
||||||
|
|
||||||
# --- 1. Load and parse XML ---
|
|
||||||
|
|
||||||
if not os.path.isfile(template_path):
|
|
||||||
print(f"File not found: {template_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(template_path, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# --- 2. Extract font palette ---
|
|
||||||
|
|
||||||
raw_fonts = []
|
|
||||||
for f_node in findall(root, "d:font"):
|
|
||||||
raw_fonts.append({
|
|
||||||
"Face": f_node.get("faceName", ""),
|
|
||||||
"Size": int(f_node.get("height", "0")),
|
|
||||||
"Bold": f_node.get("bold") == "true",
|
|
||||||
"Italic": f_node.get("italic") == "true",
|
|
||||||
"Underline": f_node.get("underline") == "true",
|
|
||||||
"Strikeout": f_node.get("strikeout") == "true",
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 3. Extract line palette ---
|
|
||||||
|
|
||||||
raw_lines = []
|
|
||||||
for l_node in findall(root, "d:line"):
|
|
||||||
raw_lines.append({"Width": int(l_node.get("width", "0"))})
|
|
||||||
|
|
||||||
# --- 4. Extract format palette ---
|
|
||||||
|
|
||||||
raw_formats = []
|
|
||||||
for fmt_node in findall(root, "d:format"):
|
|
||||||
fmt = {
|
|
||||||
"FontIdx": -1,
|
|
||||||
"LB": -1, "TB": -1, "RB": -1, "BB": -1,
|
|
||||||
"Width": 0, "Height": 0,
|
|
||||||
"HA": "", "VA": "",
|
|
||||||
"Wrap": False, "FillType": "", "DataFormat": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:font")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["FontIdx"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:leftBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["LB"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:topBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["TB"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:rightBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["RB"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:bottomBorder")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["BB"] = int(n.text)
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:width")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["Width"] = int(n.text)
|
|
||||||
n = find(fmt_node, "d:height")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["Height"] = int(n.text)
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:horizontalAlignment")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["HA"] = n.text
|
|
||||||
n = find(fmt_node, "d:verticalAlignment")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["VA"] = n.text
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:textPlacement")
|
|
||||||
if n is not None and n.text == "Wrap":
|
|
||||||
fmt["Wrap"] = True
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:fillType")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["FillType"] = n.text
|
|
||||||
|
|
||||||
n = find(fmt_node, "d:format/v8:item/v8:content")
|
|
||||||
if n is not None and n.text:
|
|
||||||
fmt["DataFormat"] = n.text
|
|
||||||
|
|
||||||
raw_formats.append(fmt)
|
|
||||||
|
|
||||||
def get_format(idx):
|
|
||||||
if idx <= 0 or idx > len(raw_formats):
|
|
||||||
return None
|
|
||||||
return raw_formats[idx - 1]
|
|
||||||
|
|
||||||
# --- 5. Extract columns and default width ---
|
|
||||||
|
|
||||||
col_node = find(root, "d:columns")
|
|
||||||
total_columns = int_of(find(col_node, "d:size"))
|
|
||||||
|
|
||||||
col_format_indices = {}
|
|
||||||
for ci in findall(col_node, "d:columnsItem"):
|
|
||||||
col_idx = int_of(find(ci, "d:index"))
|
|
||||||
fmt_idx = int_of(find(ci, "d:column/d:formatIndex"))
|
|
||||||
col_format_indices[col_idx] = fmt_idx
|
|
||||||
|
|
||||||
default_fmt_idx = 0
|
|
||||||
n = find(root, "d:defaultFormatIndex")
|
|
||||||
if n is not None and n.text:
|
|
||||||
default_fmt_idx = int(n.text)
|
|
||||||
|
|
||||||
default_width = 10
|
|
||||||
if default_fmt_idx > 0:
|
|
||||||
def_fmt = get_format(default_fmt_idx)
|
|
||||||
if def_fmt and def_fmt["Width"] > 0:
|
|
||||||
default_width = def_fmt["Width"]
|
|
||||||
|
|
||||||
# Build column width map (1-based col -> width), only non-default
|
|
||||||
col_width_map = OrderedDict()
|
|
||||||
for col0 in sorted(col_format_indices.keys()):
|
|
||||||
fmt = get_format(col_format_indices[col0])
|
|
||||||
if fmt and fmt["Width"] > 0 and fmt["Width"] != default_width:
|
|
||||||
col1 = str(col0 + 1)
|
|
||||||
col_width_map[col1] = fmt["Width"]
|
|
||||||
|
|
||||||
# --- 6. Extract merges ---
|
|
||||||
|
|
||||||
merge_map = {}
|
|
||||||
for m_node in findall(root, "d:merge"):
|
|
||||||
r = int_of(find(m_node, "d:r"))
|
|
||||||
c = int_of(find(m_node, "d:c"))
|
|
||||||
w = int_of(find(m_node, "d:w"))
|
|
||||||
h_node = find(m_node, "d:h")
|
|
||||||
h = int_of(h_node) if h_node is not None else 0
|
|
||||||
merge_map[f"{r},{c}"] = {"W": w, "H": h}
|
|
||||||
|
|
||||||
# --- 7. Extract named items ---
|
|
||||||
|
|
||||||
named_areas = []
|
|
||||||
for ni_node in findall(root, "d:namedItem"):
|
|
||||||
xsi_type = ni_node.get(f"{{{XSI_NS}}}type", "")
|
|
||||||
if xsi_type != "NamedItemCells":
|
|
||||||
continue
|
|
||||||
|
|
||||||
area_node = find(ni_node, "d:area")
|
|
||||||
area_type_node = find(area_node, "d:type")
|
|
||||||
area_type = text_of(area_type_node) or ""
|
|
||||||
if area_type != "Rows":
|
|
||||||
continue
|
|
||||||
|
|
||||||
named_areas.append({
|
|
||||||
"Name": text_of(find(ni_node, "d:name")) or "",
|
|
||||||
"BeginRow": int_of(find(area_node, "d:beginRow")),
|
|
||||||
"EndRow": int_of(find(area_node, "d:endRow")),
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 8. Extract rows ---
|
|
||||||
|
|
||||||
row_data = {}
|
|
||||||
for ri_node in findall(root, "d:rowsItem"):
|
|
||||||
row_idx = int_of(find(ri_node, "d:index"))
|
|
||||||
row_node = find(ri_node, "d:row")
|
|
||||||
|
|
||||||
index_to = row_idx
|
|
||||||
it_node = find(ri_node, "d:indexTo")
|
|
||||||
if it_node is not None and it_node.text:
|
|
||||||
index_to = int(it_node.text)
|
|
||||||
|
|
||||||
row_fmt_idx = 0
|
|
||||||
fmt_node = find(row_node, "d:formatIndex")
|
|
||||||
if fmt_node is not None and fmt_node.text:
|
|
||||||
row_fmt_idx = int(fmt_node.text)
|
|
||||||
|
|
||||||
is_empty = False
|
|
||||||
empty_node = find(row_node, "d:empty")
|
|
||||||
if empty_node is not None and empty_node.text == "true":
|
|
||||||
is_empty = True
|
|
||||||
|
|
||||||
cells = []
|
|
||||||
if not is_empty:
|
|
||||||
col = -1
|
|
||||||
for c_group in findall(row_node, "d:c"):
|
|
||||||
i_node = find(c_group, "d:i")
|
|
||||||
if i_node is not None and i_node.text:
|
|
||||||
col = int(i_node.text)
|
|
||||||
else:
|
|
||||||
col += 1
|
|
||||||
|
|
||||||
c_content = find(c_group, "d:c")
|
|
||||||
if c_content is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
cell_fmt_idx = 0
|
|
||||||
f_node = find(c_content, "d:f")
|
|
||||||
if f_node is not None and f_node.text:
|
|
||||||
cell_fmt_idx = int(f_node.text)
|
|
||||||
|
|
||||||
param = None
|
|
||||||
p_node = find(c_content, "d:parameter")
|
|
||||||
if p_node is not None and p_node.text:
|
|
||||||
param = p_node.text
|
|
||||||
|
|
||||||
detail = None
|
|
||||||
d_node = find(c_content, "d:detailParameter")
|
|
||||||
if d_node is not None and d_node.text:
|
|
||||||
detail = d_node.text
|
|
||||||
|
|
||||||
text = None
|
|
||||||
t_node = find(c_content, "d:tl/v8:item/v8:content")
|
|
||||||
if t_node is not None and t_node.text:
|
|
||||||
text = t_node.text
|
|
||||||
|
|
||||||
cells.append({
|
|
||||||
"Col": col,
|
|
||||||
"FormatIdx": cell_fmt_idx,
|
|
||||||
"Param": param,
|
|
||||||
"Detail": detail,
|
|
||||||
"Text": text,
|
|
||||||
})
|
|
||||||
|
|
||||||
for r in range(row_idx, index_to + 1):
|
|
||||||
row_data[r] = {
|
|
||||||
"FormatIdx": row_fmt_idx,
|
|
||||||
"Cells": cells,
|
|
||||||
"Empty": is_empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 9. Build style key (ignoring fillType) ---
|
|
||||||
|
|
||||||
def get_border_desc(fmt):
|
|
||||||
if not fmt:
|
|
||||||
return {"Border": "none", "Thick": False}
|
|
||||||
|
|
||||||
lb = fmt["LB"] >= 0
|
|
||||||
tb = fmt["TB"] >= 0
|
|
||||||
rb = fmt["RB"] >= 0
|
|
||||||
bb = fmt["BB"] >= 0
|
|
||||||
|
|
||||||
if not lb and not tb and not rb and not bb:
|
|
||||||
return {"Border": "none", "Thick": False}
|
|
||||||
|
|
||||||
thick = False
|
|
||||||
for b_idx in [fmt["LB"], fmt["TB"], fmt["RB"], fmt["BB"]]:
|
|
||||||
if b_idx >= 0 and b_idx < len(raw_lines) and raw_lines[b_idx]["Width"] >= 2:
|
|
||||||
thick = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if lb and tb and rb and bb:
|
|
||||||
return {"Border": "all", "Thick": thick}
|
|
||||||
|
|
||||||
sides = []
|
|
||||||
if tb:
|
|
||||||
sides.append("top")
|
|
||||||
if bb:
|
|
||||||
sides.append("bottom")
|
|
||||||
if lb:
|
|
||||||
sides.append("left")
|
|
||||||
if rb:
|
|
||||||
sides.append("right")
|
|
||||||
|
|
||||||
return {"Border": ",".join(sides), "Thick": thick}
|
|
||||||
|
|
||||||
def get_style_key(fmt):
|
|
||||||
if not fmt:
|
|
||||||
return "empty"
|
|
||||||
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
|
|
||||||
bd = get_border_desc(fmt)
|
|
||||||
return f"f={fi}|b={bd['Border']}|bw={bd['Thick']}|ha={fmt['HA']}|va={fmt['VA']}|wr={fmt['Wrap']}|df={fmt['DataFormat']}"
|
|
||||||
|
|
||||||
# --- 10. Name fonts ---
|
|
||||||
|
|
||||||
font_names = {}
|
|
||||||
font_defs = OrderedDict()
|
|
||||||
|
|
||||||
if len(raw_fonts) > 0:
|
|
||||||
font_names[0] = "default"
|
|
||||||
font_defs["default"] = raw_fonts[0]
|
|
||||||
|
|
||||||
def get_font_key(f):
|
|
||||||
return f"{f['Face']}|{f['Size']}|{f['Bold']}|{f['Italic']}|{f['Underline']}|{f['Strikeout']}"
|
|
||||||
|
|
||||||
font_key_map = {}
|
|
||||||
if len(raw_fonts) > 0:
|
|
||||||
font_key_map[get_font_key(raw_fonts[0])] = "default"
|
|
||||||
|
|
||||||
for i in range(1, len(raw_fonts)):
|
|
||||||
f = raw_fonts[i]
|
|
||||||
df = raw_fonts[0]
|
|
||||||
|
|
||||||
# Dedup: if identical font already named, reuse
|
|
||||||
f_key = get_font_key(f)
|
|
||||||
if f_key in font_key_map:
|
|
||||||
font_names[i] = font_key_map[f_key]
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = None
|
|
||||||
|
|
||||||
if f["Face"] == df["Face"] and f["Size"] == df["Size"]:
|
|
||||||
if f["Bold"] and not df["Bold"] and not f["Italic"] and not f["Underline"] and not f["Strikeout"]:
|
|
||||||
name = "bold"
|
|
||||||
elif f["Italic"] and not df["Italic"] and not f["Bold"]:
|
|
||||||
name = "italic"
|
|
||||||
elif f["Underline"] and not df["Underline"] and not f["Bold"] and not f["Italic"]:
|
|
||||||
name = "underline"
|
|
||||||
elif f["Face"] == df["Face"] and f["Size"] > df["Size"] and f["Bold"]:
|
|
||||||
name = "header"
|
|
||||||
elif f["Face"] == df["Face"] and f["Size"] < df["Size"]:
|
|
||||||
name = "small"
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
parts = []
|
|
||||||
if f["Face"] and f["Face"] != df["Face"]:
|
|
||||||
parts.append(f["Face"].lower())
|
|
||||||
parts.append(str(f["Size"]))
|
|
||||||
if f["Bold"]:
|
|
||||||
parts.append("bold")
|
|
||||||
if f["Italic"]:
|
|
||||||
parts.append("italic")
|
|
||||||
if f["Underline"]:
|
|
||||||
parts.append("underline")
|
|
||||||
if f["Strikeout"]:
|
|
||||||
parts.append("strikeout")
|
|
||||||
name = "-".join(parts)
|
|
||||||
|
|
||||||
base_name = name
|
|
||||||
suffix = 2
|
|
||||||
while name in font_defs:
|
|
||||||
name = f"{base_name}{suffix}"
|
|
||||||
suffix += 1
|
|
||||||
|
|
||||||
font_names[i] = name
|
|
||||||
font_defs[name] = f
|
|
||||||
font_key_map[f_key] = name
|
|
||||||
|
|
||||||
# --- 11. Collect and name styles ---
|
|
||||||
|
|
||||||
style_keys = OrderedDict()
|
|
||||||
format_to_style_key = {}
|
|
||||||
|
|
||||||
for rd in row_data.values():
|
|
||||||
for cell in rd["Cells"]:
|
|
||||||
fmt = get_format(cell["FormatIdx"])
|
|
||||||
if not fmt:
|
|
||||||
continue
|
|
||||||
key = get_style_key(fmt)
|
|
||||||
if key not in style_keys:
|
|
||||||
style_keys[key] = fmt
|
|
||||||
format_to_style_key[cell["FormatIdx"]] = key
|
|
||||||
|
|
||||||
def name_style(fmt):
|
|
||||||
if not fmt:
|
|
||||||
return "default"
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
|
|
||||||
if fi in font_names and font_names[fi] != "default":
|
|
||||||
parts.append(font_names[fi])
|
|
||||||
|
|
||||||
bd = get_border_desc(fmt)
|
|
||||||
if bd["Border"] != "none":
|
|
||||||
if bd["Border"] == "all":
|
|
||||||
parts.append("bordered")
|
|
||||||
else:
|
|
||||||
parts.append(f"border-{bd['Border']}")
|
|
||||||
|
|
||||||
if fmt["HA"] == "Center":
|
|
||||||
parts.append("center")
|
|
||||||
elif fmt["HA"] == "Right":
|
|
||||||
parts.append("right")
|
|
||||||
if fmt["VA"] == "Center":
|
|
||||||
parts.append("vcenter")
|
|
||||||
elif fmt["VA"] == "Top":
|
|
||||||
parts.append("vtop")
|
|
||||||
if fmt["Wrap"]:
|
|
||||||
parts.append("wrap")
|
|
||||||
if fmt["DataFormat"]:
|
|
||||||
parts.append("fmt")
|
|
||||||
|
|
||||||
if len(parts) == 0:
|
|
||||||
return "default"
|
|
||||||
return "-".join(parts)
|
|
||||||
|
|
||||||
style_names = OrderedDict()
|
|
||||||
style_defs = OrderedDict()
|
|
||||||
|
|
||||||
for key in style_keys:
|
|
||||||
fmt = style_keys[key]
|
|
||||||
name = name_style(fmt)
|
|
||||||
|
|
||||||
base_name = name
|
|
||||||
suffix = 2
|
|
||||||
while name in style_defs:
|
|
||||||
name = f"{base_name}{suffix}"
|
|
||||||
suffix += 1
|
|
||||||
|
|
||||||
style_names[key] = name
|
|
||||||
|
|
||||||
s_def = OrderedDict()
|
|
||||||
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
|
|
||||||
if fi in font_names and font_names[fi] != "default":
|
|
||||||
s_def["font"] = font_names[fi]
|
|
||||||
if fmt["HA"]:
|
|
||||||
a_map = {"Left": "left", "Center": "center", "Right": "right"}
|
|
||||||
a = a_map.get(fmt["HA"])
|
|
||||||
if a:
|
|
||||||
s_def["align"] = a
|
|
||||||
if fmt["VA"]:
|
|
||||||
va_map = {"Top": "top", "Center": "center"}
|
|
||||||
a = va_map.get(fmt["VA"])
|
|
||||||
if a:
|
|
||||||
s_def["valign"] = a
|
|
||||||
bd = get_border_desc(fmt)
|
|
||||||
if bd["Border"] != "none":
|
|
||||||
s_def["border"] = bd["Border"]
|
|
||||||
if bd["Thick"]:
|
|
||||||
s_def["borderWidth"] = "thick"
|
|
||||||
if fmt["Wrap"]:
|
|
||||||
s_def["wrap"] = True
|
|
||||||
if fmt["DataFormat"]:
|
|
||||||
s_def["format"] = fmt["DataFormat"]
|
|
||||||
|
|
||||||
style_defs[name] = s_def
|
|
||||||
|
|
||||||
def get_style_name(fmt_idx):
|
|
||||||
key = format_to_style_key.get(fmt_idx)
|
|
||||||
if key and key in style_names:
|
|
||||||
return style_names[key]
|
|
||||||
return "default"
|
|
||||||
|
|
||||||
# --- 12. Build areas ---
|
|
||||||
|
|
||||||
dsl_areas = []
|
|
||||||
|
|
||||||
for area in named_areas:
|
|
||||||
area_rows = []
|
|
||||||
|
|
||||||
for global_row in range(area["BeginRow"], area["EndRow"] + 1):
|
|
||||||
rd = row_data.get(global_row)
|
|
||||||
|
|
||||||
if not rd or rd["Empty"]:
|
|
||||||
area_rows.append(OrderedDict())
|
|
||||||
continue
|
|
||||||
|
|
||||||
dsl_row = OrderedDict()
|
|
||||||
|
|
||||||
# Row height
|
|
||||||
if rd["FormatIdx"] > 0:
|
|
||||||
row_fmt = get_format(rd["FormatIdx"])
|
|
||||||
if row_fmt and row_fmt["Height"] > 0:
|
|
||||||
dsl_row["height"] = row_fmt["Height"]
|
|
||||||
|
|
||||||
# Separate content cells from gap-fill cells
|
|
||||||
content_cells = []
|
|
||||||
gap_cells = []
|
|
||||||
|
|
||||||
for cell in rd["Cells"]:
|
|
||||||
has_content = cell["Param"] or cell["Text"]
|
|
||||||
has_merge = f"{global_row},{cell['Col']}" in merge_map
|
|
||||||
|
|
||||||
if has_content or has_merge:
|
|
||||||
content_cells.append(cell)
|
|
||||||
else:
|
|
||||||
gap_cells.append(cell)
|
|
||||||
|
|
||||||
# Detect rowStyle
|
|
||||||
row_style_name = None
|
|
||||||
row_style_key = None
|
|
||||||
|
|
||||||
if len(gap_cells) > 0:
|
|
||||||
gap_keys = {}
|
|
||||||
for gc in gap_cells:
|
|
||||||
fmt = get_format(gc["FormatIdx"])
|
|
||||||
gap_keys[get_style_key(fmt)] = True
|
|
||||||
|
|
||||||
if len(gap_keys) == 1:
|
|
||||||
row_style_key = list(gap_keys.keys())[0]
|
|
||||||
if row_style_key in style_names:
|
|
||||||
row_style_name = style_names[row_style_key]
|
|
||||||
|
|
||||||
if row_style_name and row_style_name != "default":
|
|
||||||
dsl_row["rowStyle"] = row_style_name
|
|
||||||
|
|
||||||
# Build cell list
|
|
||||||
dsl_cells = []
|
|
||||||
|
|
||||||
for cell in sorted(content_cells, key=lambda c: c["Col"]):
|
|
||||||
dsl_cell = OrderedDict()
|
|
||||||
dsl_cell["col"] = cell["Col"] + 1
|
|
||||||
|
|
||||||
# Span/rowspan from merge
|
|
||||||
mk = f"{global_row},{cell['Col']}"
|
|
||||||
if mk in merge_map:
|
|
||||||
m = merge_map[mk]
|
|
||||||
if m["W"] > 0:
|
|
||||||
dsl_cell["span"] = m["W"] + 1
|
|
||||||
if m["H"] > 0:
|
|
||||||
dsl_cell["rowspan"] = m["H"] + 1
|
|
||||||
|
|
||||||
# Style
|
|
||||||
cell_fmt = get_format(cell["FormatIdx"])
|
|
||||||
cell_style_key = get_style_key(cell_fmt)
|
|
||||||
|
|
||||||
if row_style_key and cell_style_key == row_style_key:
|
|
||||||
pass # Inherits rowStyle
|
|
||||||
else:
|
|
||||||
sn = get_style_name(cell["FormatIdx"])
|
|
||||||
if sn != "default" or not row_style_name:
|
|
||||||
dsl_cell["style"] = sn
|
|
||||||
|
|
||||||
# Content
|
|
||||||
fill_type = cell_fmt["FillType"] if cell_fmt else ""
|
|
||||||
|
|
||||||
if cell["Param"]:
|
|
||||||
dsl_cell["param"] = cell["Param"]
|
|
||||||
if cell["Detail"]:
|
|
||||||
dsl_cell["detail"] = cell["Detail"]
|
|
||||||
elif fill_type == "Template" and cell["Text"]:
|
|
||||||
dsl_cell["template"] = cell["Text"]
|
|
||||||
elif cell["Text"]:
|
|
||||||
dsl_cell["text"] = cell["Text"]
|
|
||||||
|
|
||||||
dsl_cells.append(dsl_cell)
|
|
||||||
|
|
||||||
if len(dsl_cells) > 0:
|
|
||||||
dsl_row["cells"] = dsl_cells
|
|
||||||
area_rows.append(dsl_row)
|
|
||||||
|
|
||||||
# Compress consecutive empty rows ({}) into { empty = N }
|
|
||||||
compressed_rows = []
|
|
||||||
empty_run = 0
|
|
||||||
for r in area_rows:
|
|
||||||
if len(r) == 0:
|
|
||||||
empty_run += 1
|
|
||||||
else:
|
|
||||||
if empty_run > 0:
|
|
||||||
if empty_run == 1:
|
|
||||||
compressed_rows.append(OrderedDict())
|
|
||||||
else:
|
|
||||||
compressed_rows.append(OrderedDict([("empty", empty_run)]))
|
|
||||||
empty_run = 0
|
|
||||||
compressed_rows.append(r)
|
|
||||||
if empty_run > 0:
|
|
||||||
if empty_run == 1:
|
|
||||||
compressed_rows.append(OrderedDict())
|
|
||||||
else:
|
|
||||||
compressed_rows.append(OrderedDict([("empty", empty_run)]))
|
|
||||||
|
|
||||||
dsl_areas.append(OrderedDict([
|
|
||||||
("name", area["Name"]),
|
|
||||||
("rows", compressed_rows),
|
|
||||||
]))
|
|
||||||
|
|
||||||
# --- 13. Compress columnWidths ---
|
|
||||||
|
|
||||||
compressed_widths = OrderedDict()
|
|
||||||
if len(col_width_map) > 0:
|
|
||||||
# Group columns by width
|
|
||||||
width_to_cols = {}
|
|
||||||
for col_str, width in col_width_map.items():
|
|
||||||
width_to_cols.setdefault(width, []).append(col_str)
|
|
||||||
|
|
||||||
for width, cols in width_to_cols.items():
|
|
||||||
cols_sorted = sorted(cols, key=lambda x: int(x))
|
|
||||||
|
|
||||||
ranges = []
|
|
||||||
range_start = cols_sorted[0]
|
|
||||||
range_prev = cols_sorted[0]
|
|
||||||
|
|
||||||
for i in range(1, len(cols_sorted)):
|
|
||||||
if int(cols_sorted[i]) == int(range_prev) + 1:
|
|
||||||
range_prev = cols_sorted[i]
|
|
||||||
else:
|
|
||||||
if range_start == range_prev:
|
|
||||||
ranges.append(range_start)
|
|
||||||
else:
|
|
||||||
ranges.append(f"{range_start}-{range_prev}")
|
|
||||||
range_start = cols_sorted[i]
|
|
||||||
range_prev = cols_sorted[i]
|
|
||||||
|
|
||||||
if range_start == range_prev:
|
|
||||||
ranges.append(range_start)
|
|
||||||
else:
|
|
||||||
ranges.append(f"{range_start}-{range_prev}")
|
|
||||||
|
|
||||||
for rng in ranges:
|
|
||||||
compressed_widths[rng] = width
|
|
||||||
|
|
||||||
# --- 14. Build fonts output ---
|
|
||||||
|
|
||||||
fonts_out = OrderedDict()
|
|
||||||
for name, f in font_defs.items():
|
|
||||||
f_out = OrderedDict()
|
|
||||||
f_out["face"] = f["Face"]
|
|
||||||
f_out["size"] = f["Size"]
|
|
||||||
if f["Bold"]:
|
|
||||||
f_out["bold"] = True
|
|
||||||
if f["Italic"]:
|
|
||||||
f_out["italic"] = True
|
|
||||||
if f["Underline"]:
|
|
||||||
f_out["underline"] = True
|
|
||||||
if f["Strikeout"]:
|
|
||||||
f_out["strikeout"] = True
|
|
||||||
fonts_out[name] = f_out
|
|
||||||
|
|
||||||
# --- 15. Assemble result ---
|
|
||||||
|
|
||||||
result = OrderedDict()
|
|
||||||
result["columns"] = total_columns
|
|
||||||
result["defaultWidth"] = default_width
|
|
||||||
if len(compressed_widths) > 0:
|
|
||||||
result["columnWidths"] = compressed_widths
|
|
||||||
|
|
||||||
# Remove empty "default" style
|
|
||||||
if "default" in style_defs and len(style_defs["default"]) == 0:
|
|
||||||
del style_defs["default"]
|
|
||||||
|
|
||||||
# Remove unused styles
|
|
||||||
used_styles = set()
|
|
||||||
for a in dsl_areas:
|
|
||||||
for r in a["rows"]:
|
|
||||||
if "rowStyle" in r:
|
|
||||||
used_styles.add(r["rowStyle"])
|
|
||||||
if "cells" in r:
|
|
||||||
for c in r["cells"]:
|
|
||||||
if "style" in c:
|
|
||||||
used_styles.add(c["style"])
|
|
||||||
to_remove = [s for s in style_defs if s not in used_styles]
|
|
||||||
for s in to_remove:
|
|
||||||
del style_defs[s]
|
|
||||||
|
|
||||||
result["fonts"] = fonts_out
|
|
||||||
result["styles"] = style_defs
|
|
||||||
result["areas"] = dsl_areas
|
|
||||||
|
|
||||||
# --- 16. Convert to JSON ---
|
|
||||||
|
|
||||||
json_str = json.dumps(result, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
# --- 17. Output ---
|
|
||||||
|
|
||||||
if output_path:
|
|
||||||
abs_path = os.path.join(os.getcwd(), output_path) if not os.path.isabs(output_path) else output_path
|
|
||||||
with open(abs_path, "w", encoding="utf-8") as fh:
|
|
||||||
fh.write(json_str)
|
|
||||||
print(f"[OK] Decompiled: {output_path}")
|
|
||||||
else:
|
|
||||||
print(json_str)
|
|
||||||
|
|
||||||
print(f" Areas: {len(named_areas)}, Rows: {len(row_data)}, Columns: {total_columns}", file=sys.stderr)
|
|
||||||
print(f" Fonts: {len(font_defs)}, Styles: {len(style_defs)}, Merges: {len(merge_map)}", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
# Role DSL — полная справка
|
|
||||||
|
|
||||||
Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md).
|
|
||||||
|
|
||||||
## Структура верхнего уровня
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ИмяРоли",
|
|
||||||
"synonym": "Отображаемое имя роли",
|
|
||||||
"comment": "",
|
|
||||||
"setForNewObjects": false,
|
|
||||||
"setForAttributesByDefault": true,
|
|
||||||
"independentRightsOfChildObjects": false,
|
|
||||||
"objects": [ ... ],
|
|
||||||
"templates": [ ... ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `name` — программное имя роли (обязательно)
|
|
||||||
- `synonym` — отображаемое имя (по умолчанию = name)
|
|
||||||
- `comment` — комментарий (по умолчанию пусто)
|
|
||||||
- Глобальные флаги — по умолчанию `false`, `true`, `false`
|
|
||||||
|
|
||||||
## Объекты: два формата
|
|
||||||
|
|
||||||
Массив `objects` принимает строки (shorthand) и объекты (полная форма).
|
|
||||||
|
|
||||||
### Строковый shorthand
|
|
||||||
|
|
||||||
```
|
|
||||||
"ОбъектМетаданных: @пресет"
|
|
||||||
"ОбъектМетаданных: Право1, Право2"
|
|
||||||
```
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
```json
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Номенклатура: @view",
|
|
||||||
"Document.Реализация: @edit",
|
|
||||||
"InformationRegister.Цены: Read, Update",
|
|
||||||
"DataProcessor.Загрузка: @view"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Объектная форма (для RLS и переопределений)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Document.Реализация",
|
|
||||||
"preset": "view",
|
|
||||||
"rights": { "Delete": false },
|
|
||||||
"rls": { "Read": "#ДляОбъекта(\"\")" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `preset` — базовый набор прав (`"view"`, `"edit"`)
|
|
||||||
- `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]`
|
|
||||||
- `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}`
|
|
||||||
|
|
||||||
## Пресеты — подробные таблицы
|
|
||||||
|
|
||||||
Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`.
|
|
||||||
|
|
||||||
### `@view` — просмотр
|
|
||||||
|
|
||||||
| Тип объекта | Права |
|
|
||||||
|-------------|-------|
|
|
||||||
| Catalog, ExchangePlan, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task | Read, View, InputByString |
|
|
||||||
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, Constant, DocumentJournal | Read, View |
|
|
||||||
| Sequence | Read |
|
|
||||||
| CommonForm, CommonCommand, Subsystem, FilterCriterion, CommonAttribute | View |
|
|
||||||
| DataProcessor, Report | Use, View |
|
|
||||||
| SessionParameter | Get |
|
|
||||||
| Configuration | ThinClient, WebClient, Output, SaveUserData, MainWindowModeNormal |
|
|
||||||
|
|
||||||
### `@edit` — полное редактирование
|
|
||||||
|
|
||||||
| Тип объекта | Права |
|
|
||||||
|-------------|-------|
|
|
||||||
| Catalog, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark |
|
|
||||||
| Document | Read, Insert, Update, Delete, View, Edit, InputByString, Posting, UndoPosting, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted |
|
|
||||||
| BusinessProcess | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveStart |
|
|
||||||
| Task | Read, Insert, Update, Delete, View, Edit, InputByString, Execute, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveExecute |
|
|
||||||
| InformationRegister, AccumulationRegister, AccountingRegister, Constant | Read, Update, View, Edit |
|
|
||||||
| DocumentJournal | Read, View |
|
|
||||||
| Sequence | Read, Update |
|
|
||||||
| SessionParameter | Get, Set |
|
|
||||||
| CommonAttribute | View, Edit |
|
|
||||||
|
|
||||||
Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`.
|
|
||||||
|
|
||||||
Если пресет не определён для типа объекта — предупреждение с подсказкой доступных.
|
|
||||||
|
|
||||||
## Русские синонимы
|
|
||||||
|
|
||||||
Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает.
|
|
||||||
|
|
||||||
### Типы объектов
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Справочник` | Catalog |
|
|
||||||
| `Документ` | Document |
|
|
||||||
| `РегистрСведений` | InformationRegister |
|
|
||||||
| `РегистрНакопления` | AccumulationRegister |
|
|
||||||
| `РегистрБухгалтерии` | AccountingRegister |
|
|
||||||
| `РегистрРасчета` | CalculationRegister |
|
|
||||||
| `Константа` | Constant |
|
|
||||||
| `ПланСчетов` | ChartOfAccounts |
|
|
||||||
| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes |
|
|
||||||
| `ПланВидовРасчета` | ChartOfCalculationTypes |
|
|
||||||
| `ПланОбмена` | ExchangePlan |
|
|
||||||
| `БизнесПроцесс` | BusinessProcess |
|
|
||||||
| `Задача` | Task |
|
|
||||||
| `Обработка` | DataProcessor |
|
|
||||||
| `Отчет` | Report |
|
|
||||||
| `ОбщаяФорма` | CommonForm |
|
|
||||||
| `ОбщаяКоманда` | CommonCommand |
|
|
||||||
| `Подсистема` | Subsystem |
|
|
||||||
| `КритерийОтбора` | FilterCriterion |
|
|
||||||
| `ЖурналДокументов` | DocumentJournal |
|
|
||||||
| `Последовательность` | Sequence |
|
|
||||||
| `ВебСервис` | WebService |
|
|
||||||
| `HTTPСервис` | HTTPService |
|
|
||||||
| `СервисИнтеграции` | IntegrationService |
|
|
||||||
| `ПараметрСеанса` | SessionParameter |
|
|
||||||
| `ОбщийРеквизит` | CommonAttribute |
|
|
||||||
| `Конфигурация` | Configuration |
|
|
||||||
| `Перечисление` | Enum |
|
|
||||||
|
|
||||||
### Вложенные типы
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Реквизит` | Attribute |
|
|
||||||
| `СтандартныйРеквизит` | StandardAttribute |
|
|
||||||
| `ТабличнаяЧасть` | TabularSection |
|
|
||||||
| `Измерение` | Dimension |
|
|
||||||
| `Ресурс` | Resource |
|
|
||||||
| `Команда` | Command |
|
|
||||||
| `РеквизитАдресации` | AddressingAttribute |
|
|
||||||
|
|
||||||
### Права (основные)
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Чтение` | Read |
|
|
||||||
| `Добавление` | Insert |
|
|
||||||
| `Изменение` | Update |
|
|
||||||
| `Удаление` | Delete |
|
|
||||||
| `Просмотр` | View |
|
|
||||||
| `Редактирование` | Edit |
|
|
||||||
| `ВводПоСтроке` | InputByString |
|
|
||||||
| `Проведение` | Posting |
|
|
||||||
| `ОтменаПроведения` | UndoPosting |
|
|
||||||
| `Использование` | Use |
|
|
||||||
| `Получение` | Get |
|
|
||||||
| `Установка` | Set |
|
|
||||||
| `Старт` | Start |
|
|
||||||
| `Выполнение` | Execute |
|
|
||||||
| `УправлениеИтогами` | TotalsControl |
|
|
||||||
|
|
||||||
### Права (интерактивные)
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `ИнтерактивноеДобавление` | InteractiveInsert |
|
|
||||||
| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark |
|
|
||||||
| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark |
|
|
||||||
| `ИнтерактивноеУдаление` | InteractiveDelete |
|
|
||||||
| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked |
|
|
||||||
| `ИнтерактивноеПроведение` | InteractivePosting |
|
|
||||||
| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular |
|
|
||||||
| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting |
|
|
||||||
| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted |
|
|
||||||
| `ИнтерактивныйСтарт` | InteractiveStart |
|
|
||||||
| `ИнтерактивнаяАктивация` | InteractiveActivate |
|
|
||||||
| `ИнтерактивноеВыполнение` | InteractiveExecute |
|
|
||||||
|
|
||||||
### Права (конфигурация)
|
|
||||||
|
|
||||||
| Русский | English |
|
|
||||||
|---------|---------|
|
|
||||||
| `Администрирование` | Administration |
|
|
||||||
| `АдминистрированиеДанных` | DataAdministration |
|
|
||||||
| `ТонкийКлиент` | ThinClient |
|
|
||||||
| `ТолстыйКлиент` | ThickClient |
|
|
||||||
| `ВебКлиент` | WebClient |
|
|
||||||
| `МобильныйКлиент` | MobileClient |
|
|
||||||
| `ВнешнееСоединение` | ExternalConnection |
|
|
||||||
| `Вывод` | Output |
|
|
||||||
| `СохранениеДанныхПользователя` | SaveUserData |
|
|
||||||
|
|
||||||
## Типы объектов без прав в ролях
|
|
||||||
|
|
||||||
Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`):
|
|
||||||
|
|
||||||
| Тип | Причина |
|
|
||||||
|-----|---------|
|
|
||||||
| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно |
|
|
||||||
| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли |
|
|
||||||
| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав |
|
|
||||||
| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав |
|
|
||||||
| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав |
|
|
||||||
| Language (Язык) | Конфигурационный элемент |
|
|
||||||
| FunctionalOption (ФункциональнаяОпция) | Не объект прав |
|
|
||||||
| FunctionalOptionsParameter | Не объект прав |
|
|
||||||
| EventSubscription (ПодпискаНаСобытие) | Не объект прав |
|
|
||||||
| ScheduledJob (РегламентноеЗадание) | Не объект прав |
|
|
||||||
| StyleItem (ЭлементСтиля) | Ресурс оформления |
|
|
||||||
|
|
||||||
## Шаблоны ограничений (RLS templates)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"templates": [
|
|
||||||
{
|
|
||||||
"name": "ДляОбъекта(Модификатор)",
|
|
||||||
"condition": "// текст шаблона\nГДЕ 1=1\n&Модификатор"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `&` в условии автоматически экранируется в `&` в XML
|
|
||||||
- Ссылка на шаблон в `rls`: `"#ИмяШаблона(\"параметры\")"` — начинается с `#`
|
|
||||||
- Параметры шаблона можно передавать пустыми: `#ДляОбъекта("")`
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
### 1. Простая роль (только пресеты)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ЧтениеНоменклатуры",
|
|
||||||
"synonym": "Чтение номенклатуры",
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Номенклатура: @view",
|
|
||||||
"Catalog.Контрагенты: @view",
|
|
||||||
"DataProcessor.Загрузка: @view"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Роль для регламентного задания
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ОбновлениеЦен",
|
|
||||||
"synonym": "Обновление цен номенклатуры",
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Номенклатура: Read",
|
|
||||||
"Catalog.Валюты: Read",
|
|
||||||
"InformationRegister.ЦеныНоменклатуры: Read, Update",
|
|
||||||
"Constant.ОсновнаяВалюта: Read"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Роль с RLS
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ЧтениеДокументовПоОрганизации",
|
|
||||||
"synonym": "Чтение документов (ограничение по организации)",
|
|
||||||
"objects": [
|
|
||||||
"Catalog.Организации: @view",
|
|
||||||
{
|
|
||||||
"name": "Document.РеализацияТоваровУслуг",
|
|
||||||
"preset": "view",
|
|
||||||
"rls": {
|
|
||||||
"Read": "#ДляОбъекта(\"\")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"templates": [
|
|
||||||
{
|
|
||||||
"name": "ДляОбъекта(Модификатор)",
|
|
||||||
"condition": "ГДЕ Организация = &ТекущаяОрганизация"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Роль с русскими синонимами
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ПросмотрДанных",
|
|
||||||
"synonym": "Просмотр данных",
|
|
||||||
"objects": [
|
|
||||||
"Справочник.Контрагенты: @view",
|
|
||||||
"Документ.Реализация: Чтение, Просмотр",
|
|
||||||
"РегистрСведений.Цены: @edit",
|
|
||||||
"Обработка.ЗагрузкаДанных: @view"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Роль с переопределением прав из пресета
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "ОграниченноеРедактирование",
|
|
||||||
"synonym": "Редактирование без удаления",
|
|
||||||
"objects": [
|
|
||||||
{
|
|
||||||
"name": "Catalog.Контрагенты",
|
|
||||||
"preset": "edit",
|
|
||||||
"rights": { "Delete": false }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,748 +0,0 @@
|
|||||||
# role-compile v1.5 — Compile 1C role from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$JsonPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$OutputDir
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
|
|
||||||
if (-not (Test-Path $JsonPath)) {
|
|
||||||
Write-Error "File not found: $JsonPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
|
||||||
$def = $json | ConvertFrom-Json
|
|
||||||
|
|
||||||
if (-not $def.name) {
|
|
||||||
Write-Error "JSON must have 'name' field (role programmatic name)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$roleName = "$($def.name)"
|
|
||||||
$synonym = if ($def.synonym) { "$($def.synonym)" } else { $roleName }
|
|
||||||
$comment = if ($def.comment) { "$($def.comment)" } else { "" }
|
|
||||||
|
|
||||||
# --- 2. XML helpers ---
|
|
||||||
|
|
||||||
$script:xmlBuf = $null
|
|
||||||
|
|
||||||
function X {
|
|
||||||
param([string]$text)
|
|
||||||
$script:xmlBuf.AppendLine($text) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Esc-Xml {
|
|
||||||
param([string]$s)
|
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Russian synonyms → canonical English names ---
|
|
||||||
|
|
||||||
$script:typeAliases = @{
|
|
||||||
"Справочник" = "Catalog"
|
|
||||||
"Документ" = "Document"
|
|
||||||
"РегистрСведений" = "InformationRegister"
|
|
||||||
"РегистрНакопления" = "AccumulationRegister"
|
|
||||||
"РегистрБухгалтерии" = "AccountingRegister"
|
|
||||||
"РегистрРасчета" = "CalculationRegister"
|
|
||||||
"Константа" = "Constant"
|
|
||||||
"ПланСчетов" = "ChartOfAccounts"
|
|
||||||
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
|
|
||||||
"ПланВидовРасчета" = "ChartOfCalculationTypes"
|
|
||||||
"ПланОбмена" = "ExchangePlan"
|
|
||||||
"БизнесПроцесс" = "BusinessProcess"
|
|
||||||
"Задача" = "Task"
|
|
||||||
"Обработка" = "DataProcessor"
|
|
||||||
"Отчет" = "Report"
|
|
||||||
"ОбщаяФорма" = "CommonForm"
|
|
||||||
"ОбщаяКоманда" = "CommonCommand"
|
|
||||||
"Подсистема" = "Subsystem"
|
|
||||||
"КритерийОтбора" = "FilterCriterion"
|
|
||||||
"ЖурналДокументов" = "DocumentJournal"
|
|
||||||
"Последовательность" = "Sequence"
|
|
||||||
"ВебСервис" = "WebService"
|
|
||||||
"HTTPСервис" = "HTTPService"
|
|
||||||
"СервисИнтеграции" = "IntegrationService"
|
|
||||||
"ПараметрСеанса" = "SessionParameter"
|
|
||||||
"ОбщийРеквизит" = "CommonAttribute"
|
|
||||||
"Конфигурация" = "Configuration"
|
|
||||||
"Перечисление" = "Enum"
|
|
||||||
# Nested
|
|
||||||
"Реквизит" = "Attribute"
|
|
||||||
"СтандартныйРеквизит" = "StandardAttribute"
|
|
||||||
"ТабличнаяЧасть" = "TabularSection"
|
|
||||||
"Измерение" = "Dimension"
|
|
||||||
"Ресурс" = "Resource"
|
|
||||||
"Команда" = "Command"
|
|
||||||
"РеквизитАдресации" = "AddressingAttribute"
|
|
||||||
}
|
|
||||||
|
|
||||||
$script:rightAliases = @{
|
|
||||||
"Чтение" = "Read"
|
|
||||||
"Добавление" = "Insert"
|
|
||||||
"Изменение" = "Update"
|
|
||||||
"Удаление" = "Delete"
|
|
||||||
"Просмотр" = "View"
|
|
||||||
"Редактирование" = "Edit"
|
|
||||||
"ВводПоСтроке" = "InputByString"
|
|
||||||
"Проведение" = "Posting"
|
|
||||||
"ОтменаПроведения" = "UndoPosting"
|
|
||||||
"ИнтерактивноеДобавление" = "InteractiveInsert"
|
|
||||||
"ИнтерактивнаяПометкаУдаления" = "InteractiveSetDeletionMark"
|
|
||||||
"ИнтерактивноеСнятиеПометкиУдаления" = "InteractiveClearDeletionMark"
|
|
||||||
"ИнтерактивноеУдаление" = "InteractiveDelete"
|
|
||||||
"ИнтерактивноеУдалениеПомеченных" = "InteractiveDeleteMarked"
|
|
||||||
"ИнтерактивноеПроведение" = "InteractivePosting"
|
|
||||||
"ИнтерактивноеПроведениеНеоперативное" = "InteractivePostingRegular"
|
|
||||||
"ИнтерактивнаяОтменаПроведения" = "InteractiveUndoPosting"
|
|
||||||
"ИнтерактивноеИзменениеПроведенных" = "InteractiveChangeOfPosted"
|
|
||||||
"Использование" = "Use"
|
|
||||||
"Получение" = "Get"
|
|
||||||
"Установка" = "Set"
|
|
||||||
"Старт" = "Start"
|
|
||||||
"ИнтерактивныйСтарт" = "InteractiveStart"
|
|
||||||
"ИнтерактивнаяАктивация" = "InteractiveActivate"
|
|
||||||
"Выполнение" = "Execute"
|
|
||||||
"ИнтерактивноеВыполнение" = "InteractiveExecute"
|
|
||||||
"УправлениеИтогами" = "TotalsControl"
|
|
||||||
"Администрирование" = "Administration"
|
|
||||||
"АдминистрированиеДанных" = "DataAdministration"
|
|
||||||
"ТонкийКлиент" = "ThinClient"
|
|
||||||
"ВебКлиент" = "WebClient"
|
|
||||||
"ТолстыйКлиент" = "ThickClient"
|
|
||||||
"ВнешнееСоединение" = "ExternalConnection"
|
|
||||||
"Вывод" = "Output"
|
|
||||||
"СохранениеДанныхПользователя" = "SaveUserData"
|
|
||||||
"МобильныйКлиент" = "MobileClient"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Translate Russian object name to English (e.g. "Справочник.Контрагенты" → "Catalog.Контрагенты")
|
|
||||||
function Translate-ObjectName {
|
|
||||||
param([string]$name)
|
|
||||||
$parts = $name.Split(".")
|
|
||||||
$result = @()
|
|
||||||
foreach ($p in $parts) {
|
|
||||||
if ($script:typeAliases.ContainsKey($p)) {
|
|
||||||
$result += $script:typeAliases[$p]
|
|
||||||
} else {
|
|
||||||
$result += $p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $result -join "."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Translate Russian right name to English (e.g. "Чтение" → "Read")
|
|
||||||
function Translate-RightName {
|
|
||||||
param([string]$name)
|
|
||||||
if ($script:rightAliases.ContainsKey($name)) {
|
|
||||||
return $script:rightAliases[$name]
|
|
||||||
}
|
|
||||||
return $name
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Known rights per object type (source: docs/1c-role-spec.md) ---
|
|
||||||
|
|
||||||
$script:knownRights = @{
|
|
||||||
"Configuration" = @(
|
|
||||||
"Administration","DataAdministration","UpdateDataBaseConfiguration",
|
|
||||||
"ConfigurationExtensionsAdministration","ActiveUsers","EventLog","ExclusiveMode",
|
|
||||||
"ThinClient","ThickClient","WebClient","MobileClient","ExternalConnection",
|
|
||||||
"Automation","Output","SaveUserData","TechnicalSpecialistMode",
|
|
||||||
"InteractiveOpenExtDataProcessors","InteractiveOpenExtReports",
|
|
||||||
"AnalyticsSystemClient","CollaborationSystemInfoBaseRegistration",
|
|
||||||
"MainWindowModeNormal","MainWindowModeWorkplace",
|
|
||||||
"MainWindowModeEmbeddedWorkplace","MainWindowModeFullscreenWorkplace","MainWindowModeKiosk"
|
|
||||||
)
|
|
||||||
"Catalog" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"Document" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"Posting","UndoPosting",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting",
|
|
||||||
"InteractiveChangeOfPosted",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"InformationRegister" = @(
|
|
||||||
"Read","Update","View","Edit","TotalsControl",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"AccumulationRegister" = @("Read","Update","View","Edit","TotalsControl")
|
|
||||||
"AccountingRegister" = @("Read","Update","View","Edit","TotalsControl")
|
|
||||||
"CalculationRegister" = @("Read","View")
|
|
||||||
"Constant" = @(
|
|
||||||
"Read","Update","View","Edit",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"ChartOfAccounts" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory","ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistory","UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment"
|
|
||||||
)
|
|
||||||
"ChartOfCharacteristicTypes" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"ChartOfCalculationTypes" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData"
|
|
||||||
)
|
|
||||||
"ExchangePlan" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveDeleteMarked",
|
|
||||||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
|
||||||
)
|
|
||||||
"BusinessProcess" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveActivate","InteractiveStart"
|
|
||||||
)
|
|
||||||
"Task" = @(
|
|
||||||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
|
||||||
"Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete","InteractiveActivate","InteractiveExecute"
|
|
||||||
)
|
|
||||||
"DataProcessor" = @("Use","View")
|
|
||||||
"Report" = @("Use","View")
|
|
||||||
"CommonForm" = @("View")
|
|
||||||
"CommonCommand" = @("View")
|
|
||||||
"Subsystem" = @("View")
|
|
||||||
"FilterCriterion" = @("View")
|
|
||||||
"DocumentJournal" = @("Read","View")
|
|
||||||
"Sequence" = @("Read","Update")
|
|
||||||
"WebService" = @("Use")
|
|
||||||
"HTTPService" = @("Use")
|
|
||||||
"IntegrationService" = @("Use")
|
|
||||||
"SessionParameter" = @("Get","Set")
|
|
||||||
"CommonAttribute" = @("View","Edit")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Nested objects: Attribute, StandardAttribute, TabularSection, Dimension, Resource, AddressingAttribute
|
|
||||||
$script:nestedRights = @("View","Edit")
|
|
||||||
$script:commandRights = @("View")
|
|
||||||
|
|
||||||
# --- 4. Presets (@view, @edit) ---
|
|
||||||
|
|
||||||
$script:presets = @{
|
|
||||||
"view" = @{
|
|
||||||
"Catalog" = @("Read","View","InputByString")
|
|
||||||
"ExchangePlan" = @("Read","View","InputByString")
|
|
||||||
"Document" = @("Read","View","InputByString")
|
|
||||||
"ChartOfAccounts" = @("Read","View","InputByString")
|
|
||||||
"ChartOfCharacteristicTypes" = @("Read","View","InputByString")
|
|
||||||
"ChartOfCalculationTypes" = @("Read","View","InputByString")
|
|
||||||
"BusinessProcess" = @("Read","View","InputByString")
|
|
||||||
"Task" = @("Read","View","InputByString")
|
|
||||||
"InformationRegister" = @("Read","View")
|
|
||||||
"AccumulationRegister" = @("Read","View")
|
|
||||||
"AccountingRegister" = @("Read","View")
|
|
||||||
"CalculationRegister" = @("Read","View")
|
|
||||||
"Constant" = @("Read","View")
|
|
||||||
"DocumentJournal" = @("Read","View")
|
|
||||||
"Sequence" = @("Read")
|
|
||||||
"CommonForm" = @("View")
|
|
||||||
"CommonCommand" = @("View")
|
|
||||||
"Subsystem" = @("View")
|
|
||||||
"FilterCriterion" = @("View")
|
|
||||||
"SessionParameter" = @("Get")
|
|
||||||
"CommonAttribute" = @("View")
|
|
||||||
"DataProcessor" = @("Use","View")
|
|
||||||
"Report" = @("Use","View")
|
|
||||||
"Configuration" = @("ThinClient","WebClient","Output","SaveUserData","MainWindowModeNormal")
|
|
||||||
}
|
|
||||||
"edit" = @{
|
|
||||||
"Catalog" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"ExchangePlan" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"Document" = @("Read","Insert","Update","Delete","View","Edit","InputByString","Posting","UndoPosting","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark","InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting","InteractiveChangeOfPosted")
|
|
||||||
"ChartOfAccounts" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"ChartOfCharacteristicTypes" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"ChartOfCalculationTypes" = @("Read","Insert","Update","Delete","View","Edit","InputByString","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark")
|
|
||||||
"BusinessProcess" = @("Read","Insert","Update","Delete","View","Edit","InputByString","Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark","InteractiveActivate","InteractiveStart")
|
|
||||||
"Task" = @("Read","Insert","Update","Delete","View","Edit","InputByString","Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark","InteractiveActivate","InteractiveExecute")
|
|
||||||
"InformationRegister" = @("Read","Update","View","Edit")
|
|
||||||
"AccumulationRegister" = @("Read","Update","View","Edit")
|
|
||||||
"AccountingRegister" = @("Read","Update","View","Edit")
|
|
||||||
"Constant" = @("Read","Update","View","Edit")
|
|
||||||
"DocumentJournal" = @("Read","View")
|
|
||||||
"Sequence" = @("Read","Update")
|
|
||||||
"SessionParameter" = @("Get","Set")
|
|
||||||
"CommonAttribute" = @("View","Edit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Helpers ---
|
|
||||||
|
|
||||||
function Get-ObjectType {
|
|
||||||
param([string]$objectName)
|
|
||||||
$dotIdx = $objectName.IndexOf(".")
|
|
||||||
if ($dotIdx -lt 0) { return $objectName }
|
|
||||||
return $objectName.Substring(0, $dotIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Is-NestedObject {
|
|
||||||
param([string]$objectName)
|
|
||||||
return ($objectName.Split(".").Count -ge 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Resolve-Preset {
|
|
||||||
param([string]$objectType, [string]$presetName)
|
|
||||||
|
|
||||||
$preset = $presetName.TrimStart('@')
|
|
||||||
|
|
||||||
if (-not $script:presets.ContainsKey($preset)) {
|
|
||||||
Write-Warning "Unknown preset '@$preset'. Known: @view, @edit"
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
$typeMap = $script:presets[$preset]
|
|
||||||
if (-not $typeMap.ContainsKey($objectType)) {
|
|
||||||
$available = @()
|
|
||||||
foreach ($k in $script:presets.Keys) {
|
|
||||||
if ($script:presets[$k].ContainsKey($objectType)) {
|
|
||||||
$available += "@$k"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$availStr = if ($available.Count -gt 0) { $available -join ", " } else { "none" }
|
|
||||||
Write-Warning "Preset '@$preset' not defined for type '$objectType'. Available: $availStr"
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
return @($typeMap[$objectType])
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validate-RightName {
|
|
||||||
param([string]$objectName, [string]$rightName)
|
|
||||||
|
|
||||||
$objectType = Get-ObjectType $objectName
|
|
||||||
|
|
||||||
if (Is-NestedObject $objectName) {
|
|
||||||
if ($objectName -match '\.Command\.') {
|
|
||||||
if ($rightName -notin $script:commandRights) {
|
|
||||||
Write-Warning "${objectName}: '$rightName' not valid for commands (only: View)"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($rightName -notin $script:nestedRights) {
|
|
||||||
Write-Warning "${objectName}: '$rightName' not valid for nested objects (only: View, Edit)"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $script:knownRights.ContainsKey($objectType)) {
|
|
||||||
Write-Warning "${objectName}: unknown object type '$objectType'"
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
$validRights = $script:knownRights[$objectType]
|
|
||||||
if ($rightName -notin $validRights) {
|
|
||||||
$suggestions = @($validRights | Where-Object {
|
|
||||||
$_ -like "*$rightName*" -or $rightName -like "*$_*"
|
|
||||||
})
|
|
||||||
$sugStr = if ($suggestions.Count -gt 0) { " Did you mean: $($suggestions -join ', ')?" } else { "" }
|
|
||||||
Write-Warning "${objectName}: unknown right '$rightName'.$sugStr"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Parse object entries ---
|
|
||||||
|
|
||||||
function Parse-ObjectEntry {
|
|
||||||
param($entry)
|
|
||||||
|
|
||||||
# --- String shorthand ---
|
|
||||||
if ($entry -is [string]) {
|
|
||||||
$colonIdx = $entry.IndexOf(':')
|
|
||||||
if ($colonIdx -lt 0) {
|
|
||||||
Write-Warning "Invalid string '$entry' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'"
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$objName = Translate-ObjectName ($entry.Substring(0, $colonIdx).Trim())
|
|
||||||
$rightsStr = $entry.Substring($colonIdx + 1).Trim()
|
|
||||||
$objectType = Get-ObjectType $objName
|
|
||||||
|
|
||||||
if ($rightsStr.StartsWith('@')) {
|
|
||||||
$rightNames = @(Resolve-Preset -objectType $objectType -presetName $rightsStr)
|
|
||||||
} else {
|
|
||||||
$rightNames = @($rightsStr -split ',\s*' | ForEach-Object { Translate-RightName $_.Trim() } | Where-Object { $_ })
|
|
||||||
foreach ($r in $rightNames) {
|
|
||||||
Validate-RightName -objectName $objName -rightName $r | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rights = @()
|
|
||||||
foreach ($r in $rightNames) {
|
|
||||||
$rights += ,@{Name=$r; Value="true"; Condition=$null}
|
|
||||||
}
|
|
||||||
return @{ Name = $objName; Rights = $rights }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Object form ---
|
|
||||||
$objName = Translate-ObjectName "$($entry.name)"
|
|
||||||
if (-not $objName) {
|
|
||||||
Write-Warning "Object entry missing 'name' field"
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
$objectType = Get-ObjectType $objName
|
|
||||||
$rightsMap = [ordered]@{}
|
|
||||||
|
|
||||||
# 1) Start with preset
|
|
||||||
if ($entry.preset) {
|
|
||||||
$presetRights = @(Resolve-Preset -objectType $objectType -presetName "$($entry.preset)")
|
|
||||||
foreach ($r in $presetRights) {
|
|
||||||
$rightsMap[$r] = @{Value="true"; Condition=$null}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2) Apply explicit rights
|
|
||||||
if ($entry.rights) {
|
|
||||||
if ($entry.rights -is [array]) {
|
|
||||||
foreach ($r in $entry.rights) {
|
|
||||||
$rName = Translate-RightName "$r"
|
|
||||||
Validate-RightName -objectName $objName -rightName $rName | Out-Null
|
|
||||||
$rightsMap[$rName] = @{Value="true"; Condition=$null}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foreach ($p in $entry.rights.PSObject.Properties) {
|
|
||||||
$rName = Translate-RightName $p.Name
|
|
||||||
Validate-RightName -objectName $objName -rightName $rName | Out-Null
|
|
||||||
$boolVal = $p.Value
|
|
||||||
if ($boolVal -eq $true -or "$boolVal" -eq "True") {
|
|
||||||
$rightsMap[$rName] = @{Value="true"; Condition=$null}
|
|
||||||
} else {
|
|
||||||
$rightsMap[$rName] = @{Value="false"; Condition=$null}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3) Apply RLS conditions
|
|
||||||
if ($entry.rls) {
|
|
||||||
foreach ($p in $entry.rls.PSObject.Properties) {
|
|
||||||
$rlsRight = Translate-RightName $p.Name
|
|
||||||
if ($rightsMap.Contains($rlsRight)) {
|
|
||||||
$rightsMap[$rlsRight].Condition = "$($p.Value)"
|
|
||||||
} else {
|
|
||||||
Write-Warning "${objName}: RLS for '$rlsRight' but this right is not in the rights list"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert to array
|
|
||||||
$rights = @()
|
|
||||||
foreach ($k in $rightsMap.Keys) {
|
|
||||||
$rights += ,@{
|
|
||||||
Name = $k
|
|
||||||
Value = $rightsMap[$k].Value
|
|
||||||
Condition = $rightsMap[$k].Condition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @{ Name = $objName; Rights = $rights }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Parse all object entries ---
|
|
||||||
|
|
||||||
# Synonym: accept "rights" as alias for "objects"
|
|
||||||
if (-not $def.objects -and $def.rights) { $def | Add-Member -NotePropertyName objects -NotePropertyValue $def.rights }
|
|
||||||
|
|
||||||
$parsedObjects = @()
|
|
||||||
if ($def.objects) {
|
|
||||||
foreach ($entry in $def.objects) {
|
|
||||||
$parsed = Parse-ObjectEntry -entry $entry
|
|
||||||
if ($parsed) {
|
|
||||||
$parsedObjects += ,$parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir }
|
|
||||||
$formatVersion = Detect-FormatVersion $resolvedOutputDir
|
|
||||||
|
|
||||||
# --- 8. Generate UUID ---
|
|
||||||
|
|
||||||
$uuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
# --- 9. Emit metadata XML (Roles/Name.xml) ---
|
|
||||||
|
|
||||||
$script:xmlBuf = New-Object System.Text.StringBuilder 4096
|
|
||||||
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
|
||||||
X ' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
X ' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
X ' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
|
||||||
X ' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
X ' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
X ' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
X ' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
X ' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
X ' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
X ' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
X ' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
X ' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
|
||||||
X ' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
|
||||||
X ' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
X " version=`"$formatVersion`">"
|
|
||||||
X " <Role uuid=`"$uuid`">"
|
|
||||||
X ' <Properties>'
|
|
||||||
X " <Name>$roleName</Name>"
|
|
||||||
X ' <Synonym>'
|
|
||||||
X ' <v8:item>'
|
|
||||||
X ' <v8:lang>ru</v8:lang>'
|
|
||||||
X " <v8:content>$(Esc-Xml $synonym)</v8:content>"
|
|
||||||
X ' </v8:item>'
|
|
||||||
X ' </Synonym>'
|
|
||||||
if ($comment) {
|
|
||||||
X " <Comment>$(Esc-Xml $comment)</Comment>"
|
|
||||||
} else {
|
|
||||||
X ' <Comment/>'
|
|
||||||
}
|
|
||||||
X ' </Properties>'
|
|
||||||
X ' </Role>'
|
|
||||||
X '</MetaDataObject>'
|
|
||||||
|
|
||||||
$metadataXml = $script:xmlBuf.ToString()
|
|
||||||
|
|
||||||
# --- 10. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
|
|
||||||
|
|
||||||
$script:xmlBuf = New-Object System.Text.StringBuilder 8192
|
|
||||||
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<Rights xmlns="http://v8.1c.ru/8.2/roles"'
|
|
||||||
X ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
X ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
X " xsi:type=`"Rights`" version=`"$formatVersion`">"
|
|
||||||
|
|
||||||
# Global flags (defaults match typical 1C roles)
|
|
||||||
$sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" }
|
|
||||||
$sfab = if ($null -ne $def.setForAttributesByDefault) { "$($def.setForAttributesByDefault)".ToLower() } else { "true" }
|
|
||||||
$irco = if ($null -ne $def.independentRightsOfChildObjects) { "$($def.independentRightsOfChildObjects)".ToLower() } else { "false" }
|
|
||||||
|
|
||||||
X " <setForNewObjects>$sfno</setForNewObjects>"
|
|
||||||
X " <setForAttributesByDefault>$sfab</setForAttributesByDefault>"
|
|
||||||
X " <independentRightsOfChildObjects>$irco</independentRightsOfChildObjects>"
|
|
||||||
|
|
||||||
# Object blocks
|
|
||||||
$totalRights = 0
|
|
||||||
foreach ($obj in $parsedObjects) {
|
|
||||||
X ' <object>'
|
|
||||||
X " <name>$($obj.Name)</name>"
|
|
||||||
foreach ($right in $obj.Rights) {
|
|
||||||
X ' <right>'
|
|
||||||
X " <name>$($right.Name)</name>"
|
|
||||||
X " <value>$($right.Value)</value>"
|
|
||||||
if ($right.Condition) {
|
|
||||||
X ' <restrictionByCondition>'
|
|
||||||
X " <condition>$(Esc-Xml $right.Condition)</condition>"
|
|
||||||
X ' </restrictionByCondition>'
|
|
||||||
}
|
|
||||||
X ' </right>'
|
|
||||||
$totalRights++
|
|
||||||
}
|
|
||||||
X ' </object>'
|
|
||||||
}
|
|
||||||
|
|
||||||
# RLS restriction templates
|
|
||||||
$templateCount = 0
|
|
||||||
if ($def.templates) {
|
|
||||||
foreach ($tpl in $def.templates) {
|
|
||||||
X ' <restrictionTemplate>'
|
|
||||||
X " <name>$(Esc-Xml "$($tpl.name)")</name>"
|
|
||||||
X " <condition>$(Esc-Xml "$($tpl.condition)")</condition>"
|
|
||||||
X ' </restrictionTemplate>'
|
|
||||||
$templateCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
X '</Rights>'
|
|
||||||
|
|
||||||
$rightsXml = $script:xmlBuf.ToString()
|
|
||||||
|
|
||||||
# --- 11. Write output files ---
|
|
||||||
|
|
||||||
$outDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) {
|
|
||||||
$OutputDir
|
|
||||||
} else {
|
|
||||||
Join-Path (Get-Location) $OutputDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine Roles dir and config root
|
|
||||||
# Back-compat: if OutputDir leaf is "Roles", use as-is; otherwise treat as config root
|
|
||||||
$leaf = Split-Path $outDir -Leaf
|
|
||||||
if ($leaf -eq "Roles") {
|
|
||||||
$rolesDir = $outDir
|
|
||||||
$configDir = Split-Path $outDir -Parent
|
|
||||||
} else {
|
|
||||||
$rolesDir = Join-Path $outDir "Roles"
|
|
||||||
$configDir = $outDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# Metadata: Roles/RoleName.xml
|
|
||||||
$metadataPath = Join-Path $rolesDir "$roleName.xml"
|
|
||||||
if (-not (Test-Path $rolesDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $rolesDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rights: Roles/RoleName/Ext/Rights.xml
|
|
||||||
$roleSubDir = Join-Path $rolesDir $roleName
|
|
||||||
$extDir = Join-Path $roleSubDir "Ext"
|
|
||||||
$rightsPath = Join-Path $extDir "Rights.xml"
|
|
||||||
if (-not (Test-Path $extDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($metadataPath, $metadataXml, $enc)
|
|
||||||
[System.IO.File]::WriteAllText($rightsPath, $rightsXml, $enc)
|
|
||||||
|
|
||||||
# --- 12. Register in Configuration.xml ---
|
|
||||||
|
|
||||||
$configXmlPath = Join-Path $configDir "Configuration.xml"
|
|
||||||
$regResult = $null
|
|
||||||
|
|
||||||
if (Test-Path $configXmlPath) {
|
|
||||||
$configDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$configDoc.PreserveWhitespace = $true
|
|
||||||
$configDoc.Load($configXmlPath)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
|
|
||||||
if ($childObjects) {
|
|
||||||
$existing = $childObjects.SelectNodes("md:Role", $nsMgr)
|
|
||||||
$alreadyExists = $false
|
|
||||||
foreach ($r in $existing) {
|
|
||||||
if ($r.InnerText -eq $roleName) {
|
|
||||||
$alreadyExists = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($alreadyExists) {
|
|
||||||
$regResult = "already"
|
|
||||||
} else {
|
|
||||||
$roleElem = $configDoc.CreateElement("Role", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$roleElem.InnerText = $roleName
|
|
||||||
|
|
||||||
if ($existing.Count -gt 0) {
|
|
||||||
# Insert after last existing <Role>
|
|
||||||
$lastRole = $existing[$existing.Count - 1]
|
|
||||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertAfter($newWs, $lastRole) | Out-Null
|
|
||||||
$childObjects.InsertAfter($roleElem, $newWs) | Out-Null
|
|
||||||
} else {
|
|
||||||
# No existing roles — insert before closing whitespace
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($roleElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($roleElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save
|
|
||||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$cfgSettings.Indent = $false
|
|
||||||
$stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings)
|
|
||||||
$configDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
$regResult = "added"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$regResult = "no-childobj"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$regResult = "no-config"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 13. Summary ---
|
|
||||||
|
|
||||||
Write-Host "[OK] Role '$roleName' compiled"
|
|
||||||
Write-Host " UUID: $uuid"
|
|
||||||
Write-Host " Metadata: $metadataPath"
|
|
||||||
Write-Host " Rights: $rightsPath"
|
|
||||||
Write-Host " Objects: $($parsedObjects.Count), Rights: $totalRights, Templates: $templateCount"
|
|
||||||
switch ($regResult) {
|
|
||||||
"added" { Write-Host " Configuration.xml: <Role>$roleName</Role> added to ChildObjects" }
|
|
||||||
"already" { Write-Host " Configuration.xml: <Role>$roleName</Role> already registered" }
|
|
||||||
"no-childobj" { Write-Warning "Configuration.xml found but <ChildObjects> not found" }
|
|
||||||
"no-config" { Write-Warning "Configuration.xml not found at $configXmlPath — register manually" }
|
|
||||||
}
|
|
||||||
@@ -1,656 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# role-compile v1.4 — Compile 1C role from JSON
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Russian synonyms -> canonical English names ---
|
|
||||||
|
|
||||||
TYPE_ALIASES = {
|
|
||||||
"Справочник": "Catalog",
|
|
||||||
"Документ": "Document",
|
|
||||||
"РегистрСведений": "InformationRegister",
|
|
||||||
"РегистрНакопления": "AccumulationRegister",
|
|
||||||
"РегистрБухгалтерии": "AccountingRegister",
|
|
||||||
"РегистрРасчета": "CalculationRegister",
|
|
||||||
"Константа": "Constant",
|
|
||||||
"ПланСчетов": "ChartOfAccounts",
|
|
||||||
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
|
|
||||||
"ПланВидовРасчета": "ChartOfCalculationTypes",
|
|
||||||
"ПланОбмена": "ExchangePlan",
|
|
||||||
"БизнесПроцесс": "BusinessProcess",
|
|
||||||
"Задача": "Task",
|
|
||||||
"Обработка": "DataProcessor",
|
|
||||||
"Отчет": "Report",
|
|
||||||
"ОбщаяФорма": "CommonForm",
|
|
||||||
"ОбщаяКоманда": "CommonCommand",
|
|
||||||
"Подсистема": "Subsystem",
|
|
||||||
"КритерийОтбора": "FilterCriterion",
|
|
||||||
"ЖурналДокументов": "DocumentJournal",
|
|
||||||
"Последовательность": "Sequence",
|
|
||||||
"ВебСервис": "WebService",
|
|
||||||
"HTTPСервис": "HTTPService",
|
|
||||||
"СервисИнтеграции": "IntegrationService",
|
|
||||||
"ПараметрСеанса": "SessionParameter",
|
|
||||||
"ОбщийРеквизит": "CommonAttribute",
|
|
||||||
"Конфигурация": "Configuration",
|
|
||||||
"Перечисление": "Enum",
|
|
||||||
# Nested
|
|
||||||
"Реквизит": "Attribute",
|
|
||||||
"СтандартныйРеквизит": "StandardAttribute",
|
|
||||||
"ТабличнаяЧасть": "TabularSection",
|
|
||||||
"Измерение": "Dimension",
|
|
||||||
"Ресурс": "Resource",
|
|
||||||
"Команда": "Command",
|
|
||||||
"РеквизитАдресации": "AddressingAttribute",
|
|
||||||
}
|
|
||||||
|
|
||||||
RIGHT_ALIASES = {
|
|
||||||
"Чтение": "Read",
|
|
||||||
"Добавление": "Insert",
|
|
||||||
"Изменение": "Update",
|
|
||||||
"Удаление": "Delete",
|
|
||||||
"Просмотр": "View",
|
|
||||||
"Редактирование": "Edit",
|
|
||||||
"ВводПоСтроке": "InputByString",
|
|
||||||
"Проведение": "Posting",
|
|
||||||
"ОтменаПроведения": "UndoPosting",
|
|
||||||
"ИнтерактивноеДобавление": "InteractiveInsert",
|
|
||||||
"ИнтерактивнаяПометкаУдаления": "InteractiveSetDeletionMark",
|
|
||||||
"ИнтерактивноеСнятиеПометкиУдаления": "InteractiveClearDeletionMark",
|
|
||||||
"ИнтерактивноеУдаление": "InteractiveDelete",
|
|
||||||
"ИнтерактивноеУдалениеПомеченных": "InteractiveDeleteMarked",
|
|
||||||
"ИнтерактивноеПроведение": "InteractivePosting",
|
|
||||||
"ИнтерактивноеПроведениеНеоперативное": "InteractivePostingRegular",
|
|
||||||
"ИнтерактивнаяОтменаПроведения": "InteractiveUndoPosting",
|
|
||||||
"ИнтерактивноеИзменениеПроведенных": "InteractiveChangeOfPosted",
|
|
||||||
"Использование": "Use",
|
|
||||||
"Получение": "Get",
|
|
||||||
"Установка": "Set",
|
|
||||||
"Старт": "Start",
|
|
||||||
"ИнтерактивныйСтарт": "InteractiveStart",
|
|
||||||
"ИнтерактивнаяАктивация": "InteractiveActivate",
|
|
||||||
"Выполнение": "Execute",
|
|
||||||
"ИнтерактивноеВыполнение": "InteractiveExecute",
|
|
||||||
"УправлениеИтогами": "TotalsControl",
|
|
||||||
"Администрирование": "Administration",
|
|
||||||
"АдминистрированиеДанных": "DataAdministration",
|
|
||||||
"ТонкийКлиент": "ThinClient",
|
|
||||||
"ВебКлиент": "WebClient",
|
|
||||||
"ТолстыйКлиент": "ThickClient",
|
|
||||||
"ВнешнееСоединение": "ExternalConnection",
|
|
||||||
"Вывод": "Output",
|
|
||||||
"СохранениеДанныхПользователя": "SaveUserData",
|
|
||||||
"МобильныйКлиент": "MobileClient",
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Known rights per object type ---
|
|
||||||
|
|
||||||
KNOWN_RIGHTS = {
|
|
||||||
"Configuration": [
|
|
||||||
"Administration", "DataAdministration", "UpdateDataBaseConfiguration",
|
|
||||||
"ConfigurationExtensionsAdministration", "ActiveUsers", "EventLog", "ExclusiveMode",
|
|
||||||
"ThinClient", "ThickClient", "WebClient", "MobileClient", "ExternalConnection",
|
|
||||||
"Automation", "Output", "SaveUserData", "TechnicalSpecialistMode",
|
|
||||||
"InteractiveOpenExtDataProcessors", "InteractiveOpenExtReports",
|
|
||||||
"AnalyticsSystemClient", "CollaborationSystemInfoBaseRegistration",
|
|
||||||
"MainWindowModeNormal", "MainWindowModeWorkplace",
|
|
||||||
"MainWindowModeEmbeddedWorkplace", "MainWindowModeFullscreenWorkplace", "MainWindowModeKiosk",
|
|
||||||
],
|
|
||||||
"Catalog": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"Document": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"Posting", "UndoPosting",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting",
|
|
||||||
"InteractiveChangeOfPosted",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"InformationRegister": [
|
|
||||||
"Read", "Update", "View", "Edit", "TotalsControl",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistoryOfMissingData", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"AccumulationRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
|
|
||||||
"AccountingRegister": ["Read", "Update", "View", "Edit", "TotalsControl"],
|
|
||||||
"CalculationRegister": ["Read", "View"],
|
|
||||||
"Constant": [
|
|
||||||
"Read", "Update", "View", "Edit",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"ChartOfAccounts": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory", "ReadDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistory", "UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
],
|
|
||||||
"ChartOfCharacteristicTypes": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"ChartOfCalculationTypes": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete",
|
|
||||||
"InteractiveDeletePredefinedData", "InteractiveSetDeletionMarkPredefinedData",
|
|
||||||
"InteractiveClearDeletionMarkPredefinedData", "InteractiveDeleteMarkedPredefinedData",
|
|
||||||
],
|
|
||||||
"ExchangePlan": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveDeleteMarked",
|
|
||||||
"ReadDataHistory", "ViewDataHistory", "UpdateDataHistory",
|
|
||||||
"ReadDataHistoryOfMissingData", "UpdateDataHistoryOfMissingData",
|
|
||||||
"UpdateDataHistorySettings", "UpdateDataHistoryVersionComment",
|
|
||||||
"EditDataHistoryVersionComment", "SwitchToDataHistoryVersion",
|
|
||||||
],
|
|
||||||
"BusinessProcess": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveActivate", "InteractiveStart",
|
|
||||||
],
|
|
||||||
"Task": [
|
|
||||||
"Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
|
|
||||||
"Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark",
|
|
||||||
"InteractiveDelete", "InteractiveActivate", "InteractiveExecute",
|
|
||||||
],
|
|
||||||
"DataProcessor": ["Use", "View"],
|
|
||||||
"Report": ["Use", "View"],
|
|
||||||
"CommonForm": ["View"],
|
|
||||||
"CommonCommand": ["View"],
|
|
||||||
"Subsystem": ["View"],
|
|
||||||
"FilterCriterion": ["View"],
|
|
||||||
"DocumentJournal": ["Read", "View"],
|
|
||||||
"Sequence": ["Read", "Update"],
|
|
||||||
"WebService": ["Use"],
|
|
||||||
"HTTPService": ["Use"],
|
|
||||||
"IntegrationService": ["Use"],
|
|
||||||
"SessionParameter": ["Get", "Set"],
|
|
||||||
"CommonAttribute": ["View", "Edit"],
|
|
||||||
}
|
|
||||||
|
|
||||||
NESTED_RIGHTS = ["View", "Edit"]
|
|
||||||
COMMAND_RIGHTS = ["View"]
|
|
||||||
|
|
||||||
# --- Presets ---
|
|
||||||
|
|
||||||
PRESETS = {
|
|
||||||
"view": {
|
|
||||||
"Catalog": ["Read", "View", "InputByString"],
|
|
||||||
"ExchangePlan": ["Read", "View", "InputByString"],
|
|
||||||
"Document": ["Read", "View", "InputByString"],
|
|
||||||
"ChartOfAccounts": ["Read", "View", "InputByString"],
|
|
||||||
"ChartOfCharacteristicTypes": ["Read", "View", "InputByString"],
|
|
||||||
"ChartOfCalculationTypes": ["Read", "View", "InputByString"],
|
|
||||||
"BusinessProcess": ["Read", "View", "InputByString"],
|
|
||||||
"Task": ["Read", "View", "InputByString"],
|
|
||||||
"InformationRegister": ["Read", "View"],
|
|
||||||
"AccumulationRegister": ["Read", "View"],
|
|
||||||
"AccountingRegister": ["Read", "View"],
|
|
||||||
"CalculationRegister": ["Read", "View"],
|
|
||||||
"Constant": ["Read", "View"],
|
|
||||||
"DocumentJournal": ["Read", "View"],
|
|
||||||
"Sequence": ["Read"],
|
|
||||||
"CommonForm": ["View"],
|
|
||||||
"CommonCommand": ["View"],
|
|
||||||
"Subsystem": ["View"],
|
|
||||||
"FilterCriterion": ["View"],
|
|
||||||
"SessionParameter": ["Get"],
|
|
||||||
"CommonAttribute": ["View"],
|
|
||||||
"DataProcessor": ["Use", "View"],
|
|
||||||
"Report": ["Use", "View"],
|
|
||||||
"Configuration": ["ThinClient", "WebClient", "Output", "SaveUserData", "MainWindowModeNormal"],
|
|
||||||
},
|
|
||||||
"edit": {
|
|
||||||
"Catalog": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"ExchangePlan": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"Document": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Posting", "UndoPosting", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractivePosting", "InteractivePostingRegular", "InteractiveUndoPosting", "InteractiveChangeOfPosted"],
|
|
||||||
"ChartOfAccounts": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"ChartOfCharacteristicTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"ChartOfCalculationTypes": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark"],
|
|
||||||
"BusinessProcess": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Start", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveStart"],
|
|
||||||
"Task": ["Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", "Execute", "InteractiveInsert", "InteractiveSetDeletionMark", "InteractiveClearDeletionMark", "InteractiveActivate", "InteractiveExecute"],
|
|
||||||
"InformationRegister": ["Read", "Update", "View", "Edit"],
|
|
||||||
"AccumulationRegister": ["Read", "Update", "View", "Edit"],
|
|
||||||
"AccountingRegister": ["Read", "Update", "View", "Edit"],
|
|
||||||
"Constant": ["Read", "Update", "View", "Edit"],
|
|
||||||
"DocumentJournal": ["Read", "View"],
|
|
||||||
"Sequence": ["Read", "Update"],
|
|
||||||
"SessionParameter": ["Get", "Set"],
|
|
||||||
"CommonAttribute": ["View", "Edit"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def translate_object_name(name):
|
|
||||||
parts = name.split('.')
|
|
||||||
result = []
|
|
||||||
for p in parts:
|
|
||||||
result.append(TYPE_ALIASES.get(p, p))
|
|
||||||
return '.'.join(result)
|
|
||||||
|
|
||||||
|
|
||||||
def translate_right_name(name):
|
|
||||||
return RIGHT_ALIASES.get(name, name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_type(object_name):
|
|
||||||
dot_idx = object_name.find('.')
|
|
||||||
if dot_idx < 0:
|
|
||||||
return object_name
|
|
||||||
return object_name[:dot_idx]
|
|
||||||
|
|
||||||
|
|
||||||
def is_nested_object(object_name):
|
|
||||||
return len(object_name.split('.')) >= 3
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_preset(object_type, preset_name):
|
|
||||||
preset = preset_name.lstrip('@')
|
|
||||||
if preset not in PRESETS:
|
|
||||||
print(f"WARNING: Unknown preset '@{preset}'. Known: @view, @edit", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
type_map = PRESETS[preset]
|
|
||||||
if object_type not in type_map:
|
|
||||||
available = []
|
|
||||||
for k in PRESETS:
|
|
||||||
if object_type in PRESETS[k]:
|
|
||||||
available.append(f'@{k}')
|
|
||||||
avail_str = ', '.join(available) if available else 'none'
|
|
||||||
print(f"WARNING: Preset '@{preset}' not defined for type '{object_type}'. Available: {avail_str}", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
return list(type_map[object_type])
|
|
||||||
|
|
||||||
|
|
||||||
def validate_right_name(object_name, right_name):
|
|
||||||
object_type = get_object_type(object_name)
|
|
||||||
|
|
||||||
if is_nested_object(object_name):
|
|
||||||
if '.Command.' in object_name:
|
|
||||||
if right_name not in COMMAND_RIGHTS:
|
|
||||||
print(f"WARNING: {object_name}: '{right_name}' not valid for commands (only: View)", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if right_name not in NESTED_RIGHTS:
|
|
||||||
print(f"WARNING: {object_name}: '{right_name}' not valid for nested objects (only: View, Edit)", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
if object_type not in KNOWN_RIGHTS:
|
|
||||||
print(f"WARNING: {object_name}: unknown object type '{object_type}'", file=sys.stderr)
|
|
||||||
return True
|
|
||||||
|
|
||||||
valid_rights = KNOWN_RIGHTS[object_type]
|
|
||||||
if right_name not in valid_rights:
|
|
||||||
suggestions = [r for r in valid_rights if right_name in r or r in right_name]
|
|
||||||
sug_str = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
|
|
||||||
print(f"WARNING: {object_name}: unknown right '{right_name}'.{sug_str}", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def parse_object_entry(entry):
|
|
||||||
# --- String shorthand ---
|
|
||||||
if isinstance(entry, str):
|
|
||||||
colon_idx = entry.find(':')
|
|
||||||
if colon_idx < 0:
|
|
||||||
print(f"WARNING: Invalid string '{entry}' -- expected 'Object.Name: @preset' or 'Object.Name: Right1, Right2'", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
obj_name = translate_object_name(entry[:colon_idx].strip())
|
|
||||||
rights_str = entry[colon_idx + 1:].strip()
|
|
||||||
object_type = get_object_type(obj_name)
|
|
||||||
|
|
||||||
if rights_str.startswith('@'):
|
|
||||||
right_names = resolve_preset(object_type, rights_str)
|
|
||||||
else:
|
|
||||||
right_names = [translate_right_name(r.strip()) for r in rights_str.split(',') if r.strip()]
|
|
||||||
for r in right_names:
|
|
||||||
validate_right_name(obj_name, r)
|
|
||||||
|
|
||||||
rights = []
|
|
||||||
for r in right_names:
|
|
||||||
rights.append({'Name': r, 'Value': 'true', 'Condition': None})
|
|
||||||
return {'Name': obj_name, 'Rights': rights}
|
|
||||||
|
|
||||||
# --- Object form ---
|
|
||||||
obj_name = translate_object_name(str(entry.get('name', '')))
|
|
||||||
if not obj_name:
|
|
||||||
print("WARNING: Object entry missing 'name' field", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
|
|
||||||
object_type = get_object_type(obj_name)
|
|
||||||
# Use a list of tuples to preserve insertion order
|
|
||||||
rights_map = {} # name -> {Value, Condition}
|
|
||||||
rights_order = [] # preserve order
|
|
||||||
|
|
||||||
# 1) Start with preset
|
|
||||||
if entry.get('preset'):
|
|
||||||
preset_rights = resolve_preset(object_type, str(entry['preset']))
|
|
||||||
for r in preset_rights:
|
|
||||||
if r not in rights_map:
|
|
||||||
rights_order.append(r)
|
|
||||||
rights_map[r] = {'Value': 'true', 'Condition': None}
|
|
||||||
|
|
||||||
# 2) Apply explicit rights
|
|
||||||
if entry.get('rights') is not None:
|
|
||||||
if isinstance(entry['rights'], list):
|
|
||||||
for r in entry['rights']:
|
|
||||||
r_name = translate_right_name(str(r))
|
|
||||||
validate_right_name(obj_name, r_name)
|
|
||||||
if r_name not in rights_map:
|
|
||||||
rights_order.append(r_name)
|
|
||||||
rights_map[r_name] = {'Value': 'true', 'Condition': None}
|
|
||||||
elif isinstance(entry['rights'], dict):
|
|
||||||
for p_name, p_value in entry['rights'].items():
|
|
||||||
r_name = translate_right_name(p_name)
|
|
||||||
validate_right_name(obj_name, r_name)
|
|
||||||
bool_val = 'true' if p_value is True or str(p_value) == 'True' else 'false'
|
|
||||||
if r_name not in rights_map:
|
|
||||||
rights_order.append(r_name)
|
|
||||||
rights_map[r_name] = {'Value': bool_val, 'Condition': None}
|
|
||||||
|
|
||||||
# 3) Apply RLS conditions
|
|
||||||
if entry.get('rls'):
|
|
||||||
for p_name, p_value in entry['rls'].items():
|
|
||||||
rls_right = translate_right_name(p_name)
|
|
||||||
if rls_right in rights_map:
|
|
||||||
rights_map[rls_right]['Condition'] = str(p_value)
|
|
||||||
else:
|
|
||||||
print(f"WARNING: {obj_name}: RLS for '{rls_right}' but this right is not in the rights list", file=sys.stderr)
|
|
||||||
|
|
||||||
# Convert to array
|
|
||||||
rights = []
|
|
||||||
for k in rights_order:
|
|
||||||
rights.append({
|
|
||||||
'Name': k,
|
|
||||||
'Value': rights_map[k]['Value'],
|
|
||||||
'Condition': rights_map[k]['Condition'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {'Name': obj_name, 'Rights': rights}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Compile 1C role from JSON', allow_abbrev=False)
|
|
||||||
parser.add_argument('-JsonPath', type=str, required=True)
|
|
||||||
parser.add_argument('-OutputDir', type=str, required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- 1. Load and validate JSON ---
|
|
||||||
json_path = args.JsonPath
|
|
||||||
if not os.path.exists(json_path):
|
|
||||||
print(f"File not found: {json_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
with open(json_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
defn = json.load(f)
|
|
||||||
|
|
||||||
if not defn.get('name'):
|
|
||||||
print("JSON must have 'name' field (role programmatic name)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
role_name = str(defn['name'])
|
|
||||||
synonym = str(defn['synonym']) if defn.get('synonym') else role_name
|
|
||||||
comment = str(defn['comment']) if defn.get('comment') else ''
|
|
||||||
|
|
||||||
# Synonym: accept "rights" as alias for "objects"
|
|
||||||
if not defn.get('objects') and defn.get('rights'):
|
|
||||||
defn['objects'] = defn['rights']
|
|
||||||
|
|
||||||
out_dir_resolved = args.OutputDir if os.path.isabs(args.OutputDir) else os.path.join(os.getcwd(), args.OutputDir)
|
|
||||||
format_version = detect_format_version(out_dir_resolved)
|
|
||||||
|
|
||||||
# --- 2. Parse all object entries ---
|
|
||||||
parsed_objects = []
|
|
||||||
if defn.get('objects'):
|
|
||||||
for entry in defn['objects']:
|
|
||||||
parsed = parse_object_entry(entry)
|
|
||||||
if parsed:
|
|
||||||
parsed_objects.append(parsed)
|
|
||||||
|
|
||||||
# --- 3. Generate UUID ---
|
|
||||||
uid = new_uuid()
|
|
||||||
|
|
||||||
# --- 4. Emit metadata XML (Roles/Name.xml) ---
|
|
||||||
lines = []
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"')
|
|
||||||
lines.append(' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"')
|
|
||||||
lines.append(' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"')
|
|
||||||
lines.append(' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"')
|
|
||||||
lines.append(' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"')
|
|
||||||
lines.append(' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"')
|
|
||||||
lines.append(' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"')
|
|
||||||
lines.append(' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"')
|
|
||||||
lines.append(' xmlns:v8="http://v8.1c.ru/8.1/data/core"')
|
|
||||||
lines.append(' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"')
|
|
||||||
lines.append(' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"')
|
|
||||||
lines.append(' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"')
|
|
||||||
lines.append(' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"')
|
|
||||||
lines.append(' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"')
|
|
||||||
lines.append(' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"')
|
|
||||||
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
|
||||||
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
|
||||||
lines.append(f' version="{format_version}">')
|
|
||||||
lines.append(f' <Role uuid="{uid}">')
|
|
||||||
lines.append(' <Properties>')
|
|
||||||
lines.append(f' <Name>{role_name}</Name>')
|
|
||||||
lines.append(' <Synonym>')
|
|
||||||
lines.append(' <v8:item>')
|
|
||||||
lines.append(' <v8:lang>ru</v8:lang>')
|
|
||||||
lines.append(f' <v8:content>{esc_xml(synonym)}</v8:content>')
|
|
||||||
lines.append(' </v8:item>')
|
|
||||||
lines.append(' </Synonym>')
|
|
||||||
if comment:
|
|
||||||
lines.append(f' <Comment>{esc_xml(comment)}</Comment>')
|
|
||||||
else:
|
|
||||||
lines.append(' <Comment/>')
|
|
||||||
lines.append(' </Properties>')
|
|
||||||
lines.append(' </Role>')
|
|
||||||
lines.append('</MetaDataObject>')
|
|
||||||
|
|
||||||
metadata_xml = '\n'.join(lines) + '\n'
|
|
||||||
|
|
||||||
# --- 5. Emit Rights XML (Roles/Name/Ext/Rights.xml) ---
|
|
||||||
lines = []
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<Rights xmlns="http://v8.1c.ru/8.2/roles"')
|
|
||||||
lines.append(' xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
|
||||||
lines.append(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
|
|
||||||
lines.append(f' xsi:type="Rights" version="{format_version}">')
|
|
||||||
|
|
||||||
# Global flags
|
|
||||||
sfno = str(defn['setForNewObjects']).lower() if defn.get('setForNewObjects') is not None else 'false'
|
|
||||||
sfab = str(defn['setForAttributesByDefault']).lower() if defn.get('setForAttributesByDefault') is not None else 'true'
|
|
||||||
irco = str(defn['independentRightsOfChildObjects']).lower() if defn.get('independentRightsOfChildObjects') is not None else 'false'
|
|
||||||
|
|
||||||
lines.append(f' <setForNewObjects>{sfno}</setForNewObjects>')
|
|
||||||
lines.append(f' <setForAttributesByDefault>{sfab}</setForAttributesByDefault>')
|
|
||||||
lines.append(f' <independentRightsOfChildObjects>{irco}</independentRightsOfChildObjects>')
|
|
||||||
|
|
||||||
# Object blocks
|
|
||||||
total_rights = 0
|
|
||||||
for obj in parsed_objects:
|
|
||||||
lines.append(' <object>')
|
|
||||||
lines.append(f' <name>{obj["Name"]}</name>')
|
|
||||||
for right in obj['Rights']:
|
|
||||||
lines.append(' <right>')
|
|
||||||
lines.append(f' <name>{right["Name"]}</name>')
|
|
||||||
lines.append(f' <value>{right["Value"]}</value>')
|
|
||||||
if right['Condition']:
|
|
||||||
lines.append(' <restrictionByCondition>')
|
|
||||||
lines.append(f' <condition>{esc_xml(right["Condition"])}</condition>')
|
|
||||||
lines.append(' </restrictionByCondition>')
|
|
||||||
lines.append(' </right>')
|
|
||||||
total_rights += 1
|
|
||||||
lines.append(' </object>')
|
|
||||||
|
|
||||||
# RLS restriction templates
|
|
||||||
template_count = 0
|
|
||||||
if defn.get('templates'):
|
|
||||||
for tpl in defn['templates']:
|
|
||||||
lines.append(' <restrictionTemplate>')
|
|
||||||
lines.append(f' <name>{esc_xml(str(tpl["name"]))}</name>')
|
|
||||||
lines.append(f' <condition>{esc_xml(str(tpl["condition"]))}</condition>')
|
|
||||||
lines.append(' </restrictionTemplate>')
|
|
||||||
template_count += 1
|
|
||||||
|
|
||||||
lines.append('</Rights>')
|
|
||||||
|
|
||||||
rights_xml = '\n'.join(lines) + '\n'
|
|
||||||
|
|
||||||
# --- 6. Write output files ---
|
|
||||||
out_dir = args.OutputDir
|
|
||||||
if not os.path.isabs(out_dir):
|
|
||||||
out_dir = os.path.join(os.getcwd(), out_dir)
|
|
||||||
|
|
||||||
# Determine Roles dir and config root
|
|
||||||
# Back-compat: if OutputDir leaf is "Roles", use as-is; otherwise treat as config root
|
|
||||||
leaf = os.path.basename(out_dir.rstrip(os.sep).rstrip('/'))
|
|
||||||
if leaf == 'Roles':
|
|
||||||
roles_dir = out_dir
|
|
||||||
config_dir = os.path.dirname(out_dir)
|
|
||||||
else:
|
|
||||||
roles_dir = os.path.join(out_dir, 'Roles')
|
|
||||||
config_dir = out_dir
|
|
||||||
|
|
||||||
# Metadata: Roles/RoleName.xml
|
|
||||||
metadata_path = os.path.join(roles_dir, f'{role_name}.xml')
|
|
||||||
os.makedirs(roles_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Rights: Roles/RoleName/Ext/Rights.xml
|
|
||||||
role_sub_dir = os.path.join(roles_dir, role_name)
|
|
||||||
ext_dir = os.path.join(role_sub_dir, 'Ext')
|
|
||||||
rights_path = os.path.join(ext_dir, 'Rights.xml')
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
write_utf8_bom(metadata_path, metadata_xml)
|
|
||||||
write_utf8_bom(rights_path, rights_xml)
|
|
||||||
|
|
||||||
# --- 7. Register in Configuration.xml ---
|
|
||||||
config_xml_path = os.path.join(config_dir, 'Configuration.xml')
|
|
||||||
reg_result = None
|
|
||||||
|
|
||||||
if os.path.exists(config_xml_path):
|
|
||||||
with open(config_xml_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
raw_text = f.read()
|
|
||||||
|
|
||||||
# Check if already registered
|
|
||||||
if f'<Role>{role_name}</Role>' in raw_text:
|
|
||||||
reg_result = 'already'
|
|
||||||
else:
|
|
||||||
# Find last <Role>...</Role> and insert after it
|
|
||||||
role_pattern = re.compile(r'(<Role>[^<]*</Role>)')
|
|
||||||
matches = list(role_pattern.finditer(raw_text))
|
|
||||||
new_role_tag = f'<Role>{role_name}</Role>'
|
|
||||||
|
|
||||||
if matches:
|
|
||||||
# Insert after last existing <Role>
|
|
||||||
last_match = matches[-1]
|
|
||||||
insert_pos = last_match.end()
|
|
||||||
raw_text = raw_text[:insert_pos] + f'\n\t\t\t{new_role_tag}' + raw_text[insert_pos:]
|
|
||||||
else:
|
|
||||||
# No existing roles — insert before </ChildObjects>
|
|
||||||
raw_text = raw_text.replace('</ChildObjects>', f'\t\t\t{new_role_tag}\n\t\t</ChildObjects>')
|
|
||||||
|
|
||||||
write_utf8_bom(config_xml_path, raw_text)
|
|
||||||
reg_result = 'added'
|
|
||||||
else:
|
|
||||||
reg_result = 'no-config'
|
|
||||||
|
|
||||||
# --- 8. Summary ---
|
|
||||||
print(f"[OK] Role '{role_name}' compiled")
|
|
||||||
print(f" UUID: {uid}")
|
|
||||||
print(f" Metadata: {metadata_path}")
|
|
||||||
print(f" Rights: {rights_path}")
|
|
||||||
print(f" Objects: {len(parsed_objects)}, Rights: {total_rights}, Templates: {template_count}")
|
|
||||||
if reg_result == 'added':
|
|
||||||
print(f" Configuration.xml: <Role>{role_name}</Role> added to ChildObjects")
|
|
||||||
elif reg_result == 'already':
|
|
||||||
print(f" Configuration.xml: <Role>{role_name}</Role> already registered")
|
|
||||||
elif reg_result == 'no-childobj':
|
|
||||||
print(f"WARNING: Configuration.xml found but <ChildObjects> not found", file=sys.stderr)
|
|
||||||
elif reg_result == 'no-config':
|
|
||||||
print(f"WARNING: Configuration.xml not found at {config_xml_path} -- register manually", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
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,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,553 +0,0 @@
|
|||||||
# subsystem-compile v1.5 — 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-ChildSubsystemStub([string]$childPath, [string]$childName, [string]$formatVersion, [System.Text.Encoding]$utf8Bom) {
|
|
||||||
$childUuid = New-Guid-String
|
|
||||||
$sb = New-Object System.Text.StringBuilder 2048
|
|
||||||
[void]$sb.AppendLine('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
[void]$sb.AppendLine("<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`">")
|
|
||||||
[void]$sb.AppendLine("`t<Subsystem uuid=`"$childUuid`">")
|
|
||||||
[void]$sb.AppendLine("`t`t<Properties>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<Name>$(Esc-Xml $childName)</Name>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<Synonym/>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<Comment/>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<IncludeHelpInContents>true</IncludeHelpInContents>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<IncludeInCommandInterface>true</IncludeInCommandInterface>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<UseOneCommand>false</UseOneCommand>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<Explanation/>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<Picture/>")
|
|
||||||
[void]$sb.AppendLine("`t`t`t<Content/>")
|
|
||||||
[void]$sb.AppendLine("`t`t</Properties>")
|
|
||||||
[void]$sb.AppendLine("`t`t<ChildObjects/>")
|
|
||||||
[void]$sb.AppendLine("`t</Subsystem>")
|
|
||||||
[void]$sb.AppendLine('</MetaDataObject>')
|
|
||||||
[System.IO.File]::WriteAllText($childPath, $sb.ToString(), $utf8Bom)
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Content type normalization (plural→singular, Russian→English) ---
|
|
||||||
$script:contentTypeMap = @{
|
|
||||||
# Plural English → Singular
|
|
||||||
"Catalogs" = "Catalog"
|
|
||||||
"Documents" = "Document"
|
|
||||||
"Enums" = "Enum"
|
|
||||||
"Constants" = "Constant"
|
|
||||||
"Reports" = "Report"
|
|
||||||
"DataProcessors" = "DataProcessor"
|
|
||||||
"InformationRegisters" = "InformationRegister"
|
|
||||||
"AccumulationRegisters" = "AccumulationRegister"
|
|
||||||
"AccountingRegisters" = "AccountingRegister"
|
|
||||||
"CalculationRegisters" = "CalculationRegister"
|
|
||||||
"ChartsOfAccounts" = "ChartOfAccounts"
|
|
||||||
"ChartsOfCharacteristicTypes" = "ChartOfCharacteristicTypes"
|
|
||||||
"ChartsOfCalculationTypes" = "ChartOfCalculationTypes"
|
|
||||||
"BusinessProcesses" = "BusinessProcess"
|
|
||||||
"Tasks" = "Task"
|
|
||||||
"ExchangePlans" = "ExchangePlan"
|
|
||||||
"DocumentJournals" = "DocumentJournal"
|
|
||||||
"CommonModules" = "CommonModule"
|
|
||||||
"CommonCommands" = "CommonCommand"
|
|
||||||
"CommonForms" = "CommonForm"
|
|
||||||
"CommonPictures" = "CommonPicture"
|
|
||||||
"CommonTemplates" = "CommonTemplate"
|
|
||||||
"CommonAttributes" = "CommonAttribute"
|
|
||||||
"CommandGroups" = "CommandGroup"
|
|
||||||
"Roles" = "Role"
|
|
||||||
"SessionParameters" = "SessionParameter"
|
|
||||||
"FilterCriteria" = "FilterCriterion"
|
|
||||||
"XDTOPackages" = "XDTOPackage"
|
|
||||||
"WebServices" = "WebService"
|
|
||||||
"HTTPServices" = "HTTPService"
|
|
||||||
"WSReferences" = "WSReference"
|
|
||||||
"EventSubscriptions" = "EventSubscription"
|
|
||||||
"ScheduledJobs" = "ScheduledJob"
|
|
||||||
"SettingsStorages" = "SettingsStorage"
|
|
||||||
"FunctionalOptions" = "FunctionalOption"
|
|
||||||
"FunctionalOptionsParameters" = "FunctionalOptionsParameter"
|
|
||||||
"DefinedTypes" = "DefinedType"
|
|
||||||
"DocumentNumerators" = "DocumentNumerator"
|
|
||||||
"Sequences" = "Sequence"
|
|
||||||
"Subsystems" = "Subsystem"
|
|
||||||
"StyleItems" = "StyleItem"
|
|
||||||
"IntegrationServices" = "IntegrationService"
|
|
||||||
# Russian singular → English
|
|
||||||
"Справочник" = "Catalog"
|
|
||||||
"Каталог" = "Catalog"
|
|
||||||
"Документ" = "Document"
|
|
||||||
"Перечисление" = "Enum"
|
|
||||||
"Константа" = "Constant"
|
|
||||||
"Отчёт" = "Report"
|
|
||||||
"Отчет" = "Report"
|
|
||||||
"Обработка" = "DataProcessor"
|
|
||||||
"РегистрСведений" = "InformationRegister"
|
|
||||||
"РегистрНакопления" = "AccumulationRegister"
|
|
||||||
"РегистрБухгалтерии" = "AccountingRegister"
|
|
||||||
"РегистрРасчёта" = "CalculationRegister"
|
|
||||||
"РегистрРасчета" = "CalculationRegister"
|
|
||||||
"ПланСчетов" = "ChartOfAccounts"
|
|
||||||
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
|
|
||||||
"ПланВидовРасчёта" = "ChartOfCalculationTypes"
|
|
||||||
"ПланВидовРасчета" = "ChartOfCalculationTypes"
|
|
||||||
"БизнесПроцесс" = "BusinessProcess"
|
|
||||||
"Задача" = "Task"
|
|
||||||
"ПланОбмена" = "ExchangePlan"
|
|
||||||
"ЖурналДокументов" = "DocumentJournal"
|
|
||||||
"ОбщийМодуль" = "CommonModule"
|
|
||||||
"ОбщаяКоманда" = "CommonCommand"
|
|
||||||
"ОбщаяФорма" = "CommonForm"
|
|
||||||
"ОбщаяКартинка" = "CommonPicture"
|
|
||||||
"ОбщийМакет" = "CommonTemplate"
|
|
||||||
"ОбщийРеквизит" = "CommonAttribute"
|
|
||||||
"ГруппаКоманд" = "CommandGroup"
|
|
||||||
"Роль" = "Role"
|
|
||||||
"ПараметрСеанса" = "SessionParameter"
|
|
||||||
"КритерийОтбора" = "FilterCriterion"
|
|
||||||
"ПакетXDTO" = "XDTOPackage"
|
|
||||||
"ВебСервис" = "WebService"
|
|
||||||
"HTTPСервис" = "HTTPService"
|
|
||||||
"WSСсылка" = "WSReference"
|
|
||||||
"ПодпискаНаСобытие" = "EventSubscription"
|
|
||||||
"РегламентноеЗадание" = "ScheduledJob"
|
|
||||||
"ХранилищеНастроек" = "SettingsStorage"
|
|
||||||
"ФункциональнаяОпция" = "FunctionalOption"
|
|
||||||
"ПараметрФункциональныхОпций" = "FunctionalOptionsParameter"
|
|
||||||
"ОпределяемыйТип" = "DefinedType"
|
|
||||||
"НумераторДокументов" = "DocumentNumerator"
|
|
||||||
"Последовательность" = "Sequence"
|
|
||||||
"Подсистема" = "Subsystem"
|
|
||||||
"ЭлементСтиля" = "StyleItem"
|
|
||||||
"СервисИнтеграции" = "IntegrationService"
|
|
||||||
# Russian plural → English
|
|
||||||
"Справочники" = "Catalog"
|
|
||||||
"Документы" = "Document"
|
|
||||||
"Перечисления" = "Enum"
|
|
||||||
"Константы" = "Constant"
|
|
||||||
"Отчёты" = "Report"
|
|
||||||
"Отчеты" = "Report"
|
|
||||||
"Обработки" = "DataProcessor"
|
|
||||||
"РегистрыСведений" = "InformationRegister"
|
|
||||||
"РегистрыНакопления" = "AccumulationRegister"
|
|
||||||
"РегистрыБухгалтерии" = "AccountingRegister"
|
|
||||||
"РегистрыРасчёта" = "CalculationRegister"
|
|
||||||
"РегистрыРасчета" = "CalculationRegister"
|
|
||||||
"ПланыСчетов" = "ChartOfAccounts"
|
|
||||||
"ПланыВидовХарактеристик" = "ChartOfCharacteristicTypes"
|
|
||||||
"ПланыВидовРасчёта" = "ChartOfCalculationTypes"
|
|
||||||
"ПланыВидовРасчета" = "ChartOfCalculationTypes"
|
|
||||||
"БизнесПроцессы" = "BusinessProcess"
|
|
||||||
"Задачи" = "Task"
|
|
||||||
"ПланыОбмена" = "ExchangePlan"
|
|
||||||
"ЖурналыДокументов" = "DocumentJournal"
|
|
||||||
"ОбщиеМодули" = "CommonModule"
|
|
||||||
"ОбщиеКоманды" = "CommonCommand"
|
|
||||||
"ОбщиеФормы" = "CommonForm"
|
|
||||||
"ОбщиеКартинки" = "CommonPicture"
|
|
||||||
"ОбщиеМакеты" = "CommonTemplate"
|
|
||||||
"ОбщиеРеквизиты" = "CommonAttribute"
|
|
||||||
"ГруппыКоманд" = "CommandGroup"
|
|
||||||
"Роли" = "Role"
|
|
||||||
"ПараметрыСеанса" = "SessionParameter"
|
|
||||||
"КритерииОтбора" = "FilterCriterion"
|
|
||||||
"ПакетыXDTO" = "XDTOPackage"
|
|
||||||
"ВебСервисы" = "WebService"
|
|
||||||
"HTTPСервисы" = "HTTPService"
|
|
||||||
"WSСсылки" = "WSReference"
|
|
||||||
"ПодпискиНаСобытия" = "EventSubscription"
|
|
||||||
"РегламентныеЗадания" = "ScheduledJob"
|
|
||||||
"ХранилищаНастроек" = "SettingsStorage"
|
|
||||||
"ФункциональныеОпции" = "FunctionalOption"
|
|
||||||
"ОпределяемыеТипы" = "DefinedType"
|
|
||||||
"Подсистемы" = "Subsystem"
|
|
||||||
"ЭлементыСтиля" = "StyleItem"
|
|
||||||
"СервисыИнтеграции" = "IntegrationService"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Normalize-ContentRef([string]$ref) {
|
|
||||||
if (-not $ref -or -not $ref.Contains('.')) { return $ref }
|
|
||||||
$dotIdx = $ref.IndexOf('.')
|
|
||||||
$typePart = $ref.Substring(0, $dotIdx)
|
|
||||||
$namePart = $ref.Substring($dotIdx + 1)
|
|
||||||
if ($script:contentTypeMap.ContainsKey($typePart)) {
|
|
||||||
$typePart = $script:contentTypeMap[$typePart]
|
|
||||||
}
|
|
||||||
return "$typePart.$namePart"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. 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 { "" }
|
|
||||||
|
|
||||||
# Synonym: accept "objects" as alias for "content"
|
|
||||||
if (-not $def.content -and $def.objects) { $def | Add-Member -NotePropertyName content -NotePropertyValue $def.objects }
|
|
||||||
|
|
||||||
$contentItems = @()
|
|
||||||
$normalizedCount = 0
|
|
||||||
if ($def.content) {
|
|
||||||
foreach ($c in $def.content) {
|
|
||||||
$raw = "$c"
|
|
||||||
$normalized = Normalize-ContentRef $raw
|
|
||||||
if ($normalized -ne $raw) {
|
|
||||||
Write-Host "[NORM] Content: $raw -> $normalized"
|
|
||||||
$normalizedCount++
|
|
||||||
}
|
|
||||||
$contentItems += $normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($normalizedCount -gt 0) {
|
|
||||||
Write-Host "[INFO] Normalized $normalizedCount content reference(s) to singular English form"
|
|
||||||
}
|
|
||||||
|
|
||||||
$children = @()
|
|
||||||
if ($def.children) {
|
|
||||||
foreach ($ch in $def.children) { $children += "$ch" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatVersion = Detect-FormatVersion $OutputDir
|
|
||||||
|
|
||||||
# --- 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=`"$formatVersion`">"
|
|
||||||
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 and stub files for children if they 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"
|
|
||||||
}
|
|
||||||
$seen = @{}
|
|
||||||
foreach ($ch in $children) {
|
|
||||||
if ($seen.ContainsKey($ch)) { continue }
|
|
||||||
$seen[$ch] = $true
|
|
||||||
$childXml = Join-Path $childSubsDir "$ch.xml"
|
|
||||||
if (-not (Test-Path $childXml)) {
|
|
||||||
Write-ChildSubsystemStub $childXml $ch $formatVersion $utf8Bom
|
|
||||||
Write-Host "[OK] Created stub: $childXml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 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,450 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# subsystem-compile v1.5 — 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 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 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 write_child_subsystem_stub(child_path, child_name, format_version):
|
|
||||||
child_uuid = 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" '
|
|
||||||
f'version="{format_version}">'
|
|
||||||
)
|
|
||||||
lines.append(f'\t<Subsystem uuid="{child_uuid}">')
|
|
||||||
lines.append('\t\t<Properties>')
|
|
||||||
lines.append(f'\t\t\t<Name>{esc_xml(child_name)}</Name>')
|
|
||||||
lines.append('\t\t\t<Synonym/>')
|
|
||||||
lines.append('\t\t\t<Comment/>')
|
|
||||||
lines.append('\t\t\t<IncludeHelpInContents>true</IncludeHelpInContents>')
|
|
||||||
lines.append('\t\t\t<IncludeInCommandInterface>true</IncludeInCommandInterface>')
|
|
||||||
lines.append('\t\t\t<UseOneCommand>false</UseOneCommand>')
|
|
||||||
lines.append('\t\t\t<Explanation/>')
|
|
||||||
lines.append('\t\t\t<Picture/>')
|
|
||||||
lines.append('\t\t\t<Content/>')
|
|
||||||
lines.append('\t\t</Properties>')
|
|
||||||
lines.append('\t\t<ChildObjects/>')
|
|
||||||
lines.append('\t</Subsystem>')
|
|
||||||
lines.append('</MetaDataObject>')
|
|
||||||
write_utf8_bom(child_path, '\n'.join(lines) + '\n')
|
|
||||||
|
|
||||||
|
|
||||||
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. Content type normalization (plural→singular, Russian→English) ---
|
|
||||||
CONTENT_TYPE_MAP = {
|
|
||||||
# Plural English → Singular
|
|
||||||
'Catalogs': 'Catalog', 'Documents': 'Document', 'Enums': 'Enum',
|
|
||||||
'Constants': 'Constant', 'Reports': 'Report', 'DataProcessors': 'DataProcessor',
|
|
||||||
'InformationRegisters': 'InformationRegister', 'AccumulationRegisters': 'AccumulationRegister',
|
|
||||||
'AccountingRegisters': 'AccountingRegister', 'CalculationRegisters': 'CalculationRegister',
|
|
||||||
'ChartsOfAccounts': 'ChartOfAccounts', 'ChartsOfCharacteristicTypes': 'ChartOfCharacteristicTypes',
|
|
||||||
'ChartsOfCalculationTypes': 'ChartOfCalculationTypes',
|
|
||||||
'BusinessProcesses': 'BusinessProcess', 'Tasks': 'Task',
|
|
||||||
'ExchangePlans': 'ExchangePlan', 'DocumentJournals': 'DocumentJournal',
|
|
||||||
'CommonModules': 'CommonModule', 'CommonCommands': 'CommonCommand',
|
|
||||||
'CommonForms': 'CommonForm', 'CommonPictures': 'CommonPicture',
|
|
||||||
'CommonTemplates': 'CommonTemplate', 'CommonAttributes': 'CommonAttribute',
|
|
||||||
'CommandGroups': 'CommandGroup', 'Roles': 'Role',
|
|
||||||
'SessionParameters': 'SessionParameter', 'FilterCriteria': 'FilterCriterion',
|
|
||||||
'XDTOPackages': 'XDTOPackage', 'WebServices': 'WebService',
|
|
||||||
'HTTPServices': 'HTTPService', 'WSReferences': 'WSReference',
|
|
||||||
'EventSubscriptions': 'EventSubscription', 'ScheduledJobs': 'ScheduledJob',
|
|
||||||
'SettingsStorages': 'SettingsStorage', 'FunctionalOptions': 'FunctionalOption',
|
|
||||||
'FunctionalOptionsParameters': 'FunctionalOptionsParameter',
|
|
||||||
'DefinedTypes': 'DefinedType', 'DocumentNumerators': 'DocumentNumerator',
|
|
||||||
'Sequences': 'Sequence', 'Subsystems': 'Subsystem',
|
|
||||||
'StyleItems': 'StyleItem', 'IntegrationServices': 'IntegrationService',
|
|
||||||
# Russian singular → English
|
|
||||||
'Справочник': 'Catalog', 'Каталог': 'Catalog', 'Документ': 'Document',
|
|
||||||
'Перечисление': 'Enum', 'Константа': 'Constant',
|
|
||||||
'Отчёт': 'Report', 'Отчет': 'Report', 'Обработка': 'DataProcessor',
|
|
||||||
'РегистрСведений': 'InformationRegister', 'РегистрНакопления': 'AccumulationRegister',
|
|
||||||
'РегистрБухгалтерии': 'AccountingRegister',
|
|
||||||
'РегистрРасчёта': 'CalculationRegister', 'РегистрРасчета': 'CalculationRegister',
|
|
||||||
'ПланСчетов': 'ChartOfAccounts', 'ПланВидовХарактеристик': 'ChartOfCharacteristicTypes',
|
|
||||||
'ПланВидовРасчёта': 'ChartOfCalculationTypes', 'ПланВидовРасчета': 'ChartOfCalculationTypes',
|
|
||||||
'БизнесПроцесс': 'BusinessProcess', 'Задача': 'Task',
|
|
||||||
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
|
|
||||||
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
|
|
||||||
'ОбщаяФорма': 'CommonForm', 'ОбщаяКартинка': 'CommonPicture',
|
|
||||||
'ОбщийМакет': 'CommonTemplate', 'ОбщийРеквизит': 'CommonAttribute',
|
|
||||||
'ГруппаКоманд': 'CommandGroup', 'Роль': 'Role',
|
|
||||||
'ПараметрСеанса': 'SessionParameter', 'КритерийОтбора': 'FilterCriterion',
|
|
||||||
'ПакетXDTO': 'XDTOPackage', 'ВебСервис': 'WebService',
|
|
||||||
'HTTPСервис': 'HTTPService', 'WSСсылка': 'WSReference',
|
|
||||||
'ПодпискаНаСобытие': 'EventSubscription', 'РегламентноеЗадание': 'ScheduledJob',
|
|
||||||
'ХранилищеНастроек': 'SettingsStorage', 'ФункциональнаяОпция': 'FunctionalOption',
|
|
||||||
'ПараметрФункциональныхОпций': 'FunctionalOptionsParameter',
|
|
||||||
'ОпределяемыйТип': 'DefinedType', 'НумераторДокументов': 'DocumentNumerator',
|
|
||||||
'Последовательность': 'Sequence', 'Подсистема': 'Subsystem',
|
|
||||||
'ЭлементСтиля': 'StyleItem', 'СервисИнтеграции': 'IntegrationService',
|
|
||||||
# Russian plural → English
|
|
||||||
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
|
|
||||||
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report',
|
|
||||||
'Обработки': 'DataProcessor', 'РегистрыСведений': 'InformationRegister',
|
|
||||||
'РегистрыНакопления': 'AccumulationRegister', 'РегистрыБухгалтерии': 'AccountingRegister',
|
|
||||||
'РегистрыРасчёта': 'CalculationRegister', 'РегистрыРасчета': 'CalculationRegister',
|
|
||||||
'ПланыСчетов': 'ChartOfAccounts', 'ПланыВидовХарактеристик': 'ChartOfCharacteristicTypes',
|
|
||||||
'ПланыВидовРасчёта': 'ChartOfCalculationTypes', 'ПланыВидовРасчета': 'ChartOfCalculationTypes',
|
|
||||||
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
|
|
||||||
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
|
|
||||||
'ОбщиеМодули': 'CommonModule', 'ОбщиеКоманды': 'CommonCommand',
|
|
||||||
'ОбщиеФормы': 'CommonForm', 'ОбщиеКартинки': 'CommonPicture',
|
|
||||||
'ОбщиеМакеты': 'CommonTemplate', 'ОбщиеРеквизиты': 'CommonAttribute',
|
|
||||||
'ГруппыКоманд': 'CommandGroup', 'Роли': 'Role',
|
|
||||||
'ПараметрыСеанса': 'SessionParameter', 'КритерииОтбора': 'FilterCriterion',
|
|
||||||
'ПакетыXDTO': 'XDTOPackage', 'ВебСервисы': 'WebService',
|
|
||||||
'HTTPСервисы': 'HTTPService', 'WSСсылки': 'WSReference',
|
|
||||||
'ПодпискиНаСобытия': 'EventSubscription', 'РегламентныеЗадания': 'ScheduledJob',
|
|
||||||
'ХранилищаНастроек': 'SettingsStorage', 'ФункциональныеОпции': 'FunctionalOption',
|
|
||||||
'ОпределяемыеТипы': 'DefinedType', 'Подсистемы': 'Subsystem',
|
|
||||||
'ЭлементыСтиля': 'StyleItem', 'СервисыИнтеграции': 'IntegrationService',
|
|
||||||
}
|
|
||||||
|
|
||||||
def normalize_content_ref(ref):
|
|
||||||
if not ref or '.' not in ref:
|
|
||||||
return ref
|
|
||||||
dot_idx = ref.index('.')
|
|
||||||
type_part = ref[:dot_idx]
|
|
||||||
name_part = ref[dot_idx + 1:]
|
|
||||||
if type_part in CONTENT_TYPE_MAP:
|
|
||||||
type_part = CONTENT_TYPE_MAP[type_part]
|
|
||||||
return f'{type_part}.{name_part}'
|
|
||||||
|
|
||||||
format_version = detect_format_version(output_dir)
|
|
||||||
|
|
||||||
# --- 3. 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 ''
|
|
||||||
|
|
||||||
# Synonym: accept "objects" as alias for "content"
|
|
||||||
if not defn.get('content') and defn.get('objects'):
|
|
||||||
defn['content'] = defn['objects']
|
|
||||||
|
|
||||||
content_items = []
|
|
||||||
normalized_count = 0
|
|
||||||
if defn.get('content'):
|
|
||||||
for c in defn['content']:
|
|
||||||
raw = str(c)
|
|
||||||
normalized = normalize_content_ref(raw)
|
|
||||||
if normalized != raw:
|
|
||||||
print(f'[NORM] Content: {raw} -> {normalized}')
|
|
||||||
normalized_count += 1
|
|
||||||
content_items.append(normalized)
|
|
||||||
if normalized_count > 0:
|
|
||||||
print(f'[INFO] Normalized {normalized_count} content reference(s) to singular English form')
|
|
||||||
|
|
||||||
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(f'<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}">')
|
|
||||||
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 and stub files for children if they 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}")
|
|
||||||
seen = set()
|
|
||||||
for ch in children:
|
|
||||||
if ch in seen:
|
|
||||||
continue
|
|
||||||
seen.add(ch)
|
|
||||||
child_xml = os.path.join(child_subs_dir, f'{ch}.xml')
|
|
||||||
if not os.path.exists(child_xml):
|
|
||||||
write_child_subsystem_stub(child_xml, ch, format_version)
|
|
||||||
print(f"[OK] Created stub: {child_xml}")
|
|
||||||
|
|
||||||
# --- 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,243 +0,0 @@
|
|||||||
# template-add v1.3 — 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`nОжидается: <SrcDir>/<ObjectName>/<ObjectName>.xml`nПодсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации"
|
|
||||||
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)
|
|
||||||
|
|
||||||
# --- Detect format version ---
|
|
||||||
|
|
||||||
function Detect-FormatVersion([string]$dir) {
|
|
||||||
$d = $dir
|
|
||||||
while ($d) {
|
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
|
||||||
if (Test-Path $cfgPath) {
|
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
|
||||||
}
|
|
||||||
$parent = Split-Path $d -Parent
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return "2.17"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
|
|
||||||
|
|
||||||
# --- 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=`"$formatVersion`">
|
|
||||||
<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,274 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-template v1.3 — Add template to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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 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 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]
|
|
||||||
|
|
||||||
format_version = detect_format_version(os.path.abspath(src_dir))
|
|
||||||
|
|
||||||
# --- 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)
|
|
||||||
print(f"Ожидается: <SrcDir>/<ObjectName>/<ObjectName>.xml", file=sys.stderr)
|
|
||||||
print(f"Подсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации", 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"'
|
|
||||||
f' version="{format_version}">\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()
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# remove-template v1.0 — Remove template from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
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"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
|
||||||
if not xml_bytes.endswith(b"\n"):
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
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="Remove template from 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-TemplateName", required=True)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
template_name = args.TemplateName
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- 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")
|
|
||||||
template_dir = os.path.join(templates_dir, template_name)
|
|
||||||
|
|
||||||
if not os.path.exists(template_meta_path):
|
|
||||||
print(f"Метаданные макета не найдены: {template_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Delete files ---
|
|
||||||
|
|
||||||
if os.path.isdir(template_dir):
|
|
||||||
shutil.rmtree(template_dir)
|
|
||||||
print(f"[OK] Удалён каталог: {template_dir}")
|
|
||||||
|
|
||||||
os.remove(template_meta_path)
|
|
||||||
print(f"[OK] Удалён файл: {template_meta_path}")
|
|
||||||
|
|
||||||
# --- 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()
|
|
||||||
|
|
||||||
# Remove <Template>TemplateName</Template> from ChildObjects
|
|
||||||
for node in root.findall(".//md:ChildObjects/md:Template", NSMAP):
|
|
||||||
if node.text and node.text.strip() == template_name:
|
|
||||||
parent = node.getparent()
|
|
||||||
prev = node.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
# Whitespace is in prev.tail
|
|
||||||
if prev.tail and prev.tail.strip() == "":
|
|
||||||
prev.tail = ""
|
|
||||||
else:
|
|
||||||
# First child — whitespace is in parent.text
|
|
||||||
if parent.text and parent.text.strip() == "":
|
|
||||||
parent.text = ""
|
|
||||||
parent.remove(node)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Clear MainDataCompositionSchema if it pointed to this template
|
|
||||||
main_dcs = root.find(".//md:MainDataCompositionSchema", NSMAP)
|
|
||||||
if main_dcs is not None and main_dcs.text:
|
|
||||||
if re.search(rf"Template\.{re.escape(template_name)}$", main_dcs.text):
|
|
||||||
main_dcs.text = ""
|
|
||||||
print("[OK] Очищён MainDataCompositionSchema")
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Макет {template_name} удалён из {root_xml_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
name: web-info
|
|
||||||
description: Статус Apache и веб-публикаций 1С — запущен ли сервер, какие базы опубликованы, ошибки. Используй когда пользователь спрашивает про статус веб-сервера, опубликованные базы, работает ли Apache
|
|
||||||
argument-hint: ""
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /web-info — Статус Apache и публикаций 1С
|
|
||||||
|
|
||||||
Показывает состояние Apache HTTP Server, список опубликованных баз и последние ошибки.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/web-info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры подключения
|
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта. Если задан `webPath` — используй как `-ApachePath`.
|
|
||||||
По умолчанию `tools/apache24` от корня проекта.
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/web-info/scripts/web-info.ps1 <параметры>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры скрипта
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|----------|:------------:|----------|
|
|
||||||
| `-ApachePath <путь>` | нет | Корень Apache (по умолчанию `tools/apache24`) |
|
|
||||||
|
|
||||||
## Формат вывода
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Apache Web Server ===
|
|
||||||
Status: Запущен (PID: 12345)
|
|
||||||
Path: C:\...\tools\apache24
|
|
||||||
Port: 8081
|
|
||||||
Module: C:/Program Files/1cv8/8.3.24.1691/bin/wsap24.dll
|
|
||||||
|
|
||||||
=== Опубликованные базы ===
|
|
||||||
mydb http://localhost:8081/mydb File="C:\Bases\MyDB";
|
|
||||||
|
|
||||||
=== Последние ошибки ===
|
|
||||||
(пусто)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Статус по умолчанию
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/web-info/scripts/web-info.ps1
|
|
||||||
|
|
||||||
# Указать путь к Apache
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/web-info/scripts/web-info.ps1 -ApachePath "C:\tools\apache24"
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,378 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// web-test run v1.3 — 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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize Windows backslash paths to prevent JS parse errors
|
|
||||||
// (e.g. C:\Users\... → \u triggers "Invalid Unicode escape sequence")
|
|
||||||
code = code.replace(/[A-Za-z]:\\[^\s'"`;\n)}\]]+/g, m => m.replace(/\\/g, '/'));
|
|
||||||
|
|
||||||
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)`);
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -1,58 +1,60 @@
|
|||||||
---
|
---
|
||||||
name: cf-edit
|
name: cf-edit
|
||||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
|
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
|
||||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
- Write
|
- Write
|
||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /cf-edit — редактирование конфигурации 1С
|
# /cf-edit — редактирование конфигурации 1С
|
||||||
|
|
||||||
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
|
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
|
||||||
|
|
||||||
## Параметры и команда
|
## Параметры и команда
|
||||||
|
|
||||||
| Параметр | Описание |
|
| Параметр | Описание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
| `Operation` | Операция (см. таблицу) |
|
| `Operation` | Операция (см. таблицу) |
|
||||||
| `Value` | Значение для операции (batch через `;;`) |
|
| `Value` | Значение для операции (batch через `;;`) |
|
||||||
| `DefinitionFile` | JSON-файл с массивом операций |
|
| `DefinitionFile` | JSON-файл с массивом операций |
|
||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
python ".github/skills/cf-edit/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
|
|
||||||
| Операция | Формат Value | Описание |
|
| Операция | Формат Value | Описание |
|
||||||
|----------|-------------|----------|
|
|----------|-------------|----------|
|
||||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||||
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||||
|
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
|
||||||
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
|
||||||
|
|
||||||
## Примеры
|
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
||||||
|
|
||||||
```powershell
|
## Примеры
|
||||||
# Изменить версию и поставщика
|
|
||||||
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
```powershell
|
||||||
|
# Изменить версию и поставщика
|
||||||
# Добавить объекты
|
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||||
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
|
||||||
|
# Добавить объекты
|
||||||
# Удалить объект
|
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||||
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
|
|
||||||
|
# Удалить объект
|
||||||
# Роли по умолчанию
|
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||||
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
|
|
||||||
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
# Роли по умолчанию
|
||||||
```
|
... -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
|
name: cf-info
|
||||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||||
argument-hint: <ConfigPath> [-Mode overview|brief|full]
|
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /cf-info — Структура конфигурации 1С
|
# /cf-info — Структура конфигурации 1С
|
||||||
|
|
||||||
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
|
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
|
||||||
|
|
||||||
## Параметры и команда
|
## Параметры и команда
|
||||||
|
|
||||||
| Параметр | Описание |
|
| Параметр | Описание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
|
||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||||
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
|
```powershell
|
||||||
```
|
python ".github/skills/cf-info/scripts/cf-info.py" -ConfigPath "<путь>"
|
||||||
|
```
|
||||||
## Три режима
|
|
||||||
|
## Три режима
|
||||||
| Режим | Что показывает |
|
|
||||||
|---|---|
|
| Режим | Что показывает |
|
||||||
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
|
|---|---|
|
||||||
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
|
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
|
||||||
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
|
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
|
||||||
|
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
|
||||||
## Примеры
|
|
||||||
|
## Примеры
|
||||||
```powershell
|
|
||||||
# Обзор пустой конфигурации
|
```powershell
|
||||||
... -ConfigPath upload/cfempty
|
# Обзор пустой конфигурации
|
||||||
|
... -ConfigPath src
|
||||||
# Краткая сводка реальной конфигурации
|
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode brief
|
# Краткая сводка реальной конфигурации
|
||||||
|
... -ConfigPath src -Mode brief
|
||||||
# Полная информация
|
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode full
|
# Полная информация
|
||||||
|
... -ConfigPath src -Mode full
|
||||||
# С пагинацией
|
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
|
# С пагинацией
|
||||||
```
|
... -ConfigPath src -Mode full -Limit 50 -Offset 100
|
||||||
|
|
||||||
|
# Drill-down: только начальная страница (раскладка форм с ролями)
|
||||||
|
... -ConfigPath src -Section home-page
|
||||||
|
```
|
||||||
+656
-387
File diff suppressed because it is too large
Load Diff
+265
-11
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
# cf-info v1.7 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -11,14 +12,37 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
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("-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("-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("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Output helper (collect all, paginate at the end) ---
|
# --- Output helper (collect all, paginate at the end) ---
|
||||||
lines_buf = []
|
lines_buf = []
|
||||||
@@ -37,11 +61,11 @@ if os.path.isdir(config_path):
|
|||||||
if os.path.isfile(candidate):
|
if os.path.isfile(candidate):
|
||||||
config_path = candidate
|
config_path = candidate
|
||||||
else:
|
else:
|
||||||
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
|
print(f"[ERROR] No Configuration.xml found in directory: {config_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not os.path.isfile(config_path):
|
if not os.path.isfile(config_path):
|
||||||
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
|
print(f"[ERROR] File not found: {config_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Load XML ---
|
# --- Load XML ---
|
||||||
@@ -58,12 +82,12 @@ NS = {
|
|||||||
|
|
||||||
md_root = xml_root # root is MetaDataObject itself
|
md_root = xml_root # root is MetaDataObject itself
|
||||||
if etree.QName(md_root.tag).localname != "MetaDataObject":
|
if etree.QName(md_root.tag).localname != "MetaDataObject":
|
||||||
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
|
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
cfg_node = md_root.find("md:Configuration", NS)
|
cfg_node = md_root.find("md:Configuration", NS)
|
||||||
if cfg_node is None:
|
if cfg_node is None:
|
||||||
print("[ERROR] No <Configuration> element found", file=sys.stderr)
|
print("[ERROR] No <Configuration> element found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
version = md_root.get("version", "")
|
version = md_root.get("version", "")
|
||||||
@@ -93,7 +117,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -109,6 +133,7 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
|
"Bot": "Боты",
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
@@ -125,6 +150,173 @@ type_ru_names = {
|
|||||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
"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 ---
|
# --- Count objects in ChildObjects ---
|
||||||
object_counts = OrderedDict()
|
object_counts = OrderedDict()
|
||||||
total_objects = 0
|
total_objects = 0
|
||||||
@@ -146,6 +338,7 @@ cfg_version = get_prop_text("Version")
|
|||||||
cfg_vendor = get_prop_text("Vendor")
|
cfg_vendor = get_prop_text("Vendor")
|
||||||
cfg_compat = get_prop_text("CompatibilityMode")
|
cfg_compat = get_prop_text("CompatibilityMode")
|
||||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||||
|
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||||
cfg_script = get_prop_text("ScriptVariant")
|
cfg_script = get_prop_text("ScriptVariant")
|
||||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||||
@@ -159,14 +352,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
|
|||||||
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if args.Mode == "brief":
|
if args.Mode == "brief" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
||||||
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if args.Mode == "overview":
|
if args.Mode == "overview" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -178,6 +371,8 @@ if args.Mode == "overview":
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
out(f"Совместимость: {cfg_compat}")
|
out(f"Совместимость: {cfg_compat}")
|
||||||
out(f"Режим запуска: {cfg_default_run}")
|
out(f"Режим запуска: {cfg_default_run}")
|
||||||
out(f"Язык скриптов: {cfg_script}")
|
out(f"Язык скриптов: {cfg_script}")
|
||||||
@@ -187,6 +382,20 @@ if args.Mode == "overview":
|
|||||||
out(f"Интерфейс: {cfg_intf_compat}")
|
out(f"Интерфейс: {cfg_intf_compat}")
|
||||||
out()
|
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
|
# Object counts table
|
||||||
out(f"--- Состав ({total_objects} объектов) ---")
|
out(f"--- Состав ({total_objects} объектов) ---")
|
||||||
out()
|
out()
|
||||||
@@ -207,7 +416,30 @@ if args.Mode == "overview":
|
|||||||
out(f" {padded} {count}")
|
out(f" {padded} {count}")
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- 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 ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -229,6 +461,8 @@ if args.Mode == "full":
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||||
if cfg_update_addr:
|
if cfg_update_addr:
|
||||||
out(f"Каталог обн.: {cfg_update_addr}")
|
out(f"Каталог обн.: {cfg_update_addr}")
|
||||||
@@ -283,6 +517,26 @@ if args.Mode == "full":
|
|||||||
out(f"Обычн.формы в управл.: {use_of}")
|
out(f"Обычн.формы в управл.: {use_of}")
|
||||||
out()
|
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 ---
|
# --- Section: Storages & default forms ---
|
||||||
out("--- Хранилища и формы по умолчанию ---")
|
out("--- Хранилища и формы по умолчанию ---")
|
||||||
storage_props = [
|
storage_props = [
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
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`) |
|
||||||
|
| `FormatVersion` | Версия формата выгрузки (default: `2.17`) |
|
||||||
|
|
||||||
|
Оба параметра задаются от **целевой платформы** — той, на которой конфигурация будет работать, — но по
|
||||||
|
разным правилам.
|
||||||
|
|
||||||
|
`FormatVersion` — **не выше** версии формата целевой платформы: 8.3.24 — `2.17`, 8.3.25 — `2.18`,
|
||||||
|
8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно: платформа читает свой формат и любой
|
||||||
|
более старый, поэтому дефолт `2.17` подходит для всей линейки 8.3.24 и выше.
|
||||||
|
|
||||||
|
`CompatibilityMode` влияет на доступную функциональность. Если в задаче не оговорено иное — **равен
|
||||||
|
целевой платформе** (`Version8_3_27` для 8.3.27); это то же самое, что «Не использовать» в
|
||||||
|
Конфигураторе, и именно такое значение платформа проставляет новой базе. Более низкий режим берут
|
||||||
|
осознанно — когда конфигурация должна работать и на старых платформах; он отключает возможности,
|
||||||
|
появившиеся позже. Выше целевой платформы — нельзя: такая конфигурация загрузится, но работать на ней
|
||||||
|
не будет.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".github/skills/cf-init/scripts/cf-init.py" -Name "МояКонфигурация"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Базовая конфигурация
|
||||||
|
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
|
||||||
|
|
||||||
|
# С версией и поставщиком
|
||||||
|
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
|
||||||
|
|
||||||
|
# Под платформу 8.3.27 — версия формата и режим совместимости вместе
|
||||||
|
... -Name TestCfg -FormatVersion 2.20 -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 — валидировать
|
||||||
|
```
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Synonym = $Name,
|
||||||
|
[string]$OutputDir = "src",
|
||||||
|
[string]$Version,
|
||||||
|
[string]$Vendor,
|
||||||
|
[string]$CompatibilityMode = "Version8_3_24",
|
||||||
|
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||||
|
# совместимости она не зависит. Дефолт 2.17 — нижняя граница проверенного диапазона.
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Format version ---
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
|
||||||
|
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
|
||||||
|
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
|
||||||
|
# на нечисловое значение: это опечатка, а не версия.
|
||||||
|
$formatVerifiedMin = "2.17"
|
||||||
|
$formatVerifiedMax = "2.21"
|
||||||
|
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
$formatRank = Get-FormatRank $FormatVersion
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
|
||||||
|
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
|
||||||
|
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
|
||||||
|
if ($formatRank -eq 0) {
|
||||||
|
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
|
||||||
|
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
|
||||||
|
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
|
||||||
|
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
|
||||||
|
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
|
||||||
|
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
|
||||||
|
# расхождение портов началось бы прямо здесь.
|
||||||
|
if ($CompatibilityMode -and $CompatibilityMode.ToLowerInvariant() -eq 'dontuse') {
|
||||||
|
[Console]::Error.WriteLine("WARNING: CompatibilityMode 'DontUse' is not `"no restrictions`" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).")
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 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 ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
$is221 = ($formatRank -ge 221)
|
||||||
|
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
|
||||||
|
$is218 = ($formatRank -ge 218)
|
||||||
|
|
||||||
|
$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")
|
||||||
|
)
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
|
||||||
|
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
|
||||||
|
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
|
||||||
|
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
|
||||||
|
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
|
||||||
|
if ($is218) { $mobileFuncs += ,@("TextToSpeech","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>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Optional properties ---
|
||||||
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
|
# --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
|
||||||
|
# Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
|
||||||
|
# выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
|
||||||
|
# на своё место, а не в конец.
|
||||||
|
$nl = "`r`n"
|
||||||
|
$f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
|
||||||
|
$palNs = ""
|
||||||
|
if ($is221) {
|
||||||
|
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
# Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
|
||||||
|
# `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
|
||||||
|
$f221AuxForms = $nl + ((@(
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"
|
||||||
|
) | ForEach-Object { "`t`t`t$_" }) -join $nl)
|
||||||
|
$f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
|
||||||
|
$nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
|
||||||
|
$f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
|
||||||
|
$f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
|
||||||
|
$f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 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"$palNs 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>$(Esc-XmlText ($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/>
|
||||||
|
$vendorEl
|
||||||
|
$versionEl
|
||||||
|
<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/>$f221AuxForms
|
||||||
|
<RequiredMobileApplicationPermissions/>
|
||||||
|
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||||
|
</UsedMobileApplicationFunctionalities>
|
||||||
|
<StandaloneConfigurationRestrictionRoles/>
|
||||||
|
<MobileApplicationURLs/>
|
||||||
|
<AllowedIncomingShareRequestTypes/>$f221WindowVariant
|
||||||
|
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
|
||||||
|
<DefaultInterface/>$f221Captions
|
||||||
|
<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>$f221Migration
|
||||||
|
<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"$palNs 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)
|
||||||
|
|
||||||
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
|
Write-XmlFile $langFile $langXml $enc
|
||||||
|
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
|
Write-XmlFile $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"
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
|
import sys, os, argparse, re, uuid
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
|
def new_uuid():
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False)
|
||||||
|
parser.add_argument('-Name', dest='Name', required=True)
|
||||||
|
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
||||||
|
parser.add_argument('-OutputDir', dest='OutputDir', default='src')
|
||||||
|
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) — её задаёт ПЛАТФОРМА, а не режим совместимости.
|
||||||
|
# Дефолт 2.17 — нижняя граница проверенного диапазона.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
|
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
|
||||||
|
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
|
||||||
|
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
|
||||||
|
format_rank_value = format_rank(args.FormatVersion)
|
||||||
|
if format_rank_value == 0:
|
||||||
|
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
|
||||||
|
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
|
||||||
|
f"but was not verified on that platform", file=sys.stderr)
|
||||||
|
|
||||||
|
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
|
||||||
|
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
|
||||||
|
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
|
||||||
|
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
|
||||||
|
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
|
||||||
|
# расхождение портов началось бы прямо здесь.
|
||||||
|
if (args.CompatibilityMode or "").lower() == "dontuse":
|
||||||
|
print("WARNING: CompatibilityMode 'DontUse' is not \"no restrictions\" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).", file=sys.stderr)
|
||||||
|
|
||||||
|
name = args.Name
|
||||||
|
synonym = args.Synonym if args.Synonym else name
|
||||||
|
output_dir = args.OutputDir
|
||||||
|
version = args.Version
|
||||||
|
vendor = args.Vendor
|
||||||
|
compat = args.CompatibilityMode
|
||||||
|
|
||||||
|
# --- Resolve output dir ---
|
||||||
|
if not os.path.isabs(output_dir):
|
||||||
|
output_dir = os.path.join(os.getcwd(), output_dir)
|
||||||
|
|
||||||
|
# --- Check existing ---
|
||||||
|
cfg_file = os.path.join(output_dir, "Configuration.xml")
|
||||||
|
if os.path.exists(cfg_file):
|
||||||
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Generate UUIDs ---
|
||||||
|
uuid_cfg = new_uuid()
|
||||||
|
uuid_lang = new_uuid()
|
||||||
|
co = [new_uuid() for _ in range(7)]
|
||||||
|
|
||||||
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
is_221 = format_rank_value >= 221
|
||||||
|
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
|
||||||
|
is_218 = format_rank_value >= 218
|
||||||
|
|
||||||
|
mobile_funcs = [
|
||||||
|
("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"),
|
||||||
|
]
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
|
||||||
|
# последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
|
||||||
|
# 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
|
||||||
|
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
|
||||||
|
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
|
||||||
|
if is_218:
|
||||||
|
mobile_funcs.append(("TextToSpeech", "false"))
|
||||||
|
|
||||||
|
mobile_xml = ""
|
||||||
|
for func_name, func_use in mobile_funcs:
|
||||||
|
mobile_xml += f"\r\n\t\t\t\t<app:functionality>\r\n\t\t\t\t\t<app:functionality>{func_name}</app:functionality>\r\n\t\t\t\t\t<app:use>{func_use}</app:use>\r\n\t\t\t\t</app:functionality>"
|
||||||
|
|
||||||
|
# --- Synonym XML ---
|
||||||
|
synonym_xml = ""
|
||||||
|
if synonym:
|
||||||
|
synonym_xml = f"\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>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
|
class_ids = [
|
||||||
|
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
||||||
|
"9fcd25a0-4822-11d4-9414-008048da11f9",
|
||||||
|
"e3687481-0a87-462c-a166-9f34594f9bba",
|
||||||
|
"9de14907-ec23-4a07-96f0-85521cb6b53b",
|
||||||
|
"51f2d5d8-ea4d-4064-8892-82951750031e",
|
||||||
|
"e68182ea-4237-4383-967f-90c1e3370bc7",
|
||||||
|
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
|
||||||
|
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
|
||||||
|
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
|
||||||
|
pal_ns = ""
|
||||||
|
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
|
||||||
|
if is_221:
|
||||||
|
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
f221_aux_forms = "\r\n" + "\r\n".join(
|
||||||
|
f"\t\t\t{t}" for t in (
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
|
||||||
|
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
|
||||||
|
"</MainClientApplicationWindowInterfaceVariant>"
|
||||||
|
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
|
||||||
|
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
|
||||||
|
"</ClientApplicationWindowsOpenVariant>")
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
|
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
|
||||||
|
"</Version85InterfaceMigrationMode>")
|
||||||
|
|
||||||
|
contained_objects = ""
|
||||||
|
for i in range(7):
|
||||||
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
|
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
||||||
|
\t\t\t\t<xr:ObjectId>{co[i]}</xr:ObjectId>
|
||||||
|
\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"{pal_ns} 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>
|
||||||
|
\t\t<Properties>
|
||||||
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
|
\t\t\t<Comment/>
|
||||||
|
\t\t\t<NamePrefix/>
|
||||||
|
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
||||||
|
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
|
\t\t\t<UsePurposes>
|
||||||
|
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
|
\t\t\t</UsePurposes>
|
||||||
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
|
\t\t\t<DefaultRoles/>
|
||||||
|
\t\t\t{vendor_el}
|
||||||
|
\t\t\t{version_el}
|
||||||
|
\t\t\t<UpdateCatalogAddress/>
|
||||||
|
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
|
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
|
\t\t\t<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
|
||||||
|
\t\t\t<AdditionalFullTextSearchDictionaries/>
|
||||||
|
\t\t\t<CommonSettingsStorage/>
|
||||||
|
\t\t\t<ReportsUserSettingsStorage/>
|
||||||
|
\t\t\t<ReportsVariantsStorage/>
|
||||||
|
\t\t\t<FormDataSettingsStorage/>
|
||||||
|
\t\t\t<DynamicListsUserSettingsStorage/>
|
||||||
|
\t\t\t<URLExternalDataStorage/>
|
||||||
|
\t\t\t<Content/>
|
||||||
|
\t\t\t<DefaultReportForm/>
|
||||||
|
\t\t\t<DefaultReportVariantForm/>
|
||||||
|
\t\t\t<DefaultReportSettingsForm/>
|
||||||
|
\t\t\t<DefaultReportAppearanceTemplate/>
|
||||||
|
\t\t\t<DefaultDynamicListSettingsForm/>
|
||||||
|
\t\t\t<DefaultSearchForm/>
|
||||||
|
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
||||||
|
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
||||||
|
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
|
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
|
||||||
|
\t\t\t<RequiredMobileApplicationPermissions/>
|
||||||
|
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
||||||
|
\t\t\t</UsedMobileApplicationFunctionalities>
|
||||||
|
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
||||||
|
\t\t\t<MobileApplicationURLs/>
|
||||||
|
\t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
|
||||||
|
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
|
||||||
|
\t\t\t<DefaultInterface/>{f221_captions}
|
||||||
|
\t\t\t<DefaultStyle/>
|
||||||
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
|
\t\t\t<BriefInformation/>
|
||||||
|
\t\t\t<DetailedInformation/>
|
||||||
|
\t\t\t<Copyright/>
|
||||||
|
\t\t\t<VendorInformationAddress/>
|
||||||
|
\t\t\t<ConfigurationInformationAddress/>
|
||||||
|
\t\t\t<DataLockControlMode>Managed</DataLockControlMode>
|
||||||
|
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
|
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
|
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
|
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
|
||||||
|
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
|
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||||
|
\t\t\t<DefaultConstantsForm/>
|
||||||
|
\t\t</Properties>
|
||||||
|
\t\t<ChildObjects>
|
||||||
|
\t\t\t<Language>Русский</Language>
|
||||||
|
\t\t</ChildObjects>
|
||||||
|
\t</Configuration>
|
||||||
|
</MetaDataObject>'''
|
||||||
|
|
||||||
|
# --- 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"{pal_ns} 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>
|
||||||
|
\t\t\t<Synonym>
|
||||||
|
\t\t\t\t<v8:item>
|
||||||
|
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
||||||
|
\t\t\t\t\t<v8:content>Русский</v8:content>
|
||||||
|
\t\t\t\t</v8:item>
|
||||||
|
\t\t\t</Synonym>
|
||||||
|
\t\t\t<Comment/>
|
||||||
|
\t\t\t<LanguageCode>ru</LanguageCode>
|
||||||
|
\t\t</Properties>
|
||||||
|
\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_xml_file(cfg_file, cfg_xml)
|
||||||
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
|
write_xml_file(lang_file, lang_xml)
|
||||||
|
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
|
write_xml_file(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()
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
---
|
---
|
||||||
name: cf-validate
|
name: cf-validate
|
||||||
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
||||||
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /cf-validate — валидация конфигурации 1С
|
# /cf-validate — валидация конфигурации 1С
|
||||||
|
|
||||||
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|------------|:-----:|---------|-------------------------------------------------|
|
|------------|:-----:|---------|-------------------------------------------------|
|
||||||
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
|
python ".github/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
|
python ".github/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
+631
-544
File diff suppressed because it is too large
Load Diff
+111
-10
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.1 — Validate 1C configuration XML structure
|
# cf-validate v1.8 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
def ci_parse_args(parser, argv=None):
|
||||||
|
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
||||||
|
for i, tok in enumerate(argv):
|
||||||
|
if tok.startswith('-') and tok.lower() in names:
|
||||||
|
argv[i] = names[tok.lower()]
|
||||||
|
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
||||||
|
choice_map = {}
|
||||||
|
for a in parser._actions:
|
||||||
|
if a.choices:
|
||||||
|
for s in a.option_strings:
|
||||||
|
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
||||||
|
for i in range(len(argv) - 1):
|
||||||
|
m = choice_map.get(argv[i])
|
||||||
|
if m and argv[i + 1].lower() in m:
|
||||||
|
argv[i + 1] = m[argv[i + 1].lower()]
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||||
@@ -33,11 +55,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -82,7 +105,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_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'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
@@ -90,7 +113,10 @@ VALID_ENUM_VALUES = {
|
|||||||
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
||||||
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'SynchronousPlatformExtensionAndAddInCallUseMode': ['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'],
|
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
||||||
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
||||||
'CompatibilityMode': [
|
'CompatibilityMode': [
|
||||||
@@ -100,12 +126,26 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_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',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
|
|
||||||
|
# ── Format version ───────────────────────────────────────────
|
||||||
|
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
|
||||||
|
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
|
||||||
|
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
|
||||||
|
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
|
||||||
|
FORMAT_VERIFIED_MIN = "2.17"
|
||||||
|
FORMAT_VERIFIED_MAX = "2.21"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
class Reporter:
|
class Reporter:
|
||||||
def __init__(self, max_errors, detailed=False):
|
def __init__(self, max_errors, detailed=False):
|
||||||
@@ -162,11 +202,11 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration XML structure', allow_abbrev=False
|
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('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
@@ -226,10 +266,17 @@ def main():
|
|||||||
check1_ok = False
|
check1_ok = False
|
||||||
|
|
||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
|
version_rank = format_rank(version)
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20'):
|
elif version_rank == 0:
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
r.error(f"1. Malformed version '{version}' (expected N.N)")
|
||||||
|
elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
|
||||||
|
r.warn(f"1. Format version '{version}' is below the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
|
||||||
|
r.warn(f"1. Format version '{version}' is above the tested range "
|
||||||
|
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -534,6 +581,60 @@ def main():
|
|||||||
else:
|
else:
|
||||||
pass # no ChildObjects
|
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 ---
|
# --- Final output ---
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1 if r.errors > 0 else 0)
|
sys.exit(1 if r.errors > 0 else 0)
|
||||||
@@ -1,101 +1,109 @@
|
|||||||
---
|
---
|
||||||
name: cfe-borrow
|
name: cfe-borrow
|
||||||
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
|
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
|
||||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
|
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /cfe-borrow — Заимствование объектов из конфигурации
|
# /cfe-borrow — Заимствование объектов из конфигурации
|
||||||
|
|
||||||
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
|
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
|
||||||
|
|
||||||
## Предусловие
|
## Предусловие
|
||||||
|
|
||||||
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
|
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
|
||||||
|
|
||||||
### Авто-определение ConfigPath
|
### Авто-определение ConfigPath
|
||||||
|
|
||||||
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||||
1. Прочитай `.v8-project.json` из корня проекта
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||||
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||||
4. Если `configSrc` нет — спроси у пользователя
|
4. Если `configSrc` нет — спроси у пользователя
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
| Параметр | Описание |
|
| Параметр | Описание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||||
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
| `Module` | Создать пустые модули объекта: `ObjectModule`, `ManagerModule`, `RecordSetModule`, `ValueManagerModule` (через запятую) или `None`. У типов с единственным модулем (`CommonModule`, `HTTPService`, `WebService`) он создаётся и без параметра |
|
||||||
|
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||||
## Формат -Object
|
|
||||||
|
## Формат -Object
|
||||||
- `Catalog.Контрагенты` — справочник
|
|
||||||
- `CommonModule.РаботаСФайлами` — общий модуль
|
- `Catalog.Контрагенты` — справочник
|
||||||
- `Document.РеализацияТоваров` — документ
|
- `CommonModule.РаботаСФайлами` — общий модуль
|
||||||
- `Enum.ВидыОплат` — перечисление
|
- `Document.РеализацияТоваров` — документ
|
||||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
- `Enum.ВидыОплат` — перечисление
|
||||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||||
Поддерживаются все 44 типа объектов конфигурации.
|
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||||
|
|
||||||
### Заимствование форм
|
### Заимствование форм
|
||||||
|
|
||||||
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
|
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
|
||||||
|
|
||||||
Создаётся:
|
Создаётся:
|
||||||
1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
|
1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
|
||||||
2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
|
2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
|
||||||
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
|
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
|
||||||
4. **Регистрация** — `<Form>` в ChildObjects родительского объекта
|
4. **Регистрация** — `<Form>` в ChildObjects родительского объекта
|
||||||
|
|
||||||
### Заимствование основного реквизита формы (-BorrowMainAttribute)
|
### Заимствование основного реквизита формы (-BorrowMainAttribute)
|
||||||
|
|
||||||
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
|
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
|
||||||
|
|
||||||
**Два режима**:
|
**Два режима**:
|
||||||
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
|
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
|
||||||
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
|
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
|
||||||
|
|
||||||
**Типовой сценарий** (добавление реквизита + вывод на форму):
|
**Типовой сценарий** (добавление реквизита + вывод на форму):
|
||||||
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
|
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
|
||||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||||
|
|
||||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
**Защита существующих данных**: уже заимствованный объект не перезаписывается — добавляется только недостающее. Повторный вызов безопасен: собственные реквизиты расширения, заимствованные подобъекты и код в модулях сохраняются.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
python ".github/skills/cfe-borrow/scripts/cfe-borrow.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Заимствовать один объект
|
# Заимствовать один объект
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
|
|
||||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
# Заимствовать справочник вместе с модулями объекта и менеджера
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты" -Module ObjectModule,ManagerModule
|
||||||
|
|
||||||
# Несколько объектов за раз
|
# Общий модуль без файла модуля
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "CommonModule.РаботаСФайлами" -Module None
|
||||||
|
|
||||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||||
|
|
||||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
# Несколько объектов за раз
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||||
```
|
|
||||||
|
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||||
## Верификация
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||||
|
|
||||||
```
|
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||||
/cfe-validate <ExtensionPath>
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
```
|
||||||
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
+2479
-1772
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,57 @@
|
|||||||
---
|
---
|
||||||
name: cfe-diff
|
name: cfe-diff
|
||||||
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
|
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
|
||||||
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
|
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /cfe-diff — Анализ расширения конфигурации
|
# /cfe-diff — Анализ расширения конфигурации
|
||||||
|
|
||||||
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
|
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
| Параметр | Описание | По умолчанию |
|
| Параметр | Описание | По умолчанию |
|
||||||
|----------|----------|--------------|
|
|----------|----------|--------------|
|
||||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||||
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
|
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
|
||||||
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
|
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
python ".github/skills/cfe-diff/scripts/cfe-diff.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
|
|
||||||
Для каждого объекта показывает:
|
Для каждого объекта показывает:
|
||||||
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
||||||
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
||||||
|
|
||||||
Для каждой формы заимствованного объекта показывается:
|
Для каждой формы заимствованного объекта показывается:
|
||||||
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
||||||
- callType-события формы и элементов
|
- callType-события формы и элементов
|
||||||
- callType на командах
|
- callType на командах
|
||||||
|
|
||||||
## Mode B — проверка переноса
|
## Mode B — проверка переноса
|
||||||
|
|
||||||
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
||||||
|
|
||||||
Статусы:
|
Статусы:
|
||||||
- `[TRANSFERRED]` — код найден в конфигурации
|
- `[TRANSFERRED]` — код найден в конфигурации
|
||||||
- `[NOT_TRANSFERRED]` — код не найден
|
- `[NOT_TRANSFERRED]` — код не найден
|
||||||
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
|
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор — что изменено в расширении
|
# Обзор — что изменено в расширении
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
|
|
||||||
# Проверка переноса — все ли #Вставка перенесены
|
# Проверка переноса — все ли #Вставка перенесены
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode B
|
||||||
```
|
```
|
||||||
+474
-471
@@ -1,471 +1,474 @@
|
|||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.3 — Analyze and compare 1C configuration extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
[Parameter(Mandatory)]
|
param(
|
||||||
[string]$ExtensionPath,
|
[Parameter(Mandatory, Position=0)]
|
||||||
|
[string]$ExtensionPath,
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ConfigPath,
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ConfigPath,
|
||||||
[ValidateSet("A","B")]
|
|
||||||
[string]$Mode = "A"
|
[ValidateSet("A","B")]
|
||||||
)
|
[string]$Mode = "A"
|
||||||
|
)
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
$ErrorActionPreference = "Stop"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
# --- Resolve paths ---
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
# --- Resolve paths ---
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
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 (-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 }
|
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"
|
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
|
||||||
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
|
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
|
||||||
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
|
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 = @{
|
# --- Type -> directory mapping ---
|
||||||
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
$childTypeDirMap = @{
|
||||||
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
|
||||||
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
|
||||||
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
|
||||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
|
||||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||||
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||||
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||||
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
|
||||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
|
||||||
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
|
||||||
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
|
||||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||||
"CommonAttribute"="CommonAttributes"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
}
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||||
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
# --- Parse extension Configuration.xml ---
|
"Bot"="Bots"
|
||||||
$extDoc = New-Object System.Xml.XmlDocument
|
}
|
||||||
$extDoc.PreserveWhitespace = $false
|
|
||||||
$extDoc.Load($extCfg)
|
# --- Parse extension Configuration.xml ---
|
||||||
|
$extDoc = New-Object System.Xml.XmlDocument
|
||||||
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
$extDoc.PreserveWhitespace = $false
|
||||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$extDoc.Load($extCfg)
|
||||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
|
||||||
|
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
|
||||||
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||||
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
|
||||||
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
|
||||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
|
||||||
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
|
||||||
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
|
||||||
|
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
|
||||||
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
|
||||||
Write-Host " NamePrefix: $namePrefix"
|
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
|
||||||
Write-Host ""
|
|
||||||
|
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
|
||||||
# --- Collect ChildObjects ---
|
Write-Host " NamePrefix: $namePrefix"
|
||||||
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
Write-Host ""
|
||||||
if (-not $childObjNode) {
|
|
||||||
Write-Host "[WARN] No ChildObjects in extension"
|
# --- Collect ChildObjects ---
|
||||||
exit 0
|
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
||||||
}
|
if (-not $childObjNode) {
|
||||||
|
Write-Host "[WARN] No ChildObjects in extension"
|
||||||
$objects = @()
|
exit 0
|
||||||
foreach ($child in $childObjNode.ChildNodes) {
|
}
|
||||||
if ($child.NodeType -ne 'Element') { continue }
|
|
||||||
if ($child.LocalName -eq "Language") { continue }
|
$objects = @()
|
||||||
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
foreach ($child in $childObjNode.ChildNodes) {
|
||||||
}
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
if ($child.LocalName -eq "Language") { continue }
|
||||||
if ($objects.Count -eq 0) {
|
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
|
||||||
Write-Host "No objects (besides Language) in extension."
|
}
|
||||||
exit 0
|
|
||||||
}
|
if ($objects.Count -eq 0) {
|
||||||
|
Write-Host "No objects (besides Language) in extension."
|
||||||
# --- Helper: check if object is borrowed ---
|
exit 0
|
||||||
function Get-ObjectInfo {
|
}
|
||||||
param([string]$objType, [string]$objName)
|
|
||||||
|
# --- Helper: check if object is borrowed ---
|
||||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
function Get-ObjectInfo {
|
||||||
$dirName = $childTypeDirMap[$objType]
|
param([string]$objType, [string]$objName)
|
||||||
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
|
||||||
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
$dirName = $childTypeDirMap[$objType]
|
||||||
|
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
|
||||||
$doc = New-Object System.Xml.XmlDocument
|
|
||||||
$doc.PreserveWhitespace = $false
|
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
|
||||||
$doc.Load($objFile)
|
|
||||||
|
$doc = New-Object System.Xml.XmlDocument
|
||||||
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
$doc.PreserveWhitespace = $false
|
||||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$doc.Load($objFile)
|
||||||
|
|
||||||
$objEl = $null
|
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||||
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
|
||||||
}
|
$objEl = $null
|
||||||
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||||
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
}
|
||||||
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
|
||||||
|
|
||||||
$info = @{
|
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
|
||||||
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
|
||||||
File = $objFile
|
|
||||||
Exists = $true
|
$info = @{
|
||||||
Type = $objType
|
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
|
||||||
Name = $objName
|
File = $objFile
|
||||||
DirName = $dirName
|
Exists = $true
|
||||||
ObjElement = $objEl
|
Type = $objType
|
||||||
ObjNs = $objNs
|
Name = $objName
|
||||||
}
|
DirName = $dirName
|
||||||
return $info
|
ObjElement = $objEl
|
||||||
}
|
ObjNs = $objNs
|
||||||
|
}
|
||||||
# --- Helper: find .bsl files for object ---
|
return $info
|
||||||
function Get-BslFiles {
|
}
|
||||||
param([string]$objType, [string]$objName)
|
|
||||||
|
# --- Helper: find .bsl files for object ---
|
||||||
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
function Get-BslFiles {
|
||||||
$dirName = $childTypeDirMap[$objType]
|
param([string]$objType, [string]$objName)
|
||||||
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
|
||||||
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
$dirName = $childTypeDirMap[$objType]
|
||||||
|
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
|
||||||
$bslFiles = @()
|
|
||||||
$extDir = Join-Path $objDir "Ext"
|
if (-not (Test-Path $objDir -PathType Container)) { return @() }
|
||||||
if (Test-Path $extDir) {
|
|
||||||
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
$bslFiles = @()
|
||||||
foreach ($item in $items) { $bslFiles += $item.FullName }
|
$extDir = Join-Path $objDir "Ext"
|
||||||
}
|
if (Test-Path $extDir) {
|
||||||
|
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
|
||||||
# Forms
|
foreach ($item in $items) { $bslFiles += $item.FullName }
|
||||||
$formsDir = Join-Path $objDir "Forms"
|
}
|
||||||
if (Test-Path $formsDir) {
|
|
||||||
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
# Forms
|
||||||
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
$formsDir = Join-Path $objDir "Forms"
|
||||||
}
|
if (Test-Path $formsDir) {
|
||||||
|
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
|
||||||
return $bslFiles
|
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Helper: parse interceptors from .bsl ---
|
return $bslFiles
|
||||||
function Get-Interceptors {
|
}
|
||||||
param([string]$bslPath)
|
|
||||||
|
# --- Helper: parse interceptors from .bsl ---
|
||||||
if (-not (Test-Path $bslPath)) { return @() }
|
function Get-Interceptors {
|
||||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
param([string]$bslPath)
|
||||||
$interceptors = @()
|
|
||||||
$i = 0
|
if (-not (Test-Path $bslPath)) { return @() }
|
||||||
while ($i -lt $lines.Count) {
|
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||||
$line = $lines[$i].Trim()
|
$interceptors = @()
|
||||||
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
|
$i = 0
|
||||||
$type = $Matches[1]
|
while ($i -lt $lines.Count) {
|
||||||
$method = $Matches[2]
|
$line = $lines[$i].Trim()
|
||||||
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
|
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
|
||||||
}
|
$type = $Matches[1]
|
||||||
$i++
|
$method = $Matches[2]
|
||||||
}
|
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
|
||||||
return $interceptors
|
}
|
||||||
}
|
$i++
|
||||||
|
}
|
||||||
# --- Helper: extract #Вставка blocks from .bsl ---
|
return $interceptors
|
||||||
function Get-InsertionBlocks {
|
}
|
||||||
param([string]$bslPath)
|
|
||||||
|
# --- Helper: extract #Вставка blocks from .bsl ---
|
||||||
if (-not (Test-Path $bslPath)) { return @() }
|
function Get-InsertionBlocks {
|
||||||
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
param([string]$bslPath)
|
||||||
$blocks = @()
|
|
||||||
$inBlock = $false
|
if (-not (Test-Path $bslPath)) { return @() }
|
||||||
$blockLines = @()
|
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
|
||||||
$startLine = 0
|
$blocks = @()
|
||||||
|
$inBlock = $false
|
||||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
$blockLines = @()
|
||||||
$line = $lines[$i].Trim()
|
$startLine = 0
|
||||||
if ($line -eq "#Вставка") {
|
|
||||||
$inBlock = $true
|
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||||
$blockLines = @()
|
$line = $lines[$i].Trim()
|
||||||
$startLine = $i + 1
|
if ($line -eq "#Вставка") {
|
||||||
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
|
$inBlock = $true
|
||||||
$inBlock = $false
|
$blockLines = @()
|
||||||
$blocks += @{
|
$startLine = $i + 1
|
||||||
StartLine = $startLine
|
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
|
||||||
EndLine = $i + 1
|
$inBlock = $false
|
||||||
Code = ($blockLines -join "`n").Trim()
|
$blocks += @{
|
||||||
File = $bslPath
|
StartLine = $startLine
|
||||||
}
|
EndLine = $i + 1
|
||||||
} elseif ($inBlock) {
|
Code = ($blockLines -join "`n").Trim()
|
||||||
$blockLines += $lines[$i]
|
File = $bslPath
|
||||||
}
|
}
|
||||||
}
|
} elseif ($inBlock) {
|
||||||
return $blocks
|
$blockLines += $lines[$i]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
# --- Helper: analyze form for callType events and commands ---
|
return $blocks
|
||||||
function Get-FormInterceptors {
|
}
|
||||||
param([string]$formXmlPath)
|
|
||||||
|
# --- Helper: analyze form for callType events and commands ---
|
||||||
if (-not (Test-Path $formXmlPath)) { return $null }
|
function Get-FormInterceptors {
|
||||||
|
param([string]$formXmlPath)
|
||||||
$formDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$formDoc.PreserveWhitespace = $false
|
if (-not (Test-Path $formXmlPath)) { return $null }
|
||||||
try { $formDoc.Load($formXmlPath) } catch { return $null }
|
|
||||||
|
$formDoc = New-Object System.Xml.XmlDocument
|
||||||
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
|
$formDoc.PreserveWhitespace = $false
|
||||||
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
try { $formDoc.Load($formXmlPath) } catch { return $null }
|
||||||
|
|
||||||
$fRoot = $formDoc.DocumentElement
|
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
|
||||||
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
|
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||||
$isBorrowed = ($baseForm -ne $null)
|
|
||||||
|
$fRoot = $formDoc.DocumentElement
|
||||||
$interceptors = @()
|
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
|
||||||
|
$isBorrowed = ($baseForm -ne $null)
|
||||||
# Form-level events with callType
|
|
||||||
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
|
$interceptors = @()
|
||||||
if ($eventsNode) {
|
|
||||||
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
|
# Form-level events with callType
|
||||||
$ct = $evt.GetAttribute("callType")
|
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
|
||||||
if ($ct) {
|
if ($eventsNode) {
|
||||||
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
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)) {
|
# Element-level events with callType (scan all elements recursively)
|
||||||
$elName = $evtNode.GetAttribute("name")
|
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
|
||||||
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
|
if ($childItems) {
|
||||||
$ct = $evt.GetAttribute("callType")
|
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
|
||||||
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
$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)) {
|
# Commands with callType on Action
|
||||||
$ct = $action.GetAttribute("callType")
|
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
|
||||||
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
|
$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
|
|
||||||
}
|
return @{
|
||||||
}
|
IsBorrowed = $isBorrowed
|
||||||
|
Interceptors = $interceptors
|
||||||
# ============================================================
|
}
|
||||||
# MODE A: Extension overview
|
}
|
||||||
# ============================================================
|
|
||||||
if ($Mode -eq "A") {
|
# ============================================================
|
||||||
$borrowedList = @()
|
# MODE A: Extension overview
|
||||||
$ownList = @()
|
# ============================================================
|
||||||
|
if ($Mode -eq "A") {
|
||||||
foreach ($obj in $objects) {
|
$borrowedList = @()
|
||||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
$ownList = @()
|
||||||
if (-not $info) {
|
|
||||||
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
foreach ($obj in $objects) {
|
||||||
continue
|
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||||
}
|
if (-not $info) {
|
||||||
if (-not $info.Exists) {
|
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
|
||||||
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
continue
|
||||||
continue
|
}
|
||||||
}
|
if (-not $info.Exists) {
|
||||||
|
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
|
||||||
if ($info.Borrowed) {
|
continue
|
||||||
$borrowedList += $obj
|
}
|
||||||
|
|
||||||
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
if ($info.Borrowed) {
|
||||||
|
$borrowedList += $obj
|
||||||
# Find .bsl files and interceptors
|
|
||||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
|
||||||
foreach ($bsl in $bslFiles) {
|
|
||||||
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
# Find .bsl files and interceptors
|
||||||
$interceptors = Get-Interceptors $bsl
|
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||||
if ($interceptors.Count -gt 0) {
|
foreach ($bsl in $bslFiles) {
|
||||||
foreach ($ic in $interceptors) {
|
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||||
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
|
$interceptors = Get-Interceptors $bsl
|
||||||
}
|
if ($interceptors.Count -gt 0) {
|
||||||
} else {
|
foreach ($ic in $interceptors) {
|
||||||
Write-Host " $relPath (no 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) {
|
# Check for own attributes/forms in ChildObjects
|
||||||
$ownAttrs = 0
|
if ($info.ObjElement) {
|
||||||
$ownForms = 0
|
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||||
$ownTS = 0
|
if ($childObj) {
|
||||||
$borrowedItems = 0
|
$ownAttrs = 0
|
||||||
$formNames = @()
|
$ownForms = 0
|
||||||
foreach ($c in $childObj.ChildNodes) {
|
$ownTS = 0
|
||||||
if ($c.NodeType -ne 'Element') { continue }
|
$borrowedItems = 0
|
||||||
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
|
$formNames = @()
|
||||||
if ($cProps) {
|
foreach ($c in $childObj.ChildNodes) {
|
||||||
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
if ($c.NodeType -ne 'Element') { continue }
|
||||||
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
|
||||||
$borrowedItems++
|
if ($cProps) {
|
||||||
continue
|
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
|
||||||
}
|
if ($cOb -and $cOb.InnerText -eq "Adopted") {
|
||||||
}
|
$borrowedItems++
|
||||||
switch ($c.LocalName) {
|
continue
|
||||||
"Attribute" { $ownAttrs++ }
|
}
|
||||||
"TabularSection" { $ownTS++ }
|
}
|
||||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
switch ($c.LocalName) {
|
||||||
}
|
"Attribute" { $ownAttrs++ }
|
||||||
}
|
"TabularSection" { $ownTS++ }
|
||||||
$parts = @()
|
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||||
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
}
|
||||||
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
}
|
||||||
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
$parts = @()
|
||||||
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
|
||||||
if ($parts.Count -gt 0) {
|
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
|
||||||
Write-Host " ChildObjects: $($parts -join ', ')"
|
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
|
||||||
}
|
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
|
||||||
|
if ($parts.Count -gt 0) {
|
||||||
# Analyze forms
|
Write-Host " ChildObjects: $($parts -join ', ')"
|
||||||
$borrowedFormCount = 0
|
}
|
||||||
$ownFormCount = 0
|
|
||||||
foreach ($fn in $formNames) {
|
# Analyze forms
|
||||||
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
|
$borrowedFormCount = 0
|
||||||
$fi = Get-FormInterceptors $formXmlPath
|
$ownFormCount = 0
|
||||||
if (-not $fi) {
|
foreach ($fn in $formNames) {
|
||||||
Write-Host " Form.$fn (?)"
|
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
|
||||||
continue
|
$fi = Get-FormInterceptors $formXmlPath
|
||||||
}
|
if (-not $fi) {
|
||||||
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
|
Write-Host " Form.$fn (?)"
|
||||||
if ($fi.Interceptors.Count -gt 0) {
|
continue
|
||||||
Write-Host " Form.$fn ($formTag):"
|
}
|
||||||
foreach ($ic in $fi.Interceptors) {
|
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
|
||||||
Write-Host " $ic"
|
if ($fi.Interceptors.Count -gt 0) {
|
||||||
}
|
Write-Host " Form.$fn ($formTag):"
|
||||||
} else {
|
foreach ($ic in $fi.Interceptors) {
|
||||||
Write-Host " Form.$fn ($formTag)"
|
Write-Host " $ic"
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
Write-Host " Form.$fn ($formTag)"
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
$ownList += $obj
|
}
|
||||||
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
}
|
||||||
|
} else {
|
||||||
# Brief info for own objects
|
$ownList += $obj
|
||||||
if ($info.ObjElement) {
|
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
|
||||||
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
|
||||||
if ($childObj) {
|
# Brief info for own objects
|
||||||
$attrs = 0; $forms = 0; $ts = 0
|
if ($info.ObjElement) {
|
||||||
foreach ($c in $childObj.ChildNodes) {
|
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
|
||||||
if ($c.NodeType -ne 'Element') { continue }
|
if ($childObj) {
|
||||||
switch ($c.LocalName) {
|
$attrs = 0; $forms = 0; $ts = 0
|
||||||
"Attribute" { $attrs++ }
|
foreach ($c in $childObj.ChildNodes) {
|
||||||
"TabularSection" { $ts++ }
|
if ($c.NodeType -ne 'Element') { continue }
|
||||||
"Form" { $forms++ }
|
switch ($c.LocalName) {
|
||||||
}
|
"Attribute" { $attrs++ }
|
||||||
}
|
"TabularSection" { $ts++ }
|
||||||
$parts = @()
|
"Form" { $forms++ }
|
||||||
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
}
|
||||||
if ($ts -gt 0) { $parts += "$ts TS" }
|
}
|
||||||
if ($forms -gt 0) { $parts += "$forms forms" }
|
$parts = @()
|
||||||
if ($parts.Count -gt 0) {
|
if ($attrs -gt 0) { $parts += "$attrs attrs" }
|
||||||
Write-Host " $($parts -join ', ')"
|
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 ==="
|
}
|
||||||
}
|
|
||||||
|
Write-Host ""
|
||||||
# ============================================================
|
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
|
||||||
# MODE B: Transfer check
|
}
|
||||||
# ============================================================
|
|
||||||
if ($Mode -eq "B") {
|
# ============================================================
|
||||||
$transferred = 0
|
# MODE B: Transfer check
|
||||||
$notTransferred = 0
|
# ============================================================
|
||||||
$needsReview = 0
|
if ($Mode -eq "B") {
|
||||||
|
$transferred = 0
|
||||||
foreach ($obj in $objects) {
|
$notTransferred = 0
|
||||||
$info = Get-ObjectInfo $obj.Type $obj.Name
|
$needsReview = 0
|
||||||
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
|
||||||
|
foreach ($obj in $objects) {
|
||||||
# Find .bsl files with &ИзменениеИКонтроль
|
$info = Get-ObjectInfo $obj.Type $obj.Name
|
||||||
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
|
||||||
foreach ($bsl in $bslFiles) {
|
|
||||||
$interceptors = Get-Interceptors $bsl
|
# Find .bsl files with &ИзменениеИКонтроль
|
||||||
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
$bslFiles = Get-BslFiles $obj.Type $obj.Name
|
||||||
|
foreach ($bsl in $bslFiles) {
|
||||||
if ($macInterceptors.Count -eq 0) { continue }
|
$interceptors = Get-Interceptors $bsl
|
||||||
|
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
|
||||||
foreach ($ic in $macInterceptors) {
|
|
||||||
$methodName = $ic.Method
|
if ($macInterceptors.Count -eq 0) { continue }
|
||||||
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
|
||||||
|
foreach ($ic in $macInterceptors) {
|
||||||
# Find #Вставка blocks in this file
|
$methodName = $ic.Method
|
||||||
$insertBlocks = Get-InsertionBlocks $bsl
|
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
|
||||||
|
|
||||||
if ($insertBlocks.Count -eq 0) {
|
# Find #Вставка blocks in this file
|
||||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
$insertBlocks = Get-InsertionBlocks $bsl
|
||||||
$needsReview++
|
|
||||||
continue
|
if ($insertBlocks.Count -eq 0) {
|
||||||
}
|
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
|
||||||
|
$needsReview++
|
||||||
# Find corresponding module in config
|
continue
|
||||||
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
}
|
||||||
$dirName = $childTypeDirMap[$obj.Type]
|
|
||||||
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
# Find corresponding module in config
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
|
||||||
if (-not (Test-Path $configBsl)) {
|
$dirName = $childTypeDirMap[$obj.Type]
|
||||||
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
|
||||||
$needsReview++
|
|
||||||
continue
|
if (-not (Test-Path $configBsl)) {
|
||||||
}
|
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
|
||||||
|
$needsReview++
|
||||||
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
continue
|
||||||
|
}
|
||||||
$allTransferred = $true
|
|
||||||
foreach ($block in $insertBlocks) {
|
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
|
||||||
$code = $block.Code
|
|
||||||
if (-not $code) { continue }
|
$allTransferred = $true
|
||||||
|
foreach ($block in $insertBlocks) {
|
||||||
# Normalize whitespace for comparison
|
$code = $block.Code
|
||||||
$codeNorm = $code -replace '\s+', ' '
|
if (-not $code) { continue }
|
||||||
$configNorm = $configContent -replace '\s+', ' '
|
|
||||||
|
# Normalize whitespace for comparison
|
||||||
if ($configNorm.Contains($codeNorm)) {
|
$codeNorm = $code -replace '\s+', ' '
|
||||||
# Found in config
|
$configNorm = $configContent -replace '\s+', ' '
|
||||||
} else {
|
|
||||||
$allTransferred = $false
|
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 {
|
if ($allTransferred) {
|
||||||
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
|
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
|
||||||
$notTransferred++
|
$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 ==="
|
}
|
||||||
}
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user