Merge branch 'dev' into feature/web-test-runner

This commit is contained in:
Nick Shirokov
2026-05-01 11:58:28 +03:00
426 changed files with 31078 additions and 2961 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
| Операция | Формат Value | Описание |
|----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
+6 -2
View File
@@ -13,7 +13,7 @@
### Enum
| Свойство | Допустимые значения |
|----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` |
@@ -21,7 +21,7 @@
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
@@ -35,6 +35,10 @@
Формат: `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. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
+42 -2
View File
@@ -1,7 +1,7 @@
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ConfigPath,
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
[string]$DefinitionFile,
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")]
[string]$Operation,
@@ -27,6 +27,7 @@ if (Test-Path $ConfigPath -PathType Container) {
}
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
@@ -87,6 +88,22 @@ $script:typeOrder = @(
"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) {
@@ -247,6 +264,29 @@ function Do-AddChildObject([string]$batchVal) {
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) {
+39 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -32,6 +32,22 @@ TYPE_ORDER = [
"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"]
@@ -143,7 +159,7 @@ 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("-ConfigPath", "-Path", 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)
@@ -171,6 +187,7 @@ def main():
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)
@@ -285,6 +302,25 @@ def main():
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:
@@ -502,7 +538,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path])
# --- Summary ---
print()
+1 -1
View File
@@ -1,7 +1,7 @@
# cf-info v1.0 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$ConfigPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[int]$Limit = 150,
+1 -1
View File
@@ -13,7 +13,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
@@ -1,7 +1,8 @@
# cf-validate v1.1 — Validate 1C configuration root structure
# cf-validate v1.2 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ConfigPath,
[switch]$Detailed,
@@ -145,17 +146,17 @@ $childTypeDirMap = @{
# Valid enum values for Configuration properties
$validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
"ScriptVariant" = @("Russian","English")
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
"DatabaseTablespacesUseMode" = @("DontUse","Use")
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
}
# --- 1. Parse XML ---
@@ -203,8 +204,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Must have Configuration child
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-validate v1.1 — Validate 1C configuration XML structure
# cf-validate v1.2 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re
@@ -82,7 +82,7 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
@@ -90,7 +90,10 @@ VALID_ENUM_VALUES = {
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
'CompatibilityMode': [
@@ -100,7 +103,7 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
}
@@ -162,7 +165,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration XML structure', allow_abbrev=False
)
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
@@ -228,8 +231,8 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
@@ -1,4 +1,4 @@
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -316,6 +316,25 @@ function Expand-SelfClosingElement($container, $parentIndent) {
}
}
# --- 7b. 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"
}
$script:formatVersion = Detect-FormatVersion $extDir
# --- 8. Namespaces declaration for object XML ---
$script:xmlnsDecl = '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"'
@@ -466,7 +485,7 @@ function Borrow-Form {
$newFormUuid = [guid]::NewGuid().ToString()
$formMetaSb = New-Object System.Text.StringBuilder
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
@@ -498,7 +517,7 @@ function Borrow-Form {
$srcFormEl = $srcFormDoc.DocumentElement
$formVersion = $srcFormEl.GetAttribute("version")
if (-not $formVersion) { $formVersion = "2.17" }
if (-not $formVersion) { $formVersion = $script:formatVersion }
# Find direct children: form properties, AutoCommandBar, ChildItems
$srcAutoCmd = $null
@@ -1529,7 +1548,7 @@ function Build-BorrowedObjectXml {
$sb = New-Object System.Text.StringBuilder
$sb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
$sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null
# InternalInfo
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -254,6 +254,22 @@ XMLNS_DECL = (
)
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "2.17"
def get_child_indent(container):
if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1]
@@ -365,6 +381,8 @@ def main():
cfg_resolved = os.path.abspath(cfg_path)
cfg_dir = os.path.dirname(cfg_resolved)
format_version = detect_format_version(ext_dir)
# --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(ext_resolved, xml_parser)
@@ -501,7 +519,7 @@ def main():
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">')
lines.append(f'<MetaDataObject {XMLNS_DECL} version="{format_version}">')
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
lines.append(internal_info_xml)
lines.append("\t\t<Properties>")
@@ -1086,7 +1104,7 @@ def main():
new_form_uuid = new_guid()
form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="2.17">',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
f'\t<Form uuid="{new_form_uuid}">',
'\t\t<InternalInfo/>',
'\t\t<Properties>',
@@ -1113,7 +1131,7 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", "2.17")
form_version = src_form_el.get("version", format_version)
src_auto_cmd = None
form_props = []
@@ -1,7 +1,8 @@
# cfe-validate v1.3 — Validate 1C configuration extension structure (CFE)
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ExtensionPath,
[switch]$Detailed,
@@ -145,10 +146,10 @@ $childTypeDirMap = @{
# Valid enum values for extension properties
$validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
"ScriptVariant" = @("Russian","English")
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
}
# --- 1. Parse XML ---
@@ -196,8 +197,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Must have Configuration child
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE)
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re
@@ -82,11 +82,14 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
}
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
@@ -147,7 +150,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
)
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
@@ -213,8 +216,8 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-create
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name>
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]"
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]"
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]"
allowed-tools:
- Read
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database]
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]"
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database]
allowed-tools:
- Bash
@@ -1,4 +1,4 @@
# db-load-xml v1.1 — Load 1C configuration from XML files
# db-load-xml v1.3 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -98,7 +98,10 @@ param(
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$UpdateDB
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[switch]$StrictLog
)
$OutputEncoding = [System.Text.Encoding]::UTF8
@@ -213,20 +216,58 @@ try {
$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 (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
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
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-xml v1.1 — Load 1C configuration from XML files
# db-load-xml v1.3 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -63,6 +63,11 @@ def main():
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 ---
@@ -157,22 +162,60 @@ def main():
)
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 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
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)
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-run
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]"
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]"
allowed-tools:
- Bash
-60
View File
@@ -1,60 +0,0 @@
---
name: epf-add-form
description: Добавить управляемую форму к внешней обработке 1С
argument-hint: <ProcessorName> <FormName> [Synonym]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /epf-add-form — Добавление формы
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
## Usage
```
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|-------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна существовать) |
| FormName | да | — | Имя формы |
| Synonym | нет | = FormName | Синоним формы |
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
| SrcDir | нет | `src` | Каталог исходников |
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
```
## Что создаётся
```
<SrcDir>/<ProcessorName>/Forms/
├── <FormName>.xml # Метаданные формы (1 UUID)
└── <FormName>/
└── Ext/
├── Form.xml # Описание формы (logform namespace)
└── Form/
└── Module.bsl # BSL-модуль с 4 регионами
```
## Что модифицируется
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
## Детали
- FormType: Managed
- UsePurposes: PlatformApplication, MobilePlatformApplication
- AutoCommandBar с id=-1
- Реквизит "Объект" с MainAttribute=true
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
@@ -1,207 +0,0 @@
# epf-add-form v1.0 — Add managed form to 1C processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ProcessorName,
[Parameter(Mandatory)]
[string]$FormName,
[string]$Synonym = $FormName,
[switch]$Main,
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
# --- Проверки ---
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
if (-not (Test-Path $rootXmlPath)) {
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
exit 1
}
$processorDir = Join-Path $SrcDir $ProcessorName
$formsDir = Join-Path $processorDir "Forms"
$formMetaPath = Join-Path $formsDir "$FormName.xml"
if (Test-Path $formMetaPath) {
Write-Error "Форма уже существует: $formMetaPath"
exit 1
}
# --- Создание каталогов ---
$formDir = Join-Path $formsDir $FormName
$formExtDir = Join-Path $formDir "Ext"
$formModuleDir = Join-Path $formExtDir "Form"
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
# --- Кодировка ---
$encBom = New-Object System.Text.UTF8Encoding($true)
$encNoBom = New-Object System.Text.UTF8Encoding($false)
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
$formUuid = [guid]::NewGuid().ToString()
$formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Form uuid="$formUuid">
<Properties>
<Name>$FormName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<FormType>Managed</FormType>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>
<ExtendedPresentation/>
</Properties>
</Form>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="Объект" id="1">
<Type>
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
</Attribute>
</Attributes>
</Form>
"@
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
$modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
# --- 4. Модификация корневого XML ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
exit 1
}
# Добавить <Form> перед первым <Template>, или в конец
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
if ($firstTemplate) {
# Вставить перед Template, добавив перенос строки + табуляцию
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
} else {
# Добавить в конец ChildObjects
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
if ($childObjects.ChildNodes.Count -eq 0) {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
} else {
$lastChild = $childObjects.LastChild
# Вставить перед закрывающим whitespace (если есть), или в конец
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
}
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstForm = ($existingForms.Count -eq 1)
if ($Main -or $isFirstForm) {
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
if ($defaultForm) {
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
}
}
# Сохранить с BOM
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false # Preserve original whitespace
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
Write-Host "[OK] Создана форма: $FormName"
Write-Host " Метаданные: $formMetaPath"
Write-Host " Описание: $formXmlPath"
Write-Host " Модуль: $modulePath"
if ($Main -or $isFirstForm) {
Write-Host " DefaultForm обновлён"
}
@@ -1,253 +0,0 @@
#!/usr/bin/env python3
# add-form v1.0 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?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
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-add-command
description: Добавить команду в дополнительную обработку БСП
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools:
- Read
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-init
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид>
allowed-tools:
- Read
@@ -203,6 +203,6 @@ allowed-tools:
## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/epf-add-form`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Собрать EPF: `/epf-build`
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym]
allowed-tools:
- Bash
@@ -35,7 +35,7 @@ powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "
## Дальнейшие шаги
- Добавить форму: `/epf-add-form`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать EPF: `/epf-build`
@@ -1,8 +1,9 @@
# epf-validate v1.1 — Validate 1C external data processor / report structure
# epf-validate v1.2 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
[switch]$Detailed,
@@ -184,8 +185,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
}
# Detect type: ExternalDataProcessor or ExternalReport
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-validate v1.1 — Validate 1C external data processor / report structure
# epf-validate v1.2 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -46,7 +46,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default=None)
@@ -165,8 +165,8 @@ def main():
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20"):
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
elif version not in ("2.17", "2.20", "2.21"):
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Detect type
child_elements = []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools:
- Bash
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: form-add
description: Добавить управляемую форму к объекту конфигурации 1С
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools:
- Bash
+33 -22
View File
@@ -1,4 +1,4 @@
# form-add v1.2 — Add managed form to 1C config object
# form-add v1.4 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -16,6 +16,23 @@ param(
$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)
@@ -36,6 +53,8 @@ if (-not (Test-Path $ObjectPath)) {
}
$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)
@@ -54,7 +73,7 @@ if (-not $metaDataObject) {
$supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"
)
@@ -159,7 +178,7 @@ if ($objectType -in $processorLikeTypes) {
$formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<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>
@@ -196,13 +215,10 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17">
<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">
@@ -224,13 +240,10 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17">
<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">
@@ -261,27 +274,30 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
"BusinessProcess" = "BusinessProcessObject"
"Task" = "TaskObject"
"InformationRegister" = "InformationRegisterRecordManager"
"AccumulationRegister" = "AccumulationRegisterRecordSet"
}
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
$savedDataLine = ""
if ($objectType -notin $processorLikeTypes) {
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
}
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17">
<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>
<MainAttribute>true</MainAttribute>$savedDataLine
</Attribute>
</Attributes>
</Form>
@@ -301,11 +317,6 @@ $modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
КонецПроцедуры
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
+32 -21
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# form-add v1.2 — Add managed form to 1C config object
# form-add v1.4 — 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
@@ -15,6 +16,22 @@ NSMAP = {
}
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")
@@ -67,6 +84,8 @@ def main():
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()
@@ -74,7 +93,7 @@ def main():
supported_types = [
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
]
@@ -171,7 +190,7 @@ def main():
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f' version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
@@ -225,13 +244,10 @@ def main():
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\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'
@@ -254,13 +270,10 @@ def main():
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\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'
@@ -291,19 +304,22 @@ def main():
"BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager",
"AccumulationRegister": "AccumulationRegisterRecordSet",
}
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
saved_data_line = ''
if object_type not in processor_like_types:
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\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'
@@ -311,7 +327,7 @@ def main():
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'
f'{saved_data_line}'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
@@ -329,11 +345,6 @@ def main():
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'
+26 -16
View File
@@ -1,7 +1,7 @@
---
name: form-compile
description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов
argument-hint: <JsonPath> <OutputPath>
description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму
argument-hint: <JsonPath> <OutputPath> | -FromObject <OutputPath>
allowed-tools:
- Bash
- Read
@@ -9,29 +9,30 @@ allowed-tools:
- Glob
---
# /form-compile — Генерация Form.xml из JSON DSL
# /form-compile — Генерация Form.xml
Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID.
Два режима:
1. **JSON DSL** — из JSON-определения формы
2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно.
## Использование
```
/form-compile <JsonPath> <OutputPath>
```
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно.
## Параметры
| Параметр | Обязательный | Описание |
|------------|:------------:|-----------------------------------|
| JsonPath | да | Путь к JSON-определению формы |
| OutputPath | да | Путь к выходному файлу Form.xml |
| Параметр | Обязательный | Описание |
|------------|:------------:|---------------------------------|
| JsonPath | режим 1 | Путь к JSON-определению формы |
| OutputPath | да | Путь к выходному Form.xml |
| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта |
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>"
# Режим JSON DSL
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
```
## JSON DSL — справка
@@ -167,6 +168,12 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `footer: true` | Показать подвал |
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
| `choiceMode: true` | Режим выбора (для форм выбора) |
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
| `enableDrag: true` | Разрешить перетаскивание |
| `enableStartDrag: true` | Разрешить начало перетаскивания |
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
### Страницы (pages + page)
@@ -221,6 +228,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```json
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
{ "name": "Список", "type": "DynamicList", "main": true, "settings": {
"mainTable": "Catalog.Номенклатура", "dynamicDataRead": true
}}
{ "name": "Итого", "type": "decimal(15,2)" }
{ "name": "Таблица", "type": "ValueTable", "columns": [
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
@@ -0,0 +1,126 @@
# Form Presets
Пресеты управляют раскладкой форм, генерируемых в режиме `--from-object`.
## Как работает
Цепочка merge (каждый следующий уровень перезаписывает предыдущий через deep merge):
1. **Hardcoded defaults** -- встроены в скрипт, ориентированы на ERP
2. **Built-in preset** -- файл из этой папки (`erp-standard.json` по умолчанию)
3. **Project-level preset** -- файл `presets/skills/form/<name>.json`, поиск вверх от OutputPath
Имя пресета задаётся параметром `--preset` (по умолчанию `erp-standard`).
## Project-level пресет
Чтобы переопределить стандартный пресет в своём проекте, создайте файл:
```
<project-root>/presets/skills/form/erp-standard.json
```
Скрипт ищет этот файл, поднимаясь от OutputPath к корню. Первый найденный файл применяется поверх built-in через deep merge -- не нужно копировать весь пресет, достаточно указать только переопределяемые ключи.
## Секции
Ключи верхнего уровня в JSON -- секции вида `{тип}.{назначение}`:
| Секция | Тип объекта | Назначение формы |
|--------|-------------|------------------|
| `document.item` | Document | Форма документа |
| `document.list` | Document | Форма списка |
| `document.choice` | Document | Форма выбора |
| `catalog.item` | Catalog | Форма элемента |
| `catalog.folder` | Catalog | Форма группы |
| `catalog.list` | Catalog | Форма списка |
| `catalog.choice` | Catalog | Форма выбора |
| `informationRegister.record` | InformationRegister | Форма записи |
| `informationRegister.list` | InformationRegister | Форма списка |
| `accumulationRegister.list` | AccumulationRegister | Форма списка |
| `chartOfCharacteristicTypes.*` | ChartOfCharacteristicTypes | item/folder/list/choice |
| `exchangePlan.*` | ExchangePlan | item/list/choice |
| `chartOfAccounts.*` | ChartOfAccounts | item/folder/list/choice |
### basedOn
Секция может наследовать от другой:
```json
{
"document.choice": {
"basedOn": "document.list",
"properties": { "windowOpeningMode": "LockOwnerWindow" }
}
}
```
## Ключи секций
### Форма объекта (Item/Record)
| Ключ | Описание | Допустимые значения |
|------|----------|---------------------|
| `header.position` | Где размещать шапку | `"insidePage"` -- на первой странице, `"abovePages"` -- над страницами |
| `header.layout` | Колонки шапки | `"1col"`, `"2col"` |
| `header.distribute` | Распределение в 2 колонках | `"even"`, `"left"`, `"right"` |
| `header.dateTitle` | Заголовок даты (Document) | строка, напр. `"от"` |
| `footer.fields` | Поля в подвале | массив имён реквизитов, напр. `["Комментарий"]` |
| `footer.position` | Где размещать подвал | `"insidePage"`, `"belowPages"`, `"none"` |
| `tabularSections.container` | Контейнер табчастей | `"pages"` -- на вкладках, `"inline"` -- в корне, `"single-no-pages"` -- одна ТЧ без страниц |
| `tabularSections.exclude` | Исключить табчасти | массив имён, напр. `["ДополнительныеРеквизиты"]` |
| `tabularSections.lineNumber` | Колонка НомерСтроки | `true` / `false` |
| `additional.position` | Блок доп. реквизитов | `"page"` -- отдельная вкладка, `"below"` -- под табчастями, `"none"` -- не создавать |
| `additional.layout` | Колонки доп. блока | `"1col"`, `"2col"` |
| `additional.bspGroup` | Группа ДополнительныеРеквизиты | `true` / `false` |
| `codeDescription.layout` | Код + Наименование | `"horizontal"`, `"vertical"` |
| `codeDescription.order` | Порядок Код/Наименование | `"descriptionFirst"`, `"codeFirst"` |
| `parent.title` | Заголовок поля Родитель | строка, напр. `"Входит в группу"` |
| `parent.position` | Позиция поля Родитель | `"beforeCodeDescription"`, `"afterCodeDescription"`, `"inHeader"` |
| `owner.readOnly` | Владелец только для чтения | `true` / `false` |
| `owner.position` | Позиция поля Владелец | `"first"` |
| `fieldDefaults.ref.choiceButton` | Кнопка выбора для ссылок | `true` / `false` |
| `fieldDefaults.boolean.element` | Элемент для Boolean | `"check"` (флажок) |
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
| `properties` | Свойства формы | объект: `autoTitle`, `windowOpeningMode` и др. |
### Форма списка (List/Choice)
| Ключ | Описание | Допустимые значения |
|------|----------|---------------------|
| `columns` | Какие колонки показывать | `"all"` -- все реквизиты, или массив имён |
| `columnType` | Тип элемента колонки | `"labelField"`, `"input"` |
| `hiddenRef` | Скрытая колонка Ref | `true` / `false` |
| `tableCommandBar` | Командная панель таблицы | `"auto"`, `"none"` |
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
| `choiceMode` | Режим выбора (ChoiceForm) | `true` / `false` |
| `properties` | Свойства формы | объект: `windowOpeningMode` и др. |
## Пример project-level пресета
```json
{
"name": "my-project",
"description": "Стиль форм нашего проекта",
"document.item": {
"header": {
"layout": "1col"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "СведенияОСертификатах"]
},
"additional": {
"position": "none"
}
},
"catalog.item": {
"codeDescription": {
"order": "codeFirst"
}
}
}
```
Этот файл переопределяет только указанные ключи -- остальное наследуется из built-in пресета.
@@ -0,0 +1,68 @@
{
"name": "erp-standard",
"description": "ERP 8.3.24 standard form layout",
"document.item": {
"header": {
"position": "insidePage",
"layout": "2col",
"distribute": "even",
"dateTitle": "от"
},
"footer": {
"fields": ["Комментарий"],
"position": "insidePage"
},
"tabularSections": {
"container": "pages",
"exclude": ["ДополнительныеРеквизиты"],
"lineNumber": true
},
"additional": {
"position": "page",
"layout": "2col",
"bspGroup": true
},
"properties": {
"autoTitle": false
}
},
"catalog.item": {
"codeDescription": {
"layout": "horizontal",
"order": "descriptionFirst"
},
"parent": {
"title": "Входит в группу",
"position": "afterCodeDescription"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "Представления"]
}
},
"informationRegister.record": {
"properties": {
"windowOpeningMode": "LockOwnerWindow"
}
},
"informationRegister.list": {},
"accumulationRegister.list": {},
"chartOfCharacteristicTypes.item": {
"basedOn": "catalog.item"
},
"exchangePlan.item": {
"basedOn": "catalog.item"
},
"chartOfAccounts.item": {
"parent": {
"title": "Подчинен счету"
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[Parameter(Mandatory)]
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-JsonPath", required=True)
args = parser.parse_args()
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
[Alias('Path')]
[string]$FormPath,
[int]$Limit = 150,
[int]$Offset = 0,
@@ -342,7 +342,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Analyze 1C managed form structure", allow_abbrev=False)
parser.add_argument("-FormPath", required=True, help="Path to Form.xml")
parser.add_argument("-FormPath", "-Path", required=True, help="Path to Form.xml")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
@@ -1,7 +1,8 @@
# form-validate v1.2 — Validate 1C managed form
# form-validate v1.4 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[switch]$Detailed,
@@ -58,6 +59,20 @@ $nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$root = $xmlDoc.DocumentElement
# --- Detect context: config vs EPF/ERF ---
# Walk up from FormPath looking for Configuration.xml → config context
# No Configuration.xml → external data processor / report (EPF/ERF)
$script:isConfigContext = $false
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent
for ($i = 0; $i -lt 15; $i++) {
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
$script:isConfigContext = $true
break
}
$walkDir = Split-Path $walkDir
}
# --- Counters ---
$errors = 0
@@ -112,10 +127,10 @@ if ($root.LocalName -ne "Form") {
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
} else {
$version = $root.GetAttribute("version")
if ($version -eq "2.17") {
if ($version -eq "2.17" -or $version -eq "2.20") {
Report-OK "Root element: Form version=$version"
} elseif ($version) {
Report-Warn "Form version='$version' (expected 2.17)"
Report-Warn "Form version='$version' (expected 2.17 or 2.20)"
} else {
Report-Warn "Form version attribute missing"
}
@@ -696,6 +711,7 @@ $validCfgPrefixes = @(
"ChartOfCharacteristicTypesObject","ChartOfCharacteristicTypesRef"
"ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef"
"DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef"
"ExternalDataProcessorObject","ExternalReportObject"
"InformationRegisterRecordManager","InformationRegisterRecordSet"
"ReportObject","TaskObject","TaskRef"
)
@@ -719,7 +735,15 @@ if (-not $stopped) {
$cfgVal = $Matches[1]
if ($cfgVal -eq "DynamicList") { continue }
if ($cfgVal -match '^([^.]+)\.') {
if ($Matches[1] -in $validCfgPrefixes) { continue }
$pfx = $Matches[1]
if ($pfx -in $validCfgPrefixes) {
# ExternalDataProcessorObject/ExternalReportObject valid only for EPF/ERF, not config
if ($script:isConfigContext -and ($pfx -eq "ExternalDataProcessorObject" -or $pfx -eq "ExternalReportObject")) {
Report-Error "12. Type '$tv': External* type in configuration context (use DataProcessorObject/ReportObject instead)"
$typeOk = $false; $typeInvalid++
}
continue
}
}
Report-Warn "12. Type '$tv': unrecognized cfg prefix"
$typeOk = $false
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.2 — Validate 1C managed form
# form-validate v1.4 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -43,6 +43,7 @@ VALID_CFG_PREFIXES = {
'ChartOfCharacteristicTypesObject', 'ChartOfCharacteristicTypesRef',
'ConstantsSet', 'DataProcessorObject', 'DocumentObject', 'DocumentRef',
'DynamicList', 'EnumRef', 'ExchangePlanObject', 'ExchangePlanRef',
'ExternalDataProcessorObject', 'ExternalReportObject',
'InformationRegisterRecordManager', 'InformationRegisterRecordSet',
'ReportObject', 'TaskObject', 'TaskRef',
}
@@ -56,7 +57,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Validate 1C managed form", allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
args = parser.parse_args()
@@ -103,6 +104,18 @@ def main():
root = tree.getroot()
# Detect context: config vs EPF/ERF
is_config_context = False
walk_dir = os.path.dirname(os.path.abspath(form_path))
for _ in range(15):
parent = os.path.dirname(walk_dir)
if parent == walk_dir:
break
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
is_config_context = True
break
walk_dir = parent
errors = 0
warnings = 0
ok_count = 0
@@ -148,10 +161,10 @@ def main():
report_error(f"Root element is '{localname(root)}', expected 'Form'")
else:
version = root.get("version", "")
if version == "2.17":
if version in ("2.17", "2.20"):
report_ok(f"Root element: Form version={version}")
elif version:
report_warn(f"Form version='{version}' (expected 2.17)")
report_warn(f"Form version='{version}' (expected 2.17 or 2.20)")
else:
report_warn("Form version attribute missing")
@@ -645,7 +658,10 @@ def main():
suffix = tv[4:] # after "cfg:"
prefix = suffix.split(".")[0]
if prefix in VALID_CFG_PREFIXES or suffix == "DynamicList":
pass # OK
# ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context
if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'):
report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)')
type_invalid += 1
else:
report_warn(f'12. Type "{tv}": unrecognized cfg prefix')
type_warn_count += 1
+21 -2
View File
@@ -1,4 +1,4 @@
# help-add v1.2 — Add built-in help to 1C object
# help-add v1.3 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -11,6 +11,25 @@ param(
$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
@@ -35,7 +54,7 @@ $encBom = New-Object System.Text.UTF8Encoding($true)
$helpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<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>
"@
+21 -2
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# add-help v1.2 — Add built-in help to 1C object
# 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
@@ -11,6 +12,22 @@ 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")
@@ -41,6 +58,8 @@ def main():
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)
@@ -62,7 +81,7 @@ def main():
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f' version="{format_version}">\n'
f'\t<Page>{lang}</Page>\n'
'</Help>'
)
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: img-grid
description: Наложить пронумерованную сетку на изображение для определения пропорций колонок
description: Наложить пронумерованную сетку на изображение. Используй при анализе скриншота макета или печатной формы — измерить пропорции колонок перед генерацией табличного документа
argument-hint: <ImagePath> [-c COLS]
allowed-tools:
- Bash
@@ -1,7 +1,7 @@
# interface-edit v1.2 — Edit 1C CommandInterface.xml
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$CIPath,
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[string]$DefinitionFile,
[ValidateSet("hide","show","place","order","subsystem-order","group-order")]
[string]$Operation,
@@ -23,6 +23,25 @@ if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
}
$resolvedPath = $CIPath
# --- 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 ([System.IO.Path]::GetDirectoryName($CIPath))
# --- Namespaces ---
$script:ciNs = "http://v8.1c.ru/8.3/xcf/extrnprops"
$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable"
@@ -42,7 +61,7 @@ if (-not (Test-Path $CIPath)) {
xmlns:xr="$($script:xrNs)"
xmlns:xs="$($script:xsNs)"
xmlns:xsi="$($script:xsiNs)"
version="2.17">
version="$formatVersion">
</CommandInterface>
"@
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
@@ -1,14 +1,31 @@
#!/usr/bin/env python3
# interface-edit v1.2 — Edit 1C CommandInterface.xml
# 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"
@@ -165,7 +182,7 @@ 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("-CIPath", "-Path", 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)
@@ -181,6 +198,10 @@ def main():
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):
@@ -199,7 +220,7 @@ def main():
f'\txmlns:xr="{XR_NS}"\n'
f'\txmlns:xs="{XS_NS}"\n'
f'\txmlns:xsi="{XSI_NS}"\n'
f'\tversion="2.17">\n'
f'\tversion="{format_version}">\n'
f'</CommandInterface>'
)
with open(ci_path, "w", encoding="utf-8-sig") as fh:
@@ -483,7 +504,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-CIPath", "-Path", resolved_path])
# --- Summary ---
print()
@@ -1,7 +1,7 @@
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$CIPath,
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile
@@ -79,7 +79,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C CommandInterface.xml structure', allow_abbrev=False
)
parser.add_argument('-CIPath', dest='CIPath', required=True)
parser.add_argument('-CIPath', '-Path', dest='CIPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: meta-compile
description: Создать объект метаданных 1С. Используй когда пользователь просит создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
argument-hint: <JsonPath> <OutputDir>
allowed-tools:
- Bash
@@ -6,13 +6,20 @@
|-----------|----------|-------------|
| `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 |
@@ -156,15 +156,15 @@
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `dependenceOnCalculationTypes` | `NotUsed` | DependenceOnCalculationTypes |
| `dependenceOnCalculationTypes` | `DontUse` | DependenceOnCalculationTypes |
| `actionPeriodUse` | `false` | ActionPeriodUse |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
`dependenceOnCalculationTypes`: `NotUsed`, `ExclusionAndDependence`, `ExclusionOnly`.
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
```json
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "ExclusionAndDependence" }
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
```
## Зависимости
@@ -1,4 +1,4 @@
# meta-compile v1.5 — Compile 1C metadata object from JSON
# meta-compile v1.10 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -87,8 +87,8 @@ $script:enumValueAliases = @{
"RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate"
"ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent"
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"
"Depend" = "RequireCalculationTypes"; "RequireCalculation" = "RequireCalculationTypes"
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse"
"Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod"
# InformationRegisterPeriodicity
"None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month"
"Quarterly" = "Quarter"; "Yearly" = "Year"
@@ -117,7 +117,7 @@ $script:validEnumValues = @{
"RegisterType" = @("Balance","Turnovers")
"WriteMode" = @("Independent","RecorderSubordinate")
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"DependenceOnCalculationTypes" = @("DontUse","RequireCalculationTypes")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
"DataLockControlMode" = @("Automatic","Managed")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
@@ -136,22 +136,28 @@ $script:validEnumValues = @{
"ReuseSessions" = @("DontUse","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"SubordinationUse" = @("ToItems","ToFolders","ToFoldersAndItems")
"CodeSeries" = @("WholeCatalog","WithinSubordination")
"ChoiceMode" = @("BothWays","QuickChoice","FromForm")
}
function Normalize-EnumValue {
param([string]$propName, [string]$value)
# 1. Check alias dictionary
# 1. Check alias dictionary — silent auto-correct
if ($script:enumValueAliases.ContainsKey($value)) {
return $script:enumValueAliases[$value]
}
# 2. Case-insensitive match against valid values
# 2. Case-insensitive match against valid values — silent
$valid = $script:validEnumValues[$propName]
if ($valid) {
foreach ($v in $valid) {
if ($v -ieq $value) { return $v }
}
# 3. Known property, unknown value — error with hint
Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')"
exit 1
}
# 3. Return as-is (validator will catch if wrong)
# 4. Unknown property — pass-through (no validation data)
return $value
}
@@ -495,6 +501,7 @@ function Parse-AttributeShorthand {
flags = @(if ($val.flags) { $val.flags } else { @() })
fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" }
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
}
}
@@ -757,7 +764,8 @@ function Emit-Attribute {
param([string]$indent, $parsed, [string]$context)
# $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register"
$attrName = $parsed.name
if ($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName)) {
if ($context -notin @("tabular", "processor-tabular") -and
($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
}
$uuid = New-Guid-String
@@ -784,18 +792,20 @@ function Emit-Attribute {
X "$indent`t`t<ToolTip/>"
X "$indent`t`t<MarkNegatives>false</MarkNegatives>"
X "$indent`t`t<Mask/>"
X "$indent`t`t<MultiLine>false</MultiLine>"
$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
X "$indent`t`t<ExtendedEdit>false</ExtendedEdit>"
X "$indent`t`t<MinValue xsi:nil=`"true`"/>"
X "$indent`t`t<MaxValue xsi:nil=`"true`"/>"
# FillFromFillingValue — not for tabular/processor (non-stored objects don't have these)
if ($context -notin @("tabular", "processor")) {
# FillFromFillingValue — not for tabular/processor/chart/register-other
# (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these)
if ($context -notin @("tabular", "processor", "chart", "register-other")) {
X "$indent`t`t<FillFromFillingValue>false</FillFromFillingValue>"
}
# FillValue — not for tabular/processor
if ($context -notin @("tabular", "processor")) {
# FillValue — same restriction
if ($context -notin @("tabular", "processor", "chart", "register-other")) {
Emit-FillValue "$indent`t`t" $typeStr
}
@@ -828,7 +838,10 @@ function Emit-Attribute {
X "$indent`t`t<Indexing>$indexing</Indexing>"
X "$indent`t`t<FullTextSearch>Use</FullTextSearch>"
X "$indent`t`t<DataHistory>Use</DataHistory>"
# DataHistory — not for Chart* types and non-InformationRegister register family
if ($context -notin @("chart", "register-other")) {
X "$indent`t`t<DataHistory>Use</DataHistory>"
}
}
X "$indent`t</Properties>"
@@ -924,7 +937,8 @@ function Emit-Dimension {
X "$indent`t`t<ToolTip/>"
X "$indent`t`t<MarkNegatives>false</MarkNegatives>"
X "$indent`t`t<Mask/>"
X "$indent`t`t<MultiLine>false</MultiLine>"
$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
X "$indent`t`t<ExtendedEdit>false</ExtendedEdit>"
X "$indent`t`t<MinValue xsi:nil=`"true`"/>"
X "$indent`t`t<MaxValue xsi:nil=`"true`"/>"
@@ -1017,7 +1031,8 @@ function Emit-Resource {
X "$indent`t`t<ToolTip/>"
X "$indent`t`t<MarkNegatives>false</MarkNegatives>"
X "$indent`t`t<Mask/>"
X "$indent`t`t<MultiLine>false</MultiLine>"
$multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
X "$indent`t`t<MultiLine>$multiLine</MultiLine>"
X "$indent`t`t<ExtendedEdit>false</ExtendedEdit>"
X "$indent`t`t<MinValue xsi:nil=`"true`"/>"
X "$indent`t`t<MaxValue xsi:nil=`"true`"/>"
@@ -1071,12 +1086,25 @@ function Emit-CatalogProperties {
$hierarchyType = Get-EnumProp "HierarchyType" "hierarchyType" "HierarchyFoldersAndItems"
X "$i<Hierarchical>$hierarchical</Hierarchical>"
X "$i<HierarchyType>$hierarchyType</HierarchyType>"
X "$i<LimitLevelCount>false</LimitLevelCount>"
X "$i<LevelCount>2</LevelCount>"
X "$i<FoldersOnTop>true</FoldersOnTop>"
$limitLevelCount = if ($def.limitLevelCount -eq $true) { "true" } else { "false" }
$levelCount = if ($null -ne $def.levelCount) { "$($def.levelCount)" } else { "2" }
$foldersOnTop = if ($def.foldersOnTop -eq $false) { "false" } else { "true" }
X "$i<LimitLevelCount>$limitLevelCount</LimitLevelCount>"
X "$i<LevelCount>$levelCount</LevelCount>"
X "$i<FoldersOnTop>$foldersOnTop</FoldersOnTop>"
X "$i<UseStandardCommands>true</UseStandardCommands>"
X "$i<Owners/>"
X "$i<SubordinationUse>ToItems</SubordinationUse>"
if ($def.owners -and $def.owners.Count -gt 0) {
X "$i<Owners>"
foreach ($ownerRef in $def.owners) {
$fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" }
X "$i`t<xr:Item xsi:type=`"xr:MDObjectRef`">$fullRef</xr:Item>"
}
X "$i</Owners>"
} else {
X "$i<Owners/>"
}
$subordinationUse = Get-EnumProp "SubordinationUse" "subordinationUse" "ToItems"
X "$i<SubordinationUse>$subordinationUse</SubordinationUse>"
$codeLength = if ($null -ne $def.codeLength) { "$($def.codeLength)" } else { "9" }
$descriptionLength = if ($null -ne $def.descriptionLength) { "$($def.descriptionLength)" } else { "25" }
@@ -1089,7 +1117,8 @@ function Emit-CatalogProperties {
X "$i<DescriptionLength>$descriptionLength</DescriptionLength>"
X "$i<CodeType>$codeType</CodeType>"
X "$i<CodeAllowedLength>$codeAllowedLength</CodeAllowedLength>"
X "$i<CodeSeries>WholeCatalog</CodeSeries>"
$codeSeries = Get-EnumProp "CodeSeries" "codeSeries" "WholeCatalog"
X "$i<CodeSeries>$codeSeries</CodeSeries>"
X "$i<CheckUnique>$checkUnique</CheckUnique>"
X "$i<Autonumbering>$autonumbering</Autonumbering>"
@@ -1100,8 +1129,10 @@ function Emit-CatalogProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
X "$i<QuickChoice>true</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
$quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
$choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays"
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>$choiceMode</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>Catalog.$objName.StandardAttribute.Description</xr:Field>"
X "$i`t<xr:Field>Catalog.$objName.StandardAttribute.Code</xr:Field>"
@@ -2537,13 +2568,32 @@ function Emit-AddressingAttribute {
$script:xmlnsDecl = '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"'
# --- 14a. Detect format version from existing Configuration.xml ---
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"
}
$script:formatVersion = Detect-FormatVersion $OutputDir
# --- 15. Main assembler ---
$uuid = New-Guid-String
# XML declaration
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">"
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
X "`t<$objType uuid=`"$uuid`">"
# InternalInfo
@@ -2633,6 +2683,7 @@ if ($objType -in $typesWithAttrTS) {
"Catalog" { "catalog" }
"Document" { "document" }
{ $_ -in @("DataProcessor","Report") } { "processor" }
{ $_ -in @("ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes") } { "chart" }
default { "object" }
}
foreach ($a in $attrs) {
@@ -2643,10 +2694,12 @@ if ($objType -in $typesWithAttrTS) {
Emit-TabularSection "`t`t`t" $tsName $columns $objType $objName
}
foreach ($af in $acctFlags) {
Emit-AccountingFlag "`t`t`t" "$af"
$afName = if ($af.name) { $af.name } else { "$af" }
Emit-AccountingFlag "`t`t`t" $afName
}
foreach ($edf in $extDimFlags) {
Emit-ExtDimensionAccountingFlag "`t`t`t" "$edf"
$edfName = if ($edf.name) { $edf.name } else { "$edf" }
Emit-ExtDimensionAccountingFlag "`t`t`t" $edfName
}
foreach ($aa in $addrAttrs) {
Emit-AddressingAttribute "`t`t`t" $aa
@@ -2709,8 +2762,11 @@ if ($objType -in @("InformationRegister","AccumulationRegister","AccountingRegis
foreach ($d in $dims) {
Emit-Dimension "`t`t`t" $d $objType
}
# InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory;
# AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT.
$regCtx = if ($objType -eq "InformationRegister") { "register-info" } else { "register-other" }
foreach ($a in $regAttrs) {
Emit-Attribute "`t`t`t" $a "register"
Emit-Attribute "`t`t`t" $a $regCtx
}
X "`t`t</ChildObjects>"
} else {
@@ -2904,7 +2960,7 @@ if ($objType -eq "ExchangePlan") {
$contentPath = Join-Path $extDir "Content.xml"
if (-not (Test-Path $contentPath)) {
Ensure-ExtDir
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent xmlns=`"http://v8.1c.ru/8.3/xcf/extrnprops`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" version=`"2.17`"/>`r`n"
$contentXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<ExchangePlanContent xmlns=`"http://v8.1c.ru/8.3/xcf/extrnprops`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" version=`"$($script:formatVersion)`"/>`r`n"
[System.IO.File]::WriteAllText($contentPath, $contentXml, $enc)
$modulesCreated += $contentPath
}
@@ -2913,7 +2969,7 @@ if ($objType -eq "BusinessProcess") {
$flowchartPath = Join-Path $extDir "Flowchart.xml"
if (-not (Test-Path $flowchartPath)) {
Ensure-ExtDir
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"2.17`"/>`r`n"
$flowchartXml = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>`r`n<Flowchart xmlns=`"http://v8.1c.ru/8.3/MDClasses`" version=`"$($script:formatVersion)`"/>`r`n"
[System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc)
$modulesCreated += $flowchartPath
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.5 — Compile 1C metadata object from JSON
# meta-compile v1.10 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -147,8 +147,8 @@ enum_value_aliases = {
'RecordSubordinate': 'RecorderSubordinate', 'Subordinate': 'RecorderSubordinate',
'ПодчинениеРегистратору': 'RecorderSubordinate', 'Независимый': 'Independent',
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse',
'Depend': 'RequireCalculationTypes', 'RequireCalculation': 'RequireCalculationTypes',
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse', 'NotUsed': 'DontUse',
'Depend': 'OnActionPeriod', 'ПоПериодуДействия': 'OnActionPeriod',
# InformationRegisterPeriodicity
'None': 'Nonperiodical', 'Daily': 'Day', 'Monthly': 'Month',
'Quarterly': 'Quarter', 'Yearly': 'Year',
@@ -177,7 +177,7 @@ valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'],
'WriteMode': ['Independent', 'RecorderSubordinate'],
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
'DependenceOnCalculationTypes': ['DontUse', 'RequireCalculationTypes'],
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
'DataLockControlMode': ['Automatic', 'Managed'],
'FullTextSearch': ['Use', 'DontUse'],
'DataHistory': ['Use', 'DontUse'],
@@ -196,19 +196,25 @@ valid_enum_values = {
'ReuseSessions': ['DontUse', 'AutoUse'],
'FillChecking': ['DontCheck', 'ShowError', 'ShowWarning'],
'Indexing': ['DontIndex', 'Index', 'IndexWithAdditionalOrder'],
'SubordinationUse': ['ToItems', 'ToFolders', 'ToFoldersAndItems'],
'CodeSeries': ['WholeCatalog', 'WithinSubordination'],
'ChoiceMode': ['BothWays', 'QuickChoice', 'FromForm'],
}
def normalize_enum_value(prop_name, value):
# 1. Check alias dictionary
# 1. Check alias dictionary — silent auto-correct
if value in enum_value_aliases:
return enum_value_aliases[value]
# 2. Case-insensitive match against valid values
# 2. Case-insensitive match against valid values — silent
valid = valid_enum_values.get(prop_name)
if valid:
for v in valid:
if v.lower() == value.lower():
return v
# 3. Return as-is (validator will catch if wrong)
# 3. Known property, unknown value — error with hint
print(f"Invalid value '{value}' for property '{prop_name}'. Valid values: {', '.join(valid)}", file=sys.stderr)
sys.exit(1)
# 4. Unknown property — pass-through (no validation data)
return value
def get_enum_prop(prop_name, field_name, default):
@@ -458,6 +464,7 @@ def parse_attribute_shorthand(val):
'flags': list(val.get('flags', [])),
'fillChecking': str(val['fillChecking']) if val.get('fillChecking') else '',
'indexing': str(val['indexing']) if val.get('indexing') else '',
'multiLine': True if val.get('multiLine') is True else False,
}
def parse_enum_value_shorthand(val):
@@ -722,7 +729,7 @@ RESERVED_ATTR_NAMES_RU = {
def emit_attribute(indent, parsed, context):
attr_name = parsed['name']
if attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU:
if context not in ('tabular', 'processor-tabular') and (attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU):
print(f"WARNING: Attribute '{attr_name}' conflicts with a standard attribute name. This may cause errors when loading into 1C.", file=sys.stderr)
uid = new_uuid()
X(f'{indent}<Attribute uuid="{uid}">')
@@ -743,13 +750,16 @@ def emit_attribute(indent, parsed, context):
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
X(f'{indent}\t\t<MultiLine>false</MultiLine>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
X(f'{indent}\t\t<ExtendedEdit>false</ExtendedEdit>')
X(f'{indent}\t\t<MinValue xsi:nil="true"/>')
X(f'{indent}\t\t<MaxValue xsi:nil="true"/>')
if context not in ('tabular', 'processor'):
# FillFromFillingValue / FillValue — not for tabular/processor/chart/register-other
# (Chart*, AccumulationRegister/AccountingRegister/CalculationRegister don't support these)
if context not in ('tabular', 'processor', 'chart', 'register-other'):
X(f'{indent}\t\t<FillFromFillingValue>false</FillFromFillingValue>')
if context not in ('tabular', 'processor'):
if context not in ('tabular', 'processor', 'chart', 'register-other'):
emit_fill_value(f'{indent}\t\t', type_str)
fill_checking = 'DontCheck'
if 'req' in parsed.get('flags', []):
@@ -777,7 +787,9 @@ def emit_attribute(indent, parsed, context):
indexing = parsed['indexing']
X(f'{indent}\t\t<Indexing>{indexing}</Indexing>')
X(f'{indent}\t\t<FullTextSearch>Use</FullTextSearch>')
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
# DataHistory — not for Chart* types and non-InformationRegister register family
if context not in ('chart', 'register-other'):
X(f'{indent}\t\t<DataHistory>Use</DataHistory>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Attribute>')
@@ -857,7 +869,8 @@ def emit_dimension(indent, parsed, register_type):
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
X(f'{indent}\t\t<MultiLine>false</MultiLine>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
X(f'{indent}\t\t<ExtendedEdit>false</ExtendedEdit>')
X(f'{indent}\t\t<MinValue xsi:nil="true"/>')
X(f'{indent}\t\t<MaxValue xsi:nil="true"/>')
@@ -930,7 +943,8 @@ def emit_resource(indent, parsed, register_type):
X(f'{indent}\t\t<ToolTip/>')
X(f'{indent}\t\t<MarkNegatives>false</MarkNegatives>')
X(f'{indent}\t\t<Mask/>')
X(f'{indent}\t\t<MultiLine>false</MultiLine>')
multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<MultiLine>{multi_line}</MultiLine>')
X(f'{indent}\t\t<ExtendedEdit>false</ExtendedEdit>')
X(f'{indent}\t\t<MinValue xsi:nil="true"/>')
X(f'{indent}\t\t<MaxValue xsi:nil="true"/>')
@@ -972,12 +986,24 @@ def emit_catalog_properties(indent):
hierarchy_type = get_enum_prop('HierarchyType', 'hierarchyType', 'HierarchyFoldersAndItems')
X(f'{i}<Hierarchical>{hierarchical}</Hierarchical>')
X(f'{i}<HierarchyType>{hierarchy_type}</HierarchyType>')
X(f'{i}<LimitLevelCount>false</LimitLevelCount>')
X(f'{i}<LevelCount>2</LevelCount>')
X(f'{i}<FoldersOnTop>true</FoldersOnTop>')
limit_level_count = 'true' if defn.get('limitLevelCount') is True else 'false'
level_count = str(defn['levelCount']) if defn.get('levelCount') is not None else '2'
folders_on_top = 'false' if defn.get('foldersOnTop') is False else 'true'
X(f'{i}<LimitLevelCount>{limit_level_count}</LimitLevelCount>')
X(f'{i}<LevelCount>{level_count}</LevelCount>')
X(f'{i}<FoldersOnTop>{folders_on_top}</FoldersOnTop>')
X(f'{i}<UseStandardCommands>true</UseStandardCommands>')
X(f'{i}<Owners/>')
X(f'{i}<SubordinationUse>ToItems</SubordinationUse>')
owners = defn.get('owners', [])
if owners:
X(f'{i}<Owners>')
for owner_ref in owners:
full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}'
X(f'{i}\t<xr:Item xsi:type="xr:MDObjectRef">{full_ref}</xr:Item>')
X(f'{i}</Owners>')
else:
X(f'{i}<Owners/>')
subordination_use = get_enum_prop('SubordinationUse', 'subordinationUse', 'ToItems')
X(f'{i}<SubordinationUse>{subordination_use}</SubordinationUse>')
code_length = str(defn['codeLength']) if defn.get('codeLength') is not None else '9'
description_length = str(defn['descriptionLength']) if defn.get('descriptionLength') is not None else '25'
code_type = get_enum_prop('CodeType', 'codeType', 'String')
@@ -988,7 +1014,8 @@ def emit_catalog_properties(indent):
X(f'{i}<DescriptionLength>{description_length}</DescriptionLength>')
X(f'{i}<CodeType>{code_type}</CodeType>')
X(f'{i}<CodeAllowedLength>{code_allowed_length}</CodeAllowedLength>')
X(f'{i}<CodeSeries>WholeCatalog</CodeSeries>')
code_series = get_enum_prop('CodeSeries', 'codeSeries', 'WholeCatalog')
X(f'{i}<CodeSeries>{code_series}</CodeSeries>')
X(f'{i}<CheckUnique>{check_unique}</CheckUnique>')
X(f'{i}<Autonumbering>{autonumbering}</Autonumbering>')
default_presentation = get_enum_prop('DefaultPresentation', 'defaultPresentation', 'AsDescription')
@@ -997,8 +1024,10 @@ def emit_catalog_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<QuickChoice>true</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
choice_mode = get_enum_prop('ChoiceMode', 'choiceMode', 'BothWays')
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>{choice_mode}</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>Catalog.{obj_name}.StandardAttribute.Description</xr:Field>')
X(f'{i}\t<xr:Field>Catalog.{obj_name}.StandardAttribute.Code</xr:Field>')
@@ -2200,6 +2229,27 @@ def emit_addressing_attribute(indent, addr_def):
xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" 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"'
# ---------------------------------------------------------------------------
# 14a. Detect format version from existing Configuration.xml
# ---------------------------------------------------------------------------
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"
format_version = detect_format_version(output_dir)
# ---------------------------------------------------------------------------
# 15. Main assembler
# ---------------------------------------------------------------------------
@@ -2207,7 +2257,7 @@ xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8
obj_uuid = new_uuid()
X('<?xml version="1.0" encoding="UTF-8"?>')
X(f'<MetaDataObject {xmlns_decl} version="2.17">')
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
X(f'\t<{obj_type} uuid="{obj_uuid}">')
# InternalInfo
@@ -2305,6 +2355,8 @@ if obj_type in types_with_attr_ts:
context = 'document'
elif obj_type in ('DataProcessor', 'Report'):
context = 'processor'
elif obj_type in ('ChartOfAccounts', 'ChartOfCharacteristicTypes', 'ChartOfCalculationTypes'):
context = 'chart'
else:
context = 'object'
for a in attrs:
@@ -2313,9 +2365,11 @@ if obj_type in types_with_attr_ts:
columns = ts_sections[ts_name]
emit_tabular_section('\t\t\t', ts_name, columns, obj_type, obj_name)
for af in acct_flags:
emit_accounting_flag('\t\t\t', str(af))
af_name = af['name'] if isinstance(af, dict) else str(af)
emit_accounting_flag('\t\t\t', af_name)
for edf in ext_dim_flags:
emit_ext_dimension_accounting_flag('\t\t\t', str(edf))
edf_name = edf['name'] if isinstance(edf, dict) else str(edf)
emit_ext_dimension_accounting_flag('\t\t\t', edf_name)
for aa in addr_attrs:
emit_addressing_attribute('\t\t\t', aa)
X('\t\t</ChildObjects>')
@@ -2360,8 +2414,11 @@ if obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegist
emit_resource('\t\t\t', r, obj_type)
for d in dims:
emit_dimension('\t\t\t', d, obj_type)
# InformationRegister.Attribute supports FillFromFillingValue/FillValue/DataHistory;
# AccumulationRegister/AccountingRegister/CalculationRegister.Attribute do NOT.
reg_ctx = 'register-info' if obj_type == 'InformationRegister' else 'register-other'
for a in reg_attrs:
emit_attribute('\t\t\t', a, 'register')
emit_attribute('\t\t\t', a, reg_ctx)
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
@@ -2525,7 +2582,7 @@ if obj_type == 'ExchangePlan':
content_path = os.path.join(ext_dir, 'Content.xml')
if not os.path.isfile(content_path):
ensure_ext_dir()
content_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" version="2.17"/>\r\n'
content_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<ExchangePlanContent xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" version="{format_version}"/>\r\n'
write_utf8_bom(content_path, content_xml)
modules_created.append(content_path)
@@ -2533,7 +2590,7 @@ if obj_type == 'BusinessProcess':
flowchart_path = os.path.join(ext_dir, 'Flowchart.xml')
if not os.path.isfile(flowchart_path):
ensure_ext_dir()
flowchart_xml = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="2.17"/>\r\n'
flowchart_xml = f'<?xml version="1.0" encoding="UTF-8"?>\r\n<Flowchart xmlns="http://v8.1c.ru/8.3/MDClasses" version="{format_version}"/>\r\n'
write_utf8_bom(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path)
+35 -16
View File
@@ -1,9 +1,10 @@
# meta-edit v1.5 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
# Inline mode (alternative to DefinitionFile)
@@ -48,47 +49,65 @@ $script:enumValueAliases = @{
"Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers"
"RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate"
"ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent"
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse"
"Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod"
"None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month"
"Quarterly" = "Quarter"; "Yearly" = "Year"
"Непериодический" = "Nonperiodical"; "День" = "Day"; "Месяц" = "Month"
"Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"; "Месяц" = "Month"
"Квартал" = "Quarter"; "Год" = "Year"
"ПозицияРегистратора" = "RecorderPosition"
"Автоматический" = "Automatic"; "Управляемый" = "Managed"
"Использовать" = "Use"; "НеИспользовать" = "DontUse"
"Разрешить" = "Allow"; "Запретить" = "Deny"
"ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays"
"ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode"
"НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning"
"НеИндексировать" = "DontIndex"; "Индексировать" = "Index"
"ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder"
}
$script:validEnumValues = @{
"RegisterType" = @("Balance","Turnovers")
"WriteMode" = @("Independent","RecorderSubordinate")
"RegisterType" = @("Balance","Turnovers")
"WriteMode" = @("Independent","RecorderSubordinate")
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"DependenceOnCalculationTypes" = @("DontUse","RequireCalculationTypes")
"DataLockControlMode" = @("Automatic","Managed")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
"DefaultPresentation" = @("AsDescription","AsCode")
"Posting" = @("Allow","Deny")
"RealTimePosting" = @("Allow","Deny")
"EditType" = @("InDialog","InList","BothWays")
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly")
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
"DataLockControlMode" = @("Automatic","Managed")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
"DefaultPresentation" = @("AsDescription","AsCode")
"Posting" = @("Allow","Deny")
"RealTimePosting" = @("Allow","Deny")
"EditType" = @("InDialog","InList","BothWays")
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyItemsOnly")
"CodeType" = @("String","Number")
"CodeAllowedLength" = @("Variable","Fixed")
"NumberType" = @("String","Number")
"NumberAllowedLength" = @("Variable","Fixed")
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
"ReuseSessions" = @("DontUse","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
}
function Normalize-EnumValue {
param([string]$propName, [string]$value)
# 1. Check alias dictionary — silent auto-correct
if ($script:enumValueAliases.ContainsKey($value)) {
return $script:enumValueAliases[$value]
}
# 2. Case-insensitive match against valid values — silent
$valid = $script:validEnumValues[$propName]
if ($valid) {
foreach ($v in $valid) {
if ($v -ieq $value) { return $v }
}
# 3. Known property, unknown value — error with hint
Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')"
exit 1
}
# 4. Unknown property — pass-through (no validation data)
return $value
}
+12 -9
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.5 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -88,8 +88,8 @@ enum_value_aliases = {
'RecordSubordinate': 'RecorderSubordinate', 'Subordinate': 'RecorderSubordinate',
'ПодчинениеРегистратору': 'RecorderSubordinate', 'Независимый': 'Independent',
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse',
'Depend': 'RequireCalculationTypes', 'RequireCalculation': 'RequireCalculationTypes',
'NotDependOnCalculationTypes': 'DontUse', 'NoDependence': 'DontUse', 'NotUsed': 'DontUse',
'Depend': 'OnActionPeriod', 'ПоПериодуДействия': 'OnActionPeriod',
# InformationRegisterPeriodicity
'None': 'Nonperiodical', 'Daily': 'Day', 'Monthly': 'Month',
'Quarterly': 'Quarter', 'Yearly': 'Year',
@@ -117,7 +117,7 @@ valid_enum_values = {
'RegisterType': ['Balance', 'Turnovers'],
'WriteMode': ['Independent', 'RecorderSubordinate'],
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
'DependenceOnCalculationTypes': ['DontUse', 'RequireCalculationTypes'],
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
'DataLockControlMode': ['Automatic', 'Managed'],
'FullTextSearch': ['Use', 'DontUse'],
'DataHistory': ['Use', 'DontUse'],
@@ -140,16 +140,19 @@ valid_enum_values = {
def normalize_enum_value(prop_name, value):
# 1. Check alias dictionary
# 1. Check alias dictionary — silent auto-correct
if value in enum_value_aliases:
return enum_value_aliases[value]
# 2. Case-insensitive match against valid values
# 2. Case-insensitive match against valid values — silent
valid = valid_enum_values.get(prop_name)
if valid:
for v in valid:
if v.lower() == value.lower():
return v
# 3. Return as-is (validator will catch if wrong)
# 3. Known property, unknown value — error with hint
print(f"Invalid value '{value}' for property '{prop_name}'. Valid values: {', '.join(valid)}", file=sys.stderr)
sys.exit(1)
# 4. Unknown property — pass-through (no validation data)
return value
@@ -2118,7 +2121,7 @@ def main():
parser = argparse.ArgumentParser(description="Edit existing 1C metadata object XML", allow_abbrev=False)
parser.add_argument("-DefinitionFile", default=None, help="JSON definition file")
parser.add_argument("-ObjectPath", required=True, help="Path to object XML or directory")
parser.add_argument("-ObjectPath", "-Path", required=True, help="Path to object XML or directory")
parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation")
parser.add_argument("-Value", default=None, help="Inline value")
parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation")
@@ -2254,7 +2257,7 @@ def main():
print()
print("--- Running meta-validate ---")
python_exe = sys.executable
subprocess.run([python_exe, validate_script, "-ObjectPath", resolved_path])
subprocess.run([python_exe, validate_script, "-ObjectPath", "-Path", resolved_path])
else:
print()
print(f"[SKIP] meta-validate not found at: {validate_script}")
@@ -1,7 +1,7 @@
# meta-info v1.1 — Compact summary of 1C metadata object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$ObjectPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[string]$Name,
@@ -13,7 +13,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview")
parser.add_argument("-Name", default="")
parser.add_argument("-Limit", type=int, default=150)
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: meta-remove
description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
description: Удалить объект метаданных из конфигурации 1С. Используй когда нужно удалить, убрать объект из конфигурации
argument-hint: <ConfigDir> -Object <Type.Name>
allowed-tools:
- Bash
@@ -1,7 +1,8 @@
# meta-validate v1.2 — Validate 1C metadata object structure
# meta-validate v1.3 — Validate 1C metadata object structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
[switch]$Detailed,
@@ -262,7 +263,7 @@ $validPropertyValues = @{
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"DataHistory" = @("Use","DontUse")
"DependenceOnCalculationTypes" = @("DontUse","RequireCalculationTypes")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
}
# Properties forbidden per type (would cause LoadConfigFromFiles error)
@@ -998,6 +999,18 @@ if ($propsNode) {
}
}
# CalculationRegister: ActionPeriod=true requires non-empty Schedule
if ($mdType -eq "CalculationRegister") {
$actionPeriod = $propsNode.SelectSingleNode("md:ActionPeriod", $ns)
if ($actionPeriod -and $actionPeriod.InnerText -eq "true") {
$schedule = $propsNode.SelectSingleNode("md:Schedule", $ns)
if (-not $schedule -or -not $schedule.InnerText.Trim()) {
Report-Warn "10. CalculationRegister: ActionPeriod=true but Schedule is empty — platform requires a schedule register"
$check10Issues++
}
}
}
# DocumentJournal: RegisteredDocuments should not be empty
if ($mdType -eq "DocumentJournal") {
$regDocs = $propsNode.SelectSingleNode("md:RegisteredDocuments", $ns)
@@ -1025,6 +1038,48 @@ if ($propsNode) {
}
}
# Register: must have at least one Dimension or Resource (platform rejects empty registers)
$regTypesAll = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister")
if ($regTypesAll -contains $mdType -and $childObjNode) {
$dims = $childObjNode.SelectNodes("md:Dimension", $ns).Count
$ress = $childObjNode.SelectNodes("md:Resource", $ns).Count
$attrs = $childObjNode.SelectNodes("md:Attribute", $ns).Count
if (($dims + $ress + $attrs) -eq 0) {
Report-Warn "10. $mdType`: no Dimensions, Resources, or Attributes — platform will reject"
$check10Issues++
}
}
# Document: RegisterRecords references should point to existing objects in config
if ($mdType -eq "Document" -and $script:configDir) {
$regRecords = $propsNode.SelectSingleNode("md:RegisterRecords", $ns)
if ($regRecords) {
$items = $regRecords.SelectNodes("xr:Item", $ns)
foreach ($item in $items) {
$refVal = $item.InnerText.Trim()
if (-not $refVal) { continue }
# Parse "AccumulationRegister.Name" → dir AccumulationRegisters/Name
$parts = $refVal -split '\.',2
if ($parts.Count -eq 2) {
$refType = $parts[0]; $refName = $parts[1]
$dirMap = @{
"AccumulationRegister"="AccumulationRegisters"; "InformationRegister"="InformationRegisters"
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
}
$refDir = $dirMap[$refType]
if ($refDir) {
$refPath = Join-Path $script:configDir "$refDir/$refName"
$refXml = Join-Path $script:configDir "$refDir/$refName.xml"
if (-not (Test-Path $refPath) -and -not (Test-Path $refXml)) {
Report-Warn "10. Document.RegisterRecords references '$refVal' but object not found in config"
$check10Issues++
}
}
}
}
}
}
# Register: must have at least one registrar document
$registerTypes = @("AccumulationRegister","AccountingRegister","CalculationRegister","InformationRegister")
if ($registerTypes -contains $mdType -and $script:configDir -and $objName -ne "(unknown)") {
@@ -1,4 +1,4 @@
# meta-validate v1.2 — Validate 1C metadata object structure (Python port)
# meta-validate v1.3 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default="")
@@ -31,7 +31,7 @@ if len(path_list) > 1:
batch_ok = 0
batch_fail = 0
for single_path in path_list:
cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)]
cmd = [sys.executable, __file__, "-ObjectPath", "-Path", single_path, "-MaxErrors", str(max_errors)]
if detailed:
cmd.append("-Detailed")
if out_file:
@@ -263,7 +263,7 @@ valid_property_values = {
"FillChecking": ["DontCheck", "ShowError", "ShowWarning"],
"Indexing": ["DontIndex", "Index", "IndexWithAdditionalOrder"],
"DataHistory": ["Use", "DontUse"],
"DependenceOnCalculationTypes": ["DontUse", "RequireCalculationTypes"],
"DependenceOnCalculationTypes": ["DontUse", "OnActionPeriod"],
}
# Properties forbidden per type (would cause LoadConfigFromFiles error)
@@ -942,6 +942,15 @@ if props_node is not None:
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "Task=Task.XXX"')
# CalculationRegister: ActionPeriod=true requires non-empty Schedule
if md_type == 'CalculationRegister':
action_period = find(props_node, 'md:ActionPeriod')
if action_period is not None and text_of(action_period) == 'true':
schedule = find(props_node, 'md:Schedule')
if schedule is None or not text_of(schedule):
report_warn('10. CalculationRegister: ActionPeriod=true but Schedule is empty — platform requires a schedule register')
check10_issues += 1
# DocumentJournal: RegisteredDocuments should not be empty
if md_type == 'DocumentJournal':
reg_docs = find(props_node, 'md:RegisteredDocuments')
@@ -969,6 +978,43 @@ if props_node is not None:
check10_issues += 1
print('[HINT] /meta-edit -Operation modify-property -Value "ExtDimensionTypes=ChartOfCharacteristicTypes.XXX"')
# Register: must have at least one Dimension or Resource (platform rejects empty registers)
reg_types_all = ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'InformationRegister')
if md_type in reg_types_all and child_obj_node is not None:
dims = len(find_all(child_obj_node, 'md:Dimension'))
ress = len(find_all(child_obj_node, 'md:Resource'))
attrs = len(find_all(child_obj_node, 'md:Attribute'))
if dims + ress + attrs == 0:
report_warn(f"10. {md_type}: no Dimensions, Resources, or Attributes \u2014 platform will reject")
check10_issues += 1
# Document: RegisterRecords references should point to existing objects in config
if md_type == 'Document' and config_dir:
reg_records = find(props_node, 'md:RegisterRecords')
if reg_records is not None:
rr_items = find_all(reg_records, 'xr:Item')
for item in rr_items:
ref_val = (inner_text(item) or '').strip()
if not ref_val:
continue
# Parse "AccumulationRegister.Name" -> dir AccumulationRegisters/Name
parts = ref_val.split('.', 1)
if len(parts) == 2:
ref_type, ref_name = parts
dir_map = {
'AccumulationRegister': 'AccumulationRegisters',
'InformationRegister': 'InformationRegisters',
'AccountingRegister': 'AccountingRegisters',
'CalculationRegister': 'CalculationRegisters',
}
ref_dir = dir_map.get(ref_type)
if ref_dir:
ref_path = os.path.join(config_dir, ref_dir, ref_name)
ref_xml = os.path.join(config_dir, ref_dir, ref_name + '.xml')
if not os.path.exists(ref_path) and not os.path.exists(ref_xml):
report_warn(f"10. Document.RegisterRecords references '{ref_val}' but object not found in config")
check10_issues += 1
# Register: must have at least one registrar document
register_types = ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'InformationRegister')
if md_type in register_types and config_dir and obj_name != '(unknown)':
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[string]$OutputPath
@@ -47,7 +47,7 @@ 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("-TemplatePath", "-Path", required=True, help="Path to Template.xml")
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
args = parser.parse_args()
@@ -1,6 +1,7 @@
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Alias('Path')]
[string]$TemplatePath,
[string]$ProcessorName,
[string]$TemplateName,
+1 -1
View File
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C spreadsheet (MXL) structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", default="", help="Path to Template.xml")
parser.add_argument("-TemplatePath", "-Path", default="", help="Path to Template.xml")
parser.add_argument("-ProcessorName", default="", help="Processor name (used with -TemplateName)")
parser.add_argument("-TemplateName", default="", help="Template name (used with -ProcessorName)")
parser.add_argument("-SrcDir", default="src", help="Source directory (default: src)")
@@ -1,6 +1,7 @@
# mxl-validate v1.1 — Validate 1C spreadsheet
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Alias('Path')]
[string]$TemplatePath,
[string]$ProcessorName,
[string]$TemplateName,
@@ -55,7 +55,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C spreadsheet document Template.xml', allow_abbrev=False
)
parser.add_argument('-TemplatePath', dest='TemplatePath', default='')
parser.add_argument('-TemplatePath', '-Path', dest='TemplatePath', default='')
parser.add_argument('-ProcessorName', dest='ProcessorName', default='')
parser.add_argument('-TemplateName', dest='TemplateName', default='')
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
@@ -1,4 +1,4 @@
# role-compile v1.3 — Compile 1C role from JSON
# role-compile v1.5 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -505,6 +505,26 @@ if ($def.objects) {
}
}
# --- 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()
@@ -531,7 +551,7 @@ 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="2.17">'
X " version=`"$formatVersion`">"
X " <Role uuid=`"$uuid`">"
X ' <Properties>'
X " <Name>$roleName</Name>"
@@ -560,7 +580,7 @@ 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="2.17">'
X " xsi:type=`"Rights`" version=`"$formatVersion`">"
# Global flags (defaults match typical 1C roles)
$sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# role-compile v1.3 — Compile 1C role from JSON
# role-compile v1.4 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -9,6 +9,22 @@ 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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
@@ -459,6 +475,9 @@ def main():
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'):
@@ -490,7 +509,7 @@ def main():
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(' version="2.17">')
lines.append(f' version="{format_version}">')
lines.append(f' <Role uuid="{uid}">')
lines.append(' <Properties>')
lines.append(f' <Name>{role_name}</Name>')
@@ -516,7 +535,7 @@ def main():
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(' xsi:type="Rights" version="2.17">')
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'
@@ -1,7 +1,7 @@
# role-info v1.0 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$RightsPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath,
[switch]$ShowDenied,
[int]$Limit = 150,
[int]$Offset = 0,
@@ -13,7 +13,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C role rights", allow_abbrev=False)
parser.add_argument("-RightsPath", required=True, help="Path to Rights.xml")
parser.add_argument("-RightsPath", "-Path", required=True, help="Path to Rights.xml")
parser.add_argument("-ShowDenied", action="store_true", default=False, help="Show denied rights")
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")
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$RightsPath,
[string]$OutFile,
@@ -179,7 +179,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C role Rights.xml structure', allow_abbrev=False
)
parser.add_argument('-RightsPath', dest='RightsPath', required=True)
parser.add_argument('-RightsPath', '-Path', dest='RightsPath', required=True)
parser.add_argument('-OutFile', dest='OutFile', default='')
parser.add_argument('-Detailed', dest='Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
+130 -13
View File
@@ -60,6 +60,17 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD.
**DataSetObject** — внешний набор данных (без источника-запроса). Поля описываются явно; данные передаются вторым параметром `ПроцессорКомпоновкиДанных.Инициализировать(Макет, Новый Структура("<objectName>", ТЗ), ...)`.
```json
{ "name": "ЖурналОшибок", "objectName": "ЖурналОшибок", "fields": [
{ "field": "ТекстСообщения", "title": "Текст сообщения", "type": "string(150)" },
{ "field": "Расшифровка", "title": "Описание", "type": "CatalogRef.СтруктураПредприятия" }
]}
```
`name` — имя набора в схеме, `objectName` — ключ в структуре передачи данных.
### Поля — shorthand и объектная форма
```
@@ -75,14 +86,44 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
```
`dataPath` автоматически берётся из `field`, если не указан явно.
Многоязычный заголовок: `"title": { "ru": "...", "en": "..." }`. Применимо везде, где принимается title/presentation (поля, calculatedFields, parameters, settingsVariants, availableValues и пр.). Строка эквивалентна `{ "ru": "..." }`.
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
**Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые.
Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу.
Роли: `@dimension`, `@account`, `@balance`, `@period`.
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
В объектной форме: `"useRestriction": { "field": true, "condition": true, "group": true, "order": true }` или `"restrict": ["noField", "noFilter"]`.
Дополнительные ключи объектной формы:
- `"presentationExpression": "<выражение>"` — что показывать вместо значения поля. Исходное значение остаётся «под капотом» для перехода/расшифровки.
- `"appearance": { "<параметр>": "<значение>" }` — оформление колонки по умолчанию (применяется во всех вариантах настроек). Ключи — параметры платформы (`ГоризонтальноеПоложение`, `МинимальнаяШирина`, `Формат`, `Текст` и т.п.).
```json
{ "field": "Сумма", "title": "Сумма продажи", "type": "decimal(15,2)",
"appearance": { "ГоризонтальноеПоложение": "Right", "МинимальнаяШирина": "80" } }
```
### Вычисляемые поля (calculatedFields)
Shorthand: `"Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder"` — все части кроме имени опциональны.
```json
"calculatedFields": [
"Маржа = Цена - Закупка",
"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100",
"Служебное: string = \"\" #noField #noFilter #noGroup #noOrder"
]
```
Объектная форма — когда нужна `appearance`:
```json
{ "name": "Маржа", "title": "Маржа", "expression": "Цена - Закупка", "type": "decimal(15,2)", "useRestriction": "#noField #noFilter" }
```
### Итоги (shorthand)
```json
@@ -93,11 +134,37 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
```json
"parameters": [
"Период: StandardPeriod = LastMonth @autoDates"
"Период [Отчетный период]: StandardPeriod = LastMonth @autoDates"
]
```
`@autoDates` — автоматически генерирует параметры `ДатаНачала` и `ДатаОкончания` с выражениями `&Период.ДатаНачала` / `&Период.ДатаОкончания` и `availableAsField=false`. Заменяет 5 строк на 1.
Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `<title>` (LocalStringType).
Флаги shorthand:
- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить.
- `@valueList``<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений
- `@hidden` — скрытый параметр: `availableAsField=false` + исключается из `"dataParameters": "auto"`
Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`.
Список допустимых значений (availableValues):
```json
{
"name": "ПорядокОкругления",
"type": "EnumRef.Округления",
"value": "Перечисление.Округления.Окр1_00",
"use": "Always",
"denyIncompleteValues": true,
"availableValues": [
{"value": "Перечисление.Округления.Окр1_00", "presentation": "руб. коп"},
{"value": "Перечисление.Округления.Окр1", "presentation": "руб."},
{"value": "Перечисление.Округления.Окр1000", "presentation": "тыс. руб"}
]
}
```
В варианте настроек `"dataParameters": "auto"` выводит все не-hidden параметры с `userSettingID`. Значения по умолчанию наследуются и остаются активными; параметры без значения по умолчанию отключаются (пользователь включит их в настройках).
### Фильтры — shorthand
@@ -144,7 +211,20 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне.
Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма.
Объектная форма — для сложных случаев (именованные группировки, selection/filter на уровне группировки, таблицы, диаграммы):
```json
"structure": [
{
"name": "ПоОрганизациям",
"groupFields": ["Организация"],
"selection": ["Организация", "Сумма", "Auto"],
"children": [{ "groupFields": [] }]
}
]
```
`type` по умолчанию `"group"` (можно не указывать). `groupFields` — алиас для `groupBy`. Поддержка `name`, `selection`, `order`, `filter`, `outputParameters`, рекурсивных `children`.
### Варианты настроек
@@ -187,7 +267,13 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
]
```
Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Текст` → LocalStringType, прочее → String.
Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Формат`/`Текст`/`Заголовок` → LocalStringType, прочее → String.
Типы значений фильтра: `Перечисление.*`/`Справочник.*`/`ПланСчетов.*`/`Документ.*` → DesignTimeValue (автодетект).
OrGroup в фильтре: `{"group": "Or", "items": ["условие1", "условие2"]}`.
Folder в selection: `{"folder": "Поступление", "items": ["ПолеА", "ПолеБ"]}` → SelectedItemFolder с lwsTitle и placement=Auto.
### Итоги с привязкой к группировкам
@@ -227,23 +313,49 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
]
```
Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `null` — пустая.
Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `">"` — объединение с ячейкой слева, `null` — пустая.
Двухуровневая шапка с горизонтальным объединением:
```json
"rows": [
["Вид актива", "Остаток начало", "Поступление", ">", ">", ">", "Выбытие", ">", ">", "Остаток конец"],
["|", "|", "из произв.", "из п/ф", "со сч.40", "прочее", "Реализ.", "отгруж.", "прочее", "|"],
["К1", "К2", "К3", "К4", "К5", "К6", "К7", "К8", "К9", "К10"]
]
```
Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы.
Пользовательские стили: файл `skd-styles.json` рядом с JSON или в корне проекта. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`.
Пользовательские стили: файл `skd-styles.json` рядом с JSON-определением, в текущей директории, или в `presets/skills/skd/skd-styles.json` (поиск вверх от OutputPath). Первый найденный файл побеждает. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`.
Raw XML (`"template": "<...>"`) остаётся как fallback. Детект: если есть `rows` — DSL, иначе — raw.
### Расшифровка (drilldown) в параметрах шаблона
Ключ `drilldown` в параметре шаблона автоматически генерирует `DetailsAreaTemplateParameter` и привязку `Расшифровка` в appearance ячеек:
```json
"parameters": [
{ "name": "Сырье", "expression": "ПоступлениеСырья", "drilldown": "ПоступлениеСырья" }
]
```
Генерирует: `ExpressionAreaTemplateParameter` (обычный) + `DetailsAreaTemplateParameter` с именем `Расшифровка_ПоступлениеСырья`, `fieldExpression` по полю `ИмяРесурса`, `mainAction=DrillDown`. Ячейки `{Сырье}` автоматически получают appearance `Расшифровка = Расшифровка_ПоступлениеСырья`.
### Привязки макетов к группировкам
```json
"groupTemplates": [
{ "groupField": "Счет", "templateType": "GroupHeader", "template": "Макет1" },
{ "groupField": "Счет", "templateType": "Header", "template": "Макет2" }
{ "groupName": "ДанныеОтчета", "templateType": "GroupHeader", "template": "Макет1" },
{ "groupField": "Счет", "templateType": "Header", "template": "Макет2" },
{ "groupField": "Счет", "templateType": "OverallHeader", "template": "Макет3" }
]
```
`groupField` — привязка к полю группировки, `groupName` — к именованной группировке в структуре варианта.
`templateType`: `Header` (строки данных) → `<groupTemplate>`, `OverallHeader` (итоги) → `<groupTemplate>`, `GroupHeader` (шапка) → `<groupHeaderTemplate>`.
## Примеры
### Минимальный
@@ -273,17 +385,22 @@ Raw XML (`"template": "<...>"`) остаётся как fallback. Детект:
```json
{
"dataSets": [{
"query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи",
"fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
"query": "ВЫБРАТЬ Продажи.Организация, Продажи.Номенклатура, Продажи.КоличествоОборот КАК Количество, Продажи.СуммаОборот КАК Сумма ИЗ РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода) КАК Продажи",
"fields": [
"Организация: СправочникСсылка.Организации @dimension",
"Номенклатура: СправочникСсылка.Номенклатура @dimension",
"Количество: число(15,3)",
"Сумма: число(15,2)"
]
}],
"totalFields": ["Количество: Сумма", "Сумма: Сумма"],
"parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"],
"settingsVariants": [{
"name": "Основной",
"settings": {
"selection": ["Номенклатура", "Количество", "Сумма", "Auto"],
"selection": ["Организация", "Номенклатура", "Количество", "Сумма"],
"filter": ["Организация = _ @off @user"],
"dataParameters": ["Период = LastMonth @user"],
"dataParameters": "auto",
"structure": "Организация > details"
}
}]
+490 -137
View File
@@ -1,4 +1,4 @@
# skd-compile v1.3 — Compile 1C DCS from JSON
# skd-compile v1.21 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -80,12 +80,25 @@ function Resolve-QueryValue {
}
function Emit-MLText {
param([string]$tag, [string]$text, [string]$indent)
param([string]$tag, $text, [string]$indent)
X "$indent<$tag xsi:type=`"v8:LocalStringType`">"
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>"
# Multi-lang: object form { ru: "...", en: "..." } → one <v8:item> per language
if ($text -is [System.Management.Automation.PSCustomObject] -or $text -is [hashtable] -or $text -is [System.Collections.IDictionary]) {
$props = if ($text -is [System.Management.Automation.PSCustomObject]) { $text.PSObject.Properties } else { $text.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } }
foreach ($p in $props) {
$lang = if ($p -is [hashtable]) { $p.Name } else { $p.Name }
$content = if ($p -is [hashtable]) { $p.Value } else { $p.Value }
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>$(Esc-Xml "$lang")</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml "$content")</v8:content>"
X "$indent`t</v8:item>"
}
} else {
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>"
}
@@ -179,6 +192,20 @@ function Resolve-TypeStr {
}
function Emit-ValueType {
param($typeStr, [string]$indent)
if (-not $typeStr) { return }
# Multi-type: iterate and emit each type with its qualifiers
if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) {
foreach ($t in $typeStr) { Emit-SingleValueType -typeStr "$t" -indent $indent }
return
}
Emit-SingleValueType -typeStr "$typeStr" -indent $indent
}
function Emit-SingleValueType {
param([string]$typeStr, [string]$indent)
if (-not $typeStr) { return }
@@ -259,7 +286,7 @@ function Parse-FieldShorthand {
$result = @{
dataPath = ""; field = ""; title = ""; type = ""
roles = @(); restrict = @(); appearance = @{}
roles = @(); restrict = @(); appearance = [ordered]@{}
}
# Extract @roles
@@ -300,12 +327,20 @@ function Parse-TotalShorthand {
$dataPath = $parts[0].Trim()
$funcPart = $parts[1].Trim()
# Known DCS aggregate functions (ru + en)
$aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее',
'Sum','Count','Min','Max','Avg',
'Minimum','Maximum','Average')
if ($funcPart -match '^\w+\(') {
# Already has expression form: Func(expr)
return @{ dataPath = $dataPath; expression = $funcPart }
} else {
} elseif ($funcPart -in $aggFuncs) {
# Short: Func → Func(DataPath)
return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" }
} else {
# Identity or custom expression — use as-is
return @{ dataPath = $dataPath; expression = $funcPart }
}
}
@@ -314,7 +349,7 @@ function Parse-TotalShorthand {
function Parse-ParamShorthand {
param([string]$s)
$result = @{ name = ""; type = ""; value = $null; autoDates = $false }
$result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null }
# Extract @autoDates flag
if ($s -match '@autoDates') {
@@ -322,6 +357,24 @@ function Parse-ParamShorthand {
$s = $s -replace '\s*@autoDates', ''
}
# Extract @valueList flag
if ($s -match '@valueList') {
$result.valueListAllowed = $true
$s = $s -replace '\s*@valueList', ''
}
# Extract @hidden flag
if ($s -match '@hidden') {
$result.hidden = $true
$s = $s -replace '\s*@hidden', ''
}
# Extract optional [Title] (mirrors Parse-FieldShorthand)
if ($s -match '\[([^\]]*)\]') {
$result.title = $Matches[1].Trim()
$s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim()
}
# Split "Name: Type = Value"
if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
$result.name = $Matches[1].Trim()
@@ -341,15 +394,51 @@ function Parse-ParamShorthand {
function Parse-CalcShorthand {
param([string]$s)
# "DataPath = Expression"
$idx = $s.IndexOf('=')
if ($idx -gt 0) {
return @{
dataPath = $s.Substring(0, $idx).Trim()
expression = $s.Substring($idx + 1).Trim()
}
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
# an expression (e.g. index access) isn't interpreted as a title.
# - `#restrict` flags use a known-names pattern and are extracted globally —
# the docs put them after `=`, and the closed flag set avoids matching
# `#word` that happens to appear inside a string literal.
$restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b'
$restrict = @()
foreach ($m in [regex]::Matches($s, $restrictPattern)) {
$restrict += $m.Groups[1].Value
}
$s = [regex]::Replace($s, "\s*$restrictPattern", '')
$eqIdx = $s.IndexOf('=')
if ($eqIdx -gt 0) {
$lhs = $s.Substring(0, $eqIdx)
$rhs = $s.Substring($eqIdx + 1).Trim()
} else {
$lhs = $s
$rhs = ""
}
$title = ""
if ($lhs -match '\[([^\]]+)\]') {
$title = $Matches[1]
$lhs = $lhs -replace '\s*\[[^\]]+\]', ''
}
$lhs = $lhs.Trim()
$type = ""
$dataPath = $lhs
if ($lhs.Contains(':')) {
$parts = $lhs -split ':', 2
$dataPath = $parts[0].Trim()
$type = Resolve-TypeStr ($parts[1].Trim())
}
return @{
dataPath = $dataPath
expression = $rhs
type = $type
title = $title
restrict = $restrict
}
return @{ dataPath = $s.Trim(); expression = "" }
}
# --- 8b. DataParameter shorthand parser ---
@@ -469,6 +558,9 @@ function Parse-FilterShorthand {
} elseif ($valPart -match '^\d+(\.\d+)?$') {
$result.value = $valPart
$result["valueType"] = "xs:decimal"
} elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$result.value = $valPart
$result["valueType"] = "dcscor:DesignTimeValue"
} else {
$result.value = $valPart
$result["valueType"] = "xs:string"
@@ -531,11 +623,17 @@ function Emit-Field {
$f = @{
dataPath = if ($fieldDef.dataPath) { "$($fieldDef.dataPath)" } elseif ($fieldDef.field) { "$($fieldDef.field)" } else { "" }
field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" }
title = if ($fieldDef.title) { "$($fieldDef.title)" } else { "" }
type = if ($fieldDef.type) { Resolve-TypeStr "$($fieldDef.type)" } else { "" }
title = if ($fieldDef.title) { $fieldDef.title } else { "" }
type = if ($fieldDef.type) {
if ($fieldDef.type -is [array] -or $fieldDef.type -is [System.Collections.IList]) {
@($fieldDef.type | ForEach-Object { Resolve-TypeStr "$_" })
} else {
Resolve-TypeStr "$($fieldDef.type)"
}
} else { "" }
roles = @()
restrict = @()
appearance = @{}
appearance = [ordered]@{}
}
# Parse role
if ($fieldDef.role) {
@@ -742,52 +840,90 @@ function Emit-DataSetLinks {
# === CalculatedFields ===
function Emit-CalcFields {
if (-not $def.calculatedFields) { return }
$restrictMap = @{
"noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition"
"noGroup" = "group"; "noOrder" = "order"
}
foreach ($cf in $def.calculatedFields) {
# Collect dataPath/expression/title/type/restrict/appearance from either
# shorthand string or object form. Object form accepts dataPath/field/name
# as synonyms; useRestriction/restrict accepts object, array, or flag string.
$title = ""
$typeStr = ""
$restrictTokens = @()
$restrictObj = $null
$appearance = $null
if ($cf -is [string]) {
$parsed = Parse-CalcShorthand $cf
$dataPath = "$($parsed.dataPath)"
$expression = "$($parsed.expression)"
$title = $parsed.title
$typeStr = "$($parsed.type)"
if ($parsed.restrict) { $restrictTokens = @($parsed.restrict) }
} else {
$parsed = @{
dataPath = "$($cf.dataPath)"
expression = "$($cf.expression)"
$dataPath = if ($cf.dataPath) { "$($cf.dataPath)" }
elseif ($cf.field) { "$($cf.field)" }
else { "$($cf.name)" }
$expression = "$($cf.expression)"
if ($cf.title) { $title = $cf.title }
if ($cf.type) { $typeStr = Resolve-TypeStr "$($cf.type)" }
$restrictVal = if ($cf.restrict) { $cf.restrict } elseif ($cf.useRestriction) { $cf.useRestriction } else { $null }
if ($restrictVal) {
if ($restrictVal -is [System.Management.Automation.PSCustomObject] -or $restrictVal -is [hashtable]) {
$restrictObj = $restrictVal
} elseif ($restrictVal -is [string]) {
# Flag-string form: "#noField #noFilter #noGroup #noOrder" (or without `#`)
foreach ($tok in ($restrictVal -split '\s+')) {
$t = $tok.Trim().TrimStart('#')
if ($t) { $restrictTokens += $t }
}
} else {
# Array form: ["noField", "noFilter", ...]
foreach ($r in $restrictVal) { $restrictTokens += "$r" }
}
}
if ($cf.appearance) { $appearance = $cf.appearance }
}
X "`t<calculatedField>"
X "`t`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>"
X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>"
X "`t`t<dataPath>$(Esc-Xml $dataPath)</dataPath>"
X "`t`t<expression>$(Esc-Xml $expression)</expression>"
if ($cf -isnot [string]) {
if ($cf.title) {
Emit-MLText -tag "title" -text "$($cf.title)" -indent "`t`t"
}
if ($cf.type) {
$cfType = Resolve-TypeStr "$($cf.type)"
X "`t`t<valueType>"
Emit-ValueType -typeStr $cfType -indent "`t`t`t"
X "`t`t</valueType>"
}
if ($cf.restrict) {
$restrictMap = @{
"noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition"
"noGroup" = "group"; "noOrder" = "order"
if ($title) {
Emit-MLText -tag "title" -text $title -indent "`t`t"
}
if ($typeStr) {
X "`t`t<valueType>"
Emit-ValueType -typeStr $typeStr -indent "`t`t`t"
X "`t`t</valueType>"
}
if ($restrictObj -or $restrictTokens.Count -gt 0) {
X "`t`t<useRestriction>"
if ($restrictObj) {
foreach ($prop in $restrictObj.PSObject.Properties) {
if ($prop.Value -eq $true) {
X "`t`t`t<$($prop.Name)>true</$($prop.Name)>"
}
}
X "`t`t<useRestriction>"
foreach ($r in $cf.restrict) {
} else {
foreach ($r in $restrictTokens) {
$xmlName = $restrictMap["$r"]
if ($xmlName) { X "`t`t`t<$xmlName>true</$xmlName>" }
}
X "`t`t</useRestriction>"
}
if ($cf.appearance) {
X "`t`t<appearance>"
foreach ($prop in $cf.appearance.PSObject.Properties) {
X "`t`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
X "`t`t`t`t<dcscor:parameter>$(Esc-Xml $prop.Name)</dcscor:parameter>"
X "`t`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($prop.Value)")</dcscor:value>"
X "`t`t`t</dcscor:item>"
}
X "`t`t</appearance>"
X "`t`t</useRestriction>"
}
if ($appearance) {
X "`t`t<appearance>"
foreach ($prop in $appearance.PSObject.Properties) {
X "`t`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
X "`t`t`t`t<dcscor:parameter>$(Esc-Xml $prop.Name)</dcscor:parameter>"
X "`t`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($prop.Value)")</dcscor:value>"
X "`t`t`t</dcscor:item>"
}
X "`t`t</appearance>"
}
X "`t</calculatedField>"
@@ -828,8 +964,16 @@ function Emit-SingleParam {
X "`t<parameter>"
X "`t`t<name>$(Esc-Xml $parsed.name)</name>"
# Title
$title = if ($p -isnot [string] -and $p.title) { "$($p.title)" } else { "" }
# Title (from parsed first, then from object form; accept `presentation` as
# a synonym — 1C UI labels a parameter's caption "Представление").
$title = ""
if ($parsed.title) {
$title = $parsed.title
} elseif ($p -isnot [string] -and $p.title) {
$title = $p.title
} elseif ($p -isnot [string] -and $p.presentation) {
$title = $p.presentation
}
if ($title) {
Emit-MLText -tag "title" -text $title -indent "`t`t"
}
@@ -844,8 +988,14 @@ function Emit-SingleParam {
# Value
Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t"
# Hidden implies useRestriction=true + availableAsField=false
if ($parsed.hidden -eq $true) {
$parsed.availableAsField = $false
$parsed.useRestriction = $true
}
# UseRestriction
if ($p -isnot [string] -and $p.useRestriction -eq $true) {
if ($parsed.useRestriction -eq $true -or ($p -isnot [string] -and $p.useRestriction -eq $true)) {
X "`t`t<useRestriction>true</useRestriction>"
}
@@ -859,14 +1009,50 @@ function Emit-SingleParam {
X "`t`t<availableAsField>false</availableAsField>"
}
# Use
if ($p -isnot [string] -and $p.use) {
X "`t`t<use>$(Esc-Xml "$($p.use)")</use>"
# ValueListAllowed
if ($parsed.valueListAllowed -eq $true) {
X "`t`t<valueListAllowed>true</valueListAllowed>"
}
# AvailableValues
if ($p -isnot [string] -and $p.availableValues) {
foreach ($av in $p.availableValues) {
$avVal = "$($av.value)"
$avType = "xs:string"
if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$avType = "dcscor:DesignTimeValue"
}
X "`t`t<availableValue>"
X "`t`t`t<value xsi:type=`"$avType`">$(Esc-Xml $avVal)</value>"
# `title` accepted as synonym of `presentation` — both map to the same UI label.
$avPres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { "" }
if ($avPres) {
Emit-MLText -tag "presentation" -text $avPres -indent "`t`t`t"
}
X "`t`t</availableValue>"
}
}
# DenyIncompleteValues
$deny = $parsed.denyIncompleteValues -eq $true -or (
$null -ne $p -and $p -isnot [string] -and $p.denyIncompleteValues -eq $true)
if ($deny) {
X "`t`t<denyIncompleteValues>true</denyIncompleteValues>"
}
# Use — object form wins, else parsed (set by @autoDates default)
$useVal = $null
if ($null -ne $p -and $p -isnot [string] -and $p.use) { $useVal = "$($p.use)" }
elseif ($parsed.use) { $useVal = "$($parsed.use)" }
if ($useVal) {
X "`t`t<use>$(Esc-Xml $useVal)</use>"
}
X "`t</parameter>"
}
$script:allParams = @()
function Emit-Parameters {
if (-not $def.parameters) { return }
foreach ($p in $def.parameters) {
@@ -881,22 +1067,40 @@ function Emit-Parameters {
}
if ($p.expression) { $parsed.expression = "$($p.expression)" }
if ($p.availableAsField -eq $false) { $parsed.availableAsField = $false }
if ($p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true }
if ($p.hidden -eq $true) { $parsed.hidden = $true }
if ($p.autoDates -eq $true) { $parsed.autoDates = $true }
}
# @autoDates implies use=Always + denyIncompleteValues=true by default
# (derived &НачалоПериода/&КонецПериода need a populated period).
# Explicit values in object form override these defaults.
if ($parsed.autoDates) {
$isObj = ($p -isnot [string]) -and ($null -ne $p)
if (-not ($isObj -and $null -ne $p.use)) { $parsed.use = 'Always' }
if (-not ($isObj -and $null -ne $p.denyIncompleteValues)) { $parsed.denyIncompleteValues = $true }
}
Emit-SingleParam -p $p -parsed $parsed
# @autoDates: auto-generate ДатаНачала and ДатаОкончания
# Track parameter for auto dataParameters
$script:allParams += @{ name = $parsed.name; hidden = [bool]$parsed.hidden; type = "$($parsed.type)"; value = $parsed.value }
# @autoDates: auto-generate НачалоПериода and КонецПериода (canonical БСП pattern)
if ($parsed.autoDates) {
$paramName = $parsed.name
$beginParsed = @{
name = "ДатаНачала"; type = "date"; value = $null
expression = "&$paramName.ДатаНачала"; availableAsField = $false
name = "НачалоПериода"; title = "Начало периода"
type = "date"; value = "0001-01-01T00:00:00"
useRestriction = $true
expression = "&$paramName.ДатаНачала"
}
Emit-SingleParam -p $null -parsed $beginParsed
$endParsed = @{
name = "ДатаОкончания"; type = "date"; value = $null
expression = "&$paramName.ДатаОкончания"; availableAsField = $false
name = "КонецПериода"; title = "Конец периода"
type = "date"; value = "0001-01-01T00:00:00"
useRestriction = $true
expression = "&$paramName.ДатаОкончания"
}
Emit-SingleParam -p $null -parsed $endParsed
}
@@ -911,9 +1115,12 @@ function Emit-ParamValue {
$valStr = "$val"
if ($type -eq "StandardPeriod") {
# val is a period variant string like "LastMonth"
# val is a period variant string like "LastMonth" or "Custom".
# Always emit startDate/endDate to match how 1C Designer saves the schema.
X "$indent<value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>"
X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent</value>"
} elseif ($type -match '^date') {
X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
@@ -929,6 +1136,8 @@ function Emit-ParamValue {
X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
} elseif ($valStr -eq "true" -or $valStr -eq "false") {
X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>"
} elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>"
} else {
X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>"
}
@@ -965,10 +1174,22 @@ $script:areaStylePresets = @{
}
}
# Load user presets from skd-styles.json (same dir as definition or cwd)
# Load user presets from skd-styles.json
# Search order (first found wins): 1) definition dir, 2) cwd, 3) scan-up from OutputPath for presets/skills/skd/
$script:userStylesLoaded = $false
foreach ($stylesDir in @($script:queryBaseDir, (Get-Location).Path)) {
$stylesFile = Join-Path $stylesDir "skd-styles.json"
$searchPaths = @(
(Join-Path $script:queryBaseDir "skd-styles.json"),
(Join-Path (Get-Location).Path "skd-styles.json")
)
$outResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location).Path $OutputPath }
$scanDir = [System.IO.Path]::GetDirectoryName($outResolved)
while ($scanDir) {
$searchPaths += Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "skd") "skd-styles.json"
$parentDir = Split-Path $scanDir -Parent
if ($parentDir -eq $scanDir) { break }
$scanDir = $parentDir
}
foreach ($stylesFile in $searchPaths) {
if (Test-Path $stylesFile) {
$userStyles = Get-Content -Raw -Encoding UTF8 $stylesFile | ConvertFrom-Json
foreach ($prop in $userStyles.PSObject.Properties) {
@@ -1005,7 +1226,7 @@ function Emit-ColorValue {
}
function Emit-CellAppearance {
param($style, [double]$width = 0, [bool]$vMerge = $false, [double]$minHeight = 0)
param($style, [double]$width = 0, [bool]$vMerge = $false, [bool]$hMerge = $false, [double]$minHeight = 0, $extraItems = @())
$ind = "`t`t`t`t`t"
X "`t`t`t`t<dcsat:appearance>"
# Background color
@@ -1098,6 +1319,15 @@ function Emit-CellAppearance {
X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>"
X "$ind</dcscor:item>"
}
# Horizontal merge
if ($hMerge) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ОбъединятьПоГоризонтали</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>"
X "$ind</dcscor:item>"
}
# Extra appearance items (e.g. drilldown Расшифровка)
foreach ($ei in $extraItems) { X $ei }
X "`t`t`t`t</dcsat:appearance>"
}
@@ -1115,7 +1345,7 @@ function Emit-AreaTemplateDSL {
$minHeight = if ($t.minHeight) { [double]$t.minHeight } else { 0 }
$colCount = if ($widths.Count -gt 0) { $widths.Count } else { $rows[0].Count }
# Build merge map: vMerge[row][col] = $true if cell is merged with above
# Build vertical merge map: vMerge[row][col] = $true if cell is merged with above
$vMerge = @{}
for ($r = $rows.Count - 1; $r -ge 1; $r--) {
$vMerge[$r] = @{}
@@ -1128,6 +1358,26 @@ function Emit-AreaTemplateDSL {
}
if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} }
# Build horizontal merge map: hMerge[row][col] = $true if cell is merged with left
$hMerge = @{}
for ($r = 0; $r -lt $rows.Count; $r++) {
$hMerge[$r] = @{}
for ($c = 0; $c -lt $colCount; $c++) {
$cellVal = $rows[$r][$c]
if ($cellVal -is [string] -and $cellVal -eq '>') {
$hMerge[$r][$c] = $true
}
}
}
# Build drilldown map: param_name -> drilldown_value
$drilldownMap = @{}
if ($t.parameters) {
foreach ($tp in $t.parameters) {
if ($tp.drilldown) { $drilldownMap["$($tp.name)"] = "$($tp.drilldown)" }
}
}
X "`t<template>"
X "`t`t<name>$(Esc-Xml "$($t.name)")</name>"
X "`t`t<template xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:AreaTemplate`">"
@@ -1137,41 +1387,49 @@ function Emit-AreaTemplateDSL {
for ($c = 0; $c -lt $colCount; $c++) {
$cellVal = $rows[$r][$c]
$w = if ($c -lt $widths.Count) { [double]$widths[$c] } else { 0 }
$isMerged = $vMerge[$r][$c] -eq $true
# Check if this cell starts a vertical merge (next row has "|" in same column)
$startsVMerge = $false
for ($nr = $r + 1; $nr -lt $rows.Count; $nr++) {
if ($vMerge[$nr][$c] -eq $true) { $startsVMerge = $true } else { break }
}
$isVMerged = $vMerge[$r][$c] -eq $true
$isHMerged = $hMerge[$r][$c] -eq $true
X "`t`t`t`t<dcsat:tableCell>"
if ($isMerged) {
# Merged cell — only appearance with vMerge flag + width
if ($isVMerged) {
# Vertically merged cell — only appearance with vMerge flag + width
Emit-CellAppearance $style $w $true
} elseif ($isHMerged) {
# Horizontally merged cell — only appearance with hMerge flag + width
Emit-CellAppearance $style $w $false $true
} else {
# Cell value
if ($null -ne $cellVal -and $cellVal -ne '') {
$cellStr = "$cellVal"
# Unescape \| and \>
if ($cellStr -eq '\|') { $cellStr = '|' }
elseif ($cellStr -eq '\>') { $cellStr = '>' }
if ($cellStr -match '^\{(.+)\}$') {
# Parameter reference
$paramName = $Matches[1]
X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">"
X "`t`t`t`t`t`t<dcsat:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $Matches[1])</dcsat:value>"
X "`t`t`t`t`t`t<dcsat:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $paramName)</dcsat:value>"
X "`t`t`t`t`t</dcsat:item>"
# Build drilldown appearance extra items
$cellExtraItems = @()
if ($drilldownMap.ContainsKey($paramName)) {
$ddVal = $drilldownMap[$paramName]
$cellExtraItems += "`t`t`t`t`t<dcscor:item>"
$cellExtraItems += "`t`t`t`t`t`t<dcscor:parameter>Расшифровка</dcscor:parameter>"
$cellExtraItems += "`t`t`t`t`t`t<dcscor:value xsi:type=`"dcscor:Parameter`">Расшифровка_$ddVal</dcscor:value>"
$cellExtraItems += "`t`t`t`t`t</dcscor:item>"
}
} else {
# Static text
X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">"
X "`t`t`t`t`t`t<dcsat:value xsi:type=`"v8:LocalStringType`">"
X "`t`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellStr)</v8:content>"
X "`t`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t`t</dcsat:value>"
Emit-MLText -tag "dcsat:value" -text $cellStr -indent "`t`t`t`t`t`t"
X "`t`t`t`t`t</dcsat:item>"
}
}
# Appearance
$h = if ($r -eq 0) { $minHeight } else { 0 }
Emit-CellAppearance $style $w $startsVMerge $h
if (-not $cellExtraItems) { $cellExtraItems = @() }
Emit-CellAppearance $style $w $false $false $h $cellExtraItems
$cellExtraItems = @()
}
X "`t`t`t`t</dcsat:tableCell>"
}
@@ -1186,6 +1444,18 @@ function Emit-AreaTemplateDSL {
X "`t`t`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>"
X "`t`t`t<dcsat:expression>$(Esc-Xml "$($tp.expression)")</dcsat:expression>"
X "`t`t</parameter>"
# Drilldown parameter
if ($tp.drilldown) {
$ddVal = "$($tp.drilldown)"
X "`t`t<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">"
X "`t`t`t<dcsat:name>Расшифровка_$(Esc-Xml $ddVal)</dcsat:name>"
X "`t`t`t<dcsat:fieldExpression>"
X "`t`t`t`t<dcsat:field>ИмяРесурса</dcsat:field>"
X "`t`t`t`t<dcsat:expression>`"$(Esc-Xml $ddVal)`"</dcsat:expression>"
X "`t`t`t</dcsat:fieldExpression>"
X "`t`t`t<dcsat:mainAction>DrillDown</dcsat:mainAction>"
X "`t`t</parameter>"
}
}
}
X "`t</template>"
@@ -1211,6 +1481,18 @@ function Emit-Templates {
X "`t`t`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>"
X "`t`t`t<dcsat:expression>$(Esc-Xml "$($tp.expression)")</dcsat:expression>"
X "`t`t</parameter>"
# Drilldown parameter
if ($tp.drilldown) {
$ddVal = "$($tp.drilldown)"
X "`t`t<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">"
X "`t`t`t<dcsat:name>Расшифровка_$(Esc-Xml $ddVal)</dcsat:name>"
X "`t`t`t<dcsat:fieldExpression>"
X "`t`t`t`t<dcsat:field>ИмяРесурса</dcsat:field>"
X "`t`t`t`t<dcsat:expression>`"$(Esc-Xml $ddVal)`"</dcsat:expression>"
X "`t`t`t</dcsat:fieldExpression>"
X "`t`t`t<dcsat:mainAction>DrillDown</dcsat:mainAction>"
X "`t`t</parameter>"
}
}
}
X "`t</template>"
@@ -1222,11 +1504,20 @@ function Emit-Templates {
function Emit-GroupTemplates {
if (-not $def.groupTemplates) { return }
foreach ($gt in $def.groupTemplates) {
X "`t<groupTemplate>"
X "`t`t<groupField>$(Esc-Xml "$($gt.groupField)")</groupField>"
X "`t`t<templateType>$(Esc-Xml "$($gt.templateType)")</templateType>"
$ttype = if ($gt.templateType) { "$($gt.templateType)" } else { "Header" }
$isHeader = ($ttype -eq 'GroupHeader')
$tag = if ($isHeader) { 'groupHeaderTemplate' } else { 'groupTemplate' }
$xmlTType = if ($isHeader) { 'Header' } else { $ttype }
X "`t<$tag>"
if ($gt.groupName) {
X "`t`t<groupName>$(Esc-Xml "$($gt.groupName)")</groupName>"
} elseif ($gt.groupField) {
X "`t`t<groupField>$(Esc-Xml "$($gt.groupField)")</groupField>"
}
X "`t`t<templateType>$(Esc-Xml $xmlTType)</templateType>"
X "`t`t<template>$(Esc-Xml "$($gt.template)")</template>"
X "`t</groupTemplate>"
X "`t</$tag>"
}
}
@@ -1249,6 +1540,22 @@ function Emit-Selection {
X "$indent`t`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>"
X "$indent`t</dcsset:item>"
}
} elseif ($item.folder) {
X "$indent`t<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">"
X "$indent`t`t<dcsset:lwsTitle>"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml "$($item.folder)")</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcsset:lwsTitle>"
foreach ($sub in $item.items) {
$subName = if ($sub -is [string]) { $sub } else { "$($sub.field)" }
X "$indent`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
X "$indent`t`t`t<dcsset:field>$(Esc-Xml $subName)</dcsset:field>"
X "$indent`t`t</dcsset:item>"
}
X "$indent`t`t<dcsset:placement>Auto</dcsset:placement>"
X "$indent`t</dcsset:item>"
} else {
X "$indent`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>"
@@ -1281,6 +1588,16 @@ function Emit-FilterItem {
X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>"
if ($item.items) {
foreach ($sub in $item.items) {
if ($sub -is [string]) {
$parsed = Parse-FilterShorthand $sub
$obj = @{ field = $parsed.field; op = $parsed.op }
if ($parsed.use -eq $false) { $obj.use = $false }
if ($null -ne $parsed.value) { $obj.value = $parsed.value }
if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] }
if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID }
if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode }
$sub = [pscustomobject]$obj
}
Emit-FilterItem -item $sub -indent "$indent`t"
}
}
@@ -1321,12 +1638,7 @@ function Emit-FilterItem {
}
if ($item.presentation) {
X "$indent`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml "$($item.presentation)")</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</dcsset:presentation>"
Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t"
}
if ($item.viewMode) {
@@ -1339,12 +1651,7 @@ function Emit-FilterItem {
}
if ($item.userSettingPresentation) {
X "$indent`t<dcsset:userSettingPresentation xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml "$($item.userSettingPresentation)")</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</dcsset:userSettingPresentation>"
Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t"
}
X "$indent</dcsset:item>"
@@ -1432,13 +1739,8 @@ function Emit-AppearanceValue {
X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>"
} elseif ($actualVal -eq "true" -or $actualVal -eq "false") {
X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>"
} elseif ($key -eq "Текст" -or $key -eq "Заголовок") {
X "$indent`t<dcscor:value xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml $actualVal)</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</dcscor:value>"
} elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t"
} else {
X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>"
}
@@ -1517,12 +1819,7 @@ function Emit-OutputParameters {
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
X "$indent`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
if ($ptype -eq "mltext") {
X "$indent`t`t<dcscor:value xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcscor:value>"
Emit-MLText -tag "dcscor:value" -text $val -indent "$indent`t`t"
} else {
X "$indent`t`t<dcscor:value xsi:type=`"$ptype`">$(Esc-Xml $val)</dcscor:value>"
}
@@ -1567,22 +1864,35 @@ function Emit-DataParameters {
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>"
# Value
if ($null -ne $dp.value) {
if ($dp.nilValue -eq $true) {
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
} elseif ($null -ne $dp.value) {
$vtype = "$($dp.valueType)"
if ($dp.value -is [PSCustomObject] -and $dp.value.variant) {
# StandardPeriod (object form from JSON)
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml "$($dp.value.variant)")</v8:variant>"
X "$indent`t`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent`t`t</dcscor:value>"
} elseif ($dp.value -is [hashtable] -and $dp.value.variant) {
# StandardPeriod (hashtable from shorthand parser)
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml "$($dp.value.variant)")</v8:variant>"
X "$indent`t`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent`t`t</dcscor:value>"
} elseif ($dp.value -is [bool]) {
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
$bv = "$($dp.value)".ToLower()
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $bv)</dcscor:value>"
} elseif ("$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
} elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} elseif ($vtype -match '^decimal') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} elseif ($vtype -match '^string') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} else {
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
}
@@ -1598,12 +1908,7 @@ function Emit-DataParameters {
}
if ($dp.userSettingPresentation) {
X "$indent`t`t<dcsset:userSettingPresentation xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml "$($dp.userSettingPresentation)")</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcsset:userSettingPresentation>"
Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t"
}
X "$indent`t</dcscor:item>"
@@ -1661,6 +1966,10 @@ function Parse-StructureShorthand {
if ($seg -match '^(?i)(details|детали)$') {
# Empty groupBy = detailed records
$group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @()
} elseif ($seg -match '^(.+)\[(.+)\]$') {
# Named group: "ИмяГруппы[Поле]"
$group | Add-Member -NotePropertyName "name" -NotePropertyValue $Matches[1].Trim()
$group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($Matches[2].Trim())
} else {
$group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($seg)
}
@@ -1678,7 +1987,7 @@ function Parse-StructureShorthand {
function Emit-StructureItem {
param($item, [string]$indent)
$type = "$($item.type)"
$type = if ($item.type) { "$($item.type)" } else { "group" }
if ($type -eq "group") {
X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">"
@@ -1687,7 +1996,8 @@ function Emit-StructureItem {
X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>"
}
Emit-GroupItems -groupBy $item.groupBy -indent "$indent`t"
$gb = if ($item.groupBy) { $item.groupBy } else { $item.groupFields }
Emit-GroupItems -groupBy $gb -indent "$indent`t"
# Default order to ["Auto"] if not specified
$orderItems = $item.order
@@ -1725,7 +2035,8 @@ function Emit-StructureItem {
if ($item.columns) {
foreach ($col in $item.columns) {
X "$indent`t<dcsset:column>"
Emit-GroupItems -groupBy $col.groupBy -indent "$indent`t`t"
$colGb = if ($col.groupBy) { $col.groupBy } else { $col.groupFields }
Emit-GroupItems -groupBy $colGb -indent "$indent`t`t"
$colOrder = $col.order; if (-not $colOrder) { $colOrder = @("Auto") }
Emit-Order -items $colOrder -indent "$indent`t`t"
$colSel = $col.selection; if (-not $colSel) { $colSel = @("Auto") }
@@ -1741,7 +2052,8 @@ function Emit-StructureItem {
if ($row.name) {
X "$indent`t`t<dcsset:name>$(Esc-Xml "$($row.name)")</dcsset:name>"
}
Emit-GroupItems -groupBy $row.groupBy -indent "$indent`t`t"
$rowGb = if ($row.groupBy) { $row.groupBy } else { $row.groupFields }
Emit-GroupItems -groupBy $rowGb -indent "$indent`t`t"
$rowOrder = $row.order; if (-not $rowOrder) { $rowOrder = @("Auto") }
Emit-Order -items $rowOrder -indent "$indent`t`t"
$rowSel = $row.selection; if (-not $rowSel) { $rowSel = @("Auto") }
@@ -1762,7 +2074,8 @@ function Emit-StructureItem {
# Points
if ($item.points) {
X "$indent`t<dcsset:point>"
Emit-GroupItems -groupBy $item.points.groupBy -indent "$indent`t`t"
$ptGb = if ($item.points.groupBy) { $item.points.groupBy } else { $item.points.groupFields }
Emit-GroupItems -groupBy $ptGb -indent "$indent`t`t"
$ptOrder = $item.points.order; if (-not $ptOrder) { $ptOrder = @("Auto") }
Emit-Order -items $ptOrder -indent "$indent`t`t"
$ptSel = $item.points.selection; if (-not $ptSel) { $ptSel = @("Auto") }
@@ -1773,7 +2086,8 @@ function Emit-StructureItem {
# Series
if ($item.series) {
X "$indent`t<dcsset:series>"
Emit-GroupItems -groupBy $item.series.groupBy -indent "$indent`t`t"
$srGb = if ($item.series.groupBy) { $item.series.groupBy } else { $item.series.groupFields }
Emit-GroupItems -groupBy $srGb -indent "$indent`t`t"
$srOrder = $item.series.order; if (-not $srOrder) { $srOrder = @("Auto") }
Emit-Order -items $srOrder -indent "$indent`t`t"
$srSel = $item.series.selection; if (-not $srSel) { $srSel = @("Auto") }
@@ -1830,13 +2144,8 @@ function Emit-SettingsVariants {
X "`t<settingsVariant>"
X "`t`t<dcsset:name>$(Esc-Xml "$($v.name)")</dcsset:name>"
$pres = if ($v.presentation) { "$($v.presentation)" } elseif ($v.title) { "$($v.title)" } else { "$($v.name)" }
X "`t`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">"
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 $pres)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</dcsset:presentation>"
$pres = if ($v.presentation) { $v.presentation } elseif ($v.title) { $v.title } else { "$($v.name)" }
Emit-MLText -tag "dcsset:presentation" -text $pres -indent "`t`t"
X "`t`t<dcsset:settings xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`">"
@@ -1868,7 +2177,51 @@ function Emit-SettingsVariants {
}
# DataParameters
if ($s.dataParameters) {
if ($s.dataParameters -eq 'auto') {
# Auto-generate dataParameters for all non-hidden params.
# Pattern follows 1C Designer / ERP persistence:
# - value set (non-default) → emit value, use=true (implicit)
# - value missing / Custom period → <use>false</use> + <value xsi:nil="true"/>
$autoDP = @()
foreach ($ap in $script:allParams) {
if ($ap.hidden) { continue }
$dpItem = New-Object PSObject
$dpItem | Add-Member -NotePropertyName "parameter" -NotePropertyValue $ap.name
$dpItem | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue "auto"
$hasMeaningfulValue = $false
if ($ap.type -eq 'StandardPeriod') {
# Inherit variant; Custom is treated as "empty"
$variant = 'Custom'
$av = $ap.value
if ($null -ne $av) {
if (($av -is [PSCustomObject] -or $av -is [hashtable]) -and $av.variant) {
$variant = "$($av.variant)"
} elseif ("$av") {
$variant = "$av"
}
}
$dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue @{ variant = $variant }
if ($variant -ne 'Custom') { $hasMeaningfulValue = $true }
} elseif ($null -ne $ap.value -and "$($ap.value)" -ne '') {
$dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue $ap.value
$dpItem | Add-Member -NotePropertyName "valueType" -NotePropertyValue "$($ap.type)"
$hasMeaningfulValue = $true
} else {
$dpItem | Add-Member -NotePropertyName "nilValue" -NotePropertyValue $true
}
if (-not $hasMeaningfulValue) {
$dpItem | Add-Member -NotePropertyName "use" -NotePropertyValue $false
}
$autoDP += $dpItem
}
if ($autoDP.Count -gt 0) {
Emit-DataParameters -items $autoDP -indent "`t`t`t"
}
} elseif ($s.dataParameters) {
Emit-DataParameters -items $s.dataParameters -indent "`t`t`t"
}
+467 -140
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-compile v1.3 — Compile 1C DCS from JSON
# skd-compile v1.21 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -12,6 +12,10 @@ import uuid
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def fmt_dec(v):
"""Format decimal: 30.0 → '30', 16.625 → '16.625' (match PS1 output)."""
return str(int(v)) if v == int(v) else str(v)
def resolve_query_value(val, base_dir):
if not val.startswith("@"):
@@ -37,10 +41,18 @@ def emit_mltext(lines, indent, tag, text):
lines.append(f"{indent}<{tag}/>")
return
lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">')
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>")
# Multi-lang: object form { ru: "...", en: "..." } -- one <v8:item> per language
if isinstance(text, dict):
for lang, content in text.items():
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>{esc_xml(str(lang))}</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(str(content))}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
else:
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(str(text))}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
@@ -111,7 +123,20 @@ def resolve_type_str(type_str):
return type_str
def emit_value_type(lines, type_str, indent):
def emit_value_type(lines, type_spec, indent):
if not type_spec:
return
# Multi-type: iterate and emit each type with its qualifiers
if isinstance(type_spec, list):
for t in type_spec:
emit_single_value_type(lines, str(t), indent)
return
emit_single_value_type(lines, str(type_spec), indent)
def emit_single_value_type(lines, type_str, indent):
if not type_str:
return
@@ -217,22 +242,46 @@ def parse_total_shorthand(s):
data_path = parts[0].strip()
func_part = parts[1].strip()
# Known DCS aggregate functions (ru + en)
_agg_funcs = {'Сумма','Количество','Минимум','Максимум','Среднее',
'Sum','Count','Min','Max','Avg',
'Minimum','Maximum','Average'}
if re.match(r'^\w+\(', func_part):
return {'dataPath': data_path, 'expression': func_part}
else:
elif func_part in _agg_funcs:
return {'dataPath': data_path, 'expression': f'{func_part}({data_path})'}
else:
# Identity or custom expression — use as-is
return {'dataPath': data_path, 'expression': func_part}
# --- Parameter shorthand parser ---
def parse_param_shorthand(s):
result = {'name': '', 'type': '', 'value': None, 'autoDates': False}
result = {'name': '', 'type': '', 'value': None, 'autoDates': False, 'title': None}
# Extract @autoDates flag
if '@autoDates' in s:
result['autoDates'] = True
s = re.sub(r'\s*@autoDates', '', s)
# Extract @valueList flag
if '@valueList' in s:
result['valueListAllowed'] = True
s = re.sub(r'\s*@valueList', '', s)
# Extract @hidden flag
if '@hidden' in s:
result['hidden'] = True
s = re.sub(r'\s*@hidden', '', s)
# Extract optional [Title] (mirrors parse_field_shorthand)
m = re.search(r'\[([^\]]*)\]', s)
if m:
result['title'] = m.group(1).strip()
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
# Split "Name: Type = Value"
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s)
if m:
@@ -249,13 +298,46 @@ def parse_param_shorthand(s):
# --- Calculated field shorthand parser ---
def parse_calc_shorthand(s):
idx = s.find('=')
if idx > 0:
return {
'dataPath': s[:idx].strip(),
'expression': s[idx + 1:].strip(),
}
return {'dataPath': s.strip(), 'expression': ''}
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
# an expression (e.g. index access) isn't interpreted as a title.
# - `#restrict` flags use a known-names pattern and are extracted globally —
# the docs put them after `=`, and the closed flag set avoids matching
# `#word` that happens to appear inside a string literal.
restrict_pattern = r'#(noField|noFilter|noCondition|noGroup|noOrder)\b'
restrict = re.findall(restrict_pattern, s)
s = re.sub(r'\s*' + restrict_pattern, '', s)
eq_idx = s.find('=')
if eq_idx > 0:
lhs = s[:eq_idx]
rhs = s[eq_idx + 1:].strip()
else:
lhs = s
rhs = ''
title = ''
m = re.search(r'\[([^\]]+)\]', lhs)
if m:
title = m.group(1)
lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs)
lhs = lhs.strip()
type_str = ''
data_path = lhs
if ':' in lhs:
colon_idx = lhs.index(':')
data_path = lhs[:colon_idx].strip()
type_str = resolve_type_str(lhs[colon_idx + 1:].strip())
return {
'dataPath': data_path,
'expression': rhs,
'type': type_str,
'title': title,
'restrict': restrict,
}
# --- DataParameter shorthand parser ---
@@ -361,6 +443,9 @@ def parse_filter_shorthand(s):
elif re.match(r'^\d+(\.\d+)?$', val_part):
result['value'] = val_part
result['valueType'] = 'xs:decimal'
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
result['value'] = val_part
result['valueType'] = 'dcscor:DesignTimeValue'
else:
result['value'] = val_part
result['valueType'] = 'xs:string'
@@ -419,8 +504,12 @@ def emit_field(lines, field_def, indent):
f = {
'dataPath': str(field_def.get('dataPath', '')) or str(field_def.get('field', '')),
'field': str(field_def.get('field', '')) or str(field_def.get('dataPath', '')),
'title': str(field_def.get('title', '')) if field_def.get('title') else '',
'type': resolve_type_str(str(field_def['type'])) if field_def.get('type') else '',
'title': field_def.get('title') if field_def.get('title') else '',
'type': (
[resolve_type_str(str(t)) for t in field_def['type']]
if isinstance(field_def['type'], list)
else resolve_type_str(str(field_def['type']))
) if field_def.get('type') else '',
'roles': [],
'restrict': [],
'appearance': {},
@@ -592,48 +681,81 @@ def emit_data_set_links(lines, defn):
def emit_calc_fields(lines, defn):
if not defn.get('calculatedFields'):
return
restrict_map = {
'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
'noGroup': 'group', 'noOrder': 'order',
}
for cf in defn['calculatedFields']:
# Collect dataPath/expression/title/type/restrict/appearance from either
# shorthand string or object form. Object form accepts dataPath/field/name
# as synonyms; useRestriction/restrict accepts object, array, or flag string.
title = ''
type_str = ''
restrict_tokens = []
restrict_obj = None
appearance = None
if isinstance(cf, str):
parsed = parse_calc_shorthand(cf)
is_obj = False
data_path = parsed['dataPath']
expression = parsed['expression']
title = parsed.get('title', '') or ''
type_str = parsed.get('type', '') or ''
restrict_tokens = list(parsed.get('restrict') or [])
else:
parsed = {
'dataPath': str(cf.get('dataPath', '')),
'expression': str(cf.get('expression', '')),
}
is_obj = True
data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name') or '')
expression = str(cf.get('expression', ''))
if cf.get('title'):
title = cf['title']
if cf.get('type'):
type_str = resolve_type_str(str(cf['type']))
restrict_val = cf.get('restrict') if cf.get('restrict') is not None else cf.get('useRestriction')
if restrict_val:
if isinstance(restrict_val, dict):
restrict_obj = restrict_val
elif isinstance(restrict_val, str):
# Flag-string form: "#noField #noFilter #noGroup #noOrder" (or without `#`)
for tok in restrict_val.split():
t = tok.strip().lstrip('#')
if t:
restrict_tokens.append(t)
else:
# Array form: ["noField", "noFilter", ...]
for r in restrict_val:
restrict_tokens.append(str(r))
appearance = cf.get('appearance')
lines.append('\t<calculatedField>')
lines.append(f'\t\t<dataPath>{esc_xml(parsed["dataPath"])}</dataPath>')
lines.append(f'\t\t<expression>{esc_xml(parsed["expression"])}</expression>')
lines.append(f'\t\t<dataPath>{esc_xml(data_path)}</dataPath>')
lines.append(f'\t\t<expression>{esc_xml(expression)}</expression>')
if is_obj:
if cf.get('title'):
emit_mltext(lines, '\t\t', 'title', str(cf['title']))
if cf.get('type'):
cf_type = resolve_type_str(str(cf['type']))
lines.append('\t\t<valueType>')
emit_value_type(lines, cf_type, '\t\t\t')
lines.append('\t\t</valueType>')
if cf.get('restrict'):
restrict_map = {
'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
'noGroup': 'group', 'noOrder': 'order',
}
lines.append('\t\t<useRestriction>')
for r in cf['restrict']:
if title:
emit_mltext(lines, '\t\t', 'title', title)
if type_str:
lines.append('\t\t<valueType>')
emit_value_type(lines, type_str, '\t\t\t')
lines.append('\t\t</valueType>')
if restrict_obj or restrict_tokens:
lines.append('\t\t<useRestriction>')
if restrict_obj:
for xml_name, flag in restrict_obj.items():
if flag:
lines.append(f'\t\t\t<{esc_xml(str(xml_name))}>true</{esc_xml(str(xml_name))}>')
else:
for r in restrict_tokens:
xml_name = restrict_map.get(str(r))
if xml_name:
lines.append(f'\t\t\t<{xml_name}>true</{xml_name}>')
lines.append('\t\t</useRestriction>')
if cf.get('appearance'):
lines.append('\t\t<appearance>')
for k, v in cf['appearance'].items():
lines.append('\t\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f'\t\t\t\t<dcscor:parameter>{esc_xml(k)}</dcscor:parameter>')
lines.append(f'\t\t\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(v))}</dcscor:value>')
lines.append('\t\t\t</dcscor:item>')
lines.append('\t\t</appearance>')
lines.append('\t\t</useRestriction>')
if appearance:
lines.append('\t\t<appearance>')
for k, v in appearance.items():
lines.append('\t\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f'\t\t\t\t<dcscor:parameter>{esc_xml(k)}</dcscor:parameter>')
lines.append(f'\t\t\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(v))}</dcscor:value>')
lines.append('\t\t\t</dcscor:item>')
lines.append('\t\t</appearance>')
lines.append('\t</calculatedField>')
@@ -675,8 +797,11 @@ def emit_param_value(lines, type_str, val, indent):
val_str = str(val)
if type_str == 'StandardPeriod':
# Always emit startDate/endDate to match how 1C Designer saves the schema.
lines.append(f'{indent}<value xsi:type="v8:StandardPeriod">')
lines.append(f'{indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
lines.append(f'{indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>')
lines.append(f'{indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>')
lines.append(f'{indent}</value>')
elif type_str and re.match(r'^date', type_str):
lines.append(f'{indent}<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
@@ -692,6 +817,8 @@ def emit_param_value(lines, type_str, val, indent):
lines.append(f'{indent}<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
elif val_str == 'true' or val_str == 'false':
lines.append(f'{indent}<value xsi:type="xs:boolean">{esc_xml(val_str)}</value>')
elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
lines.append(f'{indent}<value xsi:type="dcscor:DesignTimeValue">{esc_xml(val_str)}</value>')
else:
lines.append(f'{indent}<value xsi:type="xs:string">{esc_xml(val_str)}</value>')
@@ -700,10 +827,15 @@ def emit_single_param(lines, p, parsed):
lines.append('\t<parameter>')
lines.append(f'\t\t<name>{esc_xml(parsed["name"])}</name>')
# Title
# Title (from parsed first, then from object form; accept `presentation` as
# a synonym — 1C UI labels a parameter's caption "Представление").
title = ''
if p is not None and not isinstance(p, str) and p.get('title'):
title = str(p['title'])
if parsed.get('title'):
title = parsed['title']
elif p is not None and not isinstance(p, str) and p.get('title'):
title = p['title']
elif p is not None and not isinstance(p, str) and p.get('presentation'):
title = p['presentation']
if title:
emit_mltext(lines, '\t\t', 'title', title)
@@ -716,26 +848,68 @@ def emit_single_param(lines, p, parsed):
# Value
emit_param_value(lines, parsed.get('type', ''), parsed.get('value'), '\t\t')
# Hidden implies useRestriction=true + availableAsField=false
if parsed.get('hidden') is True:
parsed['availableAsField'] = False
parsed['useRestriction'] = True
# UseRestriction
if p is not None and not isinstance(p, str) and p.get('useRestriction') is True:
if parsed.get('useRestriction') is True or (p is not None and not isinstance(p, str) and p.get('useRestriction') is True):
lines.append('\t\t<useRestriction>true</useRestriction>')
# Expression
if parsed.get('expression'):
lines.append(f'\t\t<expression>{esc_xml(parsed["expression"])}</expression>')
if parsed.get('hidden'):
parsed['availableAsField'] = False
# AvailableAsField
if parsed.get('availableAsField') is False:
lines.append('\t\t<availableAsField>false</availableAsField>')
# ValueListAllowed
if parsed.get('valueListAllowed'):
lines.append('\t\t<valueListAllowed>true</valueListAllowed>')
# AvailableValues
if p is not None and not isinstance(p, str) and p.get('availableValues'):
for av in p['availableValues']:
av_val = str(av.get('value', ''))
av_type = 'xs:string'
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_val):
av_type = 'dcscor:DesignTimeValue'
lines.append('\t\t<availableValue>')
lines.append(f'\t\t\t<value xsi:type="{av_type}">{esc_xml(av_val)}</value>')
# `title` accepted as synonym of `presentation` — both map to the same UI label.
av_pres = av.get('presentation') or av.get('title') or ''
if av_pres:
emit_mltext(lines, '\t\t\t', 'presentation', av_pres)
lines.append('\t\t</availableValue>')
# DenyIncompleteValues
deny = parsed.get('denyIncompleteValues') is True or (
p is not None and not isinstance(p, str) and p.get('denyIncompleteValues') is True)
if deny:
lines.append('\t\t<denyIncompleteValues>true</denyIncompleteValues>')
# Use
use_val = None
if p is not None and not isinstance(p, str) and p.get('use'):
lines.append(f'\t\t<use>{esc_xml(str(p["use"]))}</use>')
use_val = str(p['use'])
elif parsed.get('use'):
use_val = str(parsed['use'])
if use_val:
lines.append(f'\t\t<use>{esc_xml(use_val)}</use>')
lines.append('\t</parameter>')
_all_params = []
def emit_parameters(lines, defn):
global _all_params
_all_params = []
if not defn.get('parameters'):
return
for p in defn['parameters']:
@@ -752,26 +926,50 @@ def emit_parameters(lines, defn):
parsed['expression'] = str(p['expression'])
if p.get('availableAsField') is False:
parsed['availableAsField'] = False
if p.get('valueListAllowed') is True:
parsed['valueListAllowed'] = True
if p.get('hidden') is True:
parsed['hidden'] = True
if p.get('autoDates') is True:
parsed['autoDates'] = True
# @autoDates implies use=Always + denyIncompleteValues=true by default
# (derived &НачалоПериода/&КонецПериода need a populated period).
# Explicit values in object form override these defaults.
if parsed.get('autoDates'):
is_obj = p is not None and not isinstance(p, str)
if not (is_obj and p.get('use') is not None):
parsed['use'] = 'Always'
if not (is_obj and p.get('denyIncompleteValues') is not None):
parsed['denyIncompleteValues'] = True
emit_single_param(lines, p, parsed)
# @autoDates: auto-generate ДатаНачала and ДатаОкончания
# Track parameter for auto dataParameters
_all_params.append({
'name': parsed['name'],
'hidden': bool(parsed.get('hidden')),
'type': parsed.get('type', ''),
'value': parsed.get('value'),
})
# @autoDates: auto-generate НачалоПериода and КонецПериода (canonical БСП pattern)
if parsed.get('autoDates'):
param_name = parsed['name']
begin_parsed = {
'name': '\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430',
'type': 'date', 'value': None,
'name': '\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0438\u043e\u0434\u0430',
'title': '\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430',
'type': 'date', 'value': '0001-01-01T00:00:00',
'useRestriction': True,
'expression': f'&{param_name}.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430',
'availableAsField': False,
}
emit_single_param(lines, None, begin_parsed)
end_parsed = {
'name': '\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f',
'type': 'date', 'value': None,
'name': '\u041a\u043e\u043d\u0435\u0446\u041f\u0435\u0440\u0438\u043e\u0434\u0430',
'title': '\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430',
'type': 'date', 'value': '0001-01-01T00:00:00',
'useRestriction': True,
'expression': f'&{param_name}.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f',
'availableAsField': False,
}
emit_single_param(lines, None, end_parsed)
@@ -806,9 +1004,21 @@ AREA_STYLE_PRESETS = {
}
def load_user_styles(base_dir):
for d in [base_dir, os.getcwd()]:
p = os.path.join(d, 'skd-styles.json')
def load_user_styles(base_dir, output_path=None):
# Search order (first found wins): 1) definition dir, 2) cwd, 3) scan-up from OutputPath for presets/skills/skd/
search_paths = [
os.path.join(base_dir, 'skd-styles.json'),
os.path.join(os.getcwd(), 'skd-styles.json'),
]
if output_path:
scan_dir = os.path.dirname(output_path)
while scan_dir:
search_paths.append(os.path.join(scan_dir, 'presets', 'skills', 'skd', 'skd-styles.json'))
parent_dir = os.path.dirname(scan_dir)
if parent_dir == scan_dir:
break
scan_dir = parent_dir
for p in search_paths:
if os.path.isfile(p):
with open(p, 'r', encoding='utf-8-sig') as f:
user_styles = json.load(f)
@@ -827,7 +1037,7 @@ def _emit_color_value(lines, color, indent):
lines.append(f'{indent}<dcscor:value xsi:type="v8ui:Color">{esc_xml(color)}</dcscor:value>')
def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0):
def _emit_cell_appearance(lines, style, width=0, v_merge=False, h_merge=False, min_height=0, extra_items=None):
ind = '\t\t\t\t\t'
lines.append('\t\t\t\t<dcsat:appearance>')
# Background color
@@ -891,11 +1101,11 @@ def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0):
if width and width > 0:
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{width}</dcscor:value>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{fmt_dec(width)}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{width}</dcscor:value>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{fmt_dec(width)}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Min height
if min_height and min_height > 0:
@@ -909,6 +1119,16 @@ def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0):
lines.append(f'{ind}\t<dcscor:parameter>\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:boolean">true</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Horizontal merge
if h_merge:
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u0438</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:boolean">true</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Extra appearance items (e.g. drilldown)
if extra_items:
for ei in extra_items:
lines.append(ei)
lines.append('\t\t\t\t</dcsat:appearance>')
@@ -924,7 +1144,7 @@ def _emit_area_template_dsl(lines, t):
min_height = float(t.get('minHeight', 0))
col_count = len(widths) if widths else len(rows[0])
# Build merge map
# Build vertical merge map
v_merge = {}
for r in range(len(rows) - 1, 0, -1):
v_merge[r] = {}
@@ -935,6 +1155,22 @@ def _emit_area_template_dsl(lines, t):
if 0 not in v_merge:
v_merge[0] = {}
# Build horizontal merge map
h_merge = {}
for r in range(len(rows)):
h_merge[r] = {}
for c in range(col_count):
cell_val = rows[r][c] if c < len(rows[r]) else None
if isinstance(cell_val, str) and cell_val == '>':
h_merge[r][c] = True
# Build drilldown map: param_name -> drilldown_value
drilldown_map = {}
if t.get('parameters'):
for tp in t['parameters']:
if tp.get('drilldown'):
drilldown_map[str(tp['name'])] = str(tp['drilldown'])
lines.append('\t<template>')
lines.append(f'\t\t<name>{esc_xml(str(t["name"]))}</name>')
lines.append('\t\t<template xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:AreaTemplate">')
@@ -944,37 +1180,41 @@ def _emit_area_template_dsl(lines, t):
for c in range(col_count):
cell_val = rows[r][c] if c < len(rows[r]) else None
w = float(widths[c]) if c < len(widths) else 0
is_merged = v_merge.get(r, {}).get(c, False)
# Check if this cell starts a vertical merge
starts_v_merge = False
for nr in range(r + 1, len(rows)):
if v_merge.get(nr, {}).get(c, False):
starts_v_merge = True
else:
break
is_v_merged = v_merge.get(r, {}).get(c, False)
is_h_merged = h_merge.get(r, {}).get(c, False)
lines.append('\t\t\t\t<dcsat:tableCell>')
if is_merged:
if is_v_merged:
_emit_cell_appearance(lines, style, w, True)
elif is_h_merged:
_emit_cell_appearance(lines, style, w, h_merge=True)
else:
cell_extra_items = []
if cell_val is not None and str(cell_val) != '':
cell_str = str(cell_val)
# Unescape \| and \>
if cell_str == '\\|':
cell_str = '|'
elif cell_str == '\\>':
cell_str = '>'
m = re.match(r'^\{(.+)\}$', cell_str)
if m:
param_name = m.group(1)
lines.append('\t\t\t\t\t<dcsat:item xsi:type="dcsat:Field">')
lines.append(f'\t\t\t\t\t\t<dcsat:value xsi:type="dcscor:Parameter">{esc_xml(m.group(1))}</dcsat:value>')
lines.append(f'\t\t\t\t\t\t<dcsat:value xsi:type="dcscor:Parameter">{esc_xml(param_name)}</dcsat:value>')
lines.append('\t\t\t\t\t</dcsat:item>')
# Build drilldown appearance extra items
if param_name in drilldown_map:
dd_val = drilldown_map[param_name]
cell_extra_items.append('\t\t\t\t\t<dcscor:item>')
cell_extra_items.append(f'\t\t\t\t\t\t<dcscor:parameter>\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430</dcscor:parameter>')
cell_extra_items.append(f'\t\t\t\t\t\t<dcscor:value xsi:type="dcscor:Parameter">\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{dd_val}</dcscor:value>')
cell_extra_items.append('\t\t\t\t\t</dcscor:item>')
else:
lines.append('\t\t\t\t\t<dcsat:item xsi:type="dcsat:Field">')
lines.append('\t\t\t\t\t\t<dcsat:value xsi:type="v8:LocalStringType">')
lines.append('\t\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_str)}</v8:content>')
lines.append('\t\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t\t</dcsat:value>')
emit_mltext(lines, '\t\t\t\t\t\t', 'dcsat:value', cell_str)
lines.append('\t\t\t\t\t</dcsat:item>')
h = min_height if r == 0 else 0
_emit_cell_appearance(lines, style, w, starts_v_merge, h)
_emit_cell_appearance(lines, style, w, False, False, h, cell_extra_items or None)
lines.append('\t\t\t\t</dcsat:tableCell>')
lines.append('\t\t\t</dcsat:item>')
@@ -985,6 +1225,17 @@ def _emit_area_template_dsl(lines, t):
lines.append(f'\t\t\t<dcsat:name>{esc_xml(str(tp["name"]))}</dcsat:name>')
lines.append(f'\t\t\t<dcsat:expression>{esc_xml(str(tp["expression"]))}</dcsat:expression>')
lines.append('\t\t</parameter>')
# Drilldown parameter
if tp.get('drilldown'):
dd_val = str(tp['drilldown'])
lines.append('\t\t<parameter xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:DetailsAreaTemplateParameter">')
lines.append(f'\t\t\t<dcsat:name>\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}</dcsat:name>')
lines.append('\t\t\t<dcsat:fieldExpression>')
lines.append('\t\t\t\t<dcsat:field>\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430</dcsat:field>')
lines.append(f'\t\t\t\t<dcsat:expression>"{esc_xml(dd_val)}"</dcsat:expression>')
lines.append('\t\t\t</dcsat:fieldExpression>')
lines.append('\t\t\t<dcsat:mainAction>DrillDown</dcsat:mainAction>')
lines.append('\t\t</parameter>')
lines.append('\t</template>')
@@ -1007,6 +1258,17 @@ def emit_templates(lines, defn):
lines.append(f'\t\t\t<dcsat:name>{esc_xml(str(tp["name"]))}</dcsat:name>')
lines.append(f'\t\t\t<dcsat:expression>{esc_xml(str(tp["expression"]))}</dcsat:expression>')
lines.append('\t\t</parameter>')
# Drilldown parameter
if tp.get('drilldown'):
dd_val = str(tp['drilldown'])
lines.append('\t\t<parameter xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:DetailsAreaTemplateParameter">')
lines.append(f'\t\t\t<dcsat:name>\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}</dcsat:name>')
lines.append('\t\t\t<dcsat:fieldExpression>')
lines.append('\t\t\t\t<dcsat:field>\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430</dcsat:field>')
lines.append(f'\t\t\t\t<dcsat:expression>"{esc_xml(dd_val)}"</dcsat:expression>')
lines.append('\t\t\t</dcsat:fieldExpression>')
lines.append('\t\t\t<dcsat:mainAction>DrillDown</dcsat:mainAction>')
lines.append('\t\t</parameter>')
lines.append('\t</template>')
@@ -1016,11 +1278,19 @@ def emit_group_templates(lines, defn):
if not defn.get('groupTemplates'):
return
for gt in defn['groupTemplates']:
lines.append('\t<groupTemplate>')
lines.append(f'\t\t<groupField>{esc_xml(str(gt["groupField"]))}</groupField>')
lines.append(f'\t\t<templateType>{esc_xml(str(gt["templateType"]))}</templateType>')
ttype = str(gt.get('templateType', '')) or 'Header'
is_header = (ttype == 'GroupHeader')
tag = 'groupHeaderTemplate' if is_header else 'groupTemplate'
xml_ttype = 'Header' if is_header else ttype
lines.append(f'\t<{tag}>')
if gt.get('groupName'):
lines.append(f'\t\t<groupName>{esc_xml(str(gt["groupName"]))}</groupName>')
elif gt.get('groupField'):
lines.append(f'\t\t<groupField>{esc_xml(str(gt["groupField"]))}</groupField>')
lines.append(f'\t\t<templateType>{esc_xml(xml_ttype)}</templateType>')
lines.append(f'\t\t<template>{esc_xml(str(gt["template"]))}</template>')
lines.append('\t</groupTemplate>')
lines.append(f'\t</{tag}>')
# === Settings Variants ===
@@ -1039,6 +1309,21 @@ def emit_selection(lines, items, indent, skip_auto=False):
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:SelectedItemField">')
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(item)}</dcsset:field>')
lines.append(f'{indent}\t</dcsset:item>')
elif item.get('folder'):
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:SelectedItemFolder">')
lines.append(f'{indent}\t\t<dcsset:lwsTitle>')
lines.append(f'{indent}\t\t\t<v8:item>')
lines.append(f'{indent}\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{indent}\t\t\t\t<v8:content>{esc_xml(str(item["folder"]))}</v8:content>')
lines.append(f'{indent}\t\t\t</v8:item>')
lines.append(f'{indent}\t\t</dcsset:lwsTitle>')
for sub in (item.get('items') or []):
sub_name = str(sub.get('field', sub)) if isinstance(sub, dict) else str(sub)
lines.append(f'{indent}\t\t<dcsset:item xsi:type="dcsset:SelectedItemField">')
lines.append(f'{indent}\t\t\t<dcsset:field>{esc_xml(sub_name)}</dcsset:field>')
lines.append(f'{indent}\t\t</dcsset:item>')
lines.append(f'{indent}\t\t<dcsset:placement>Auto</dcsset:placement>')
lines.append(f'{indent}\t</dcsset:item>')
else:
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:SelectedItemField">')
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(item["field"]))}</dcsset:field>')
@@ -1062,6 +1347,19 @@ def emit_filter_item(lines, item, indent):
lines.append(f'{indent}\t<dcsset:groupType>{group_type}</dcsset:groupType>')
if item.get('items'):
for sub in item['items']:
if isinstance(sub, str):
parsed = parse_filter_shorthand(sub)
sub = {'field': parsed['field'], 'op': parsed['op']}
if parsed['use'] is False:
sub['use'] = False
if parsed.get('value') is not None:
sub['value'] = parsed['value']
if parsed.get('valueType'):
sub['valueType'] = parsed['valueType']
if parsed.get('userSettingID'):
sub['userSettingID'] = parsed['userSettingID']
if parsed.get('viewMode'):
sub['viewMode'] = parsed['viewMode']
emit_filter_item(lines, sub, f'{indent}\t')
lines.append(f'{indent}</dcsset:item>')
return
@@ -1097,12 +1395,7 @@ def emit_filter_item(lines, item, indent):
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>')
if item.get('presentation'):
lines.append(f'{indent}\t<dcsset:presentation xsi:type="v8:LocalStringType">')
lines.append(f'{indent}\t\t<v8:item>')
lines.append(f'{indent}\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{indent}\t\t\t<v8:content>{esc_xml(str(item["presentation"]))}</v8:content>')
lines.append(f'{indent}\t\t</v8:item>')
lines.append(f'{indent}\t</dcsset:presentation>')
emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item["presentation"])
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
@@ -1112,12 +1405,7 @@ def emit_filter_item(lines, item, indent):
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
lines.append(f'{indent}\t<dcsset:userSettingPresentation xsi:type="v8:LocalStringType">')
lines.append(f'{indent}\t\t<v8:item>')
lines.append(f'{indent}\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{indent}\t\t\t<v8:content>{esc_xml(str(item["userSettingPresentation"]))}</v8:content>')
lines.append(f'{indent}\t\t</v8:item>')
lines.append(f'{indent}\t</dcsset:userSettingPresentation>')
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item["userSettingPresentation"])
lines.append(f'{indent}</dcsset:item>')
@@ -1191,13 +1479,8 @@ def emit_appearance_value(lines, key, val, indent):
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>')
elif actual_val == 'true' or actual_val == 'false':
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:boolean">{actual_val}</dcscor:value>')
elif key == '\u0422\u0435\u043a\u0441\u0442' or key == '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a':
lines.append(f'{indent}\t<dcscor:value xsi:type="v8:LocalStringType">')
lines.append(f'{indent}\t\t<v8:item>')
lines.append(f'{indent}\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{indent}\t\t\t<v8:content>{esc_xml(actual_val)}</v8:content>')
lines.append(f'{indent}\t\t</v8:item>')
lines.append(f'{indent}\t</dcscor:value>')
elif key in ('\u0422\u0435\u043a\u0441\u0442', '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a', '\u0424\u043e\u0440\u043c\u0430\u0442'):
emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val)
else:
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>')
lines.append(f'{indent}</dcscor:item>')
@@ -1262,12 +1545,7 @@ def emit_output_parameters(lines, params, indent):
lines.append(f'{indent}\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f'{indent}\t\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>')
if ptype == 'mltext':
lines.append(f'{indent}\t\t<dcscor:value xsi:type="v8:LocalStringType">')
lines.append(f'{indent}\t\t\t<v8:item>')
lines.append(f'{indent}\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{indent}\t\t\t\t<v8:content>{esc_xml(val_str)}</v8:content>')
lines.append(f'{indent}\t\t\t</v8:item>')
lines.append(f'{indent}\t\t</dcscor:value>')
emit_mltext(lines, f'{indent}\t\t', 'dcscor:value', val_str)
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="{ptype}">{esc_xml(val_str)}</dcscor:value>')
lines.append(f'{indent}\t</dcscor:item>')
@@ -1303,18 +1581,29 @@ def emit_data_parameters(lines, items, indent):
lines.append(f'{indent}\t\t<dcscor:parameter>{esc_xml(str(dp["parameter"]))}</dcscor:parameter>')
# Value
if dp.get('value') is not None:
if dp.get('nilValue') is True:
lines.append(f'{indent}\t\t<dcscor:value xsi:nil="true"/>')
elif dp.get('value') is not None:
val = dp['value']
vtype = str(dp.get('valueType') or '')
if isinstance(val, dict) and val.get('variant'):
# StandardPeriod
lines.append(f'{indent}\t\t<dcscor:value xsi:type="v8:StandardPeriod">')
lines.append(f'{indent}\t\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(str(val["variant"]))}</v8:variant>')
lines.append(f'{indent}\t\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>')
lines.append(f'{indent}\t\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>')
lines.append(f'{indent}\t\t</dcscor:value>')
elif isinstance(val, bool):
elif vtype == 'boolean' or isinstance(val, bool):
bv = str(val).lower()
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:boolean">{esc_xml(bv)}</dcscor:value>')
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(val))}</dcscor:value>')
elif re.match(r'^decimal', vtype):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:decimal">{esc_xml(str(val))}</dcscor:value>')
elif re.match(r'^string', vtype):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
elif re.match(r'^(\u041f\u043b\u0430\u043d\u0421\u0447\u0435\u0442\u043e\u0432|\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a|\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435|\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a|\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430|\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441|\u0417\u0430\u0434\u0430\u0447\u0430|\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439|\u041f\u043b\u0430\u043d\u041e\u0431\u043c\u0435\u043d\u0430)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="dcscor:DesignTimeValue">{esc_xml(str(val))}</dcscor:value>')
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
@@ -1326,12 +1615,7 @@ def emit_data_parameters(lines, items, indent):
lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if dp.get('userSettingPresentation'):
lines.append(f'{indent}\t\t<dcsset:userSettingPresentation xsi:type="v8:LocalStringType">')
lines.append(f'{indent}\t\t\t<v8:item>')
lines.append(f'{indent}\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{indent}\t\t\t\t<v8:content>{esc_xml(str(dp["userSettingPresentation"]))}</v8:content>')
lines.append(f'{indent}\t\t\t</v8:item>')
lines.append(f'{indent}\t\t</dcsset:userSettingPresentation>')
emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp["userSettingPresentation"])
lines.append(f'{indent}\t</dcscor:item>')
lines.append(f'{indent}</dcsset:dataParameters>')
@@ -1376,7 +1660,13 @@ def parse_structure_shorthand(s):
if re.match(r'(?i)^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg):
group['groupBy'] = []
else:
group['groupBy'] = [seg]
# Named group: "ИмяГруппы[Поле]"
m_named = re.match(r'^(.+)\[(.+)\]$', seg)
if m_named:
group['name'] = m_named.group(1).strip()
group['groupBy'] = [m_named.group(2).strip()]
else:
group['groupBy'] = [seg]
if innermost is not None:
group['children'] = [innermost]
@@ -1388,7 +1678,7 @@ def parse_structure_shorthand(s):
def emit_structure_item(lines, item, indent):
item_type = str(item.get('type', ''))
item_type = str(item.get('type', 'group'))
if item_type == 'group':
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:StructureItemGroup">')
@@ -1396,7 +1686,7 @@ def emit_structure_item(lines, item, indent):
if item.get('name'):
lines.append(f'{indent}\t<dcsset:name>{esc_xml(str(item["name"]))}</dcsset:name>')
emit_group_items(lines, item.get('groupBy'), f'{indent}\t')
emit_group_items(lines, item.get('groupBy') or item.get('groupFields'), f'{indent}\t')
# Default order to ["Auto"] if not specified
order_items = item.get('order') or ['Auto']
@@ -1428,7 +1718,7 @@ def emit_structure_item(lines, item, indent):
if item.get('columns'):
for col in item['columns']:
lines.append(f'{indent}\t<dcsset:column>')
emit_group_items(lines, col.get('groupBy'), f'{indent}\t\t')
emit_group_items(lines, col.get('groupBy') or col.get('groupFields'), f'{indent}\t\t')
col_order = col.get('order') or ['Auto']
emit_order(lines, col_order, f'{indent}\t\t')
col_sel = col.get('selection') or ['Auto']
@@ -1441,7 +1731,7 @@ def emit_structure_item(lines, item, indent):
lines.append(f'{indent}\t<dcsset:row>')
if row.get('name'):
lines.append(f'{indent}\t\t<dcsset:name>{esc_xml(str(row["name"]))}</dcsset:name>')
emit_group_items(lines, row.get('groupBy'), f'{indent}\t\t')
emit_group_items(lines, row.get('groupBy') or row.get('groupFields'), f'{indent}\t\t')
row_order = row.get('order') or ['Auto']
emit_order(lines, row_order, f'{indent}\t\t')
row_sel = row.get('selection') or ['Auto']
@@ -1459,7 +1749,7 @@ def emit_structure_item(lines, item, indent):
# Points
if item.get('points'):
lines.append(f'{indent}\t<dcsset:point>')
emit_group_items(lines, item['points'].get('groupBy'), f'{indent}\t\t')
emit_group_items(lines, item['points'].get('groupBy') or item['points'].get('groupFields'), f'{indent}\t\t')
pt_order = item['points'].get('order') or ['Auto']
emit_order(lines, pt_order, f'{indent}\t\t')
pt_sel = item['points'].get('selection') or ['Auto']
@@ -1469,7 +1759,7 @@ def emit_structure_item(lines, item, indent):
# Series
if item.get('series'):
lines.append(f'{indent}\t<dcsset:series>')
emit_group_items(lines, item['series'].get('groupBy'), f'{indent}\t\t')
emit_group_items(lines, item['series'].get('groupBy') or item['series'].get('groupFields'), f'{indent}\t\t')
sr_order = item['series'].get('order') or ['Auto']
emit_order(lines, sr_order, f'{indent}\t\t')
sr_sel = item['series'].get('selection') or ['Auto']
@@ -1507,13 +1797,8 @@ def emit_settings_variants(lines, defn):
lines.append('\t<settingsVariant>')
lines.append(f'\t\t<dcsset:name>{esc_xml(str(v["name"]))}</dcsset:name>')
pres = str(v.get('presentation', '')) or str(v.get('title', '')) or str(v['name'])
lines.append('\t\t<dcsset:presentation xsi:type="v8:LocalStringType">')
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(pres)}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append('\t\t</dcsset:presentation>')
pres = v.get('presentation') or v.get('title') or v['name']
emit_mltext(lines, '\t\t', 'dcsset:presentation', pres)
lines.append('\t\t<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">')
@@ -1540,7 +1825,46 @@ def emit_settings_variants(lines, defn):
emit_output_parameters(lines, s['outputParameters'], '\t\t\t')
# DataParameters
if s.get('dataParameters'):
if s.get('dataParameters') == 'auto':
# Auto-generate dataParameters for all non-hidden params.
# Pattern follows 1C Designer / ERP persistence:
# value set (non-default) → emit value, use=true (implicit)
# value missing / Custom period → <use>false</use> + <value xsi:nil="true"/>
auto_dp = []
for ap in _all_params:
if ap['hidden']:
continue
item = {
'parameter': ap['name'],
'userSettingID': 'auto',
}
has_meaningful_value = False
if ap.get('type') == 'StandardPeriod':
variant = 'Custom'
av = ap.get('value')
if av is not None:
if isinstance(av, dict) and av.get('variant'):
variant = str(av['variant'])
elif str(av):
variant = str(av)
item['value'] = {'variant': variant}
if variant != 'Custom':
has_meaningful_value = True
elif ap.get('value') is not None and str(ap.get('value')) != '':
item['value'] = ap['value']
item['valueType'] = str(ap.get('type') or '')
has_meaningful_value = True
else:
item['nilValue'] = True
if not has_meaningful_value:
item['use'] = False
auto_dp.append(item)
if auto_dp:
emit_data_parameters(lines, auto_dp, '\t\t\t')
elif s.get('dataParameters'):
emit_data_parameters(lines, s['dataParameters'], '\t\t\t')
# Structure (supports string shorthand)
@@ -1548,6 +1872,8 @@ def emit_settings_variants(lines, defn):
struct_items = s['structure']
if isinstance(struct_items, str):
struct_items = parse_structure_shorthand(struct_items)
elif isinstance(struct_items, dict):
struct_items = [struct_items]
for item in struct_items:
emit_structure_item(lines, item, '\t\t\t')
@@ -1595,7 +1921,8 @@ def main():
query_base_dir = os.path.dirname(def_file) if args.DefinitionFile else os.getcwd()
# Load user style presets
load_user_styles(query_base_dir)
out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath)
load_user_styles(query_base_dir, out_path_resolved)
# --- 2. Resolve defaults ---
+83 -7
View File
@@ -61,27 +61,63 @@ Shorthand: `"Имя [Заголовок]: тип @роль #ограничени
### add-calculated-field — добавить вычисляемое поле
Shorthand: `"Имя [Заголовок]: тип = Выражение"`.
Shorthand: `"Имя [Заголовок]: тип = Выражение #noFilter #noOrder #noGroup"`.
```
"Маржа = Продажа - Закупка"
"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100"
"Служебное: string = \"\" #noFilter #noOrder #noGroup"
```
`#noFilter`, `#noOrder`, `#noGroup`, `#noField``<useRestriction>` (аналогично add-field).
Также добавляется в selection варианта.
### add-parameter — добавить параметр
```
"Период: StandardPeriod = LastMonth @autoDates"
"Период [Отчетный период]: StandardPeriod = LastMonth @autoDates"
"Организация: CatalogRef.Организации"
```
`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически.
Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `<title>`.
`@autoDates` генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра — для БСП-отчётов, чтобы получить пару полей «Начало/Конец» в панели быстрых настроек.
### modify-parameter — изменить существующий параметр
Находит параметр по имени, добавляет/обновляет свойства.
```
"ПорядокОкругления use=Always"
"ПорядокОкругления [Округление сумм] denyIncompleteValues=true"
"ПериодОтчета [Отчетный период]" # только title
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1 presentation=руб."
```
`[Заголовок]` опциональный — устанавливает или заменяет `<title>`. Можно вызывать без других kv-пар, чтобы только обновить title.
`availableValue=` добавляет один элемент списка допустимых значений (можно несколько через `;;`). Тип значения определяется автоматически (DesignTimeValue для ссылок).
### rename-parameter — переименовать параметр
Shorthand: `"OldName => NewName"`. Атомарно обновляет имя параметра, ссылки `&Имя` в выражениях других параметров (только полные совпадения, `&ПериодX` не задевается), и записи в `dataParameters` всех вариантов. Текст запроса не трогает — переименование строго в области параметров.
```
"Период => ПериодОтчета"
```
### reorder-parameters — переставить параметры в указанном порядке
Shorthand: `"Имя1, Имя2, Имя3"`. Частичный список — указанные параметры идут первыми в заданном порядке, остальные сохраняют исходный порядок и идут в конце. Параметры из списка, которых нет в схеме — warning, пропуск.
```
"ПериодОтчета, НачалоПериода, КонецПериода"
```
### add-filter — добавить фильтр в вариант
Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`.
Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`.
```
"Номенклатура = _ @off @user"
@@ -112,6 +148,15 @@ Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто
```
"Номенклатура"
"Auto"
"Folder(Поступление: ПолеА, ПолеБ, ПолеВ)"
```
`Folder(Название: поле1, поле2)` — группа полей (SelectedItemFolder) с заголовком и `placement=Auto`.
`@group=ИмяГруппировки` — добавить в selection именованной группировки (вместо уровня варианта):
```
"Folder(Поступление: ПолеА, ПолеБ) @group=ДанныеОтчета"
```
### add-dataSetLink — добавить связь наборов данных
@@ -157,7 +202,28 @@ Shorthand: `"Параметр = значение [when условие] [for По
"Формат = ЧДЦ=2 for Цена, Сумма"
```
Типы значений (автодетект): `web:*`/`style:*`/`win:*`цвет, `true`/`false`boolean, иначе строка.
Типы значений appearance (автодетект): `web:*`/`style:*`/`win:*`Color, `true`/`false`Boolean, параметр `Формат`/`Текст`/`Заголовок` → LocalStringType, иначе String.
Типы значений фильтра (автодетект): `Перечисление.*`/`Справочник.*`/`ПланСчетов.*`/`Документ.*` → DesignTimeValue, `true`/`false` → Boolean, дата → DateTime, числа → Decimal, иначе String.
OrGroup: несколько условий через ` or ` в `when` объединяются в FilterItemGroup/OrGroup:
```
"Формат = ЧЦ=15; ЧДЦ=0 when ПараметрыДанных.Округление = Перечисление.Округления.Окр1 or ПараметрыДанных.Округление = Перечисление.Округления.Окр1000"
```
**Важно**: для параметров данных используйте префикс `ПараметрыДанных.` в поле фильтра.
### add-drilldown — подключить расшифровку к ресурсам в шаблонах
Value — имена ресурсов (как в полях/вычисляемых полях СКД) через запятую.
```
"ПоступлениеИзПроизводства, ВыбытиеПрочее"
"Сумма_Дт83, Сумма_Дт99, Сумма_68, Сумма_84"
```
Подключает DrillDown по `ИмяРесурса` ко всем шаблонам, содержащим указанные ресурсы. Идемпотентно.
### set-query — заменить текст запроса
@@ -188,8 +254,11 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
```
"Организация > Номенклатура > details"
"details"
"СчетМеждународногоУчета @name=ДанныеОтчета"
```
`@name=Имя` — присваивает имя группировке (`<dcsset:name>`). Используется для привязки шаблонов через `groupName`.
### modify-field — изменить существующее поле
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
@@ -200,11 +269,18 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
### modify-filter — изменить существующий фильтр
Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги.
Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. См. правило для `<use>` ниже.
### modify-dataParameter — изменить параметр данных
Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги.
Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги. См. правило для `<use>` ниже.
#### Правило `<use>` для modify-filter / modify-dataParameter
В отличие от `add-*`, в `modify-*` поле `<use>` обновляется **только если флаг задан явно**:
- `@off` — установить `<use>false</use>`
- `@on` — убрать существующий `<use>false</use>` (включить параметр)
- ни `@off`, ни `@on` не задано — `<use>` не трогается, существующее значение сохраняется (важно: это значит, что отключённый параметр останется отключённым после модификации других свойств)
### remove-* и clear-*
+664 -52
View File
@@ -1,16 +1,18 @@
# skd-edit v1.3 — Atomic 1C DCS editor
# skd-edit v1.11 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[Parameter(Mandatory)]
[ValidateSet(
"add-field","add-total","add-calculated-field","add-parameter","add-filter",
"add-dataParameter","add-order","add-selection","add-dataSetLink",
"add-dataSet","add-variant","add-conditionalAppearance",
"add-dataSet","add-variant","add-conditionalAppearance","add-drilldown",
"set-query","patch-query","set-outputParameter","set-structure",
"modify-field","modify-filter","modify-dataParameter",
"modify-field","modify-filter","modify-dataParameter","modify-parameter",
"rename-parameter","reorder-parameters",
"clear-selection","clear-order","clear-filter",
"remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")]
[string]$Operation,
@@ -250,40 +252,64 @@ function Parse-TotalShorthand {
function Parse-CalcShorthand {
param([string]$s)
$title = ""
# Extract [Title] first
if ($s -match '\[([^\]]+)\]') {
$title = $Matches[1]
$s = $s -replace '\s*\[[^\]]+\]', ''
}
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
# an expression (e.g. index access) isn't interpreted as a title.
# - `#restrict` flags use a known-names pattern and are extracted globally —
# the docs put them after `=`, and the closed flag set avoids matching
# `#word` that happens to appear inside a string literal.
$restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b'
$restrict = @()
foreach ($m in [regex]::Matches($s, $restrictPattern)) {
$restrict += $m.Groups[1].Value
}
$s = [regex]::Replace($s, "\s*$restrictPattern", '')
# Support "Name: Type = Expression" and "Name = Expression"
$eqIdx = $s.IndexOf('=')
if ($eqIdx -gt 0) {
$left = $s.Substring(0, $eqIdx).Trim()
$expression = $s.Substring($eqIdx + 1).Trim()
if ($left.Contains(':')) {
$colonIdx = $left.IndexOf(':')
$dataPath = $left.Substring(0, $colonIdx).Trim()
$type = Resolve-TypeStr ($left.Substring($colonIdx + 1).Trim())
return @{ dataPath = $dataPath; expression = $expression; type = $type; title = $title }
}
return @{ dataPath = $left; expression = $expression; type = ""; title = $title }
$lhs = $s.Substring(0, $eqIdx)
$rhs = $s.Substring($eqIdx + 1).Trim()
} else {
$lhs = $s
$rhs = $null
}
return @{ dataPath = $s.Trim(); expression = ""; type = ""; title = $title }
$title = ""
if ($lhs -match '\[([^\]]+)\]') {
$title = $Matches[1]
$lhs = $lhs -replace '\s*\[[^\]]+\]', ''
}
$lhs = $lhs.Trim()
if ($null -ne $rhs) {
if ($lhs.Contains(':')) {
$colonIdx = $lhs.IndexOf(':')
$dataPath = $lhs.Substring(0, $colonIdx).Trim()
$type = Resolve-TypeStr ($lhs.Substring($colonIdx + 1).Trim())
return @{ dataPath = $dataPath; expression = $rhs; type = $type; title = $title; restrict = $restrict }
}
return @{ dataPath = $lhs; expression = $rhs; type = ""; title = $title; restrict = $restrict }
}
return @{ dataPath = $lhs; expression = ""; type = ""; title = $title; restrict = $restrict }
}
function Parse-ParamShorthand {
param([string]$s)
$result = @{ name = ""; type = ""; value = $null; autoDates = $false }
$result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null }
if ($s -match '@autoDates') {
$result.autoDates = $true
$s = $s -replace '\s*@autoDates', ''
}
# Extract optional [Title] (mirrors Parse-FieldShorthand)
if ($s -match '\[([^\]]*)\]') {
$result.title = $Matches[1].Trim()
$s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim()
}
if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
$result.name = $Matches[1].Trim()
$result.type = Resolve-TypeStr ($Matches[2].Trim())
@@ -300,7 +326,9 @@ function Parse-ParamShorthand {
function Parse-FilterShorthand {
param([string]$s)
$result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null }
# use is tristate: $null = not specified (modify-* won't touch),
# $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false.
$result = @{ field = ""; op = "Equal"; value = $null; use = $null; userSettingID = $null; viewMode = $null }
if ($s -match '@user') {
$result.userSettingID = "auto"
@@ -310,6 +338,10 @@ function Parse-FilterShorthand {
$result.use = $false
$s = $s -replace '\s*@off', ''
}
if ($s -match '@on\b') {
$result.use = $true
$s = $s -replace '\s*@on\b', ''
}
if ($s -match '@quickAccess') {
$result.viewMode = "QuickAccess"
$s = $s -replace '\s*@quickAccess', ''
@@ -357,6 +389,9 @@ function Parse-FilterShorthand {
} elseif ($valPart -match '^\d+(\.\d+)?$') {
$result.value = $valPart
$result["valueType"] = "xs:decimal"
} elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$result.value = $valPart
$result["valueType"] = "dcscor:DesignTimeValue"
} else {
$result.value = $valPart
$result["valueType"] = "xs:string"
@@ -372,7 +407,9 @@ function Parse-FilterShorthand {
function Parse-DataParamShorthand {
param([string]$s)
$result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null }
# use is tristate: $null = not specified (modify-* won't touch),
# $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false.
$result = @{ parameter = ""; value = $null; use = $null; userSettingID = $null; viewMode = $null }
if ($s -match '@user') {
$result.userSettingID = "auto"
@@ -382,6 +419,10 @@ function Parse-DataParamShorthand {
$result.use = $false
$s = $s -replace '\s*@off', ''
}
if ($s -match '@on\b') {
$result.use = $true
$s = $s -replace '\s*@on\b', ''
}
if ($s -match '@quickAccess') {
$result.viewMode = "QuickAccess"
$s = $s -replace '\s*@quickAccess', ''
@@ -503,12 +544,17 @@ function Parse-ConditionalAppearanceShorthand {
$result.fields = @($forPart -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
# Parse "when" filter
# Parse "when" filter (supports " or " for OrGroup)
if ($whenIdx -ge 0) {
$whenEnd = $s.Length
if ($forIdx -gt $whenIdx) { $whenEnd = $forIdx }
$whenPart = $s.Substring($whenIdx + 6, $whenEnd - $whenIdx - 6).Trim()
$result.filter = Parse-FilterShorthand $whenPart
$orParts = $whenPart -split '\s+or\s+'
if ($orParts.Count -gt 1) {
$result.filter = @($orParts | ForEach-Object { Parse-FilterShorthand $_.Trim() })
} else {
$result.filter = Parse-FilterShorthand $whenPart
}
}
# Parse main part: "Param = Value"
@@ -535,6 +581,11 @@ function Parse-StructureShorthand {
$seg = $segments[$i].Trim()
$group = @{ type = "group" }
if ($seg -match '@name=(.+)') {
$group["name"] = $Matches[1].Trim()
$seg = ($seg -replace '\s*@name=.+', '').Trim()
}
if ($seg -match '^(?i)(details|детали)$') {
$group["groupBy"] = @()
} else {
@@ -739,6 +790,10 @@ function Build-CalcFieldFragment {
$lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t")
}
if ($parsed.restrict -and $parsed.restrict.Count -gt 0) {
$lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t")
}
if ($parsed.type) {
$lines += "$i`t<valueType>"
$lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
@@ -759,6 +814,10 @@ function Build-ParamFragment {
$lines += "$i<parameter>"
$lines += "$i`t<name>$(Esc-Xml $parsed.name)</name>"
if ($parsed.title) {
$lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t")
}
if ($parsed.type) {
$lines += "$i`t<valueType>"
$lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
@@ -770,6 +829,8 @@ function Build-ParamFragment {
if ($parsed.type -eq "StandardPeriod") {
$lines += "$i`t<value xsi:type=`"v8:StandardPeriod`">"
$lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>"
$lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
$lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
$lines += "$i`t</value>"
} elseif ($parsed.type -match '^date') {
$lines += "$i`t<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
@@ -788,25 +849,30 @@ function Build-ParamFragment {
if ($parsed.autoDates) {
$paramName = $parsed.name
# Canonical БСП pattern: title + valueType + value + useRestriction + expression
$bLines = @()
$bLines += "$i<parameter>"
$bLines += "$i`t<name>ДатаНачала</name>"
$bLines += (Build-MLTextXml -tag "title" -text "Начало периода" -indent "$i`t")
$bLines += "$i`t<valueType>"
$bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t")
$bLines += "$i`t</valueType>"
$bLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>"
$bLines += "$i`t<useRestriction>true</useRestriction>"
$bLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаНачала")</expression>"
$bLines += "$i`t<availableAsField>false</availableAsField>"
$bLines += "$i</parameter>"
$fragments += ($bLines -join "`r`n")
$eLines = @()
$eLines += "$i<parameter>"
$eLines += "$i`t<name>ДатаОкончания</name>"
$eLines += (Build-MLTextXml -tag "title" -text "Конец периода" -indent "$i`t")
$eLines += "$i`t<valueType>"
$eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t")
$eLines += "$i`t</valueType>"
$eLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>"
$eLines += "$i`t<useRestriction>true</useRestriction>"
$eLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаОкончания")</expression>"
$eLines += "$i`t<availableAsField>false</availableAsField>"
$eLines += "$i</parameter>"
$fragments += ($eLines -join "`r`n")
}
@@ -853,6 +919,32 @@ function Build-SelectionItemFragment {
$lines = @()
if ($fieldName -eq "Auto") {
$lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>"
} elseif ($fieldName -match '^Folder\((.+)\)$') {
$inner = $Matches[1]
$colonIdx = $inner.IndexOf(':')
if ($colonIdx -gt 0) {
$title = $inner.Substring(0, $colonIdx).Trim()
$items = $inner.Substring($colonIdx + 1) -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
} else {
$title = ""
$items = $inner -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
}
$lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">"
if ($title) {
$lines += "$i`t<dcsset:lwsTitle>"
$lines += "$i`t`t<v8:item>"
$lines += "$i`t`t`t<v8:lang>ru</v8:lang>"
$lines += "$i`t`t`t<v8:content>$(Esc-Xml $title)</v8:content>"
$lines += "$i`t`t</v8:item>"
$lines += "$i`t</dcsset:lwsTitle>"
}
foreach ($item in $items) {
$lines += "$i`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
$lines += "$i`t`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>"
$lines += "$i`t</dcsset:item>"
}
$lines += "$i`t<dcsset:placement>Auto</dcsset:placement>"
$lines += "$i</dcsset:item>"
} else {
$lines += "$i<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
$lines += "$i`t<dcsset:field>$(Esc-Xml $fieldName)</dcsset:field>"
@@ -878,6 +970,8 @@ function Build-DataParamFragment {
if ($parsed.value -is [hashtable] -and $parsed.value.variant) {
$lines += "$i`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
$lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>"
$lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
$lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
$lines += "$i`t</dcscor:value>"
} elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') {
$lines += "$i`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>"
@@ -973,6 +1067,20 @@ function Build-VariantFragment {
return $lines -join "`r`n"
}
function Emit-FilterComparison {
param($f, [string]$indent)
$lines = @()
$lines += "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
$lines += "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml $f.field)</dcsset:left>"
$lines += "$indent`t<dcsset:comparisonType>$(Esc-Xml $f.op)</dcsset:comparisonType>"
if ($null -ne $f.value) {
$vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" }
$lines += "$indent`t<dcsset:right xsi:type=`"$vt`">$(Esc-Xml "$($f.value)")</dcsset:right>"
}
$lines += "$indent</dcsset:item>"
return $lines
}
function Build-ConditionalAppearanceItemFragment {
param($parsed, [string]$indent)
@@ -996,15 +1104,17 @@ function Build-ConditionalAppearanceItemFragment {
# filter
if ($parsed.filter) {
$lines += "$i`t<dcsset:filter>"
$f = $parsed.filter
$lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
$lines += "$i`t`t`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml $f.field)</dcsset:left>"
$lines += "$i`t`t`t<dcsset:comparisonType>$(Esc-Xml $f.op)</dcsset:comparisonType>"
if ($null -ne $f.value) {
$vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" }
$lines += "$i`t`t`t<dcsset:right xsi:type=`"$vt`">$(Esc-Xml "$($f.value)")</dcsset:right>"
if ($parsed.filter -is [array]) {
# OrGroup
$lines += "$i`t`t<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">"
$lines += "$i`t`t`t<dcsset:groupType>OrGroup</dcsset:groupType>"
foreach ($f in $parsed.filter) {
$lines += Emit-FilterComparison $f "$i`t`t`t"
}
$lines += "$i`t`t</dcsset:item>"
} else {
$lines += Emit-FilterComparison $parsed.filter "$i`t`t"
}
$lines += "$i`t`t</dcsset:item>"
$lines += "$i`t</dcsset:filter>"
} else {
$lines += "$i`t<dcsset:filter/>"
@@ -1013,18 +1123,25 @@ function Build-ConditionalAppearanceItemFragment {
# appearance
$lines += "$i`t<dcsset:appearance>"
# Auto-detect value type
$val = $parsed.value
$valType = "xs:string"
if ($val -match '^(web|style|win):') {
$valType = "v8ui:Color"
} elseif ($val -eq "true" -or $val -eq "false") {
$valType = "xs:boolean"
}
$lines += "$i`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
$lines += "$i`t`t`t<dcscor:parameter>$(Esc-Xml $parsed.param)</dcscor:parameter>"
$lines += "$i`t`t`t<dcscor:value xsi:type=`"$valType`">$(Esc-Xml $val)</dcscor:value>"
if ($val -match '^(web|style|win):') {
$lines += "$i`t`t`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $val)</dcscor:value>"
} elseif ($val -eq "true" -or $val -eq "false") {
$lines += "$i`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $val)</dcscor:value>"
} elseif ($parsed.param -eq "Формат" -or $parsed.param -eq "Текст" -or $parsed.param -eq "Заголовок") {
$lines += "$i`t`t`t<dcscor:value xsi:type=`"v8:LocalStringType`">"
$lines += "$i`t`t`t`t<v8:item>"
$lines += "$i`t`t`t`t`t<v8:lang>ru</v8:lang>"
$lines += "$i`t`t`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>"
$lines += "$i`t`t`t`t</v8:item>"
$lines += "$i`t`t`t</dcscor:value>"
} else {
$lines += "$i`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $val)</dcscor:value>"
}
$lines += "$i`t`t</dcscor:item>"
$lines += "$i`t</dcsset:appearance>"
@@ -1039,6 +1156,11 @@ function Build-StructureItemFragment {
$lines = @()
$lines += "$i<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">"
# name
if ($item["name"]) {
$lines += "$i`t<dcsset:name>$(Esc-Xml $item["name"])</dcsset:name>"
}
# groupItems
$groupBy = $item["groupBy"]
if (-not $groupBy -or $groupBy.Count -eq 0) {
@@ -1445,6 +1567,14 @@ $corNs = "http://v8.1c.ru/8.1/data-composition-system/core"
if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "add-dataSet") {
$values = @($Value)
} elseif ($Operation -eq "patch-query") {
$values = @($Value -split ';;' | Where-Object { $_.Trim() })
} elseif ($Operation -eq "add-drilldown") {
if ($Value.Contains(';;')) {
$values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} else {
$values = @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
} else {
$values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
@@ -1622,6 +1752,293 @@ switch ($Operation) {
}
}
"modify-parameter" {
foreach ($val in $values) {
# Parse: "ParamName [Title] key=value key=value"
# Extract optional [Title] first (mirrors Parse-FieldShorthand)
$titleVal = $null
if ($val -match '\[([^\]]*)\]') {
$titleVal = $Matches[1].Trim()
$val = ($val -replace '\s*\[[^\]]*\]\s*', ' ').Trim()
}
$parts = $val -split '\s+', 2
$paramName = $parts[0].Trim()
$rest = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" }
# Find parameter element
$paramEl = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $paramName $schNs
if (-not $paramEl) {
Write-Host "[WARN] Parameter `"$paramName`" not found — skipped"
continue
}
$childIndent = Get-ChildIndent $paramEl
# Set/replace title (must come right after <name>, before <valueType>)
if ($null -ne $titleVal) {
$existingTitle = $null
foreach ($ch in $paramEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'title') {
$existingTitle = $ch; break
}
}
if ($existingTitle) {
Remove-NodeWithWhitespace $existingTitle
}
# Insert before first of (valueType, value, useRestriction, expression, availableAsField, ...)
$titleRef = $null
foreach ($ch in $paramEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -ne 'name') {
$titleRef = $ch; break
}
}
$titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent
$titleNodes = Import-Fragment $xmlDoc $titleFrag
foreach ($node in $titleNodes) {
Insert-BeforeElement $paramEl $node $titleRef $childIndent
}
Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`""
}
# Separate availableValue=... from simple kv pairs
$simpleRest = $rest
$avPart = $null
$avIdx = $rest.IndexOf('availableValue=')
if ($avIdx -ge 0) {
$simpleRest = $rest.Substring(0, $avIdx).Trim()
$avPart = $rest.Substring($avIdx)
}
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
if ($simpleRest) {
$kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)')
foreach ($kv in $kvPairs) {
$key = $kv.Groups[1].Value
$value = $kv.Groups[2].Value
$existing = $paramEl.SelectSingleNode($key)
if ($existing) {
$existing.InnerText = $value
Write-Host "[OK] Parameter `"$paramName`": $key updated to $value"
} else {
# Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use
$refNode = $null
if ($key -eq "denyIncompleteValues") {
foreach ($child in $paramEl.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'use') {
$refNode = $child; break
}
}
}
$fragXml = "$childIndent<$key>$(Esc-Xml $value)</$key>"
$nodes = Import-Fragment $xmlDoc $fragXml
foreach ($node in $nodes) {
Insert-BeforeElement $paramEl $node $refNode $childIndent
}
Write-Host "[OK] Parameter `"$paramName`": $key=$value added"
}
}
}
# Process availableValue
if ($avPart) {
$avRest = $avPart -replace '^availableValue=', ''
# Parse: "Перечисление...X presentation=текст с пробелами"
$avParts = $avRest -split '\s+presentation=', 2
$avValue = $avParts[0].Trim()
$avPresentation = if ($avParts.Count -gt 1) { $avParts[1].Trim() } else { "" }
# Detect value type
$avType = "xs:string"
if ($avValue -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$avType = "dcscor:DesignTimeValue"
}
$avLines = @()
$avLines += "$childIndent<availableValue>"
$avLines += "$childIndent`t<value xsi:type=`"$avType`">$(Esc-Xml $avValue)</value>"
if ($avPresentation) {
$avLines += "$childIndent`t<presentation xsi:type=`"v8:LocalStringType`">"
$avLines += "$childIndent`t`t<v8:item>"
$avLines += "$childIndent`t`t`t<v8:lang>ru</v8:lang>"
$avLines += "$childIndent`t`t`t<v8:content>$(Esc-Xml $avPresentation)</v8:content>"
$avLines += "$childIndent`t`t</v8:item>"
$avLines += "$childIndent`t</presentation>"
}
$avLines += "$childIndent</availableValue>"
$fragXml = $avLines -join "`r`n"
# Insert before first of (denyIncompleteValues, use) in document order
$refNode = $null
foreach ($child in $paramEl.ChildNodes) {
if ($child.NodeType -eq 'Element' -and ($child.LocalName -eq 'denyIncompleteValues' -or $child.LocalName -eq 'use')) {
$refNode = $child; break
}
}
$nodes = Import-Fragment $xmlDoc $fragXml
foreach ($node in $nodes) {
Insert-BeforeElement $paramEl $node $refNode $childIndent
}
Write-Host "[OK] Parameter `"$paramName`": availableValue added"
}
}
}
"rename-parameter" {
foreach ($val in $values) {
# Shorthand: "OldName => NewName"
if ($val -notmatch '^\s*(.+?)\s*=>\s*(.+?)\s*$') {
Write-Host "[WARN] rename-parameter expects 'OldName => NewName', got: $val"
continue
}
$oldName = $Matches[1].Trim()
$newName = $Matches[2].Trim()
if ($oldName -eq $newName) {
Write-Host "[WARN] rename-parameter: old and new names are equal — skipped"
continue
}
# 1. Rename <parameter><name>OldName</name>
$root = $xmlDoc.DocumentElement
$paramEl = Find-ElementByChildValue $root "parameter" "name" $oldName $schNs
if (-not $paramEl) {
Write-Host "[WARN] Parameter `"$oldName`" not found — skipped"
continue
}
foreach ($ch in $paramEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $schNs) {
$ch.InnerText = $newName
break
}
}
# 2. Update <expression> in other <parameter> elements.
# Regex matches "&OldName" only when followed by a non-identifier char (or end),
# so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета".
$escOld = [regex]::Escape($oldName)
$exprRegex = "&$escOld(?=[^\w\u0400-\u04FF]|$)"
$exprUpdated = 0
foreach ($ch in $root.ChildNodes) {
if ($ch.NodeType -ne 'Element' -or $ch.LocalName -ne 'parameter' -or $ch.NamespaceURI -ne $schNs) { continue }
foreach ($gc in $ch.ChildNodes) {
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'expression' -and $gc.NamespaceURI -eq $schNs) {
$oldExpr = $gc.InnerText
$newExpr = [regex]::Replace($oldExpr, $exprRegex, "&$newName")
if ($newExpr -ne $oldExpr) {
$gc.InnerText = $newExpr
$exprUpdated++
}
}
}
}
# 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants.
# Note: <settingsVariant> is in schNs, but <settings> and <dataParameters> are in setNs.
# IMPORTANT: don't use $variant — it collides with script parameter [string]$Variant
# (PowerShell vars are case-insensitive, and the [string] type would coerce XmlNode to "").
$dpUpdated = 0
foreach ($variantNode in $root.ChildNodes) {
if ($variantNode.NodeType -ne 'Element' -or $variantNode.LocalName -ne 'settingsVariant' -or $variantNode.NamespaceURI -ne $schNs) { continue }
$settings = Find-FirstElement $variantNode @("settings") $setNs
if (-not $settings) { continue }
$dpEl = Find-FirstElement $settings @("dataParameters") $setNs
if (-not $dpEl) { continue }
foreach ($item in $dpEl.ChildNodes) {
if ($item.NodeType -ne 'Element' -or $item.LocalName -ne 'item') { continue }
foreach ($gc in $item.ChildNodes) {
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'parameter' -and $gc.NamespaceURI -eq $corNs) {
if ($gc.InnerText.Trim() -eq $oldName) {
$gc.InnerText = $newName
$dpUpdated++
}
}
}
}
}
Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)"
}
}
"reorder-parameters" {
foreach ($val in $values) {
# Shorthand: "Name1, Name2, Name3" — partial list, listed names go first in order, rest preserve original order
$order = @($val -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($order.Count -eq 0) {
Write-Host "[WARN] reorder-parameters: empty list — skipped"
continue
}
$root = $xmlDoc.DocumentElement
# Collect all <parameter> in document order with their child indent
$allParams = @()
foreach ($ch in $root.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'parameter' -and $ch.NamespaceURI -eq $schNs) {
$allParams += $ch
}
}
if ($allParams.Count -eq 0) {
Write-Host "[WARN] reorder-parameters: no parameters in schema"
continue
}
$childIndent = Get-ChildIndent $root
# Build name -> element map
$byName = @{}
foreach ($pe in $allParams) {
foreach ($gc in $pe.ChildNodes) {
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) {
$byName[$gc.InnerText.Trim()] = $pe
break
}
}
}
# Build new order
$newOrder = @()
$used = @{}
foreach ($name in $order) {
if ($byName.ContainsKey($name)) {
$newOrder += $byName[$name]
$used[$name] = $true
} else {
Write-Host "[WARN] reorder-parameters: parameter `"$name`" not found — skipped"
}
}
foreach ($pe in $allParams) {
$peName = $null
foreach ($gc in $pe.ChildNodes) {
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) {
$peName = $gc.InnerText.Trim(); break
}
}
if ($peName -and -not $used.ContainsKey($peName)) {
$newOrder += $pe
}
}
# Find anchor: element right after the last parameter in original order
$lastParam = $allParams[-1]
$anchor = $lastParam.NextSibling
# Remove all parameters with surrounding whitespace
foreach ($pe in $allParams) {
Remove-NodeWithWhitespace $pe
}
# Re-insert in new order before anchor
foreach ($pe in $newOrder) {
Insert-BeforeElement $root $pe $anchor $childIndent
}
Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)"
}
}
"add-filter" {
$settings = Resolve-VariantSettings
$varName = Get-VariantName
@@ -1710,8 +2127,50 @@ switch ($Operation) {
foreach ($val in $values) {
$fieldName = $val.Trim()
$groupName = $null
# Extract @group=Name
if ($fieldName -match '\s*@group=(\S+)') {
$groupName = $Matches[1]
$fieldName = ($fieldName -replace '\s*@group=\S+', '').Trim()
}
if ($groupName) {
# Find named StructureItemGroup
$dcssetNs = "http://v8.1c.ru/8.1/data-composition-system/settings"
$xsiNs = "http://www.w3.org/2001/XMLSchema-instance"
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("dcsset", $dcssetNs)
$nsMgr.AddNamespace("xsi", $xsiNs)
$groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$groupName']", $nsMgr)
if (-not $groupEl) {
Write-Host "[WARN] StructureItemGroup `"$groupName`" not found — adding to variant level"
$targetEl = $settings
} else {
$targetEl = $groupEl
}
} else {
$targetEl = $settings
}
$selection = Ensure-SettingsChild $targetEl "selection" @()
# Dedup: skip if SelectedItemAuto already exists
if ($fieldName -eq "Auto") {
$isDup = $false
foreach ($ch in $selection.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') {
$typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($typeAttr -and $typeAttr.Contains("SelectedItemAuto")) { $isDup = $true; break }
}
}
if ($isDup) {
$target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" }
Write-Host "[WARN] SelectedItemAuto already exists in $target — skipped"
continue
}
}
$selection = Ensure-SettingsChild $settings "selection" @()
$selIndent = Get-ContainerChildIndent $selection
$selXml = Build-SelectionItemFragment -fieldName $fieldName -indent $selIndent
@@ -1720,7 +2179,8 @@ switch ($Operation) {
Insert-BeforeElement $selection $node $null $selIndent
}
Write-Host "[OK] Selection `"$fieldName`" added to variant `"$varName`""
$target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" }
Write-Host "[OK] Selection `"$fieldName`" added to $target"
}
}
@@ -2046,11 +2506,11 @@ switch ($Operation) {
Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent
}
# Update use
# Update use (only when explicitly set via @off / @on)
if ($parsed.use -eq $false) {
Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent
} else {
# If explicitly not @off, remove use=false if exists
} elseif ($parsed.use -eq $true) {
# @on: remove existing use=false if any
$useEl = $null
foreach ($ch in $filterItem.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) {
@@ -2116,6 +2576,8 @@ switch ($Operation) {
if ($parsed.value -is [hashtable] -and $parsed.value.variant) {
$valLines += "$itemIndent<dcscor:value xsi:type=`"v8:StandardPeriod`">"
$valLines += "$itemIndent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>"
$valLines += "$itemIndent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
$valLines += "$itemIndent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
$valLines += "$itemIndent</dcscor:value>"
} elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') {
$valLines += "$itemIndent<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>"
@@ -2131,10 +2593,11 @@ switch ($Operation) {
}
}
# Update use
# Update use (only when explicitly set via @off / @on)
if ($parsed.use -eq $false) {
Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent
} else {
} elseif ($parsed.use -eq $true) {
# @on: remove existing use=false if any
$useEl = $null
foreach ($ch in $dpItem.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) {
@@ -2331,6 +2794,155 @@ switch ($Operation) {
Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`""
}
}
"add-drilldown" {
# String-based manipulation — templates use dcsat namespace with inline xmlns
$rawText = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8)
$nl = "`r`n"
$dcsatNsDecl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"'
# Find all outer <template> blocks by nesting-aware scan
$tplStarts = [System.Collections.ArrayList]::new()
$nameRegex = [regex]'<template>\s*<name>([^<]+)</name>'
foreach ($m in $nameRegex.Matches($rawText)) {
[void]$tplStarts.Add(@{ pos = $m.Index; name = $m.Groups[1].Value })
}
# For each start, find closing </template> at nesting depth 0
$tplBlocks = [System.Collections.ArrayList]::new()
foreach ($ts in $tplStarts) {
$depth = 1
$scanPos = $ts.pos + 10 # skip past opening <template>
while ($depth -gt 0 -and $scanPos -lt $rawText.Length) {
$nextOpen = $rawText.IndexOf("<template", $scanPos)
$nextClose = $rawText.IndexOf("</template>", $scanPos)
if ($nextClose -lt 0) { break }
if ($nextOpen -ge 0 -and $nextOpen -lt $nextClose) {
$depth++
$scanPos = $nextOpen + 10
} else {
$depth--
if ($depth -eq 0) {
$endPos = $nextClose + "</template>".Length
[void]$tplBlocks.Add(@{ name = $ts.name; start = $ts.pos; text = $rawText.Substring($ts.pos, $endPos - $ts.pos) })
}
$scanPos = $nextClose + 11
}
}
}
if ($tplBlocks.Count -eq 0) {
Write-Host "[WARN] No named templates found in schema"
}
# Collect all insertions as (position, text) — apply in reverse order
$insertions = [System.Collections.ArrayList]::new()
foreach ($tplBlock in $tplBlocks) {
$tplName = $tplBlock.name
$tplText = $tplBlock.text
$tplStart = $tplBlock.start
# Build map: expression → paramName from ExpressionAreaTemplateParameter
$exprMap = @{}
$exprRegex = [regex]'(?s)<parameter[^>]*ExpressionAreaTemplateParameter[^>]*>\s*<dcsat:name>([^<]+)</dcsat:name>\s*<dcsat:expression>([^<]+)</dcsat:expression>\s*</parameter>'
foreach ($em in $exprRegex.Matches($tplText)) {
$pName = $em.Groups[1].Value
$pExpr = $em.Groups[2].Value
$exprMap[$pExpr] = $pName
}
foreach ($resource in $values) {
$drillName = "Расшифровка_$resource"
# Idempotency: check if already exists
if ($tplText.Contains($drillName)) {
Write-Host "[INFO] $drillName already exists in $tplName — skipped"
continue
}
# Find ExpressionAreaTemplateParameter by expression
$paramName = $null
if ($exprMap.ContainsKey($resource)) {
$paramName = $exprMap[$resource]
} else {
Write-Host "[WARN] Expression `"$resource`" not found in template $tplName — skipped"
continue
}
$cellCount = 0
# Step 1: Insert DetailsAreaTemplateParameter after last </parameter> in template
$lastParamEndTag = "</parameter>"
$lastParamPos = $tplText.LastIndexOf($lastParamEndTag)
if ($lastParamPos -ge 0) {
$insertPos = $tplStart + $lastParamPos + $lastParamEndTag.Length
# Detect indent from context
$prevNewline = $tplText.LastIndexOf("`n", $lastParamPos)
$indent = "`t`t"
if ($prevNewline -ge 0) {
$lineStart = $prevNewline + 1
$indentMatch = [regex]::Match($tplText.Substring($lineStart), '^(\s*)')
if ($indentMatch.Success) { $indent = $indentMatch.Groups[1].Value }
}
$detailsXml = "$nl$indent<parameter $dcsatNsDecl xsi:type=`"dcsat:DetailsAreaTemplateParameter`">" +
"$nl$indent`t<dcsat:name>$drillName</dcsat:name>" +
"$nl$indent`t<dcsat:fieldExpression>" +
"$nl$indent`t`t<dcsat:field>ИмяРесурса</dcsat:field>" +
"$nl$indent`t`t<dcsat:expression>`"$resource`"</dcsat:expression>" +
"$nl$indent`t</dcsat:fieldExpression>" +
"$nl$indent`t<dcsat:mainAction>DrillDown</dcsat:mainAction>" +
"$nl$indent</parameter>"
[void]$insertions.Add(@{ pos = $insertPos; text = $detailsXml })
}
# Step 2: Insert appearance binding in cells referencing this parameter
$cellTag = '<dcsat:value xsi:type="dcscor:Parameter">' + $paramName + '</dcsat:value>'
$searchStart = 0
while (($cellIdx = $tplText.IndexOf($cellTag, $searchStart)) -ge 0) {
$cellEnd = $tplText.IndexOf("</dcsat:tableCell>", $cellIdx)
if ($cellEnd -lt 0) { break }
$appEnd = $tplText.LastIndexOf("</dcsat:appearance>", $cellEnd)
if ($appEnd -lt $cellIdx) { $searchStart = $cellEnd + 1; continue }
# Detect indent for appearance items — insert after \n, before indent of </dcsat:appearance>
$appPrevNl = $tplText.LastIndexOf("`n", $appEnd)
$appIndent = "`t`t`t`t`t`t"
if ($appPrevNl -ge 0) {
$appLineStart = $appPrevNl + 1
$appIndentMatch = [regex]::Match($tplText.Substring($appLineStart), '^(\s*)')
if ($appIndentMatch.Success) { $appIndent = $appIndentMatch.Groups[1].Value }
}
$itemIndent = $appIndent + "`t"
$appearanceXml = "$itemIndent<dcscor:item>$nl" +
"$itemIndent`t<dcscor:parameter>Расшифровка</dcscor:parameter>$nl" +
"$itemIndent`t<dcscor:value xsi:type=`"dcscor:Parameter`">$drillName</dcscor:value>$nl" +
"$itemIndent</dcscor:item>$nl"
# Insert after \n (before indent of closing tag), not before the tag itself
$insertAt = if ($appPrevNl -ge 0) { $tplStart + $appPrevNl + 1 } else { $tplStart + $appEnd }
[void]$insertions.Add(@{ pos = $insertAt; text = $appearanceXml })
$cellCount++
$searchStart = $cellEnd + 1
}
Write-Host "[OK] $drillName$tplName (param + $cellCount cell(s))"
}
}
# Apply insertions in reverse order to preserve offsets.
# For same position: reverse insertion order so first resource ends up first in file.
$idx = 0; foreach ($ins in $insertions) { $ins.seq = $idx; $idx++ }
$sorted = $insertions | Sort-Object { $_.pos }, { $_.seq } -Descending
foreach ($ins in $sorted) {
$rawText = $rawText.Insert($ins.pos, $ins.text)
}
# Write directly — skip DOM save
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $rawText, $enc)
Write-Host "[OK] Saved $resolvedPath"
exit 0
}
}
# --- 9. Save ---
+575 -48
View File
@@ -1,4 +1,4 @@
# skd-edit v1.3 — Atomic 1C DCS editor (Python port)
# skd-edit v1.11 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -16,15 +16,16 @@ sys.stderr.reconfigure(encoding="utf-8")
VALID_OPS = [
"add-field", "add-total", "add-calculated-field", "add-parameter", "add-filter",
"add-dataParameter", "add-order", "add-selection", "add-dataSetLink",
"add-dataSet", "add-variant", "add-conditionalAppearance",
"add-dataSet", "add-variant", "add-conditionalAppearance", "add-drilldown",
"set-query", "patch-query", "set-outputParameter", "set-structure",
"modify-field", "modify-filter", "modify-dataParameter",
"modify-field", "modify-filter", "modify-dataParameter", "modify-parameter",
"rename-parameter", "reorder-parameters",
"clear-selection", "clear-order", "clear-filter",
"remove-field", "remove-total", "remove-calculated-field", "remove-parameter", "remove-filter",
]
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True)
parser.add_argument("-TemplatePath", "-Path", required=True)
parser.add_argument("-Operation", required=True, choices=VALID_OPS)
parser.add_argument("-Value", required=True)
parser.add_argument("-DataSet", default="")
@@ -259,32 +260,57 @@ def parse_total_shorthand(s):
def parse_calc_shorthand(s):
title = ""
m = re.search(r'\[([^\]]+)\]', s)
if m:
title = m.group(1)
s = re.sub(r'\s*\[[^\]]+\]', '', s)
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
# an expression (e.g. index access) isn't interpreted as a title.
# - `#restrict` flags use a known-names pattern and are extracted globally —
# the docs put them after `=`, and the closed flag set avoids matching
# `#word` that happens to appear inside a string literal.
restrict_pattern = r'#(noField|noFilter|noCondition|noGroup|noOrder)\b'
restrict_matches = re.findall(restrict_pattern, s)
s = re.sub(r'\s*' + restrict_pattern, '', s)
eq_idx = s.find("=")
if eq_idx > 0:
left = s[:eq_idx].strip()
expression = s[eq_idx + 1:].strip()
if ":" in left:
colon_idx = left.index(":")
data_path = left[:colon_idx].strip()
type_str = resolve_type_str(left[colon_idx + 1:].strip())
return {"dataPath": data_path, "expression": expression, "type": type_str, "title": title}
return {"dataPath": left, "expression": expression, "type": "", "title": title}
return {"dataPath": s.strip(), "expression": "", "type": "", "title": title}
lhs = s[:eq_idx]
rhs = s[eq_idx + 1:].strip()
has_rhs = True
else:
lhs = s
rhs = ""
has_rhs = False
title = ""
m = re.search(r'\[([^\]]+)\]', lhs)
if m:
title = m.group(1)
lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs)
lhs = lhs.strip()
if has_rhs:
if ":" in lhs:
colon_idx = lhs.index(":")
data_path = lhs[:colon_idx].strip()
type_str = resolve_type_str(lhs[colon_idx + 1:].strip())
return {"dataPath": data_path, "expression": rhs, "type": type_str, "title": title, "restrict": restrict_matches}
return {"dataPath": lhs, "expression": rhs, "type": "", "title": title, "restrict": restrict_matches}
return {"dataPath": lhs, "expression": "", "type": "", "title": title, "restrict": restrict_matches}
def parse_param_shorthand(s):
result = {"name": "", "type": "", "value": None, "autoDates": False}
result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None}
if re.search(r'@autoDates', s):
result["autoDates"] = True
s = re.sub(r'\s*@autoDates', '', s)
# Extract optional [Title] (mirrors parse_field_shorthand)
m = re.search(r'\[([^\]]*)\]', s)
if m:
result["title"] = m.group(1).strip()
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s)
if m:
result["name"] = m.group(1).strip()
@@ -298,7 +324,9 @@ def parse_param_shorthand(s):
def parse_filter_shorthand(s):
result = {"field": "", "op": "Equal", "value": None, "use": True, "userSettingID": None, "viewMode": None}
# use is tristate: None = not specified (modify-* won't touch),
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
result = {"field": "", "op": "Equal", "value": None, "use": None, "userSettingID": None, "viewMode": None}
if re.search(r'@user', s):
result["userSettingID"] = "auto"
@@ -306,6 +334,9 @@ def parse_filter_shorthand(s):
if re.search(r'@off', s):
result["use"] = False
s = re.sub(r'\s*@off', '', s)
if re.search(r'@on\b', s):
result["use"] = True
s = re.sub(r'\s*@on\b', '', s)
if re.search(r'@quickAccess', s):
result["viewMode"] = "QuickAccess"
s = re.sub(r'\s*@quickAccess', '', s)
@@ -350,6 +381,9 @@ def parse_filter_shorthand(s):
elif re.match(r'^\d+(\.\d+)?$', val_part):
result["value"] = val_part
result["valueType"] = "xs:decimal"
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
result["value"] = val_part
result["valueType"] = "dcscor:DesignTimeValue"
else:
result["value"] = val_part
result["valueType"] = "xs:string"
@@ -360,7 +394,9 @@ def parse_filter_shorthand(s):
def parse_data_param_shorthand(s):
result = {"parameter": "", "value": None, "use": True, "userSettingID": None, "viewMode": None}
# use is tristate: None = not specified (modify-* won't touch),
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
result = {"parameter": "", "value": None, "use": None, "userSettingID": None, "viewMode": None}
if re.search(r'@user', s):
result["userSettingID"] = "auto"
@@ -368,6 +404,9 @@ def parse_data_param_shorthand(s):
if re.search(r'@off', s):
result["use"] = False
s = re.sub(r'\s*@off', '', s)
if re.search(r'@on\b', s):
result["use"] = True
s = re.sub(r'\s*@on\b', '', s)
if re.search(r'@quickAccess', s):
result["viewMode"] = "QuickAccess"
s = re.sub(r'\s*@quickAccess', '', s)
@@ -408,7 +447,7 @@ def parse_order_shorthand(s):
parts = s.split(None, 1)
field = parts[0]
direction = "Asc"
if len(parts) > 1 and re.match(r'(?i)^desc$', parts[1]):
if len(parts) > 1 and re.match(r'^desc$', parts[1], re.IGNORECASE):
direction = "Desc"
return {"field": field, "direction": direction}
@@ -480,7 +519,11 @@ def parse_conditional_appearance_shorthand(s):
if for_idx > when_idx:
when_end = for_idx
when_part = s[when_idx + 6:when_end].strip()
result["filter"] = parse_filter_shorthand(when_part)
or_parts = re.split(r'\s+or\s+', when_part)
if len(or_parts) > 1:
result["filter"] = [parse_filter_shorthand(p.strip()) for p in or_parts]
else:
result["filter"] = parse_filter_shorthand(when_part)
main_part = s[:main_end].strip()
eq_idx = main_part.find("=")
@@ -502,7 +545,12 @@ def parse_structure_shorthand(s):
seg = segments[i].strip()
group = {"type": "group"}
if re.match(r'^(?i)(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg):
name_m = re.search(r'\s*@name=(.+)', seg)
if name_m:
group["name"] = name_m.group(1).strip()
seg = re.sub(r'\s*@name=.+', '', seg).strip()
if re.match(r'^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg, re.IGNORECASE):
group["groupBy"] = []
else:
group["groupBy"] = [seg]
@@ -666,6 +714,8 @@ def build_calc_field_fragment(parsed, indent):
]
if parsed.get("title"):
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
if parsed.get("restrict"):
lines.append(build_restriction_xml(parsed["restrict"], f"{i}\t"))
if parsed.get("type"):
lines.append(f"{i}\t<valueType>")
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
@@ -679,6 +729,10 @@ def build_param_fragment(parsed, indent):
fragments = []
lines = [f"{i}<parameter>", f"{i}\t<name>{esc_xml(parsed['name'])}</name>"]
if parsed.get("title"):
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
if parsed.get("type"):
lines.append(f"{i}\t<valueType>")
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
@@ -689,6 +743,8 @@ def build_param_fragment(parsed, indent):
if parsed.get("type") == "StandardPeriod":
lines.append(f'{i}\t<value xsi:type="v8:StandardPeriod">')
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
lines.append(f"{i}\t</value>")
elif parsed.get("type", "").startswith("date"):
lines.append(f'{i}\t<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
@@ -704,14 +760,17 @@ def build_param_fragment(parsed, indent):
if parsed.get("autoDates"):
param_name = parsed["name"]
# Canonical БСП pattern: title + valueType + value + useRestriction + expression
b_lines = [
f"{i}<parameter>",
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430</name>",
build_mltext_xml("title", "\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
f"{i}\t<valueType>",
build_value_type_xml("date", f"{i}\t\t"),
f"{i}\t</valueType>",
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
f"{i}\t<useRestriction>true</useRestriction>",
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430')}</expression>",
f"{i}\t<availableAsField>false</availableAsField>",
f"{i}</parameter>",
]
fragments.append("\r\n".join(b_lines))
@@ -719,11 +778,13 @@ def build_param_fragment(parsed, indent):
e_lines = [
f"{i}<parameter>",
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f</name>",
build_mltext_xml("title", "\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
f"{i}\t<valueType>",
build_value_type_xml("date", f"{i}\t\t"),
f"{i}\t</valueType>",
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
f"{i}\t<useRestriction>true</useRestriction>",
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f')}</expression>",
f"{i}\t<availableAsField>false</availableAsField>",
f"{i}</parameter>",
]
fragments.append("\r\n".join(e_lines))
@@ -760,6 +821,31 @@ def build_selection_item_fragment(field_name, indent):
i = indent
if field_name == "Auto":
return f'{i}<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>'
m = re.match(r'^Folder\((.+)\)$', field_name)
if m:
inner = m.group(1)
colon_idx = inner.find(':')
if colon_idx > 0:
title = inner[:colon_idx].strip()
items = [x.strip() for x in inner[colon_idx + 1:].split(',') if x.strip()]
else:
title = ""
items = [x.strip() for x in inner.split(',') if x.strip()]
lines = [f'{i}<dcsset:item xsi:type="dcsset:SelectedItemFolder">']
if title:
lines.append(f"{i}\t<dcsset:lwsTitle>")
lines.append(f"{i}\t\t<v8:item>")
lines.append(f"{i}\t\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{i}\t\t\t<v8:content>{esc_xml(title)}</v8:content>")
lines.append(f"{i}\t\t</v8:item>")
lines.append(f"{i}\t</dcsset:lwsTitle>")
for item in items:
lines.append(f'{i}\t<dcsset:item xsi:type="dcsset:SelectedItemField">')
lines.append(f"{i}\t\t<dcsset:field>{esc_xml(item)}</dcsset:field>")
lines.append(f"{i}\t</dcsset:item>")
lines.append(f"{i}\t<dcsset:placement>Auto</dcsset:placement>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
lines = [
f'{i}<dcsset:item xsi:type="dcsset:SelectedItemField">',
f"{i}\t<dcsset:field>{esc_xml(field_name)}</dcsset:field>",
@@ -782,6 +868,8 @@ def build_data_param_fragment(parsed, indent):
if isinstance(val, dict) and val.get("variant"):
lines.append(f'{i}\t<dcscor:value xsi:type="v8:StandardPeriod">')
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val["variant"])}</v8:variant>')
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
lines.append(f"{i}\t</dcscor:value>")
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
lines.append(f'{i}\t<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(val))}</dcscor:value>')
@@ -866,6 +954,16 @@ def build_variant_fragment(parsed, indent):
return "\r\n".join(lines)
def _emit_filter_comparison(lines, f, indent):
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">')
lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(f["field"])}</dcsset:left>')
lines.append(f"{indent}\t<dcsset:comparisonType>{esc_xml(f['op'])}</dcsset:comparisonType>")
if f.get("value") is not None:
vt = f.get("valueType", "xs:string")
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{esc_xml(str(f["value"]))}</dcsset:right>')
lines.append(f"{indent}</dcsset:item>")
def build_conditional_appearance_item_fragment(parsed, indent):
i = indent
lines = [f"{i}<dcsset:item>"]
@@ -881,15 +979,17 @@ def build_conditional_appearance_item_fragment(parsed, indent):
lines.append(f"{i}\t<dcsset:selection/>")
if parsed.get("filter"):
f = parsed["filter"]
flt = parsed["filter"]
lines.append(f"{i}\t<dcsset:filter>")
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:FilterItemComparison">')
lines.append(f'{i}\t\t\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(f["field"])}</dcsset:left>')
lines.append(f"{i}\t\t\t<dcsset:comparisonType>{esc_xml(f['op'])}</dcsset:comparisonType>")
if f.get("value") is not None:
vt = f.get("valueType", "xs:string")
lines.append(f'{i}\t\t\t<dcsset:right xsi:type="{vt}">{esc_xml(str(f["value"]))}</dcsset:right>')
lines.append(f"{i}\t\t</dcsset:item>")
if isinstance(flt, list):
# OrGroup
lines.append(f'{i}\t\t<dcsset:item xsi:type="dcsset:FilterItemGroup">')
lines.append(f"{i}\t\t\t<dcsset:groupType>OrGroup</dcsset:groupType>")
for f in flt:
_emit_filter_comparison(lines, f, f"{i}\t\t\t")
lines.append(f"{i}\t\t</dcsset:item>")
else:
_emit_filter_comparison(lines, flt, f"{i}\t\t")
lines.append(f"{i}\t</dcsset:filter>")
else:
lines.append(f"{i}\t<dcsset:filter/>")
@@ -897,15 +997,23 @@ def build_conditional_appearance_item_fragment(parsed, indent):
# appearance
lines.append(f"{i}\t<dcsset:appearance>")
val = parsed["value"]
val_type = "xs:string"
if re.match(r'^(web|style|win):', val):
val_type = "v8ui:Color"
elif val in ("true", "false"):
val_type = "xs:boolean"
lines.append(f'{i}\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f"{i}\t\t\t<dcscor:parameter>{esc_xml(parsed['param'])}</dcscor:parameter>")
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="{val_type}">{esc_xml(val)}</dcscor:value>')
if re.match(r'^(web|style|win):', val):
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(val)}</dcscor:value>')
elif val in ("true", "false"):
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="xs:boolean">{esc_xml(val)}</dcscor:value>')
elif parsed["param"] in ("Формат", "Текст", "Заголовок"):
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="v8:LocalStringType">')
lines.append(f"{i}\t\t\t\t<v8:item>")
lines.append(f"{i}\t\t\t\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{i}\t\t\t\t\t<v8:content>{esc_xml(val)}</v8:content>")
lines.append(f"{i}\t\t\t\t</v8:item>")
lines.append(f"{i}\t\t\t</dcscor:value>")
else:
lines.append(f'{i}\t\t\t<dcscor:value xsi:type="xs:string">{esc_xml(val)}</dcscor:value>')
lines.append(f"{i}\t\t</dcscor:item>")
lines.append(f"{i}\t</dcsset:appearance>")
@@ -917,6 +1025,9 @@ def build_structure_item_fragment(item, indent):
i = indent
lines = [f'{i}<dcsset:item xsi:type="dcsset:StructureItemGroup">']
if item.get("name"):
lines.append(f"{i}\t<dcsset:name>{esc_xml(item['name'])}</dcsset:name>")
group_by = item.get("groupBy", [])
if not group_by:
lines.append(f"{i}\t<dcsset:groupItems/>")
@@ -1165,7 +1276,7 @@ def resolve_variant_settings():
break
if sv:
break
if not sv:
if sv is None:
print(f"Variant '{variant_arg}' not found", file=sys.stderr)
sys.exit(1)
else:
@@ -1173,7 +1284,7 @@ def resolve_variant_settings():
if isinstance(child.tag, str) and local_name(child) == "settingsVariant" and etree.QName(child.tag).namespace == SCH_NS:
sv = child
break
if not sv:
if sv is None:
print("No settingsVariant found in DCS", file=sys.stderr)
sys.exit(1)
@@ -1252,6 +1363,13 @@ xml_doc = tree.getroot()
if operation in ("set-query", "set-structure", "add-dataSet"):
values = [value_arg]
elif operation == "patch-query":
values = [v for v in value_arg.split(";;") if v.strip()]
elif operation == "add-drilldown":
if ";;" in value_arg:
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
else:
values = [v.strip() for v in value_arg.split(",") if v.strip()]
else:
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
@@ -1406,6 +1524,220 @@ elif operation == "add-parameter":
if parsed.get("autoDates"):
print('[OK] Auto-parameters "\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430", "\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f" added')
elif operation == "modify-parameter":
for val in values:
# Extract optional [Title] first (mirrors parse_field_shorthand)
title_val = None
m_title = re.search(r'\[([^\]]*)\]', val)
if m_title:
title_val = m_title.group(1).strip()
val = re.sub(r'\s*\[[^\]]*\]\s*', ' ', val).strip()
parts = val.split(None, 1)
param_name = parts[0].strip()
rest = parts[1].strip() if len(parts) > 1 else ""
param_el = find_element_by_child_value(xml_doc, "parameter", "name", param_name, SCH_NS)
if param_el is None:
print(f'[WARN] Parameter "{param_name}" not found -- skipped')
continue
child_indent = get_child_indent(param_el)
# Set/replace title (must come right after <name>, before <valueType>)
if title_val is not None:
existing_title = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "title"), None)
if existing_title is not None:
remove_node_with_whitespace(existing_title)
# Insert before the first child after <name>
title_ref = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) != "name"), None)
title_frag = build_mltext_xml("title", title_val, child_indent)
for node in import_fragment(xml_doc, title_frag):
insert_before_element(param_el, node, title_ref, child_indent)
print(f'[OK] Parameter "{param_name}": title set to "{title_val}"')
# Separate availableValue=... from simple kv pairs
simple_rest = rest
av_part = None
av_idx = rest.find("availableValue=")
if av_idx >= 0:
simple_rest = rest[:av_idx].strip()
av_part = rest[av_idx:]
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
if simple_rest:
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
key, value = m.group(1), m.group(2)
existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key), None)
if existing is not None:
existing.text = value
print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
else:
# Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use
ref_node = None
if key == "denyIncompleteValues":
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "use"), None)
frag_xml = f"{child_indent}<{key}>{esc_xml(value)}</{key}>"
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": {key}={value} added')
# Process availableValue
if av_part:
av_rest = av_part[len("availableValue="):]
# Parse: "Перечисление...X presentation=текст с пробелами"
av_parts = re.split(r'\s+presentation=', av_rest, 1)
av_value = av_parts[0].strip()
av_presentation = av_parts[1].strip() if len(av_parts) > 1 else ""
av_type = "xs:string"
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_value):
av_type = "dcscor:DesignTimeValue"
av_lines = [f"{child_indent}<availableValue>"]
av_lines.append(f'{child_indent}\t<value xsi:type="{av_type}">{esc_xml(av_value)}</value>')
if av_presentation:
av_lines.append(f'{child_indent}\t<presentation xsi:type="v8:LocalStringType">')
av_lines.append(f"{child_indent}\t\t<v8:item>")
av_lines.append(f"{child_indent}\t\t\t<v8:lang>ru</v8:lang>")
av_lines.append(f"{child_indent}\t\t\t<v8:content>{esc_xml(av_presentation)}</v8:content>")
av_lines.append(f"{child_indent}\t\t</v8:item>")
av_lines.append(f"{child_indent}\t</presentation>")
av_lines.append(f"{child_indent}</availableValue>")
frag_xml = "\r\n".join(av_lines)
# Insert before first of (denyIncompleteValues, use) in document order
ref_node = None
for child in param_el:
if isinstance(child.tag, str) and local_name(child) in ("denyIncompleteValues", "use"):
ref_node = child
break
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": availableValue added')
elif operation == "rename-parameter":
root = xml_doc
for val in values:
m_rn = re.match(r'^\s*(.+?)\s*=>\s*(.+?)\s*$', val)
if not m_rn:
print(f'[WARN] rename-parameter expects "OldName => NewName", got: {val}')
continue
old_name = m_rn.group(1).strip()
new_name = m_rn.group(2).strip()
if old_name == new_name:
print('[WARN] rename-parameter: old and new names are equal -- skipped')
continue
# 1. Rename <parameter><name>OldName</name>
param_el = find_element_by_child_value(root, "parameter", "name", old_name, SCH_NS)
if param_el is None:
print(f'[WARN] Parameter "{old_name}" not found -- skipped')
continue
for ch in param_el:
if isinstance(ch.tag, str) and local_name(ch) == "name" and etree.QName(ch.tag).namespace == SCH_NS:
ch.text = new_name
break
# 2. Update <expression> in other <parameter> elements.
# Regex matches "&OldName" only when followed by a non-identifier char (or end),
# so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета".
esc_old = re.escape(old_name)
expr_regex = re.compile(rf'&{esc_old}(?=[^\w\u0400-\u04FF]|$)')
expr_updated = 0
for ch in root:
if not (isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS):
continue
for gc in ch:
if isinstance(gc.tag, str) and local_name(gc) == "expression" and etree.QName(gc.tag).namespace == SCH_NS:
old_expr = gc.text or ""
new_expr = expr_regex.sub(f'&{new_name}', old_expr)
if new_expr != old_expr:
gc.text = new_expr
expr_updated += 1
# 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants.
dp_updated = 0
for variant_node in root:
if not (isinstance(variant_node.tag, str) and local_name(variant_node) == "settingsVariant" and etree.QName(variant_node.tag).namespace == SCH_NS):
continue
settings_node = find_first_element(variant_node, ["settings"], SET_NS)
if settings_node is None:
continue
dp_el = find_first_element(settings_node, ["dataParameters"], SET_NS)
if dp_el is None:
continue
for item in dp_el:
if not (isinstance(item.tag, str) and local_name(item) == "item"):
continue
for gc in item:
if isinstance(gc.tag, str) and local_name(gc) == "parameter" and etree.QName(gc.tag).namespace == COR_NS:
if (gc.text or "").strip() == old_name:
gc.text = new_name
dp_updated += 1
print(f'[OK] Parameter renamed: "{old_name}" => "{new_name}" (expressions updated: {expr_updated}, dataParameters updated: {dp_updated})')
elif operation == "reorder-parameters":
root = xml_doc
for val in values:
order = [s.strip() for s in val.split(",") if s.strip()]
if not order:
print('[WARN] reorder-parameters: empty list -- skipped')
continue
all_params = []
for ch in root:
if isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS:
all_params.append(ch)
if not all_params:
print('[WARN] reorder-parameters: no parameters in schema')
continue
child_indent = get_child_indent(root)
by_name = {}
for pe in all_params:
for gc in pe:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
by_name[(gc.text or "").strip()] = pe
break
new_order = []
used = set()
for name in order:
if name in by_name:
new_order.append(by_name[name])
used.add(name)
else:
print(f'[WARN] reorder-parameters: parameter "{name}" not found -- skipped')
for pe in all_params:
pe_name = None
for gc in pe:
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
pe_name = (gc.text or "").strip()
break
if pe_name and pe_name not in used:
new_order.append(pe)
# Anchor: element right after the last parameter in original order
last_param = all_params[-1]
anchor = last_param.getnext()
# Remove all parameters with surrounding whitespace
for pe in all_params:
remove_node_with_whitespace(pe)
# Re-insert in new order before anchor
for pe in new_order:
insert_before_element(root, pe, anchor, child_indent)
print(f'[OK] Parameters reordered ({len(all_params)} total, {len(order)} explicit)')
elif operation == "add-filter":
settings = resolve_variant_settings()
var_name = get_variant_name()
@@ -1470,13 +1802,53 @@ elif operation == "add-selection":
var_name = get_variant_name()
for val in values:
field_name = val.strip()
selection = ensure_settings_child(settings, "selection", [])
group_name = None
# Extract @group=Name
gm = re.search(r'\s*@group=(\S+)', field_name)
if gm:
group_name = gm.group(1)
field_name = re.sub(r'\s*@group=\S+', '', field_name).strip()
if group_name:
# Find named StructureItemGroup
target_el = None
for item in settings.iter(f"{{{SET_NS}}}item"):
xsi_type = item.get(f"{{{XSI_NS}}}type", "")
if "StructureItemGroup" in xsi_type:
name_el = item.find(f"{{{SET_NS}}}name")
if name_el is not None and name_el.text == group_name:
target_el = item
break
if target_el is None:
print(f'[WARN] StructureItemGroup "{group_name}" not found -- adding to variant level')
target_el = settings
else:
target_el = settings
selection = ensure_settings_child(target_el, "selection", [])
# Dedup: skip if SelectedItemAuto already exists
if field_name == "Auto":
is_dup = False
for ch in selection:
if isinstance(ch.tag, str) and local_name(ch) == "item":
type_attr = ch.get(XSI_TYPE, "")
if "SelectedItemAuto" in type_attr:
is_dup = True
break
if is_dup:
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
print(f'[WARN] SelectedItemAuto already exists in {target} -- skipped')
continue
sel_indent = get_container_child_indent(selection)
sel_xml = build_selection_item_fragment(field_name, sel_indent)
sel_nodes = import_fragment(xml_doc, sel_xml)
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
print(f'[OK] Selection "{field_name}" added to variant "{var_name}"')
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
print(f'[OK] Selection "{field_name}" added to {target}')
elif operation == "set-query":
ds_node = resolve_data_set()
@@ -1674,7 +2046,11 @@ elif operation == "add-conditionalAppearance":
desc = f"{parsed['param']} = {parsed['value']}"
if parsed.get("filter"):
desc += f" when {parsed['filter']['field']} {parsed['filter']['op']}"
flt = parsed["filter"]
if isinstance(flt, list):
desc += f" when OrGroup({len(flt)} conditions)"
else:
desc += f" when {flt['field']} {flt['op']}"
if parsed.get("fields"):
desc += f" for {', '.join(parsed['fields'])}"
print(f'[OK] ConditionalAppearance "{desc}" added to variant "{var_name}"')
@@ -1731,9 +2107,11 @@ elif operation == "modify-filter":
vt = parsed.get("valueType", "xs:string")
set_or_create_child_element_with_attr(filter_item, "right", SET_NS, str(parsed["value"]), vt, item_indent)
# Update use (only when explicitly set via @off / @on)
if parsed.get("use") is False:
set_or_create_child_element(filter_item, "use", SET_NS, "false", item_indent)
else:
elif parsed.get("use") is True:
# @on: remove existing use=false if any
for ch in filter_item:
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SET_NS:
if (ch.text or "").strip() == "false":
@@ -1780,6 +2158,8 @@ elif operation == "modify-dataParameter":
if isinstance(pv, dict) and pv.get("variant"):
val_lines.append(f'{item_indent}<dcscor:value xsi:type="v8:StandardPeriod">')
val_lines.append(f'{item_indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(pv["variant"])}</v8:variant>')
val_lines.append(f"{item_indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
val_lines.append(f"{item_indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
val_lines.append(f"{item_indent}</dcscor:value>")
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(pv)):
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(pv))}</dcscor:value>')
@@ -1793,9 +2173,11 @@ elif operation == "modify-dataParameter":
for node in val_nodes:
insert_before_element(dp_item, node, None, item_indent)
# Update use (only when explicitly set via @off / @on)
if parsed.get("use") is False:
set_or_create_child_element(dp_item, "use", COR_NS, "false", item_indent)
else:
elif parsed.get("use") is True:
# @on: remove existing use=false if any
for ch in dp_item:
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == COR_NS:
if (ch.text or "").strip() == "false":
@@ -1937,6 +2319,151 @@ elif operation == "remove-filter":
remove_node_with_whitespace(filter_item)
print(f'[OK] Filter for "{field_name}" removed from variant "{var_name}"')
elif operation == "add-drilldown":
# String-based manipulation — templates use dcsat namespace with inline xmlns
with open(resolved_path, "r", encoding="utf-8-sig") as f:
raw_text = f.read()
nl = "\r\n"
dcsat_ns_decl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"'
# Find all outer <template> blocks by nesting-aware scan
name_regex = re.compile(r'<template>\s*<name>([^<]+)</name>')
tpl_starts = [(m.start(), m.group(1)) for m in name_regex.finditer(raw_text)]
# For each start, find closing </template> at nesting depth 0
tpl_blocks = []
for ts_pos, ts_name in tpl_starts:
depth = 1
scan_pos = ts_pos + 10 # skip past opening <template>
while depth > 0 and scan_pos < len(raw_text):
next_open = raw_text.find("<template", scan_pos)
next_close = raw_text.find("</template>", scan_pos)
if next_close < 0:
break
if next_open >= 0 and next_open < next_close:
depth += 1
scan_pos = next_open + 10
else:
depth -= 1
if depth == 0:
end_pos = next_close + len("</template>")
tpl_blocks.append((ts_name, ts_pos, raw_text[ts_pos:end_pos]))
scan_pos = next_close + 11
if not tpl_blocks:
print("[WARN] No named templates found in schema")
# Collect all insertions as (position, text) — apply in reverse order
insertions = []
expr_regex = re.compile(
r'(?s)<parameter[^>]*ExpressionAreaTemplateParameter[^>]*>\s*'
r'<dcsat:name>([^<]+)</dcsat:name>\s*'
r'<dcsat:expression>([^<]+)</dcsat:expression>\s*</parameter>'
)
for tpl_name, tpl_start, tpl_text in tpl_blocks:
# Build map: expression → paramName from ExpressionAreaTemplateParameter
expr_map = {}
for em in expr_regex.finditer(tpl_text):
p_name = em.group(1)
p_expr = em.group(2)
expr_map[p_expr] = p_name
for resource in values:
drill_name = f"Расшифровка_{resource}"
# Idempotency: check if already exists
if drill_name in tpl_text:
print(f"[INFO] {drill_name} already exists in {tpl_name} — skipped")
continue
# Find ExpressionAreaTemplateParameter by expression
param_name = expr_map.get(resource)
if param_name is None:
print(f'[WARN] Expression "{resource}" not found in template {tpl_name} — skipped')
continue
cell_count = 0
# Step 1: Insert DetailsAreaTemplateParameter after last </parameter> in template
last_param_end_tag = "</parameter>"
last_param_pos = tpl_text.rfind(last_param_end_tag)
if last_param_pos >= 0:
insert_pos = tpl_start + last_param_pos + len(last_param_end_tag)
# Detect indent from context
prev_nl = tpl_text.rfind("\n", 0, last_param_pos)
indent = "\t\t"
if prev_nl >= 0:
line_start = prev_nl + 1
indent_match = re.match(r'^(\s*)', tpl_text[line_start:])
if indent_match:
indent = indent_match.group(1)
details_xml = (
f'{nl}{indent}<parameter {dcsat_ns_decl} xsi:type="dcsat:DetailsAreaTemplateParameter">'
f'{nl}{indent}\t<dcsat:name>{drill_name}</dcsat:name>'
f'{nl}{indent}\t<dcsat:fieldExpression>'
f'{nl}{indent}\t\t<dcsat:field>ИмяРесурса</dcsat:field>'
f'{nl}{indent}\t\t<dcsat:expression>"{resource}"</dcsat:expression>'
f'{nl}{indent}\t</dcsat:fieldExpression>'
f'{nl}{indent}\t<dcsat:mainAction>DrillDown</dcsat:mainAction>'
f'{nl}{indent}</parameter>'
)
insertions.append((insert_pos, details_xml))
# Step 2: Insert appearance binding in cells referencing this parameter
cell_tag = f'<dcsat:value xsi:type="dcscor:Parameter">{param_name}</dcsat:value>'
search_start = 0
while True:
cell_idx = tpl_text.find(cell_tag, search_start)
if cell_idx < 0:
break
cell_end = tpl_text.find("</dcsat:tableCell>", cell_idx)
if cell_end < 0:
break
app_end = tpl_text.rfind("</dcsat:appearance>", cell_idx, cell_end)
if app_end < cell_idx:
search_start = cell_end + 1
continue
# Detect indent for appearance items — insert after \n, before indent of </dcsat:appearance>
app_prev_nl = tpl_text.rfind("\n", 0, app_end)
app_indent = "\t\t\t\t\t\t"
if app_prev_nl >= 0:
app_line_start = app_prev_nl + 1
app_indent_match = re.match(r'^(\s*)', tpl_text[app_line_start:])
if app_indent_match:
app_indent = app_indent_match.group(1)
item_indent = app_indent + "\t"
appearance_xml = (
f'{item_indent}<dcscor:item>{nl}'
f'{item_indent}\t<dcscor:parameter>Расшифровка</dcscor:parameter>{nl}'
f'{item_indent}\t<dcscor:value xsi:type="dcscor:Parameter">{drill_name}</dcscor:value>{nl}'
f'{item_indent}</dcscor:item>{nl}'
)
# Insert after \n (before indent of closing tag), not before the tag itself
insert_at = (tpl_start + app_prev_nl + 1) if app_prev_nl >= 0 else (tpl_start + app_end)
insertions.append((insert_at, appearance_xml))
cell_count += 1
search_start = cell_end + 1
print(f"[OK] {drill_name} \u2192 {tpl_name} (param + {cell_count} cell(s))")
# Apply insertions in reverse order to preserve offsets.
# For same position: reverse insertion order so first resource ends up first in file.
insertions = [(pos, text, seq) for seq, (pos, text) in enumerate(insertions)]
insertions.sort(key=lambda x: (x[0], x[2]), reverse=True)
for pos, text, _seq in insertions:
raw_text = raw_text[:pos] + text + raw_text[pos:]
# Write directly — skip lxml save
with open(resolved_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
f.write(raw_text.encode("utf-8"))
print(f"[OK] Saved {resolved_path}")
sys.exit(0)
# ── 9. Save ─────────────────────────────────────────────────
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
+63 -4
View File
@@ -1,7 +1,8 @@
# skd-info v1.0 — Analyze 1C DCS structure
# skd-info v1.3 — Analyze 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
[Alias('Path')]
[string]$TemplatePath,
[ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates", "full")]
[string]$Mode = "overview",
@@ -17,6 +18,8 @@ $ErrorActionPreference = "Stop"
# --- Resolve path ---
$originalPath = $TemplatePath
if (-not $TemplatePath.EndsWith(".xml")) {
$candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml"
if (Test-Path $candidate) {
@@ -24,7 +27,48 @@ if (-not $TemplatePath.EndsWith(".xml")) {
}
}
if (-not (Test-Path $TemplatePath)) {
# If still not a file, try resolving from object directory (Reports/X, DataProcessors/X)
if (-not (Test-Path $TemplatePath -PathType Leaf)) {
$templatesDir = Join-Path $originalPath "Templates"
if (Test-Path $templatesDir) {
$dcsTemplates = @()
foreach ($metaXml in (Get-ChildItem $templatesDir -Filter "*.xml" -File)) {
[xml]$meta = Get-Content $metaXml.FullName -Encoding UTF8
$tt = $meta.SelectSingleNode("//*[local-name()='TemplateType']")
if ($tt -and $tt.InnerText -eq "DataCompositionSchema") {
$tplName = [System.IO.Path]::GetFileNameWithoutExtension($metaXml.Name)
$tplPath = Join-Path (Join-Path (Join-Path $templatesDir $tplName) "Ext") "Template.xml"
if (Test-Path $tplPath) {
$dcsTemplates += $tplPath
}
}
}
if ($dcsTemplates.Count -eq 1) {
$TemplatePath = $dcsTemplates[0]
$resolvedMsg = (Resolve-Path $TemplatePath).Path
$cwd = (Get-Location).Path
if ($resolvedMsg.StartsWith($cwd)) {
$resolvedMsg = $resolvedMsg.Substring($cwd.Length + 1)
}
Write-Host "[i] Resolved: $resolvedMsg"
} elseif ($dcsTemplates.Count -gt 1) {
Write-Host "Multiple DCS templates found in: $originalPath"
$cwd = (Get-Location).Path
for ($i = 0; $i -lt $dcsTemplates.Count; $i++) {
$p = (Resolve-Path $dcsTemplates[$i]).Path
if ($p.StartsWith($cwd)) { $p = $p.Substring($cwd.Length + 1) }
Write-Host " $($i+1). $p"
}
Write-Host "Specify the template path."
exit 1
} else {
Write-Error "No DCS templates found in: $originalPath"
exit 1
}
}
}
if (-not (Test-Path $TemplatePath -PathType Leaf)) {
Write-Error "File not found: $TemplatePath"
exit 1
}
@@ -402,7 +446,11 @@ function Show-Overview {
if ($fieldTpls.Count -gt 0) { $parts += "$($fieldTpls.Count) field" }
$grpCount = $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count
if ($grpCount -gt 0) { $parts += "$grpCount group" }
$lines.Add("Templates: $($tplDefs.Count) defined ($($parts -join ', ') bindings)")
if ($parts.Count -gt 0) {
$lines.Add("Templates: $($tplDefs.Count) defined ($($parts -join ', ') bindings)")
} else {
$lines.Add("Templates: $($tplDefs.Count) defined")
}
}
# Parameters — split visible/hidden
@@ -1305,7 +1353,18 @@ if ($Mode -eq "variant") {
elseif ($Mode -eq "full") {
Show-Overview
$lines.Add(""); $lines.Add("--- query ---"); $lines.Add("")
Show-Query
$hasQuery = $root.SelectNodes("descendant::s:dataSet[@xsi:type='DataSetQuery']", $ns).Count -gt 0
if ($hasQuery) {
Show-Query
} else {
$objNodes = $root.SelectNodes("descendant::s:dataSet[@xsi:type='DataSetObject']/s:objectName", $ns)
if ($objNodes.Count -gt 0) {
$names = @(); foreach ($n in $objNodes) { $names += $n.InnerText }
$lines.Add("(no query datasets; external datasets: $($names -join ', '))")
} else {
$lines.Add("(no query datasets)")
}
}
$lines.Add(""); $lines.Add("--- fields ---"); $lines.Add("")
Show-Fields
$lines.Add(""); $lines.Add("--- resources ---"); $lines.Add("")
+51 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-info v1.0 — Analyze 1C DCS structure
# skd-info v1.3 — Analyze 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -269,7 +269,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Analyze 1C DCS structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True)
parser.add_argument("-TemplatePath", "-Path", required=True)
parser.add_argument("-Mode", default="overview",
choices=["overview", "query", "fields", "links", "calculated",
"resources", "params", "variant", "trace", "templates", "full"])
@@ -281,12 +281,48 @@ def main():
args = parser.parse_args()
# --- Resolve path ---
template_path = args.TemplatePath
original_path = args.TemplatePath
template_path = original_path
if not template_path.endswith(".xml"):
candidate = os.path.join(template_path, "Ext", "Template.xml")
if os.path.isfile(candidate):
template_path = candidate
# If still not found, try resolving from object directory (Reports/X, DataProcessors/X)
if not os.path.isfile(template_path) and not template_path.endswith(".xml"):
templates_dir = os.path.join(original_path, "Templates")
if os.path.isdir(templates_dir):
dcs_templates = []
for fname in os.listdir(templates_dir):
if not fname.endswith(".xml"):
continue
meta_path = os.path.join(templates_dir, fname)
if not os.path.isfile(meta_path):
continue
try:
meta_tree = etree.parse(meta_path, etree.XMLParser(remove_blank_text=True))
tt_nodes = meta_tree.xpath("//*[local-name()='TemplateType']")
if tt_nodes and (tt_nodes[0].text or "").strip() == "DataCompositionSchema":
tpl_name = os.path.splitext(fname)[0]
tpl_path = os.path.join(templates_dir, tpl_name, "Ext", "Template.xml")
if os.path.isfile(tpl_path):
dcs_templates.append(tpl_path)
except Exception:
continue
if len(dcs_templates) == 1:
template_path = dcs_templates[0]
resolved_display = os.path.relpath(os.path.abspath(template_path))
print(f"[i] Resolved: {resolved_display}")
elif len(dcs_templates) > 1:
print(f"Multiple DCS templates found in: {original_path}")
for i, p in enumerate(dcs_templates):
print(f" {i+1}. {os.path.relpath(os.path.abspath(p))}")
print("Specify the template path.")
sys.exit(1)
else:
print(f"No DCS templates found in: {original_path}", file=sys.stderr)
sys.exit(1)
if not os.path.isabs(template_path):
template_path = os.path.join(os.getcwd(), template_path)
@@ -417,7 +453,10 @@ def main():
grp_count = len(group_tpls) + len(group_header_tpls) + len(group_footer_tpls)
if grp_count > 0:
parts.append(f"{grp_count} group")
lines.append(f"Templates: {len(tpl_defs)} defined ({', '.join(parts)} bindings)")
if parts:
lines.append(f"Templates: {len(tpl_defs)} defined ({', '.join(parts)} bindings)")
else:
lines.append(f"Templates: {len(tpl_defs)} defined")
# Parameters -- split visible/hidden
params = root.findall("s:parameter", NSMAP)
@@ -1626,7 +1665,14 @@ def main():
lines.append("")
lines.append("--- query ---")
lines.append("")
show_query()
if root.findall(".//s:dataSet[@xsi:type='DataSetQuery']", NSMAP):
show_query()
else:
obj_names = [n.text for n in root.findall(".//s:dataSet[@xsi:type='DataSetObject']/s:objectName", NSMAP) if n.text]
if obj_names:
lines.append(f"(no query datasets; external datasets: {', '.join(obj_names)})")
else:
lines.append("(no query datasets)")
lines.append("")
lines.append("--- fields ---")
lines.append("")
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[switch]$Detailed,
@@ -12,7 +12,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True)
parser.add_argument("-TemplatePath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=20)
parser.add_argument("-OutFile", default="")
+2 -8
View File
@@ -1,6 +1,6 @@
---
name: subsystem-compile
description: Создать подсистему 1С — XML-исходники из JSON-определения. Используй когда пользователь просит добавить подсистему (раздел) в конфигурацию
description: Создать подсистему 1С — XML-исходники из JSON-определения. Используй когда нужно добавить подсистему (раздел) в конфигурацию
argument-hint: "[-DefinitionFile <json> | -Value <json-string>] -OutputDir <ConfigDir> [-Parent <path>]"
allowed-tools:
- Bash
@@ -38,8 +38,7 @@ powershell.exe -NoProfile -File '.claude/skills/subsystem-compile/scripts/subsys
"useOneCommand": false,
"explanation": "Описание раздела",
"picture": "CommonPicture.МояКартинка",
"content": ["Catalog.Товары", "Document.Заказ"],
"children": ["ДочерняяА", "ДочерняяБ"]
"content": ["Catalog.Товары", "Document.Заказ"]
}
```
@@ -58,8 +57,3 @@ powershell.exe -NoProfile -File '.claude/skills/subsystem-compile/scripts/subsys
... -Value '{"name":"Дочерняя"}' -OutputDir config/ -Parent config/Subsystems/Продажи.xml
```
## Что генерируется
- `{OutputDir}/Subsystems/{Name}.xml` — определение подсистемы
- `{OutputDir}/Subsystems/{Name}/` — каталог (если есть children)
- `Configuration.xml` или родительская подсистема — регистрация в `<ChildObjects>`
@@ -1,4 +1,4 @@
# subsystem-compile v1.2 — Create 1C subsystem from JSON definition
# subsystem-compile v1.5 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -85,6 +85,29 @@ 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
@@ -267,12 +290,31 @@ 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="2.17">'
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>"
@@ -366,13 +408,23 @@ $utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($targetXml, $xmlContent, $utf8Bom)
Write-Host "[OK] Created: $targetXml"
# Create subdirectory if children exist
# 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 ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# subsystem-compile v1.2 — Create 1C subsystem from JSON definition
# subsystem-compile v1.5 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -10,6 +10,22 @@ 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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
@@ -44,6 +60,48 @@ def split_camel_case(name):
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")
@@ -169,6 +227,8 @@ def main():
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 ''
@@ -205,7 +265,7 @@ def main():
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">')
lines.append(f'<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>')
@@ -287,12 +347,21 @@ def main():
write_utf8_bom(target_xml, xml_content)
print(f"[OK] Created: {target_xml}")
# Create subdirectory if children exist
# 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
@@ -1,7 +1,7 @@
# subsystem-edit v1.1 — Edit existing 1C subsystem XML
# subsystem-edit v1.2 — Edit existing 1C subsystem XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$SubsystemPath,
[Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath,
[string]$DefinitionFile,
[ValidateSet("add-content","remove-content","add-child","remove-child","set-property")]
[string]$Operation,
@@ -119,12 +119,17 @@ if (-not (Test-Path $SubsystemPath)) {
}
if (-not (Test-Path $SubsystemPath)) { Write-Error "File not found: $SubsystemPath"; exit 1 }
$resolvedPath = (Resolve-Path $SubsystemPath).Path
$script:resolvedPath = $resolvedPath
# --- Load XML with PreserveWhitespace ---
$script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true
$script:xmlDoc.Load($resolvedPath)
$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version")
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
$script:utf8Bom = New-Object System.Text.UTF8Encoding($true)
$script:addCount = 0
$script:removeCount = 0
$script:modifyCount = 0
@@ -164,6 +169,37 @@ foreach ($child in $script:propsEl.ChildNodes) {
Info "Subsystem: $($script:objName)"
# --- XML manipulation helpers (from meta-edit pattern) ---
function Esc-Xml([string]$s) {
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
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)
}
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
@@ -337,6 +373,20 @@ function Do-AddChild([string]$childName) {
Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent
$script:addCount++
Info "Added child subsystem: $childName"
# Write stub XML for the new child if it doesn't exist yet
$parentDir = [System.IO.Path]::GetDirectoryName($script:resolvedPath)
$parentBaseName = [System.IO.Path]::GetFileNameWithoutExtension($script:resolvedPath)
$childSubsDir = Join-Path (Join-Path $parentDir $parentBaseName) "Subsystems"
if (-not (Test-Path $childSubsDir)) {
New-Item -ItemType Directory -Path $childSubsDir -Force | Out-Null
Info "Created directory: $childSubsDir"
}
$childXml = Join-Path $childSubsDir "$childName.xml"
if (-not (Test-Path $childXml)) {
Write-ChildSubsystemStub $childXml $childName $script:formatVersion $script:utf8Bom
Info "Created stub: $childXml"
}
}
function Do-RemoveChild([string]$childName) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# subsystem-edit v1.1 — Edit existing 1C subsystem XML
# subsystem-edit v1.2 — Edit existing 1C subsystem XML
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -7,8 +7,64 @@ import json
import os
import subprocess
import sys
import uuid
from lxml import etree
def new_uuid():
return str(uuid.uuid4())
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
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')
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"
@@ -214,7 +270,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Edit existing 1C subsystem XML", allow_abbrev=False)
parser.add_argument("-SubsystemPath", required=True)
parser.add_argument("-SubsystemPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["add-content", "remove-content", "add-child", "remove-child", "set-property"])
parser.add_argument("-Value", default=None)
@@ -264,6 +320,7 @@ def main():
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
format_version = xml_root.get("version") or "2.17"
add_count = 0
remove_count = 0
@@ -381,6 +438,18 @@ def main():
add_count += 1
info(f"Added child subsystem: {child_name}")
# Write stub XML for the new child if it doesn't exist yet
parent_dir = os.path.dirname(resolved_path)
parent_base_name = os.path.splitext(os.path.basename(resolved_path))[0]
child_subs_dir = os.path.join(parent_dir, parent_base_name, 'Subsystems')
if not os.path.exists(child_subs_dir):
os.makedirs(child_subs_dir, exist_ok=True)
info(f"Created directory: {child_subs_dir}")
child_xml = os.path.join(child_subs_dir, f'{child_name}.xml')
if not os.path.exists(child_xml):
write_child_subsystem_stub(child_xml, child_name, format_version)
info(f"Created stub: {child_xml}")
def do_remove_child(child_name):
nonlocal remove_count
if child_objs_el is None:
@@ -533,7 +602,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running subsystem-validate ---")
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-SubsystemPath", "-Path", resolved_path])
# --- Summary ---
print()
@@ -1,7 +1,7 @@
# subsystem-info v1.0 — Compact summary of 1C subsystem structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$SubsystemPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$SubsystemPath,
[ValidateSet("overview","content","ci","tree","full")]
[string]$Mode = "overview",
[string]$Name,
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C subsystem structure", allow_abbrev=False)
parser.add_argument("-SubsystemPath", required=True, help="Path to subsystem XML or Subsystems/ directory")
parser.add_argument("-SubsystemPath", "-Path", required=True, help="Path to subsystem XML or Subsystems/ directory")
parser.add_argument("-Mode", choices=["overview", "content", "ci", "tree", "full"], default="overview", help="Output mode")
parser.add_argument("-Name", default="", help="Filter by name/type")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
@@ -1,7 +1,7 @@
# subsystem-validate v1.2 — Validate 1C subsystem XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$SubsystemPath,
[Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile
@@ -82,7 +82,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C subsystem XML structure', allow_abbrev=False
)
parser.add_argument('-SubsystemPath', dest='SubsystemPath', required=True)
parser.add_argument('-SubsystemPath', '-Path', dest='SubsystemPath', required=True)
parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='')
+11 -3
View File
@@ -1,6 +1,6 @@
---
name: template-add
description: Добавить макет к объекту 1С (обработка, отчёт, справочник, документ и др.)
description: Добавить пустой макет к объекту 1С. Используй когда нужно создать у объекта новый макет
argument-hint: <ObjectName> <TemplateName> <TemplateType>
allowed-tools:
- Bash
@@ -27,8 +27,8 @@ allowed-tools:
| TemplateName | да | — | Имя макета |
| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData, DataCompositionSchema |
| Synonym | нет | = TemplateName | Синоним макета |
| SrcDir | нет | `src` | Каталог исходников |
| --SetMainSKD | нет | — | Принудительно установить MainDataCompositionSchema |
| SrcDir | нет | `src` | Путь к папке типа объектов (`Reports`, `DataProcessors`, `Catalogs`, `Documents`...), внутри которой лежит `<ObjectName>.xml`. Дефолт `src` подходит для каталогов с внешними обработками/отчётами, лежащими рядом |
| -SetMainSKD | нет | — | Принудительно установить MainDataCompositionSchema |
## Команда
@@ -36,6 +36,14 @@ allowed-tools:
powershell.exe -NoProfile -File .claude/skills/template-add/scripts/add-template.ps1 -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" -TemplateType "<TemplateType>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-SetMainSKD]
```
## Пример
Добавить основную СКД к отчёту в расширении:
```powershell
powershell.exe -NoProfile -File .claude/skills/template-add/scripts/add-template.ps1 -ObjectName "ОтчётПродажи" -TemplateName "ОсновнаяСхемаКомпоновкиДанных" -TemplateType "DataCompositionSchema" -SrcDir "src/cfe/МоёРасширение/Reports"
```
## Маппинг типов
Пользователь может указать тип в свободной форме. Определи нужный по контексту:
@@ -1,4 +1,4 @@
# template-add v1.1 — Add template to 1C object
# template-add v1.4 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -35,10 +35,31 @@ $tmpl = $typeMap[$TemplateType]
# --- Проверки ---
$objectTypeFolders = @(
"Reports", "DataProcessors", "Documents", "Catalogs",
"InformationRegisters", "AccumulationRegisters",
"ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes",
"BusinessProcesses", "Tasks", "ExchangePlans"
)
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
if (-not (Test-Path $rootXmlPath)) {
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
exit 1
$candidates = @()
foreach ($folder in $objectTypeFolders) {
$probe = Join-Path (Join-Path $SrcDir $folder) "$ObjectName.xml"
if (Test-Path $probe) { $candidates += (Join-Path $SrcDir $folder) }
}
if ($candidates.Count -eq 1) {
$SrcDir = $candidates[0]
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
Write-Host "[INFO] SrcDir расширен до: $SrcDir"
} elseif ($candidates.Count -gt 1) {
Write-Error "Объект '$ObjectName' найден в нескольких подпапках: $($candidates -join ', ')`nУкажи SrcDir явно"
exit 1
} else {
Write-Error "Корневой файл объекта не найден: $rootXmlPath`nОжидается: <SrcDir>/<ObjectName>.xml`nПодсказка: SrcDir должен указывать на папку типа объектов (например Reports), а не на корень конфигурации"
exit 1
}
}
$processorDir = Join-Path $SrcDir $ObjectName
@@ -59,13 +80,32 @@ 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="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version=`"$formatVersion`">
<Template uuid="$templateUuid">
<Properties>
<Name>$TemplateName</Name>
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# add-template v1.0 — Add template to 1C object
# add-template v1.4 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
import uuid
@@ -37,6 +38,22 @@ def write_text_with_bom(path, text):
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")
@@ -59,12 +76,37 @@ def main():
tmpl = TYPE_MAP[template_type]
format_version = detect_format_version(os.path.abspath(src_dir))
# --- Checks ---
object_type_folders = [
"Reports", "DataProcessors", "Documents", "Catalogs",
"InformationRegisters", "AccumulationRegisters",
"ChartsOfCharacteristicTypes", "ChartsOfAccounts", "ChartsOfCalculationTypes",
"BusinessProcesses", "Tasks", "ExchangePlans",
]
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)
candidates = []
for folder in object_type_folders:
probe = os.path.join(src_dir, folder, f"{object_name}.xml")
if os.path.exists(probe):
candidates.append(os.path.join(src_dir, folder))
if len(candidates) == 1:
src_dir = candidates[0]
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
print(f"[INFO] SrcDir расширен до: {src_dir}")
elif len(candidates) > 1:
print(f"Объект '{object_name}' найден в нескольких подпапках: {', '.join(candidates)}", file=sys.stderr)
print(f"Укажи SrcDir явно", file=sys.stderr)
sys.exit(1)
else:
print(f"Корневой файл объекта не найден: {root_xml_path}", file=sys.stderr)
print(f"Ожидается: <SrcDir>/<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")
@@ -102,7 +144,7 @@ def main():
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n'
f' version="{format_version}">\n'
f'\t<Template uuid="{template_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{template_name}</Name>\n'
+2
View File
@@ -155,6 +155,8 @@ const form = await getFormState();
**confirmation** — if present, a Yes/No dialog is shown. Call `clickElement('Да')` or `clickElement('Нет')`.
**errors.stateText** — array of SpreadsheetDocument state messages (e.g. `"Не установлено значение параметра \"X\""`, `"Отчет не сформирован..."`, `"Изменились настройки..."`). Present when the report area shows an info bar instead of data.
### Reading data
#### `readTable({ maxRows?, offset?, table? })``{ columns, rows, total, shown, offset }`

Some files were not shown because too many files have changed in this diff Show More