diff --git a/.claude/skills/cf-edit/SKILL.md b/.claude/skills/cf-edit/SKILL.md
index 34809a3e..e2c0e395 100644
--- a/.claude/skills/cf-edit/SKILL.md
+++ b/.claude/skills/cf-edit/SKILL.md
@@ -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` | Удалить роль по умолчанию |
diff --git a/.claude/skills/cf-edit/reference.md b/.claude/skills/cf-edit/reference.md
index 02c6a36a..57a8c0f9 100644
--- a/.claude/skills/cf-edit/reference.md
+++ b/.claude/skills/cf-edit/reference.md
@@ -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`**: операция регистрирует в `` Configuration.xml только объект, **файл которого уже существует на диске** (например `Catalogs/Товары.xml`). Если файла нет — скрипт падает с exit 1 и подсказкой. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его в Configuration.xml за один вызов.
+
+Когда `add-childObject` всё-таки нужен: откатили Configuration.xml (или перезаписали из выгрузки БД), а файлы объектов остались — нужно восстановить ссылки в ``.
+
При добавлении объект вставляется в каноническую позицию:
1. Находит последний элемент того же типа → вставляет после
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
diff --git a/.claude/skills/cf-edit/scripts/cf-edit.ps1 b/.claude/skills/cf-edit/scripts/cf-edit.ps1
index fcf91b47..72e48a24 100644
--- a/.claude/skills/cf-edit/scripts/cf-edit.ps1
+++ b/.claude/skills/cf-edit/scripts/cf-edit.ps1
@@ -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) {
diff --git a/.claude/skills/cf-edit/scripts/cf-edit.py b/.claude/skills/cf-edit/scripts/cf-edit.py
index 39313f06..614902e8 100644
--- a/.claude/skills/cf-edit/scripts/cf-edit.py
+++ b/.claude/skills/cf-edit/scripts/cf-edit.py
@@ -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()
diff --git a/.claude/skills/cf-info/scripts/cf-info.ps1 b/.claude/skills/cf-info/scripts/cf-info.ps1
index c2a9e6fc..adf18ec3 100644
--- a/.claude/skills/cf-info/scripts/cf-info.ps1
+++ b/.claude/skills/cf-info/scripts/cf-info.ps1
@@ -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,
diff --git a/.claude/skills/cf-info/scripts/cf-info.py b/.claude/skills/cf-info/scripts/cf-info.py
index 836556a7..0677436a 100644
--- a/.claude/skills/cf-info/scripts/cf-info.py
+++ b/.claude/skills/cf-info/scripts/cf-info.py
@@ -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")
diff --git a/.claude/skills/cf-validate/scripts/cf-validate.ps1 b/.claude/skills/cf-validate/scripts/cf-validate.ps1
index ad13c404..06d2bcb0 100644
--- a/.claude/skills/cf-validate/scripts/cf-validate.ps1
+++ b/.claude/skills/cf-validate/scripts/cf-validate.ps1
@@ -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
diff --git a/.claude/skills/cf-validate/scripts/cf-validate.py b/.claude/skills/cf-validate/scripts/cf-validate.py
index 0c04ce77..30a901cd 100644
--- a/.claude/skills/cf-validate/scripts/cf-validate.py
+++ b/.claude/skills/cf-validate/scripts/cf-validate.py
@@ -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
diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1
index c186c3f9..099ddac2 100644
--- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1
+++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1
@@ -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 ']+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("") | Out-Null
- $formMetaSb.AppendLine("") | Out-Null
+ $formMetaSb.AppendLine("") | Out-Null
$formMetaSb.AppendLine("`t
- if '' in raw_text:
- insert_line = f'\t\t\t
\n'
- raw_text = raw_text.replace('', insert_line + '\t\t', 1)
- elif '' in raw_text:
- replacement = f'\n\t\t\t\n\t\t'
- raw_text = raw_text.replace('', replacement, 1)
-
- write_utf8_bom(object_xml_path, raw_text)
- print(f" Registered: in {object_name}.xml")
-
- # --- 5. Summary ---
- el_count = _next_id
- print(f"[OK] Compiled: {args.OutputPath}")
- print(f" Elements+IDs: {el_count}")
- if defn.get('attributes'):
- print(f" Attributes: {len(defn['attributes'])}")
- if defn.get('commands'):
- print(f" Commands: {len(defn['commands'])}")
- if defn.get('parameters'):
- print(f" Parameters: {len(defn['parameters'])}")
-
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python3
+# form-compile v1.6 — Compile 1C managed form from JSON or object metadata
+# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
+import argparse
+import copy
+import json
+import os
+import re
+import sys
+import uuid
+import xml.etree.ElementTree as ET
+from collections import OrderedDict
+
+# ═══════════════════════════════════════════════════════════════════════════
+# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation
+# ═══════════════════════════════════════════════════════════════════════════
+
+NS = {
+ 'md': 'http://v8.1c.ru/8.3/MDClasses',
+ 'xr': 'http://v8.1c.ru/8.3/xcf/readable',
+ 'v8': 'http://v8.1c.ru/8.1/data/core',
+}
+
+
+def _et_find(node, path):
+ """Find with namespace map."""
+ return node.find(path, NS)
+
+
+def _et_findall(node, path):
+ """Findall with namespace map."""
+ return node.findall(path, NS)
+
+
+def _et_text(node, path, default=''):
+ """Get text of a sub-element, or default."""
+ el = node.find(path, NS)
+ return el.text if el is not None and el.text else default
+
+
+def parse_object_meta(object_path):
+ """Parse 1C metadata XML and return dict with Type, Name, Synonym, Attributes, TabularSections, etc."""
+ tree = ET.parse(object_path)
+ root = tree.getroot()
+
+ # Detect object type from root child
+ meta_root = _et_find(root, '.')
+ # Root is MetaDataObject; first child is the type node
+ type_node = None
+ for child in root:
+ type_node = child
+ break
+ if type_node is None:
+ print("Not a 1C metadata XML: " + object_path, file=sys.stderr)
+ sys.exit(1)
+
+ # Extract local name (strip namespace)
+ obj_type = type_node.tag.split('}')[-1] if '}' in type_node.tag else type_node.tag
+
+ props_node = _et_find(type_node, 'md:Properties')
+ child_objs = _et_find(type_node, 'md:ChildObjects')
+
+ # Name
+ obj_name = _et_text(props_node, 'md:Name')
+
+ # Synonym (Russian)
+ synonym = obj_name
+ syn_node = _et_find(props_node, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
+ if syn_node is not None and syn_node.text:
+ synonym = syn_node.text
+
+ def extract_type(type_parent):
+ """Extract type string from md:Type element."""
+ if type_parent is None:
+ return 'string'
+ types = []
+ for t in _et_findall(type_parent, 'v8:Type'):
+ if t.text:
+ types.append(t.text)
+ if not types:
+ return 'string'
+ return ' | '.join(types)
+
+ def is_ref_type(t):
+ return bool(re.search(r'Ref\.', t) or re.search(r'\u0441\u0441\u044b\u043b\u043a\u0430\.', t))
+
+ def extract_fields(parent_node, tag_name='Attribute'):
+ """Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag)."""
+ result = []
+ if parent_node is None:
+ return result
+ for field_node in _et_findall(parent_node, f'md:{tag_name}'):
+ fp = _et_find(field_node, 'md:Properties')
+ f_name = _et_text(fp, 'md:Name')
+ f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
+ f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name
+ f_type_node = _et_find(fp, 'md:Type')
+ f_type = extract_type(f_type_node)
+ result.append({
+ 'Name': f_name,
+ 'Synonym': f_syn,
+ 'Type': f_type,
+ 'IsRef': is_ref_type(f_type),
+ })
+ return result
+
+ # Attributes
+ attributes = extract_fields(child_objs, 'Attribute')
+
+ # Tabular sections
+ tabular_sections = []
+ if child_objs is not None:
+ for ts_node in _et_findall(child_objs, 'md:TabularSection'):
+ tsp = _et_find(ts_node, 'md:Properties')
+ ts_name = _et_text(tsp, 'md:Name')
+ ts_syn_node = _et_find(tsp, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
+ ts_syn = ts_syn_node.text if ts_syn_node is not None and ts_syn_node.text else ts_name
+ ts_co = _et_find(ts_node, 'md:ChildObjects')
+ ts_cols = extract_fields(ts_co, 'Attribute')
+ tabular_sections.append({
+ 'Name': ts_name,
+ 'Synonym': ts_syn,
+ 'Columns': ts_cols,
+ })
+
+ meta = {
+ 'Type': obj_type,
+ 'Name': obj_name,
+ 'Synonym': synonym,
+ 'Attributes': attributes,
+ 'TabularSections': tabular_sections,
+ }
+
+ # Type-specific properties
+ if obj_type == 'Document':
+ nt_node = _et_find(props_node, 'md:NumberType')
+ meta['NumberType'] = nt_node.text if nt_node is not None and nt_node.text else 'String'
+ elif obj_type == 'Catalog':
+ cl_node = _et_find(props_node, 'md:CodeLength')
+ meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
+ dl_node = _et_find(props_node, 'md:DescriptionLength')
+ meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
+ hi_node = _et_find(props_node, 'md:Hierarchical')
+ meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true')
+ ht_node = _et_find(props_node, 'md:HierarchyType')
+ meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
+ owners = []
+ for ow in _et_findall(props_node, 'md:Owners/xr:Item'):
+ if ow.text:
+ owners.append(ow.text)
+ meta['Owners'] = owners
+ elif obj_type == 'InformationRegister':
+ meta['Dimensions'] = extract_fields(child_objs, 'Dimension')
+ meta['Resources'] = extract_fields(child_objs, 'Resource')
+ prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity')
+ meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical'
+ wm_node = _et_find(props_node, 'md:WriteMode')
+ meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent'
+ elif obj_type == 'AccumulationRegister':
+ meta['Dimensions'] = extract_fields(child_objs, 'Dimension')
+ meta['Resources'] = extract_fields(child_objs, 'Resource')
+ rt_node = _et_find(props_node, 'md:RegisterType')
+ meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances'
+ elif obj_type == 'ChartOfCharacteristicTypes':
+ cl_node = _et_find(props_node, 'md:CodeLength')
+ meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
+ dl_node = _et_find(props_node, 'md:DescriptionLength')
+ meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
+ hi_node = _et_find(props_node, 'md:Hierarchical')
+ meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true')
+ ht_node = _et_find(props_node, 'md:HierarchyType')
+ meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
+ owners = []
+ for ow in _et_findall(props_node, 'md:Owners/xr:Item'):
+ if ow.text:
+ owners.append(ow.text)
+ meta['Owners'] = owners
+ meta['HasValueType'] = True
+ elif obj_type == 'ExchangePlan':
+ cl_node = _et_find(props_node, 'md:CodeLength')
+ meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
+ dl_node = _et_find(props_node, 'md:DescriptionLength')
+ meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
+ meta['Hierarchical'] = False
+ meta['HierarchyType'] = None
+ meta['Owners'] = []
+ elif obj_type == 'ChartOfAccounts':
+ cl_node = _et_find(props_node, 'md:CodeLength')
+ meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
+ dl_node = _et_find(props_node, 'md:DescriptionLength')
+ meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
+ meta['Hierarchical'] = True
+ ht_node = _et_find(props_node, 'md:HierarchyType')
+ meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
+ meta['Owners'] = []
+ max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount')
+ meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0
+ meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag')
+ meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag')
+
+ return meta
+
+
+def _deep_merge(base, overlay):
+ """Deep merge two dicts. overlay wins on conflicts."""
+ if not overlay:
+ return base
+ if not base:
+ return overlay
+ result = {}
+ for k in base:
+ result[k] = base[k]
+ for k in overlay:
+ if k in result and isinstance(result[k], dict) and isinstance(overlay[k], dict):
+ result[k] = _deep_merge(result[k], overlay[k])
+ else:
+ result[k] = overlay[k]
+ return result
+
+
+def load_preset(preset_name, script_dir, out_path_resolved):
+ """Load preset: hardcoded defaults -> built-in JSON -> project-level JSON, with deep merge."""
+ defaults = {
+ 'document.item': {
+ 'header': {'position': 'insidePage', 'layout': '2col', 'distribute': 'even', 'dateTitle': '\u043e\u0442'},
+ 'footer': {'fields': ['\u041a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439'], 'position': 'insidePage'},
+ 'tabularSections': {'container': 'pages', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'], 'lineNumber': True},
+ 'additional': {'position': 'page', 'layout': '2col', 'bspGroup': True},
+ 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
+ 'commandBar': 'auto',
+ 'properties': {'autoTitle': False},
+ },
+ 'document.list': {
+ 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True,
+ 'tableCommandBar': 'none', 'commandBar': 'auto',
+ 'properties': {},
+ },
+ 'document.choice': {
+ 'basedOn': 'document.list',
+ 'properties': {'windowOpeningMode': 'LockOwnerWindow'},
+ },
+ 'catalog.item': {
+ 'header': {'layout': '1col', 'distribute': 'left'},
+ 'codeDescription': {'layout': 'horizontal', 'order': 'descriptionFirst'},
+ 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443', 'position': 'afterCodeDescription'},
+ 'owner': {'readOnly': True, 'position': 'first'},
+ 'tabularSections': {'container': 'inline', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'], 'lineNumber': True},
+ 'footer': {'fields': [], 'position': 'none'},
+ 'additional': {'position': 'none', 'bspGroup': True},
+ 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
+ 'commandBar': 'auto',
+ 'properties': {},
+ },
+ 'catalog.folder': {
+ 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443'},
+ 'properties': {'windowOpeningMode': 'LockOwnerWindow'},
+ },
+ 'catalog.list': {
+ 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True,
+ 'tableCommandBar': 'none', 'commandBar': 'auto',
+ 'properties': {},
+ },
+ 'catalog.choice': {
+ 'basedOn': 'catalog.list', 'choiceMode': True,
+ 'properties': {'windowOpeningMode': 'LockOwnerWindow'},
+ },
+ # --- Register defaults ---
+ 'informationRegister.record': {
+ 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
+ 'properties': {'windowOpeningMode': 'LockOwnerWindow'},
+ },
+ 'informationRegister.list': {
+ 'columns': 'all', 'columnType': 'labelField',
+ 'tableCommandBar': 'none', 'commandBar': 'auto',
+ 'properties': {},
+ },
+ 'accumulationRegister.list': {
+ 'columns': 'all', 'columnType': 'labelField',
+ 'tableCommandBar': 'none', 'commandBar': 'auto',
+ 'properties': {},
+ },
+ # --- Catalog-like type defaults ---
+ 'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'},
+ 'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'},
+ 'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'},
+ 'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'},
+ 'exchangePlan.item': {'basedOn': 'catalog.item'},
+ 'exchangePlan.list': {'basedOn': 'catalog.list'},
+ 'exchangePlan.choice': {'basedOn': 'catalog.choice'},
+ # --- ChartOfAccounts defaults ---
+ 'chartOfAccounts.item': {
+ 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'},
+ 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
+ 'properties': {},
+ },
+ 'chartOfAccounts.folder': {
+ 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'},
+ 'properties': {'windowOpeningMode': 'LockOwnerWindow'},
+ },
+ 'chartOfAccounts.list': {'basedOn': 'catalog.list'},
+ 'chartOfAccounts.choice': {'basedOn': 'catalog.choice'},
+ }
+
+ # Try built-in preset
+ preset_dir = os.path.join(os.path.dirname(script_dir), 'presets')
+ built_in_path = os.path.join(preset_dir, f'{preset_name}.json')
+ if os.path.isfile(built_in_path):
+ with open(built_in_path, 'r', encoding='utf-8-sig') as f:
+ preset_data = json.load(f)
+ for k in list(preset_data.keys()):
+ defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
+
+ # Try project-level preset (scan up from output path)
+ scan_dir = os.path.dirname(out_path_resolved)
+ while scan_dir:
+ proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json')
+ if os.path.isfile(proj_preset):
+ with open(proj_preset, 'r', encoding='utf-8-sig') as f:
+ proj_data = json.load(f)
+ for k in list(proj_data.keys()):
+ defaults[k] = _deep_merge(defaults.get(k), proj_data[k])
+ break
+ parent_dir = os.path.dirname(scan_dir)
+ if parent_dir == scan_dir:
+ break
+ scan_dir = parent_dir
+
+ # Resolve basedOn references
+ for k in list(defaults.keys()):
+ sect = defaults[k]
+ if isinstance(sect, dict) and 'basedOn' in sect:
+ base_name = sect['basedOn']
+ if base_name in defaults:
+ merged = _deep_merge(defaults[base_name], sect)
+ merged.pop('basedOn', None)
+ defaults[k] = merged
+
+ return defaults
+
+
+# Non-displayable types — cannot be bound to form elements
+NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения')
+
+def is_displayable_type(type_str):
+ return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES)
+
+def new_field_element(attr_name, data_path, attr_type, field_defaults, extra_props=None):
+ """Build a field element DSL entry."""
+ is_ref = bool(re.search(r'Ref\.', attr_type))
+ is_bool = bool(re.match(r'^\s*xs:boolean\s*$', attr_type) or attr_type == 'boolean' or re.search(r'Boolean', attr_type))
+
+ el_type = 'input'
+ if is_bool and field_defaults and field_defaults.get('boolean') and field_defaults['boolean'].get('element') == 'check':
+ el_type = 'check'
+
+ el = OrderedDict()
+ el[el_type] = attr_name
+ el['path'] = data_path
+
+ # Apply ref defaults
+ if is_ref and field_defaults and field_defaults.get('ref'):
+ if field_defaults['ref'].get('choiceButton') is True:
+ el['choiceButton'] = True
+
+ # Extra props
+ if extra_props:
+ for k in extra_props:
+ el[k] = extra_props[k]
+
+ return el
+
+
+# --- Catalog DSL generators ---
+
+def generate_catalog_dsl(meta, preset_data, purpose):
+ purpose_key = f"catalog.{purpose.lower()}"
+ p = preset_data.get(purpose_key, {})
+ fd = p.get('fieldDefaults', {})
+
+ dispatch = {
+ 'Folder': lambda: generate_catalog_folder_dsl(meta, p),
+ 'List': lambda: generate_catalog_list_dsl(meta, p),
+ 'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data),
+ 'Item': lambda: generate_catalog_item_dsl(meta, p, fd),
+ }
+ return dispatch[purpose]()
+
+
+def generate_catalog_folder_dsl(meta, p):
+ elements = []
+ # Code (if CodeLength > 0)
+ if meta.get('CodeLength', 0) > 0:
+ elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
+ # Description
+ elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
+ # Parent
+ parent_title = p.get('parent', {}).get('title')
+ parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')])
+ if parent_title:
+ parent_el['title'] = parent_title
+ elements.append(parent_el)
+
+ props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')])
+ if p.get('properties'):
+ for k in p['properties']:
+ props[k] = p['properties'][k]
+
+ form_props = OrderedDict([('useForFoldersAndItems', 'Folders')])
+ for k in props:
+ form_props[k] = props[k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', form_props),
+ ('elements', elements),
+ ('attributes', [
+ OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)])
+ ]),
+ ])
+
+
+def generate_catalog_list_dsl(meta, p):
+ columns = []
+ # Description always first
+ columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')]))
+ # Code if present
+ if meta.get('CodeLength', 0) > 0:
+ columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')]))
+ # Custom attributes
+ for attr in meta['Attributes']:
+ if not is_displayable_type(attr['Type']):
+ continue
+ columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
+ # Hidden ref
+ if p.get('hiddenRef', True) is not False:
+ columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)]))
+
+ table_el = OrderedDict([
+ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
+ ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
+ ('commandBarLocation', 'None'),
+ ('tableAutofill', False),
+ ('columns', columns),
+ ])
+ # Hierarchical properties
+ if meta.get('Hierarchical'):
+ table_el['initialTreeView'] = 'ExpandTopLevel'
+ table_el['enableStartDrag'] = True
+ table_el['enableDrag'] = True
+
+ form_props = OrderedDict()
+ if p.get('properties'):
+ for k in p['properties']:
+ form_props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', form_props),
+ ('elements', [table_el]),
+ ('attributes', [
+ OrderedDict([
+ ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True),
+ ('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])),
+ ])
+ ]),
+ ])
+
+
+def generate_catalog_choice_dsl(meta, p, preset_data):
+ # Start from list
+ list_key = 'catalog.list'
+ lp = preset_data.get(list_key, {})
+ dsl = generate_catalog_list_dsl(meta, lp)
+
+ # Add choice-specific properties
+ dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow'
+ if p.get('properties'):
+ for k in p['properties']:
+ dsl['properties'][k] = p['properties'][k]
+
+ # Set ChoiceMode on table
+ dsl['elements'][0]['choiceMode'] = True
+
+ return dsl
+
+
+def generate_catalog_item_dsl(meta, p, fd):
+ header_children = []
+
+ # Owner (if subordinate)
+ if meta.get('Owners') and len(meta['Owners']) > 0:
+ owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)])
+ header_children.append(owner_el)
+
+ # Code + Description
+ cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal')
+ cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst')
+ has_code = meta.get('CodeLength', 0) > 0
+
+ if cd_layout == 'horizontal' and has_code:
+ cd_children = []
+ desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])
+ code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])
+ if cd_order == 'descriptionFirst':
+ cd_children = [desc_el, code_el]
+ else:
+ cd_children = [code_el, desc_el]
+ header_children.append(OrderedDict([
+ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False),
+ ('representation', 'none'), ('children', cd_children),
+ ]))
+ else:
+ # Vertical or no code
+ header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
+ if has_code:
+ header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
+
+ # Parent (for hierarchical catalogs)
+ parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription')
+ parent_title = (p.get('parent') or {}).get('title')
+ if meta.get('Hierarchical'):
+ parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')])
+ if parent_title:
+ parent_el['title'] = parent_title
+ if parent_pos == 'beforeCodeDescription':
+ insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0
+ header_children.insert(insert_idx, parent_el)
+ else:
+ # afterCodeDescription (default)
+ header_children.append(parent_el)
+
+ # Custom attributes -> header
+ footer_field_names = (p.get('footer') or {}).get('fields', [])
+
+ for attr in meta['Attributes']:
+ if attr['Name'] in footer_field_names:
+ continue
+ if not is_displayable_type(attr['Type']):
+ continue
+ header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
+
+ # Build root elements
+ root_elements = []
+
+ # ГруппаШапка
+ root_elements.append(OrderedDict([
+ ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False),
+ ('representation', 'none'), ('children', header_children),
+ ]))
+
+ # Tabular sections
+ ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f']
+ if (p.get('tabularSections') or {}).get('exclude'):
+ ts_exclude = p['tabularSections']['exclude']
+ ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True)
+
+ visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude]
+
+ for ts in visible_ts:
+ ts_cols = []
+ if ts_line_number:
+ ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")]))
+ for col in ts['Columns']:
+ ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
+ root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]))
+
+ # Footer fields
+ for fn in footer_field_names:
+ f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None)
+ if f_attr:
+ root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd))
+
+ # BSP group
+ bsp_group = (p.get('additional') or {}).get('bspGroup', True)
+ if bsp_group:
+ root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
+
+ # Properties
+ form_props = OrderedDict()
+ if p.get('properties'):
+ for k in p['properties']:
+ form_props[k] = p['properties'][k]
+ # UseForFoldersAndItems
+ if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems':
+ form_props['useForFoldersAndItems'] = 'Items'
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', form_props),
+ ('elements', root_elements),
+ ('attributes', [
+ OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)])
+ ]),
+ ])
+
+
+# --- Document DSL generators ---
+
+def generate_document_dsl(meta, preset_data, purpose):
+ purpose_key = f"document.{purpose.lower()}"
+ p = preset_data.get(purpose_key, {})
+ fd = p.get('fieldDefaults', {})
+
+ dispatch = {
+ 'List': lambda: generate_document_list_dsl(meta, p),
+ 'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data),
+ 'Item': lambda: generate_document_item_dsl(meta, p, fd),
+ }
+ return dispatch[purpose]()
+
+
+def generate_document_list_dsl(meta, p):
+ columns = []
+ # Standard columns: Number + Date
+ columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')]))
+ columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')]))
+ # All custom attributes as labelField
+ for attr in meta['Attributes']:
+ if not is_displayable_type(attr['Type']):
+ continue
+ columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
+ # Hidden ref
+ if p.get('hiddenRef', True):
+ columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)]))
+
+ table_el = OrderedDict([
+ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
+ ('commandBarLocation', 'None'),
+ ('tableAutofill', False),
+ ('columns', columns),
+ ])
+
+ form_props = OrderedDict()
+ if p.get('properties'):
+ for k in p['properties']:
+ form_props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', form_props),
+ ('elements', [table_el]),
+ ('attributes', [
+ OrderedDict([
+ ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True),
+ ('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])),
+ ])
+ ]),
+ ])
+
+
+def generate_document_choice_dsl(meta, p, preset_data):
+ list_key = 'document.list'
+ lp = preset_data.get(list_key, {})
+ dsl = generate_document_list_dsl(meta, lp)
+
+ dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow'
+ if p.get('properties'):
+ for k in p['properties']:
+ dsl['properties'][k] = p['properties'][k]
+
+ return dsl
+
+
+def generate_document_item_dsl(meta, p, fd):
+ header_pos = (p.get('header') or {}).get('position', 'insidePage')
+ header_layout = (p.get('header') or {}).get('layout', '2col')
+ header_distribute = (p.get('header') or {}).get('distribute', 'even')
+ date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442')
+
+ footer_fields = (p.get('footer') or {}).get('fields', [])
+ footer_pos = (p.get('footer') or {}).get('position', 'insidePage')
+
+ add_pos = (p.get('additional') or {}).get('position', 'page')
+ add_layout = (p.get('additional') or {}).get('layout', '2col')
+ add_bsp_group = (p.get('additional') or {}).get('bspGroup', True)
+ add_left = (p.get('additional') or {}).get('left', [])
+ add_right = (p.get('additional') or {}).get('right', [])
+
+ header_right = (p.get('header') or {}).get('right', [])
+
+ ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b']
+ if (p.get('tabularSections') or {}).get('exclude'):
+ ts_exclude = p['tabularSections']['exclude']
+ ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True)
+
+ # Classify attributes
+ claimed = {}
+ for fn in footer_fields:
+ claimed[fn] = 'footer'
+ for fn in header_right:
+ claimed[fn] = 'header.right'
+ for fn in add_left:
+ claimed[fn] = 'additional.left'
+ for fn in add_right:
+ claimed[fn] = 'additional.right'
+
+ unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed and is_displayable_type(attr['Type'])]
+
+ # Distribute unclaimed
+ left_attrs = []
+ right_extra_attrs = []
+ if header_distribute == 'left':
+ left_attrs = unclaimed
+ elif header_distribute == 'right':
+ right_extra_attrs = unclaimed
+ else: # "even"
+ import math
+ half = math.ceil(len(unclaimed) / 2) if unclaimed else 0
+ left_attrs = unclaimed[:half]
+ right_extra_attrs = unclaimed[half:]
+
+ # Build ГруппаНомерДата
+ num_date_children = [
+ OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]),
+ OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]),
+ ]
+ num_date_group = OrderedDict([
+ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children),
+ ])
+
+ # Build left column
+ left_children = [num_date_group]
+ for attr in left_attrs:
+ left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
+
+ # Build right column
+ right_children = []
+ for rn in header_right:
+ r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None)
+ if r_attr:
+ right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd))
+ for attr in right_extra_attrs:
+ right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
+
+ # Header group
+ if header_layout == '2col' and len(right_children) > 0:
+ header_group = OrderedDict([
+ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'),
+ ('children', [
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]),
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]),
+ ]),
+ ])
+ else:
+ # 1col or no right items
+ all_header_fields = left_children + right_children
+ header_group = OrderedDict([
+ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'),
+ ('children', [
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]),
+ ]),
+ ])
+
+ # Footer elements
+ footer_elements = []
+ for fn in footer_fields:
+ f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None)
+ if f_attr:
+ footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd))
+
+ # Visible tabular sections
+ visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude]
+
+ # Additional page content
+ additional_page = None
+ if add_pos == 'page':
+ add_left_els = []
+ add_right_els = []
+ for aln in add_left:
+ al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None)
+ if al_attr:
+ add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd))
+ for arn in add_right:
+ ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None)
+ if ar_attr:
+ add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd))
+ add_page_children = []
+ if add_layout == '2col':
+ add_page_children.append(OrderedDict([
+ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False),
+ ('children', [
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]),
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]),
+ ]),
+ ]))
+ else:
+ add_page_children.extend(add_left_els + add_right_els)
+ if add_bsp_group:
+ add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
+ additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)])
+
+ # Build TS page elements
+ ts_pages = []
+ for ts in visible_ts:
+ ts_cols = []
+ if ts_line_number:
+ ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")]))
+ for col in ts['Columns']:
+ ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
+ ts_pages.append(OrderedDict([
+ ('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']),
+ ('children', [
+ OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])
+ ]),
+ ]))
+
+ # Assemble root elements
+ root_elements = []
+
+ if len(visible_ts) == 0:
+ # Simple form - no Pages
+ root_elements.append(header_group)
+ if footer_elements:
+ root_elements.extend(footer_elements)
+ if add_bsp_group and add_pos != 'none':
+ root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
+ else:
+ # Pages form
+ if header_pos == 'abovePages':
+ root_elements.append(header_group)
+ pages_children = list(ts_pages)
+ if additional_page:
+ pages_children.append(additional_page)
+ root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)]))
+ else:
+ # insidePage (default)
+ osnovnoe_children = [header_group]
+ if footer_pos == 'insidePage' and footer_elements:
+ osnovnoe_children.extend(footer_elements)
+ pages_children = []
+ pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)]))
+ pages_children.extend(ts_pages)
+ if additional_page:
+ pages_children.append(additional_page)
+ root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)]))
+
+ # Footer below pages
+ if footer_pos == 'belowPages' and footer_elements:
+ root_elements.extend(footer_elements)
+
+ # Properties
+ form_props = OrderedDict([('autoTitle', False)])
+ if p.get('properties'):
+ for k in p['properties']:
+ form_props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', form_props),
+ ('elements', root_elements),
+ ('attributes', [
+ OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)])
+ ]),
+ ])
+
+
+# --- InformationRegister DSL generators ---
+
+def generate_information_register_dsl(meta, preset_data, purpose):
+ p_key = f"informationRegister.{purpose.lower()}"
+ p = preset_data.get(p_key, {})
+ fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}
+ dispatch = {
+ 'Record': lambda: generate_information_register_record_dsl(meta, p, fd),
+ 'List': lambda: generate_information_register_list_dsl(meta, p),
+ }
+ return dispatch[purpose]()
+
+
+def generate_information_register_record_dsl(meta, p, fd):
+ elements = OrderedDict()
+ is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical'
+
+ # Period first (if periodic)
+ if is_periodic:
+ elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'}
+ # Dimensions
+ for dim in meta.get('Dimensions', []):
+ if not is_displayable_type(dim['Type']):
+ continue
+ elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd)
+ # Resources
+ for res in meta.get('Resources', []):
+ if not is_displayable_type(res['Type']):
+ continue
+ elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd)
+ # Attributes
+ for attr in meta['Attributes']:
+ if not is_displayable_type(attr['Type']):
+ continue
+ elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd)
+
+ props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')])
+ if p.get('properties'):
+ for k in p['properties']:
+ props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', props),
+ ('elements', elements),
+ ('attributes', [
+ {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True}
+ ]),
+ ])
+
+
+def generate_information_register_list_dsl(meta, p):
+ is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical'
+ is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate'
+
+ columns_list = []
+ # Period
+ if is_periodic:
+ columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')]))
+ # Recorder/LineNumber for subordinate registers
+ if is_recorder_subordinate:
+ columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')]))
+ columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')]))
+ # Dimensions
+ for dim in meta.get('Dimensions', []):
+ if not is_displayable_type(dim['Type']):
+ continue
+ columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")]))
+ # Resources
+ for res in meta.get('Resources', []):
+ if not is_displayable_type(res['Type']):
+ continue
+ el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField'
+ columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")]))
+ # Attributes
+ for attr in meta['Attributes']:
+ if not is_displayable_type(attr['Type']):
+ continue
+ el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField'
+ columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
+
+ table_el = OrderedDict([
+ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'),
+ ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
+ ('commandBarLocation', 'None'),
+ ('tableAutofill', False),
+ ('columns', columns_list),
+ ])
+
+ props = OrderedDict()
+ if p.get('properties'):
+ for k in p['properties']:
+ props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', props),
+ ('elements', [table_el]),
+ ('attributes', [
+ {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}}
+ ]),
+ ])
+
+
+# --- AccumulationRegister DSL generators ---
+
+def generate_accumulation_register_dsl(meta, preset_data, purpose):
+ p_key = f"accumulationRegister.{purpose.lower()}"
+ p = preset_data.get(p_key, {})
+ dispatch = {
+ 'List': lambda: generate_accumulation_register_list_dsl(meta, p),
+ }
+ return dispatch[purpose]()
+
+
+def generate_accumulation_register_list_dsl(meta, p):
+ columns_list = []
+ # AccumulationRegisters always have Period, Recorder, LineNumber
+ columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')]))
+ columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')]))
+ columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')]))
+ # Dimensions
+ for dim in meta.get('Dimensions', []):
+ if not is_displayable_type(dim['Type']):
+ continue
+ columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")]))
+ # Resources
+ for res in meta.get('Resources', []):
+ if not is_displayable_type(res['Type']):
+ continue
+ el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField'
+ columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")]))
+ # Attributes
+ for attr in meta['Attributes']:
+ if not is_displayable_type(attr['Type']):
+ continue
+ el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField'
+ columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
+
+ table_el = OrderedDict([
+ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'),
+ ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
+ ('commandBarLocation', 'None'),
+ ('tableAutofill', False),
+ ('columns', columns_list),
+ ])
+
+ props = OrderedDict()
+ if p.get('properties'):
+ for k in p['properties']:
+ props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', props),
+ ('elements', [table_el]),
+ ('attributes', [
+ {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}}
+ ]),
+ ])
+
+
+# --- ChartOfCharacteristicTypes (delegates to Catalog) ---
+
+def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose):
+ # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc.
+ dsl = generate_catalog_dsl(meta, preset_data, purpose)
+
+ # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types
+ cat_obj_type = f"CatalogObject.{meta['Name']}"
+ ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}"
+ cat_list_type = f"Catalog.{meta['Name']}"
+ ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}"
+
+ for a in dsl['attributes']:
+ if a.get('type') == cat_obj_type:
+ a['type'] = ccoct_obj_type
+ if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type:
+ a['settings']['mainTable'] = ccoct_list_type
+
+ # For Item forms: inject ValueType field after Description/ГруппаКодНаименование
+ if purpose == 'Item' and dsl.get('elements'):
+ vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')])
+ els = dsl['elements']
+ if isinstance(els, list):
+ inserted = False
+ new_els = []
+ for el in els:
+ new_els.append(el)
+ if not inserted and isinstance(el, dict):
+ name = el.get('input') or el.get('group') or ''
+ if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'):
+ new_els.append(vt_el)
+ inserted = True
+ if not inserted:
+ new_els.append(vt_el)
+ dsl['elements'] = new_els
+
+ return dsl
+
+
+# --- ExchangePlan (delegates to Catalog) ---
+
+def generate_exchange_plan_dsl(meta, preset_data, purpose):
+ # ExchangePlans are not hierarchical and have no Folder form
+ dsl = generate_catalog_dsl(meta, preset_data, purpose)
+
+ # Post-patch: replace Catalog types with ExchangePlan types
+ cat_obj_type = f"CatalogObject.{meta['Name']}"
+ ep_obj_type = f"ExchangePlanObject.{meta['Name']}"
+ cat_list_type = f"Catalog.{meta['Name']}"
+ ep_list_type = f"ExchangePlan.{meta['Name']}"
+
+ for a in dsl['attributes']:
+ if a.get('type') == cat_obj_type:
+ a['type'] = ep_obj_type
+ if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type:
+ a['settings']['mainTable'] = ep_list_type
+
+ # For Item forms: inject SentNo, ReceivedNo after Code/Description
+ if purpose == 'Item' and dsl.get('elements'):
+ sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)])
+ recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)])
+ els = dsl['elements']
+ if isinstance(els, list):
+ inserted = False
+ new_els = []
+ for el in els:
+ new_els.append(el)
+ if not inserted and isinstance(el, dict):
+ name = el.get('input') or el.get('group') or ''
+ if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'):
+ new_els.append(sent_el)
+ new_els.append(recv_el)
+ inserted = True
+ if not inserted:
+ new_els.append(sent_el)
+ new_els.append(recv_el)
+ dsl['elements'] = new_els
+
+ return dsl
+
+
+# --- ChartOfAccounts DSL generators ---
+
+def generate_chart_of_accounts_dsl(meta, preset_data, purpose):
+ p_key = f"chartOfAccounts.{purpose.lower()}"
+ p = preset_data.get(p_key, {})
+ fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}
+ dispatch = {
+ 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data),
+ 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p),
+ 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data),
+ 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data),
+ }
+ return dispatch[purpose]()
+
+
+def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data):
+ elements = []
+
+ # Header: Code + Parent
+ header_left_children = []
+ if meta.get('CodeLength', 0) > 0:
+ header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
+ header_right_children = []
+ if meta.get('Hierarchical'):
+ parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443')
+ header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)]))
+
+ if len(header_right_children) > 0:
+ elements.append(OrderedDict([
+ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'),
+ ('children', [
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]),
+ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]),
+ ]),
+ ]))
+ elif len(header_left_children) > 0:
+ elements.extend(header_left_children)
+
+ # Description
+ if meta.get('DescriptionLength', 0) > 0:
+ elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
+
+ # OffBalance
+ elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')]))
+
+ # AccountingFlags as checkboxes
+ if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0:
+ flag_children = []
+ for flag in meta['AccountingFlags']:
+ flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")]))
+ elements.append(OrderedDict([
+ ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'),
+ ('children', flag_children),
+ ]))
+
+ # ExtDimensionTypes table
+ if meta.get('MaxExtDimensionCount', 0) > 0:
+ ed_cols = []
+ ed_cols.append(OrderedDict([('input', '\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')]))
+ ed_cols.append(OrderedDict([('check', '\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')]))
+ if meta.get('ExtDimensionAccountingFlags'):
+ for ed_flag in meta['ExtDimensionAccountingFlags']:
+ ed_cols.append(OrderedDict([('check', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")]))
+ elements.append(OrderedDict([
+ ('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'),
+ ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'),
+ ('columns', ed_cols),
+ ]))
+
+ # Custom attributes
+ for attr in meta['Attributes']:
+ if not is_displayable_type(attr['Type']):
+ continue
+ elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
+
+ # Tabular sections
+ ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f']
+ for ts in meta['TabularSections']:
+ if ts['Name'] in ts_exclude:
+ continue
+ ts_cols = []
+ for col in ts['Columns']:
+ if not is_displayable_type(col['Type']):
+ continue
+ ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
+ elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]))
+
+ props = OrderedDict()
+ if p.get('properties'):
+ for k in p['properties']:
+ props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('properties', props),
+ ('elements', elements),
+ ('attributes', [
+ {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True}
+ ]),
+ ])
+
+
+def generate_chart_of_accounts_folder_dsl(meta, p):
+ elements = []
+ if meta.get('CodeLength', 0) > 0:
+ elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
+ if meta.get('DescriptionLength', 0) > 0:
+ elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
+ if meta.get('Hierarchical'):
+ parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443')
+ elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)]))
+
+ props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')])
+ if p.get('properties'):
+ for k in p['properties']:
+ props[k] = p['properties'][k]
+
+ return OrderedDict([
+ ('title', meta['Synonym']),
+ ('useForFoldersAndItems', 'Folders'),
+ ('properties', props),
+ ('elements', elements),
+ ('attributes', [
+ {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True}
+ ]),
+ ])
+
+
+def generate_chart_of_accounts_list_dsl(meta, preset_data):
+ # Delegate to Catalog List and patch types
+ dsl = generate_catalog_dsl(meta, preset_data, 'List')
+ for a in dsl['attributes']:
+ if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}":
+ a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}"
+ return dsl
+
+
+def generate_chart_of_accounts_choice_dsl(meta, preset_data):
+ dsl = generate_catalog_dsl(meta, preset_data, 'Choice')
+ for a in dsl['attributes']:
+ if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}":
+ a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}"
+ return dsl
+
+
+# ═══════════════════════════════════════════════════════════════════════════
+# END OF FROM-OBJECT MODE FUNCTIONS
+# ═══════════════════════════════════════════════════════════════════════════
+
+
+def esc_xml(s):
+ return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
+
+
+def emit_mltext(lines, indent, tag, text):
+ if not text:
+ lines.append(f"{indent}<{tag}/>")
+ return
+ lines.append(f"{indent}<{tag}>")
+ lines.append(f"{indent}\t")
+ lines.append(f"{indent}\t\tru")
+ lines.append(f"{indent}\t\t{esc_xml(text)}")
+ lines.append(f"{indent}\t")
+ lines.append(f"{indent}{tag}>")
+
+
+def new_uuid():
+ return str(uuid.uuid4())
+
+
+def write_utf8_bom(path, content):
+ with open(path, 'w', encoding='utf-8-sig', newline='') as f:
+ f.write(content)
+
+
+# --- ID allocator ---
+_next_id = 0
+
+def new_id():
+ global _next_id
+ _next_id += 1
+ return _next_id
+
+
+# --- Event handler name generator ---
+
+EVENT_SUFFIX_MAP = {
+ "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438",
+ "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430",
+ "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430",
+ "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440",
+ "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430",
+ "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435",
+ "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435",
+ "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438",
+ "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
+ "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c",
+ "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f",
+ "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
+ "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
+ "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438",
+ "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b",
+ "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430",
+ "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438",
+ "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f",
+ "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435",
+ "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f",
+ "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
+ "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f",
+}
+
+KNOWN_EVENTS = {
+ "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
+ "check": ["OnChange"],
+ "label": ["Click", "URLProcessing"],
+ "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"],
+ "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"],
+ "pages": ["OnCurrentPageChange"],
+ "page": ["OnCurrentPageChange"],
+ "button": ["Click"],
+ "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"],
+ "calendar": ["OnChange", "OnActivate"],
+ "picture": ["Click"],
+ "cmdBar": [],
+ "popup": [],
+ "group": [],
+}
+
+KNOWN_FORM_EVENTS = [
+ "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing",
+ "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer",
+ "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer",
+ "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer",
+ "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening",
+]
+
+KNOWN_KEYS = {
+ "group", "input", "check", "label", "labelField", "table", "pages", "page",
+ "button", "picture", "picField", "calendar", "cmdBar", "popup",
+ "name", "path", "title",
+ "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible",
+ "on", "handlers",
+ "titleLocation", "representation", "width", "height",
+ "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight",
+ "multiLine", "passwordMode", "choiceButton", "clearButton",
+ "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint",
+ "hyperlink",
+ "showTitle", "united",
+ "children", "columns",
+ "changeRowSet", "changeRowOrder", "header", "footer",
+ "commandBarLocation", "searchStringLocation",
+ "pagesRepresentation",
+ "type", "command", "stdCommand", "defaultButton", "locationInCommandBar",
+ "src",
+ "autofill",
+ "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag",
+ "rowPictureDataPath", "tableAutofill",
+}
+
+TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page",
+ "button", "picture", "picField", "calendar", "cmdBar", "popup"]
+
+
+def get_handler_name(element_name, event_name):
+ suffix = EVENT_SUFFIX_MAP.get(event_name)
+ if suffix:
+ return f"{element_name}{suffix}"
+ return f"{element_name}{event_name}"
+
+
+def get_element_name(el, type_key):
+ if el.get('name'):
+ return str(el['name'])
+ return str(el.get(type_key, ''))
+
+
+def emit_events(lines, el, element_name, indent, type_key):
+ if not el.get('on'):
+ return
+
+ # Validate event names
+ if type_key and type_key in KNOWN_EVENTS:
+ allowed = KNOWN_EVENTS[type_key]
+ for evt in el['on']:
+ if allowed and str(evt) not in allowed:
+ print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}")
+
+ lines.append(f"{indent}")
+ for evt in el['on']:
+ evt_name = str(evt)
+ handlers = el.get('handlers')
+ if handlers and handlers.get(evt_name):
+ handler = str(handlers[evt_name])
+ else:
+ handler = get_handler_name(element_name, evt_name)
+ lines.append(f'{indent}\t{handler}')
+ lines.append(f"{indent}")
+
+
+def emit_companion(lines, tag, name, indent):
+ cid = new_id()
+ lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>')
+
+
+def emit_common_flags(lines, el, indent):
+ if el.get('visible') is False or el.get('hidden') is True:
+ lines.append(f"{indent}false")
+ if el.get('userVisible') is False:
+ lines.append(f"{indent}")
+ lines.append(f"{indent}\tfalse")
+ lines.append(f"{indent}")
+ if el.get('enabled') is False or el.get('disabled') is True:
+ lines.append(f"{indent}false")
+ if el.get('readOnly') is True:
+ lines.append(f"{indent}true")
+
+
+def emit_title(lines, el, name, indent):
+ if el.get('title'):
+ emit_mltext(lines, indent, 'Title', str(el['title']))
+
+
+# --- Type emitter ---
+
+V8_TYPES = {
+ "ValueTable": "v8:ValueTable",
+ "ValueTree": "v8:ValueTree",
+ "ValueList": "v8:ValueListType",
+ "TypeDescription": "v8:TypeDescription",
+ "Universal": "v8:Universal",
+ "FixedArray": "v8:FixedArray",
+ "FixedStructure": "v8:FixedStructure",
+}
+
+UI_TYPES = {
+ "FormattedString": "v8ui:FormattedString",
+ "Picture": "v8ui:Picture",
+ "Color": "v8ui:Color",
+ "Font": "v8ui:Font",
+}
+
+DCS_MAP = {
+ "DataCompositionSettings": "dcsset:DataCompositionSettings",
+ "DataCompositionSchema": "dcssch:DataCompositionSchema",
+ "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType",
+}
+
+CFG_REF_PATTERN = re.compile(
+ r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|'
+ r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|'
+ r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|'
+ r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|'
+ r'InformationRegisterRecordSet|InformationRegisterRecordManager|'
+ r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|'
+ r'ConstantsSet|DataProcessorObject|ReportObject)\.'
+)
+
+KNOWN_INVALID_TYPES = {
+ 'FormDataStructure': 'Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)',
+ 'FormDataCollection': 'Runtime type. Use ValueTable',
+ 'FormDataTree': 'Runtime type. Use ValueTree',
+ 'FormDataTreeItem': 'Runtime type, not valid in XML',
+ 'FormDataCollectionItem': 'Runtime type, not valid in XML',
+ 'FormGroup': 'UI element type, not a data type',
+ 'FormField': 'UI element type, not a data type',
+ 'FormButton': 'UI element type, not a data type',
+ 'FormDecoration': 'UI element type, not a data type',
+ 'FormTable': 'UI element type, not a data type',
+}
+
+
+_FORM_TYPE_SYNONYMS = {
+ "строка": "string", "число": "decimal", "булево": "boolean",
+ "дата": "date", "датавремя": "dateTime",
+ "number": "decimal", "bool": "boolean",
+ "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject",
+ "документссылка": "DocumentRef", "документобъект": "DocumentObject",
+ "перечислениессылка": "EnumRef",
+ "плансчетовссылка": "ChartOfAccountsRef",
+ "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef",
+ "планвидоврасчётассылка": "ChartOfCalculationTypesRef",
+ "планвидоврасчетассылка": "ChartOfCalculationTypesRef",
+ "планобменассылка": "ExchangePlanRef",
+ "бизнеспроцессссылка": "BusinessProcessRef",
+ "задачассылка": "TaskRef",
+ "определяемыйтип": "DefinedType",
+}
+
+
+def resolve_type_str(type_str):
+ if not type_str:
+ return type_str
+ m = re.match(r'^([^(]+)\((.+)\)$', type_str)
+ if m:
+ base, params = m.group(1).strip(), m.group(2)
+ r = _FORM_TYPE_SYNONYMS.get(base.lower())
+ return f"{r}({params})" if r else type_str
+ if '.' in type_str:
+ i = type_str.index('.')
+ prefix, suffix = type_str[:i], type_str[i:]
+ r = _FORM_TYPE_SYNONYMS.get(prefix.lower())
+ return f"{r}{suffix}" if r else type_str
+ r = _FORM_TYPE_SYNONYMS.get(type_str.lower())
+ return r if r else type_str
+
+
+def emit_single_type(lines, type_str, indent):
+ type_str = resolve_type_str(type_str)
+ # boolean
+ if type_str == 'boolean':
+ lines.append(f'{indent}xs:boolean')
+ return
+
+ # string or string(N)
+ m = re.match(r'^string(\((\d+)\))?$', type_str)
+ if m:
+ length = m.group(2) if m.group(2) else '0'
+ lines.append(f'{indent}xs:string')
+ lines.append(f'{indent}')
+ lines.append(f'{indent}\t{length}')
+ lines.append(f'{indent}\tVariable')
+ lines.append(f'{indent}')
+ return
+
+ # decimal(D,F) or decimal(D,F,nonneg)
+ m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str)
+ if m:
+ digits = m.group(1)
+ fraction = m.group(2)
+ sign = 'Nonnegative' if m.group(3) else 'Any'
+ lines.append(f'{indent}xs:decimal')
+ lines.append(f'{indent}')
+ lines.append(f'{indent}\t{digits}')
+ lines.append(f'{indent}\t{fraction}')
+ lines.append(f'{indent}\t{sign}')
+ lines.append(f'{indent}')
+ return
+
+ # date / dateTime / time
+ m = re.match(r'^(date|dateTime|time)$', type_str)
+ if m:
+ fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'}
+ fractions = fractions_map[type_str]
+ lines.append(f'{indent}xs:dateTime')
+ lines.append(f'{indent}')
+ lines.append(f'{indent}\t{fractions}')
+ lines.append(f'{indent}')
+ return
+
+ # V8 types
+ if type_str in V8_TYPES:
+ lines.append(f'{indent}{V8_TYPES[type_str]}')
+ return
+
+ # UI types
+ if type_str in UI_TYPES:
+ lines.append(f'{indent}{UI_TYPES[type_str]}')
+ return
+
+ # DCS types
+ if type_str.startswith('DataComposition'):
+ if type_str in DCS_MAP:
+ lines.append(f'{indent}{DCS_MAP[type_str]}')
+ return
+
+ # DynamicList
+ if type_str == 'DynamicList':
+ lines.append(f'{indent}cfg:DynamicList')
+ return
+
+ # cfg: references
+ if CFG_REF_PATTERN.match(type_str):
+ lines.append(f'{indent}cfg:{type_str}')
+ return
+
+ # Fallback with validation
+ if type_str in KNOWN_INVALID_TYPES:
+ raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}")
+ if '.' in type_str:
+ lines.append(f'{indent}cfg:{type_str}')
+ else:
+ print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr)
+ lines.append(f'{indent}{type_str}')
+
+
+def emit_type(lines, type_str, indent):
+ if not type_str:
+ lines.append(f'{indent}')
+ return
+
+ type_string = str(type_str)
+ parts = [p.strip() for p in re.split(r'[|+]', type_string)]
+
+ lines.append(f'{indent}')
+ for part in parts:
+ emit_single_type(lines, part, f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+# --- Element emitters ---
+
+def emit_element(lines, el, indent):
+ type_key = None
+ for key in TYPE_KEYS:
+ if el.get(key) is not None:
+ type_key = key
+ break
+
+ if not type_key:
+ print("WARNING: Unknown element type, skipping", file=sys.stderr)
+ return
+
+ # Validate known keys
+ for p_name in el.keys():
+ if p_name not in KNOWN_KEYS:
+ print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr)
+
+ name = get_element_name(el, type_key)
+ eid = new_id()
+
+ emitters = {
+ 'group': emit_group,
+ 'input': emit_input,
+ 'check': emit_check,
+ 'label': emit_label,
+ 'labelField': emit_label_field,
+ 'table': emit_table,
+ 'pages': emit_pages,
+ 'page': emit_page,
+ 'button': emit_button,
+ 'picture': emit_picture_decoration,
+ 'picField': emit_picture_field,
+ 'calendar': emit_calendar,
+ 'cmdBar': emit_command_bar,
+ 'popup': emit_popup,
+ }
+
+ emitter = emitters.get(type_key)
+ if emitter:
+ emitter(lines, el, name, eid, indent)
+
+
+def emit_group(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ emit_title(lines, el, name, inner)
+
+ # Group orientation
+ group_val = str(el.get('group', ''))
+ orientation_map = {
+ 'horizontal': 'Horizontal',
+ 'vertical': 'Vertical',
+ 'alwaysHorizontal': 'AlwaysHorizontal',
+ 'alwaysVertical': 'AlwaysVertical',
+ }
+ orientation = orientation_map.get(group_val)
+ if orientation:
+ lines.append(f'{inner}{orientation}')
+
+ # Behavior
+ if group_val == 'collapsible':
+ lines.append(f'{inner}Vertical')
+ lines.append(f'{inner}Collapsible')
+
+ # Representation
+ if el.get('representation'):
+ repr_map = {
+ 'none': 'None',
+ 'normal': 'NormalSeparation',
+ 'weak': 'WeakSeparation',
+ 'strong': 'StrongSeparation',
+ }
+ repr_val = repr_map.get(str(el['representation']), str(el['representation']))
+ lines.append(f'{inner}{repr_val}')
+
+ # ShowTitle
+ if el.get('showTitle') is False:
+ lines.append(f'{inner}false')
+
+ # United
+ if el.get('united') is False:
+ lines.append(f'{inner}false')
+
+ emit_common_flags(lines, el, inner)
+
+ # Companion: ExtendedTooltip
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ # Children
+ if el.get('children') and len(el['children']) > 0:
+ lines.append(f'{inner}')
+ for child in el['children']:
+ emit_element(lines, child, f'{inner}\t')
+ lines.append(f'{inner}')
+
+ lines.append(f'{indent}')
+
+
+def emit_input(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('path'):
+ lines.append(f'{inner}{el["path"]}')
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('titleLocation'):
+ loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'}
+ loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation']))
+ lines.append(f'{inner}{loc}')
+
+ if el.get('multiLine') is True:
+ lines.append(f'{inner}true')
+ if el.get('passwordMode') is True:
+ lines.append(f'{inner}true')
+ if el.get('choiceButton') is False:
+ lines.append(f'{inner}false')
+ if el.get('clearButton') is True:
+ lines.append(f'{inner}true')
+ if el.get('spinButton') is True:
+ lines.append(f'{inner}true')
+ if el.get('dropListButton') is True:
+ lines.append(f'{inner}true')
+ if el.get('markIncomplete') is True:
+ lines.append(f'{inner}true')
+ if el.get('skipOnInput') is True:
+ lines.append(f'{inner}true')
+ if el.get('autoMaxWidth') is False:
+ lines.append(f'{inner}false')
+ if el.get('autoMaxHeight') is False:
+ lines.append(f'{inner}false')
+ if el.get('width'):
+ lines.append(f'{inner}{el["width"]}')
+ if el.get('height'):
+ lines.append(f'{inner}{el["height"]}')
+ if el.get('horizontalStretch') is True:
+ lines.append(f'{inner}true')
+ if el.get('verticalStretch') is True:
+ lines.append(f'{inner}true')
+
+ if el.get('inputHint'):
+ emit_mltext(lines, inner, 'InputHint', str(el['inputHint']))
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'input')
+
+ lines.append(f'{indent}')
+
+
+def emit_check(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('path'):
+ lines.append(f'{inner}{el["path"]}')
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('titleLocation'):
+ lines.append(f'{inner}{el["titleLocation"]}')
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'check')
+
+ lines.append(f'{indent}')
+
+
+def emit_label(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('title'):
+ formatted = 'true' if el.get('hyperlink') is True else 'false'
+ lines.append(f'{inner}')
+ lines.append(f'{inner}\t')
+ lines.append(f'{inner}\t\tru')
+ lines.append(f'{inner}\t\t{esc_xml(str(el["title"]))}')
+ lines.append(f'{inner}\t')
+ lines.append(f'{inner}')
+
+ emit_common_flags(lines, el, inner)
+
+ if el.get('hyperlink') is True:
+ lines.append(f'{inner}true')
+ if el.get('autoMaxWidth') is False:
+ lines.append(f'{inner}false')
+ if el.get('autoMaxHeight') is False:
+ lines.append(f'{inner}false')
+ if el.get('width'):
+ lines.append(f'{inner}{el["width"]}')
+ if el.get('height'):
+ lines.append(f'{inner}{el["height"]}')
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'label')
+
+ lines.append(f'{indent}')
+
+
+def emit_label_field(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('path'):
+ lines.append(f'{inner}{el["path"]}')
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('hyperlink') is True:
+ lines.append(f'{inner}true')
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'labelField')
+
+ lines.append(f'{indent}')
+
+
+def emit_table(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('path'):
+ lines.append(f'{inner}{el["path"]}')
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('representation'):
+ lines.append(f'{inner}{el["representation"]}')
+ if el.get('changeRowSet') is True:
+ lines.append(f'{inner}true')
+ if el.get('changeRowOrder') is True:
+ lines.append(f'{inner}true')
+ if el.get('height'):
+ lines.append(f'{inner}{el["height"]}')
+ if el.get('header') is False:
+ lines.append(f'{inner}')
+ if el.get('footer') is True:
+ lines.append(f'{inner}')
+
+ if el.get('commandBarLocation'):
+ lines.append(f'{inner}{el["commandBarLocation"]}')
+ if el.get('searchStringLocation'):
+ lines.append(f'{inner}{el["searchStringLocation"]}')
+
+ if el.get('choiceMode') is True:
+ lines.append(f'{inner}true')
+ if el.get('initialTreeView'):
+ lines.append(f'{inner}{el["initialTreeView"]}')
+ if el.get('enableStartDrag') is True:
+ lines.append(f'{inner}true')
+ if el.get('enableDrag') is True:
+ lines.append(f'{inner}true')
+ if el.get('rowPictureDataPath'):
+ lines.append(f'{inner}{el["rowPictureDataPath"]}')
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ # AutoCommandBar — with optional Autofill control
+ if el.get('tableAutofill') is not None:
+ acb_id = new_id()
+ acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
+ af_val = 'true' if el['tableAutofill'] else 'false'
+ lines.append(f'{inner}')
+ lines.append(f'{inner}\t{af_val}')
+ lines.append(f'{inner}')
+ else:
+ emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner)
+ emit_companion(lines, 'SearchStringAddition', f'{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430', inner)
+ emit_companion(lines, 'ViewStatusAddition', f'{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430', inner)
+ emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner)
+
+ # Columns
+ if el.get('columns') and len(el['columns']) > 0:
+ lines.append(f'{inner}')
+ for col in el['columns']:
+ emit_element(lines, col, f'{inner}\t')
+ lines.append(f'{inner}')
+
+ emit_events(lines, el, name, inner, 'table')
+
+ lines.append(f'{indent}
')
+
+
+def emit_pages(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('pagesRepresentation'):
+ lines.append(f'{inner}{el["pagesRepresentation"]}')
+
+ emit_common_flags(lines, el, inner)
+
+ # Companion
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'pages')
+
+ # Children (pages)
+ if el.get('children') and len(el['children']) > 0:
+ lines.append(f'{inner}')
+ for child in el['children']:
+ emit_element(lines, child, f'{inner}\t')
+ lines.append(f'{inner}')
+
+ lines.append(f'{indent}')
+
+
+def emit_page(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('group'):
+ orientation_map = {
+ 'horizontal': 'Horizontal',
+ 'vertical': 'Vertical',
+ 'alwaysHorizontal': 'AlwaysHorizontal',
+ 'alwaysVertical': 'AlwaysVertical',
+ }
+ orientation = orientation_map.get(str(el['group']))
+ if orientation:
+ lines.append(f'{inner}{orientation}')
+
+ # Companion
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ # Children
+ if el.get('children') and len(el['children']) > 0:
+ lines.append(f'{inner}')
+ for child in el['children']:
+ emit_element(lines, child, f'{inner}\t')
+ lines.append(f'{inner}')
+
+ lines.append(f'{indent}')
+
+
+def emit_button(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+
+
+def emit_picture_decoration(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('picture') or el.get('src'):
+ ref = str(el.get('src') or el.get('picture'))
+ lines.append(f'{inner}')
+ lines.append(f'{inner}\t{ref}')
+ lines.append(f'{inner}\ttrue')
+ lines.append(f'{inner}')
+
+ if el.get('hyperlink') is True:
+ lines.append(f'{inner}true')
+ if el.get('width'):
+ lines.append(f'{inner}{el["width"]}')
+ if el.get('height'):
+ lines.append(f'{inner}{el["height"]}')
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'picture')
+
+ lines.append(f'{indent}')
+
+
+def emit_picture_field(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('path'):
+ lines.append(f'{inner}{el["path"]}')
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('width'):
+ lines.append(f'{inner}{el["width"]}')
+ if el.get('height'):
+ lines.append(f'{inner}{el["height"]}')
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'picField')
+
+ lines.append(f'{indent}')
+
+
+def emit_calendar(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('path'):
+ lines.append(f'{inner}{el["path"]}')
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ # Companions
+ emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
+ emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner)
+
+ emit_events(lines, el, name, inner, 'calendar')
+
+ lines.append(f'{indent}')
+
+
+def emit_command_bar(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ if el.get('autofill') is True:
+ lines.append(f'{inner}true')
+
+ emit_common_flags(lines, el, inner)
+
+ # Children
+ if el.get('children') and len(el['children']) > 0:
+ lines.append(f'{inner}')
+ for child in el['children']:
+ emit_element(lines, child, f'{inner}\t')
+ lines.append(f'{inner}')
+
+ lines.append(f'{indent}')
+
+
+def emit_popup(lines, el, name, eid, indent):
+ lines.append(f'{indent}')
+ inner = f'{indent}\t'
+
+ emit_title(lines, el, name, inner)
+ emit_common_flags(lines, el, inner)
+
+ if el.get('picture'):
+ lines.append(f'{inner}')
+ lines.append(f'{inner}\t{el["picture"]}')
+ lines.append(f'{inner}\ttrue')
+ lines.append(f'{inner}')
+
+ if el.get('representation'):
+ lines.append(f'{inner}{el["representation"]}')
+
+ # Children
+ if el.get('children') and len(el['children']) > 0:
+ lines.append(f'{inner}')
+ for child in el['children']:
+ emit_element(lines, child, f'{inner}\t')
+ lines.append(f'{inner}')
+
+ lines.append(f'{indent}')
+
+
+# --- Attribute emitter ---
+
+def emit_attributes(lines, attrs, indent):
+ if not attrs or len(attrs) == 0:
+ return
+
+ lines.append(f'{indent}')
+ for attr in attrs:
+ attr_id = new_id()
+ attr_name = str(attr['name'])
+
+ lines.append(f'{indent}\t')
+ inner = f'{indent}\t\t'
+
+ if attr.get('title'):
+ emit_mltext(lines, inner, 'Title', str(attr['title']))
+
+ # Type
+ if attr.get('type'):
+ emit_type(lines, str(attr['type']), inner)
+ else:
+ lines.append(f'{inner}')
+
+ if attr.get('main') is True:
+ lines.append(f'{inner}true')
+ if attr.get('savedData') is True:
+ lines.append(f'{inner}true')
+ if attr.get('fillChecking'):
+ lines.append(f'{inner}{attr["fillChecking"]}')
+
+ # Columns (for ValueTable/ValueTree)
+ if attr.get('columns') and len(attr['columns']) > 0:
+ lines.append(f'{inner}')
+ for col in attr['columns']:
+ col_id = new_id()
+ lines.append(f'{inner}\t')
+ if col.get('title'):
+ emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title']))
+ emit_type(lines, str(col.get('type', '')), f'{inner}\t\t')
+ lines.append(f'{inner}\t')
+ lines.append(f'{inner}')
+
+ # Settings (for DynamicList)
+ if attr.get('settings'):
+ s = attr['settings']
+ lines.append(f'{inner}')
+ si = f'{inner}\t'
+ if s.get('mainTable'):
+ lines.append(f'{si}{s["mainTable"]}')
+ mq = 'true' if s.get('manualQuery') else 'false'
+ lines.append(f'{si}{mq}')
+ ddr = 'true' if s.get('dynamicDataRead') else 'false'
+ lines.append(f'{si}{ddr}')
+ lines.append(f'{inner}')
+
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+# --- Parameter emitter ---
+
+def emit_parameters(lines, params, indent):
+ if not params or len(params) == 0:
+ return
+
+ lines.append(f'{indent}')
+ for param in params:
+ lines.append(f'{indent}\t')
+ inner = f'{indent}\t\t'
+
+ emit_type(lines, str(param.get('type', '')), inner)
+
+ if param.get('key') is True:
+ lines.append(f'{inner}true')
+
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+# --- Command emitter ---
+
+def emit_commands(lines, cmds, indent):
+ if not cmds or len(cmds) == 0:
+ return
+
+ lines.append(f'{indent}')
+ for cmd in cmds:
+ cmd_id = new_id()
+ lines.append(f'{indent}\t')
+ inner = f'{indent}\t\t'
+
+ if cmd.get('title'):
+ emit_mltext(lines, inner, 'Title', str(cmd['title']))
+
+ if cmd.get('action'):
+ lines.append(f'{inner}{cmd["action"]}')
+
+ if cmd.get('shortcut'):
+ lines.append(f'{inner}{cmd["shortcut"]}')
+
+ if cmd.get('picture'):
+ lines.append(f'{inner}')
+ lines.append(f'{inner}\t{cmd["picture"]}')
+ lines.append(f'{inner}\ttrue')
+ lines.append(f'{inner}')
+
+ if cmd.get('representation'):
+ lines.append(f'{inner}{cmd["representation"]}')
+
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+# --- Properties emitter ---
+
+PROP_MAP = {
+ "autoTitle": "AutoTitle",
+ "windowOpeningMode": "WindowOpeningMode",
+ "commandBarLocation": "CommandBarLocation",
+ "saveDataInSettings": "SaveDataInSettings",
+ "autoSaveDataInSettings": "AutoSaveDataInSettings",
+ "autoTime": "AutoTime",
+ "usePostingMode": "UsePostingMode",
+ "repostOnWrite": "RepostOnWrite",
+ "autoURL": "AutoURL",
+ "autoFillCheck": "AutoFillCheck",
+ "customizable": "Customizable",
+ "enterKeyBehavior": "EnterKeyBehavior",
+ "verticalScroll": "VerticalScroll",
+ "scalingMode": "ScalingMode",
+ "useForFoldersAndItems": "UseForFoldersAndItems",
+ "reportResult": "ReportResult",
+ "detailsData": "DetailsData",
+ "reportFormType": "ReportFormType",
+ "autoShowState": "AutoShowState",
+ "width": "Width",
+ "height": "Height",
+ "group": "Group",
+}
+
+
+def emit_properties(lines, props, indent):
+ if not props:
+ return
+
+ for p_name, p_value in props.items():
+ xml_name = PROP_MAP.get(p_name)
+ if not xml_name:
+ # Auto PascalCase
+ xml_name = p_name[0].upper() + p_name[1:]
+
+ # Convert boolean to lowercase
+ if isinstance(p_value, bool):
+ val = 'true' if p_value else 'false'
+ else:
+ val = str(p_value)
+ lines.append(f'{indent}<{xml_name}>{val}{xml_name}>')
+
+
+
+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']+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 _normalize_elements(defn):
+ """Convert dict-style elements from --from-object generators to list-style expected by compiler.
+ Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...}
+ Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...]
+ Also handles nested 'elements' in groups and 'columns' in tables recursively.
+ """
+ def convert_elements(els):
+ if isinstance(els, list):
+ # Already list format — but may have nested dicts inside groups
+ result = []
+ for el in els:
+ if isinstance(el, dict):
+ el = dict(el) # copy
+ if 'elements' in el and isinstance(el['elements'], dict):
+ el['elements'] = convert_elements(el['elements'])
+ if 'columns' in el and isinstance(el['columns'], dict):
+ el['columns'] = convert_columns(el['columns'])
+ result.append(el)
+ return result
+ if isinstance(els, dict):
+ result = []
+ for name, props in els.items():
+ if not isinstance(props, dict):
+ continue
+ new_el = {}
+ el_type = props.get('element', 'input')
+ # Map element type to the key name used in JSON DSL
+ type_map = {
+ 'input': 'input', 'check': 'check', 'labelField': 'labelField',
+ 'table': 'table', 'group': 'group', 'pages': 'pages',
+ 'page': 'page', 'label': 'label', 'button': 'button',
+ 'checkBox': 'check', 'radioButton': 'radioButton',
+ 'pictureField': 'pictureField',
+ }
+ mapped_type = type_map.get(el_type, el_type)
+ new_el[mapped_type] = name
+ for k, v in props.items():
+ if k == 'element':
+ continue
+ if k == 'elements' and isinstance(v, dict):
+ new_el['elements'] = convert_elements(v)
+ elif k == 'columns' and isinstance(v, dict):
+ new_el['columns'] = convert_columns(v)
+ elif k == 'groupType':
+ # groupType → group property in DSL
+ new_el['group'] = v
+ elif k == 'showTitle':
+ new_el['showTitle'] = v
+ elif k == 'representation':
+ new_el['representation'] = v
+ elif k == 'autoCommandBar':
+ new_el['autoCommandBar'] = v
+ elif k == 'commandBarLocation':
+ new_el['commandBarLocation'] = v
+ else:
+ new_el[k] = v
+ result.append(new_el)
+ return result
+ return els
+
+ def convert_columns(cols):
+ if isinstance(cols, list):
+ return cols
+ if isinstance(cols, dict):
+ result = []
+ for name, props in cols.items():
+ if not isinstance(props, dict):
+ continue
+ new_col = {}
+ el_type = props.get('element', 'input')
+ type_map = {
+ 'input': 'input', 'check': 'check', 'labelField': 'labelField',
+ 'checkBox': 'check',
+ }
+ mapped_type = type_map.get(el_type, el_type)
+ new_col[mapped_type] = name
+ for k, v in props.items():
+ if k == 'element':
+ continue
+ new_col[k] = v
+ result.append(new_col)
+ return result
+ return cols
+
+ if 'elements' in defn:
+ defn['elements'] = convert_elements(defn['elements'])
+ return defn
+
+
+def main():
+ sys.stdout.reconfigure(encoding="utf-8")
+ sys.stderr.reconfigure(encoding="utf-8")
+ global _next_id
+
+ parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False)
+ parser.add_argument('-JsonPath', type=str, default=None)
+ parser.add_argument('-OutputPath', type=str, required=True)
+ parser.add_argument('-FromObject', action='store_true', default=False)
+ parser.add_argument('-ObjectPath', type=str, default=None)
+ parser.add_argument('-Purpose', type=str, default=None)
+ parser.add_argument('-Preset', type=str, default='erp-standard')
+ parser.add_argument('-EmitDsl', type=str, default=None)
+ args = parser.parse_args()
+
+ # Form name -> purpose mapping
+ _FORM_NAME_TO_PURPOSE = {
+ '\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента
+ '\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента
+ '\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка
+ '\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора
+ '\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы
+ '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи
+ '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета
+ '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла
+ }
+
+ # Mutual exclusion validation
+ if args.FromObject and args.JsonPath:
+ print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr)
+ sys.exit(1)
+ if not args.FromObject and not args.JsonPath:
+ print("Either -JsonPath or -FromObject is required.", file=sys.stderr)
+ sys.exit(1)
+
+ # Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing
+ if args.FromObject:
+ out_norm = args.OutputPath.rstrip('/\\')
+ if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm):
+ if re.search(r'[/\\]Ext$', out_norm):
+ args.OutputPath = out_norm + '/Form.xml'
+ else:
+ args.OutputPath = out_norm + '/Ext/Form.xml'
+ print(f"[resolved] OutputPath -> {args.OutputPath}")
+
+ # --- Detect XML format version ---
+ out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath)
+ format_version = detect_format_version(os.path.dirname(out_path_resolved))
+
+ # --- 0. From-object mode ---
+ if args.FromObject:
+ # Resolve object path and purpose from OutputPath convention:
+ # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
+ out_abs = out_path_resolved
+ parts = re.split(r'[/\\]', out_abs)
+ forms_idx = -1
+ for i in range(len(parts) - 1, -1, -1):
+ if parts[i] == 'Forms':
+ forms_idx = i
+ break
+
+ resolved_object_path = None
+ resolved_purpose = None
+
+ if forms_idx >= 2:
+ form_name = parts[forms_idx + 1]
+ object_name = parts[forms_idx - 1]
+ type_plural_and_above = os.sep.join(parts[:forms_idx - 1])
+
+ if form_name in _FORM_NAME_TO_PURPOSE:
+ resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name]
+
+ candidate = os.path.join(type_plural_and_above, f'{object_name}.xml')
+ if os.path.exists(candidate):
+ resolved_object_path = candidate
+
+ # Apply: explicit -ObjectPath / -Purpose override resolved
+ from_obj_path = None
+ if args.ObjectPath:
+ from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath)
+ if not from_obj_path.endswith('.xml'):
+ from_obj_path += '.xml'
+ elif resolved_object_path:
+ from_obj_path = resolved_object_path
+ print(f"[resolved] ObjectPath -> {from_obj_path}")
+ else:
+ print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr)
+ sys.exit(1)
+
+ if not os.path.exists(from_obj_path):
+ print(f"Object file not found: {from_obj_path}", file=sys.stderr)
+ sys.exit(1)
+
+ purpose = args.Purpose or resolved_purpose or 'Item'
+ if resolved_purpose and not args.Purpose:
+ print(f"[resolved] Purpose -> {purpose}")
+
+ meta = parse_object_meta(from_obj_path)
+ print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}")
+
+ preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved)
+
+ supported = {
+ 'Document': ['Item', 'List', 'Choice'],
+ 'Catalog': ['Item', 'Folder', 'List', 'Choice'],
+ 'InformationRegister': ['Record', 'List'],
+ 'AccumulationRegister': ['List'],
+ 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'],
+ 'ExchangePlan': ['Item', 'List', 'Choice'],
+ 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'],
+ }
+ if meta['Type'] not in supported:
+ print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", file=sys.stderr)
+ sys.exit(1)
+ if purpose not in supported[meta['Type']]:
+ print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr)
+ sys.exit(1)
+
+ dsl_dispatch = {
+ 'Document': generate_document_dsl,
+ 'Catalog': generate_catalog_dsl,
+ 'InformationRegister': generate_information_register_dsl,
+ 'AccumulationRegister': generate_accumulation_register_dsl,
+ 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl,
+ 'ExchangePlan': generate_exchange_plan_dsl,
+ 'ChartOfAccounts': generate_chart_of_accounts_dsl,
+ }
+ dsl = dsl_dispatch[meta['Type']](meta, preset_data, purpose)
+
+ if args.EmitDsl:
+ dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl)
+ os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True)
+ with open(dsl_path, 'w', encoding='utf-8') as f:
+ json.dump(dsl, f, ensure_ascii=False, indent=2)
+ print(f"[from-object] DSL saved: {dsl_path}")
+
+ defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict
+ # Convert dict-style elements (from generators) to list-style (expected by compiler)
+ defn = _normalize_elements(defn)
+ else:
+ # --- 1. Load and validate JSON ---
+ json_path = args.JsonPath
+ if not os.path.exists(json_path):
+ print(f"File not found: {json_path}", file=sys.stderr)
+ sys.exit(1)
+
+ with open(json_path, 'r', encoding='utf-8-sig') as f:
+ defn = json.load(f)
+
+ # --- 2. Main compilation ---
+ _next_id = 0
+ lines = []
+
+ lines.append('')
+ lines.append(f'')
+
+ # --- 3. Write output ---
+ out_path = args.OutputPath
+ if not os.path.isabs(out_path):
+ out_path = os.path.join(os.getcwd(), out_path)
+ out_dir = os.path.dirname(out_path)
+ if out_dir and not os.path.exists(out_dir):
+ os.makedirs(out_dir, exist_ok=True)
+
+ content = '\n'.join(lines) + '\n'
+ write_utf8_bom(out_path, content)
+
+ # --- 4. Auto-register form in parent object XML ---
+ # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
+ form_xml_dir = os.path.dirname(out_path) # Ext
+ form_name_dir = os.path.dirname(form_xml_dir) # FormName
+ forms_dir = os.path.dirname(form_name_dir) # Forms
+ object_dir = os.path.dirname(forms_dir) # ObjectName
+ type_plural_dir = os.path.dirname(object_dir) # TypePlural
+
+ form_name = os.path.basename(form_name_dir)
+ object_name = os.path.basename(object_dir)
+ forms_leaf = os.path.basename(forms_dir)
+
+ if forms_leaf == 'Forms':
+ object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml')
+ if os.path.exists(object_xml_path):
+ with open(object_xml_path, 'r', encoding='utf-8-sig') as f:
+ raw_text = f.read()
+
+ # Check if already registered
+ if f'' not in raw_text:
+ # Insert before
+ if '' in raw_text:
+ insert_line = f'\t\t\t\n'
+ raw_text = raw_text.replace('', insert_line + '\t\t', 1)
+ elif '' in raw_text:
+ replacement = f'\n\t\t\t\n\t\t'
+ raw_text = raw_text.replace('', replacement, 1)
+
+ write_utf8_bom(object_xml_path, raw_text)
+ print(f" Registered: in {object_name}.xml")
+
+ # --- 5. Summary ---
+ el_count = _next_id
+ print(f"[OK] Compiled: {args.OutputPath}")
+ print(f" Elements+IDs: {el_count}")
+ if defn.get('attributes'):
+ print(f" Attributes: {len(defn['attributes'])}")
+ if defn.get('commands'):
+ print(f" Commands: {len(defn['commands'])}")
+ if defn.get('parameters'):
+ print(f" Parameters: {len(defn['parameters'])}")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1
index e80afe6e..1e953f18 100644
--- a/.claude/skills/form-edit/scripts/form-edit.ps1
+++ b/.claude/skills/form-edit/scripts/form-edit.ps1
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
+ [Alias('Path')]
[string]$FormPath,
[Parameter(Mandatory)]
diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py
index c4e03281..594b7a97 100644
--- a/.claude/skills/form-edit/scripts/form-edit.py
+++ b/.claude/skills/form-edit/scripts/form-edit.py
@@ -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()
diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1
index 2749537f..4e15cb0d 100644
--- a/.claude/skills/form-info/scripts/form-info.ps1
+++ b/.claude/skills/form-info/scripts/form-info.ps1
@@ -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,
diff --git a/.claude/skills/form-info/scripts/form-info.py b/.claude/skills/form-info/scripts/form-info.py
index 3e7656c2..ba18fcfb 100644
--- a/.claude/skills/form-info/scripts/form-info.py
+++ b/.claude/skills/form-info/scripts/form-info.py
@@ -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")
diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1
index 268266a4..7ef4f3f6 100644
--- a/.claude/skills/form-validate/scripts/form-validate.ps1
+++ b/.claude/skills/form-validate/scripts/form-validate.ps1
@@ -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
diff --git a/.claude/skills/form-validate/scripts/form-validate.py b/.claude/skills/form-validate/scripts/form-validate.py
index b0a753c6..5a42b17a 100644
--- a/.claude/skills/form-validate/scripts/form-validate.py
+++ b/.claude/skills/form-validate/scripts/form-validate.py
@@ -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
diff --git a/.claude/skills/help-add/scripts/add-help.ps1 b/.claude/skills/help-add/scripts/add-help.ps1
index 5211f700..ae067ec5 100644
--- a/.claude/skills/help-add/scripts/add-help.ps1
+++ b/.claude/skills/help-add/scripts/add-help.ps1
@@ -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 ']+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 = @"
-
+
$Lang
"@
diff --git a/.claude/skills/help-add/scripts/add-help.py b/.claude/skills/help-add/scripts/add-help.py
index 03567724..ce6526d5 100644
--- a/.claude/skills/help-add/scripts/add-help.py
+++ b/.claude/skills/help-add/scripts/add-help.py
@@ -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']+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():
'\n'
+ f' version="{format_version}">\n'
f'\t{lang}\n'
''
)
diff --git a/.claude/skills/img-grid/SKILL.md b/.claude/skills/img-grid/SKILL.md
index 25d17eb3..707decd1 100644
--- a/.claude/skills/img-grid/SKILL.md
+++ b/.claude/skills/img-grid/SKILL.md
@@ -1,6 +1,6 @@
---
name: img-grid
-description: Наложить пронумерованную сетку на изображение для определения пропорций колонок
+description: Наложить пронумерованную сетку на изображение. Используй при анализе скриншота макета или печатной формы — измерить пропорции колонок перед генерацией табличного документа
argument-hint: [-c COLS]
allowed-tools:
- Bash
diff --git a/.claude/skills/interface-edit/scripts/interface-edit.ps1 b/.claude/skills/interface-edit/scripts/interface-edit.ps1
index adace28b..8a24551f 100644
--- a/.claude/skills/interface-edit/scripts/interface-edit.ps1
+++ b/.claude/skills/interface-edit/scripts/interface-edit.ps1
@@ -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 ']+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">
"@
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
diff --git a/.claude/skills/interface-edit/scripts/interface-edit.py b/.claude/skills/interface-edit/scripts/interface-edit.py
index 0401a78a..23a279c0 100644
--- a/.claude/skills/interface-edit/scripts/interface-edit.py
+++ b/.claude/skills/interface-edit/scripts/interface-edit.py
@@ -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']+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''
)
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()
diff --git a/.claude/skills/interface-validate/scripts/interface-validate.ps1 b/.claude/skills/interface-validate/scripts/interface-validate.ps1
index 0bd5191a..e76dd667 100644
--- a/.claude/skills/interface-validate/scripts/interface-validate.ps1
+++ b/.claude/skills/interface-validate/scripts/interface-validate.ps1
@@ -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
diff --git a/.claude/skills/interface-validate/scripts/interface-validate.py b/.claude/skills/interface-validate/scripts/interface-validate.py
index 4eeb181b..878ebc77 100644
--- a/.claude/skills/interface-validate/scripts/interface-validate.py
+++ b/.claude/skills/interface-validate/scripts/interface-validate.py
@@ -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='')
diff --git a/.claude/skills/meta-compile/SKILL.md b/.claude/skills/meta-compile/SKILL.md
index 7e354753..342946a3 100644
--- a/.claude/skills/meta-compile/SKILL.md
+++ b/.claude/skills/meta-compile/SKILL.md
@@ -1,6 +1,6 @@
---
name: meta-compile
-description: Создать объект метаданных 1С. Используй когда пользователь просит создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
+description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
argument-hint:
allowed-tools:
- Bash
diff --git a/.claude/skills/meta-compile/reference/types-basic.md b/.claude/skills/meta-compile/reference/types-basic.md
index 2763635a..ad093af0 100644
--- a/.claude/skills/meta-compile/reference/types-basic.md
+++ b/.claude/skills/meta-compile/reference/types-basic.md
@@ -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 |
diff --git a/.claude/skills/meta-compile/reference/types-registers.md b/.claude/skills/meta-compile/reference/types-registers.md
index 4c87c46e..6f452e4f 100644
--- a/.claude/skills/meta-compile/reference/types-registers.md
+++ b/.claude/skills/meta-compile/reference/types-registers.md
@@ -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" }
```
## Зависимости
diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1
index 4f3a6ed4..2e863e2e 100644
--- a/.claude/skills/meta-compile/scripts/meta-compile.ps1
+++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1
@@ -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"
X "$indent`t`tfalse"
X "$indent`t`t"
- X "$indent`t`tfalse"
+ $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
+ X "$indent`t`t$multiLine"
X "$indent`t`tfalse"
X "$indent`t`t"
X "$indent`t`t"
- # 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`tfalse"
}
- # 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"
X "$indent`t`tUse"
- X "$indent`t`tUse"
+ # DataHistory — not for Chart* types and non-InformationRegister register family
+ if ($context -notin @("chart", "register-other")) {
+ X "$indent`t`tUse"
+ }
}
X "$indent`t"
@@ -924,7 +937,8 @@ function Emit-Dimension {
X "$indent`t`t"
X "$indent`t`tfalse"
X "$indent`t`t"
- X "$indent`t`tfalse"
+ $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
+ X "$indent`t`t$multiLine"
X "$indent`t`tfalse"
X "$indent`t`t"
X "$indent`t`t"
@@ -1017,7 +1031,8 @@ function Emit-Resource {
X "$indent`t`t"
X "$indent`t`tfalse"
X "$indent`t`t"
- X "$indent`t`tfalse"
+ $multiLine = if ($parsed.multiLine -eq $true -or $parsed.flags -contains "multiline") { "true" } else { "false" }
+ X "$indent`t`t$multiLine"
X "$indent`t`tfalse"
X "$indent`t`t"
X "$indent`t`t"
@@ -1071,12 +1086,25 @@ function Emit-CatalogProperties {
$hierarchyType = Get-EnumProp "HierarchyType" "hierarchyType" "HierarchyFoldersAndItems"
X "$i$hierarchical"
X "$i$hierarchyType"
- X "$ifalse"
- X "$i2"
- X "$itrue"
+ $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"
+ X "$i$levelCount"
+ X "$i$foldersOnTop"
X "$itrue"
- X "$i"
- X "$iToItems"
+ if ($def.owners -and $def.owners.Count -gt 0) {
+ X "$i"
+ foreach ($ownerRef in $def.owners) {
+ $fullRef = if ("$ownerRef" -match '\.') { "$ownerRef" } else { "Catalog.$ownerRef" }
+ X "$i`t$fullRef"
+ }
+ X "$i"
+ } else {
+ X "$i"
+ }
+ $subordinationUse = Get-EnumProp "SubordinationUse" "subordinationUse" "ToItems"
+ X "$i$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"
X "$i$codeType"
X "$i$codeAllowedLength"
- X "$iWholeCatalog"
+ $codeSeries = Get-EnumProp "CodeSeries" "codeSeries" "WholeCatalog"
+ X "$i$codeSeries"
X "$i$checkUnique"
X "$i$autonumbering"
@@ -1100,8 +1129,10 @@ function Emit-CatalogProperties {
X "$i"
X "$iAuto"
X "$iInDialog"
- X "$itrue"
- X "$iBothWays"
+ $quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
+ $choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays"
+ X "$i$quickChoice"
+ X "$i$choiceMode"
X "$i"
X "$i`tCatalog.$objName.StandardAttribute.Description"
X "$i`tCatalog.$objName.StandardAttribute.Code"
@@ -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 ']+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 ''
-X ""
+X ""
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"
} else {
@@ -2904,7 +2960,7 @@ if ($objType -eq "ExchangePlan") {
$contentPath = Join-Path $extDir "Content.xml"
if (-not (Test-Path $contentPath)) {
Ensure-ExtDir
- $contentXml = "`r`n`r`n"
+ $contentXml = "`r`n`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 = "`r`n`r`n"
+ $flowchartXml = "`r`n`r`n"
[System.IO.File]::WriteAllText($flowchartPath, $flowchartXml, $enc)
$modulesCreated += $flowchartPath
}
diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py
index 3adbb1b9..ecf1e27c 100644
--- a/.claude/skills/meta-compile/scripts/meta-compile.py
+++ b/.claude/skills/meta-compile/scripts/meta-compile.py
@@ -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}')
@@ -743,13 +750,16 @@ def emit_attribute(indent, parsed, context):
X(f'{indent}\t\t')
X(f'{indent}\t\tfalse')
X(f'{indent}\t\t')
- X(f'{indent}\t\tfalse')
+ multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
+ X(f'{indent}\t\t{multi_line}')
X(f'{indent}\t\tfalse')
X(f'{indent}\t\t')
X(f'{indent}\t\t')
- 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\tfalse')
- 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}')
X(f'{indent}\t\tUse')
- X(f'{indent}\t\tUse')
+ # DataHistory — not for Chart* types and non-InformationRegister register family
+ if context not in ('chart', 'register-other'):
+ X(f'{indent}\t\tUse')
X(f'{indent}\t')
X(f'{indent}')
@@ -857,7 +869,8 @@ def emit_dimension(indent, parsed, register_type):
X(f'{indent}\t\t')
X(f'{indent}\t\tfalse')
X(f'{indent}\t\t')
- X(f'{indent}\t\tfalse')
+ multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
+ X(f'{indent}\t\t{multi_line}')
X(f'{indent}\t\tfalse')
X(f'{indent}\t\t')
X(f'{indent}\t\t')
@@ -930,7 +943,8 @@ def emit_resource(indent, parsed, register_type):
X(f'{indent}\t\t')
X(f'{indent}\t\tfalse')
X(f'{indent}\t\t')
- X(f'{indent}\t\tfalse')
+ multi_line = 'true' if (parsed.get('multiLine') is True or 'multiline' in parsed.get('flags', [])) else 'false'
+ X(f'{indent}\t\t{multi_line}')
X(f'{indent}\t\tfalse')
X(f'{indent}\t\t')
X(f'{indent}\t\t')
@@ -972,12 +986,24 @@ def emit_catalog_properties(indent):
hierarchy_type = get_enum_prop('HierarchyType', 'hierarchyType', 'HierarchyFoldersAndItems')
X(f'{i}{hierarchical}')
X(f'{i}{hierarchy_type}')
- X(f'{i}false')
- X(f'{i}2')
- X(f'{i}true')
+ 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}{limit_level_count}')
+ X(f'{i}{level_count}')
+ X(f'{i}{folders_on_top}')
X(f'{i}true')
- X(f'{i}')
- X(f'{i}ToItems')
+ owners = defn.get('owners', [])
+ if owners:
+ X(f'{i}')
+ for owner_ref in owners:
+ full_ref = owner_ref if '.' in str(owner_ref) else f'Catalog.{owner_ref}'
+ X(f'{i}\t{full_ref}')
+ X(f'{i}')
+ else:
+ X(f'{i}')
+ subordination_use = get_enum_prop('SubordinationUse', 'subordinationUse', 'ToItems')
+ X(f'{i}{subordination_use}')
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}{description_length}')
X(f'{i}{code_type}')
X(f'{i}{code_allowed_length}')
- X(f'{i}WholeCatalog')
+ code_series = get_enum_prop('CodeSeries', 'codeSeries', 'WholeCatalog')
+ X(f'{i}{code_series}')
X(f'{i}{check_unique}')
X(f'{i}{autonumbering}')
default_presentation = get_enum_prop('DefaultPresentation', 'defaultPresentation', 'AsDescription')
@@ -997,8 +1024,10 @@ def emit_catalog_properties(indent):
X(f'{i}')
X(f'{i}Auto')
X(f'{i}InDialog')
- X(f'{i}true')
- X(f'{i}BothWays')
+ quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
+ choice_mode = get_enum_prop('ChoiceMode', 'choiceMode', 'BothWays')
+ X(f'{i}{quick_choice}')
+ X(f'{i}{choice_mode}')
X(f'{i}')
X(f'{i}\tCatalog.{obj_name}.StandardAttribute.Description')
X(f'{i}\tCatalog.{obj_name}.StandardAttribute.Code')
@@ -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']+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('')
-X(f'')
+X(f'')
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')
@@ -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')
else:
X('\t\t')
@@ -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 = '\r\n\r\n'
+ content_xml = f'\r\n\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 = '\r\n\r\n'
+ flowchart_xml = f'\r\n\r\n'
write_utf8_bom(flowchart_path, flowchart_xml)
modules_created.append(flowchart_path)
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1
index 78d52e29..cdec281f 100644
--- a/.claude/skills/meta-edit/scripts/meta-edit.ps1
+++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1
@@ -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
}
diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py
index e7d10262..53a9e942 100644
--- a/.claude/skills/meta-edit/scripts/meta-edit.py
+++ b/.claude/skills/meta-edit/scripts/meta-edit.py
@@ -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}")
diff --git a/.claude/skills/meta-info/scripts/meta-info.ps1 b/.claude/skills/meta-info/scripts/meta-info.ps1
index 9f09cb5e..7e6d658e 100644
--- a/.claude/skills/meta-info/scripts/meta-info.ps1
+++ b/.claude/skills/meta-info/scripts/meta-info.ps1
@@ -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,
diff --git a/.claude/skills/meta-info/scripts/meta-info.py b/.claude/skills/meta-info/scripts/meta-info.py
index 8e400bcc..61c73325 100644
--- a/.claude/skills/meta-info/scripts/meta-info.py
+++ b/.claude/skills/meta-info/scripts/meta-info.py
@@ -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)
diff --git a/.claude/skills/meta-remove/SKILL.md b/.claude/skills/meta-remove/SKILL.md
index 00db5ba8..4551fda4 100644
--- a/.claude/skills/meta-remove/SKILL.md
+++ b/.claude/skills/meta-remove/SKILL.md
@@ -1,6 +1,6 @@
---
name: meta-remove
-description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
+description: Удалить объект метаданных из конфигурации 1С. Используй когда нужно удалить, убрать объект из конфигурации
argument-hint: -Object
allowed-tools:
- Bash
diff --git a/.claude/skills/meta-validate/scripts/meta-validate.ps1 b/.claude/skills/meta-validate/scripts/meta-validate.ps1
index d30f210a..343c1d61 100644
--- a/.claude/skills/meta-validate/scripts/meta-validate.ps1
+++ b/.claude/skills/meta-validate/scripts/meta-validate.ps1
@@ -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)") {
diff --git a/.claude/skills/meta-validate/scripts/meta-validate.py b/.claude/skills/meta-validate/scripts/meta-validate.py
index c55ef8ae..b682d3ba 100644
--- a/.claude/skills/meta-validate/scripts/meta-validate.py
+++ b/.claude/skills/meta-validate/scripts/meta-validate.py
@@ -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)':
diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1
index 9899981c..376af68e 100644
--- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1
+++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
+ [Alias('Path')]
[string]$TemplatePath,
[string]$OutputPath
diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py
index 481cc89c..280eb0ef 100644
--- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py
+++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py
@@ -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()
diff --git a/.claude/skills/mxl-info/scripts/mxl-info.ps1 b/.claude/skills/mxl-info/scripts/mxl-info.ps1
index d857dd0f..ef1dda07 100644
--- a/.claude/skills/mxl-info/scripts/mxl-info.ps1
+++ b/.claude/skills/mxl-info/scripts/mxl-info.ps1
@@ -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,
diff --git a/.claude/skills/mxl-info/scripts/mxl-info.py b/.claude/skills/mxl-info/scripts/mxl-info.py
index 607cfc2c..fe235e27 100644
--- a/.claude/skills/mxl-info/scripts/mxl-info.py
+++ b/.claude/skills/mxl-info/scripts/mxl-info.py
@@ -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)")
diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1
index 0657100e..86271ea7 100644
--- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1
+++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1
@@ -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,
diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.py b/.claude/skills/mxl-validate/scripts/mxl-validate.py
index fcbef905..94b88842 100644
--- a/.claude/skills/mxl-validate/scripts/mxl-validate.py
+++ b/.claude/skills/mxl-validate/scripts/mxl-validate.py
@@ -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')
diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1
index 508ba64c..c193a267 100644
--- a/.claude/skills/role-compile/scripts/role-compile.ps1
+++ b/.claude/skills/role-compile/scripts/role-compile.ps1
@@ -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 ']+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 " "
X ' '
X " $roleName"
@@ -560,7 +580,7 @@ X ''
X ''
+X " xsi:type=`"Rights`" version=`"$formatVersion`">"
# Global flags (defaults match typical 1C roles)
$sfno = if ($null -ne $def.setForNewObjects) { "$($def.setForNewObjects)".ToLower() } else { "false" }
diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py
index 65dca59f..826fb2c9 100644
--- a/.claude/skills/role-compile/scripts/role-compile.py
+++ b/.claude/skills/role-compile/scripts/role-compile.py
@@ -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']+version="(\d+\.\d+)"', head)
+ if m:
+ return m.group(1)
+ parent = os.path.dirname(d)
+ if parent == d:
+ break
+ d = parent
+ return "2.17"
+
+
def esc_xml(s):
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
@@ -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' ')
lines.append(' ')
lines.append(f' {role_name}')
@@ -516,7 +535,7 @@ def main():
lines.append('')
+ 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'
diff --git a/.claude/skills/role-info/scripts/role-info.ps1 b/.claude/skills/role-info/scripts/role-info.ps1
index d127714c..ed6c5bb9 100644
--- a/.claude/skills/role-info/scripts/role-info.ps1
+++ b/.claude/skills/role-info/scripts/role-info.ps1
@@ -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,
diff --git a/.claude/skills/role-info/scripts/role-info.py b/.claude/skills/role-info/scripts/role-info.py
index d66d887f..4ea4f9d3 100644
--- a/.claude/skills/role-info/scripts/role-info.py
+++ b/.claude/skills/role-info/scripts/role-info.py
@@ -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")
diff --git a/.claude/skills/role-validate/scripts/role-validate.ps1 b/.claude/skills/role-validate/scripts/role-validate.ps1
index 15de5400..e2c639ca 100644
--- a/.claude/skills/role-validate/scripts/role-validate.ps1
+++ b/.claude/skills/role-validate/scripts/role-validate.ps1
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
+ [Alias('Path')]
[string]$RightsPath,
[string]$OutFile,
diff --git a/.claude/skills/role-validate/scripts/role-validate.py b/.claude/skills/role-validate/scripts/role-validate.py
index 7ff6d86a..c4bb580a 100644
--- a/.claude/skills/role-validate/scripts/role-validate.py
+++ b/.claude/skills/role-validate/scripts/role-validate.py
@@ -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)
diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md
index afa3beaa..8d932d59 100644
--- a/.claude/skills/skd-compile/SKILL.md
+++ b/.claude/skills/skd-compile/SKILL.md
@@ -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** — внешний набор данных (без источника-запроса). Поля описываются явно; данные передаются вторым параметром `ПроцессорКомпоновкиДанных.Инициализировать(Макет, Новый Структура("", ТЗ), ...)`.
+
+```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: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `` (LocalStringType).
+
+Флаги shorthand:
+- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить.
+- `@valueList` — `true` — разрешает передавать список значений
+- `@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` (строки данных) → ``, `OverallHeader` (итоги) → ``, `GroupHeader` (шапка) → ``.
+
## Примеры
### Минимальный
@@ -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"
}
}]
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1
index 528c5916..3ce1b65d 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.ps1
+++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1
@@ -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"
- X "$indent`t`tru"
- X "$indent`t`t$(Esc-Xml $text)"
- X "$indent`t"
+ # Multi-lang: object form { ru: "...", en: "..." } → one 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"
+ X "$indent`t`t$(Esc-Xml "$lang")"
+ X "$indent`t`t$(Esc-Xml "$content")"
+ X "$indent`t"
+ }
+ } else {
+ X "$indent`t"
+ X "$indent`t`tru"
+ X "$indent`t`t$(Esc-Xml "$text")"
+ X "$indent`t"
+ }
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"
- X "`t`t$(Esc-Xml $parsed.dataPath)"
- X "`t`t$(Esc-Xml $parsed.expression)"
+ X "`t`t$(Esc-Xml $dataPath)"
+ X "`t`t$(Esc-Xml $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"
- Emit-ValueType -typeStr $cfType -indent "`t`t`t"
- X "`t`t"
- }
- 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"
+ Emit-ValueType -typeStr $typeStr -indent "`t`t`t"
+ X "`t`t"
+ }
+ if ($restrictObj -or $restrictTokens.Count -gt 0) {
+ X "`t`t"
+ 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"
- 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"
}
- if ($cf.appearance) {
- X "`t`t"
- foreach ($prop in $cf.appearance.PSObject.Properties) {
- X "`t`t`t"
- X "`t`t`t`t$(Esc-Xml $prop.Name)"
- X "`t`t`t`t$(Esc-Xml "$($prop.Value)")"
- X "`t`t`t"
- }
- X "`t`t"
+ X "`t`t"
+ }
+ if ($appearance) {
+ X "`t`t"
+ foreach ($prop in $appearance.PSObject.Properties) {
+ X "`t`t`t"
+ X "`t`t`t`t$(Esc-Xml $prop.Name)"
+ X "`t`t`t`t$(Esc-Xml "$($prop.Value)")"
+ X "`t`t`t"
}
+ X "`t`t"
}
X "`t"
@@ -828,8 +964,16 @@ function Emit-SingleParam {
X "`t"
X "`t`t$(Esc-Xml $parsed.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`ttrue"
}
@@ -859,14 +1009,50 @@ function Emit-SingleParam {
X "`t`tfalse"
}
- # Use
- if ($p -isnot [string] -and $p.use) {
- X "`t`t"
+ # ValueListAllowed
+ if ($parsed.valueListAllowed -eq $true) {
+ X "`t`ttrue"
+ }
+
+ # 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"
+ X "`t`t`t$(Esc-Xml $avVal)"
+ # `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"
+ }
+ }
+
+ # DenyIncompleteValues
+ $deny = $parsed.denyIncompleteValues -eq $true -or (
+ $null -ne $p -and $p -isnot [string] -and $p.denyIncompleteValues -eq $true)
+ if ($deny) {
+ X "`t`ttrue"
+ }
+
+ # 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"
}
X "`t"
}
+$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"
X "$indent`t$(Esc-Xml $valStr)"
+ X "$indent`t0001-01-01T00:00:00"
+ X "$indent`t0001-01-01T00:00:00"
X "$indent"
} elseif ($type -match '^date') {
X "$indent$(Esc-Xml $valStr)"
@@ -929,6 +1136,8 @@ function Emit-ParamValue {
X "$indent$(Esc-Xml $valStr)"
} elseif ($valStr -eq "true" -or $valStr -eq "false") {
X "$indent$(Esc-Xml $valStr)"
+ } elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
+ X "$indent$(Esc-Xml $valStr)"
} else {
X "$indent$(Esc-Xml $valStr)"
}
@@ -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"
# Background color
@@ -1098,6 +1319,15 @@ function Emit-CellAppearance {
X "$ind`ttrue"
X "$ind"
}
+ # Horizontal merge
+ if ($hMerge) {
+ X "$ind"
+ X "$ind`tОбъединятьПоГоризонтали"
+ X "$ind`ttrue"
+ X "$ind"
+ }
+ # Extra appearance items (e.g. drilldown Расшифровка)
+ foreach ($ei in $extraItems) { X $ei }
X "`t`t`t`t"
}
@@ -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"
X "`t`t$(Esc-Xml "$($t.name)")"
X "`t`t"
@@ -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"
- 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"
- X "`t`t`t`t`t`t$(Esc-Xml $Matches[1])"
+ X "`t`t`t`t`t`t$(Esc-Xml $paramName)"
X "`t`t`t`t`t"
+ # Build drilldown appearance extra items
+ $cellExtraItems = @()
+ if ($drilldownMap.ContainsKey($paramName)) {
+ $ddVal = $drilldownMap[$paramName]
+ $cellExtraItems += "`t`t`t`t`t"
+ $cellExtraItems += "`t`t`t`t`t`tРасшифровка"
+ $cellExtraItems += "`t`t`t`t`t`tРасшифровка_$ddVal"
+ $cellExtraItems += "`t`t`t`t`t"
+ }
} else {
# Static text
X "`t`t`t`t`t"
- X "`t`t`t`t`t`t"
- X "`t`t`t`t`t`t`t"
- X "`t`t`t`t`t`t`t`tru"
- X "`t`t`t`t`t`t`t`t$(Esc-Xml $cellStr)"
- X "`t`t`t`t`t`t`t"
- X "`t`t`t`t`t`t"
+ Emit-MLText -tag "dcsat:value" -text $cellStr -indent "`t`t`t`t`t`t"
X "`t`t`t`t`t"
}
}
# 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"
}
@@ -1186,6 +1444,18 @@ function Emit-AreaTemplateDSL {
X "`t`t`t$(Esc-Xml "$($tp.name)")"
X "`t`t`t$(Esc-Xml "$($tp.expression)")"
X "`t`t"
+ # Drilldown parameter
+ if ($tp.drilldown) {
+ $ddVal = "$($tp.drilldown)"
+ X "`t`t"
+ X "`t`t`tРасшифровка_$(Esc-Xml $ddVal)"
+ X "`t`t`t"
+ X "`t`t`t`tИмяРесурса"
+ X "`t`t`t`t`"$(Esc-Xml $ddVal)`""
+ X "`t`t`t"
+ X "`t`t`tDrillDown"
+ X "`t`t"
+ }
}
}
X "`t"
@@ -1211,6 +1481,18 @@ function Emit-Templates {
X "`t`t`t$(Esc-Xml "$($tp.name)")"
X "`t`t`t$(Esc-Xml "$($tp.expression)")"
X "`t`t"
+ # Drilldown parameter
+ if ($tp.drilldown) {
+ $ddVal = "$($tp.drilldown)"
+ X "`t`t"
+ X "`t`t`tРасшифровка_$(Esc-Xml $ddVal)"
+ X "`t`t`t"
+ X "`t`t`t`tИмяРесурса"
+ X "`t`t`t`t`"$(Esc-Xml $ddVal)`""
+ X "`t`t`t"
+ X "`t`t`tDrillDown"
+ X "`t`t"
+ }
}
}
X "`t"
@@ -1222,11 +1504,20 @@ function Emit-Templates {
function Emit-GroupTemplates {
if (-not $def.groupTemplates) { return }
foreach ($gt in $def.groupTemplates) {
- X "`t"
- X "`t`t$(Esc-Xml "$($gt.groupField)")"
- X "`t`t$(Esc-Xml "$($gt.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$(Esc-Xml "$($gt.groupName)")"
+ } elseif ($gt.groupField) {
+ X "`t`t$(Esc-Xml "$($gt.groupField)")"
+ }
+ X "`t`t$(Esc-Xml $xmlTType)"
X "`t`t$(Esc-Xml "$($gt.template)")"
- X "`t"
+ X "`t$tag>"
}
}
@@ -1249,6 +1540,22 @@ function Emit-Selection {
X "$indent`t`t$(Esc-Xml $item)"
X "$indent`t"
}
+ } elseif ($item.folder) {
+ X "$indent`t"
+ X "$indent`t`t"
+ X "$indent`t`t`t"
+ X "$indent`t`t`t`tru"
+ X "$indent`t`t`t`t$(Esc-Xml "$($item.folder)")"
+ X "$indent`t`t`t"
+ X "$indent`t`t"
+ foreach ($sub in $item.items) {
+ $subName = if ($sub -is [string]) { $sub } else { "$($sub.field)" }
+ X "$indent`t`t"
+ X "$indent`t`t`t$(Esc-Xml $subName)"
+ X "$indent`t`t"
+ }
+ X "$indent`t`tAuto"
+ X "$indent`t"
} else {
X "$indent`t"
X "$indent`t`t$(Esc-Xml "$($item.field)")"
@@ -1281,6 +1588,16 @@ function Emit-FilterItem {
X "$indent`t$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"
- X "$indent`t`t"
- X "$indent`t`t`tru"
- X "$indent`t`t`t$(Esc-Xml "$($item.presentation)")"
- X "$indent`t`t"
- X "$indent`t"
+ 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"
- X "$indent`t`t"
- X "$indent`t`t`tru"
- X "$indent`t`t`t$(Esc-Xml "$($item.userSettingPresentation)")"
- X "$indent`t`t"
- X "$indent`t"
+ Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t"
}
X "$indent"
@@ -1432,13 +1739,8 @@ function Emit-AppearanceValue {
X "$indent`t$(Esc-Xml $actualVal)"
} elseif ($actualVal -eq "true" -or $actualVal -eq "false") {
X "$indent`t$actualVal"
- } elseif ($key -eq "Текст" -or $key -eq "Заголовок") {
- X "$indent`t"
- X "$indent`t`t"
- X "$indent`t`t`tru"
- X "$indent`t`t`t$(Esc-Xml $actualVal)"
- X "$indent`t`t"
- X "$indent`t"
+ } elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
+ Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t"
} else {
X "$indent`t$(Esc-Xml $actualVal)"
}
@@ -1517,12 +1819,7 @@ function Emit-OutputParameters {
X "$indent`t"
X "$indent`t`t$(Esc-Xml $key)"
if ($ptype -eq "mltext") {
- X "$indent`t`t"
- X "$indent`t`t`t"
- X "$indent`t`t`t`tru"
- X "$indent`t`t`t`t$(Esc-Xml $val)"
- X "$indent`t`t`t"
- X "$indent`t`t"
+ Emit-MLText -tag "dcscor:value" -text $val -indent "$indent`t`t"
} else {
X "$indent`t`t$(Esc-Xml $val)"
}
@@ -1567,22 +1864,35 @@ function Emit-DataParameters {
X "$indent`t`t$(Esc-Xml "$($dp.parameter)")"
# Value
- if ($null -ne $dp.value) {
+ if ($dp.nilValue -eq $true) {
+ X "$indent`t`t"
+ } 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"
X "$indent`t`t`t$(Esc-Xml "$($dp.value.variant)")"
+ X "$indent`t`t`t0001-01-01T00:00:00"
+ X "$indent`t`t`t0001-01-01T00:00:00"
X "$indent`t`t"
} elseif ($dp.value -is [hashtable] -and $dp.value.variant) {
# StandardPeriod (hashtable from shorthand parser)
X "$indent`t`t"
X "$indent`t`t`t$(Esc-Xml "$($dp.value.variant)")"
+ X "$indent`t`t`t0001-01-01T00:00:00"
+ X "$indent`t`t`t0001-01-01T00:00:00"
X "$indent`t`t"
- } elseif ($dp.value -is [bool]) {
+ } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
$bv = "$($dp.value)".ToLower()
X "$indent`t`t$(Esc-Xml $bv)"
- } 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$(Esc-Xml "$($dp.value)")"
+ } elseif ($vtype -match '^decimal') {
+ X "$indent`t`t$(Esc-Xml "$($dp.value)")"
+ } elseif ($vtype -match '^string') {
+ X "$indent`t`t$(Esc-Xml "$($dp.value)")"
+ } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
+ X "$indent`t`t$(Esc-Xml "$($dp.value)")"
} else {
X "$indent`t`t$(Esc-Xml "$($dp.value)")"
}
@@ -1598,12 +1908,7 @@ function Emit-DataParameters {
}
if ($dp.userSettingPresentation) {
- X "$indent`t`t"
- X "$indent`t`t`t"
- X "$indent`t`t`t`tru"
- X "$indent`t`t`t`t$(Esc-Xml "$($dp.userSettingPresentation)")"
- X "$indent`t`t`t"
- X "$indent`t`t"
+ Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t"
}
X "$indent`t"
@@ -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"
@@ -1687,7 +1996,8 @@ function Emit-StructureItem {
X "$indent`t$(Esc-Xml "$($item.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"
- 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$(Esc-Xml "$($row.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"
- 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"
- 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"
X "`t`t$(Esc-Xml "$($v.name)")"
- $pres = if ($v.presentation) { "$($v.presentation)" } elseif ($v.title) { "$($v.title)" } else { "$($v.name)" }
- X "`t`t"
- X "`t`t`t"
- X "`t`t`t`tru"
- X "`t`t`t`t$(Esc-Xml $pres)"
- X "`t`t`t"
- X "`t`t"
+ $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"
@@ -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 → +
+ $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"
}
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py
index 33198a0d..9a32ca18 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.py
+++ b/.claude/skills/skd-compile/scripts/skd-compile.py
@@ -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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
+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")
- lines.append(f"{indent}\t\tru")
- lines.append(f"{indent}\t\t{esc_xml(text)}")
- lines.append(f"{indent}\t")
+ # Multi-lang: object form { ru: "...", en: "..." } -- one per language
+ if isinstance(text, dict):
+ for lang, content in text.items():
+ lines.append(f"{indent}\t")
+ lines.append(f"{indent}\t\t{esc_xml(str(lang))}")
+ lines.append(f"{indent}\t\t{esc_xml(str(content))}")
+ lines.append(f"{indent}\t")
+ else:
+ lines.append(f"{indent}\t")
+ lines.append(f"{indent}\t\tru")
+ lines.append(f"{indent}\t\t{esc_xml(str(text))}")
+ lines.append(f"{indent}\t")
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')
- lines.append(f'\t\t{esc_xml(parsed["dataPath"])}')
- lines.append(f'\t\t{esc_xml(parsed["expression"])}')
+ lines.append(f'\t\t{esc_xml(data_path)}')
+ lines.append(f'\t\t{esc_xml(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')
- emit_value_type(lines, cf_type, '\t\t\t')
- lines.append('\t\t')
- if cf.get('restrict'):
- restrict_map = {
- 'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
- 'noGroup': 'group', 'noOrder': 'order',
- }
- lines.append('\t\t')
- for r in cf['restrict']:
+ if title:
+ emit_mltext(lines, '\t\t', 'title', title)
+ if type_str:
+ lines.append('\t\t')
+ emit_value_type(lines, type_str, '\t\t\t')
+ lines.append('\t\t')
+ if restrict_obj or restrict_tokens:
+ lines.append('\t\t')
+ 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')
- if cf.get('appearance'):
- lines.append('\t\t')
- for k, v in cf['appearance'].items():
- lines.append('\t\t\t')
- lines.append(f'\t\t\t\t{esc_xml(k)}')
- lines.append(f'\t\t\t\t{esc_xml(str(v))}')
- lines.append('\t\t\t')
- lines.append('\t\t')
+ lines.append('\t\t')
+ if appearance:
+ lines.append('\t\t')
+ for k, v in appearance.items():
+ lines.append('\t\t\t')
+ lines.append(f'\t\t\t\t{esc_xml(k)}')
+ lines.append(f'\t\t\t\t{esc_xml(str(v))}')
+ lines.append('\t\t\t')
+ lines.append('\t\t')
lines.append('\t')
@@ -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}')
lines.append(f'{indent}\t{esc_xml(val_str)}')
+ lines.append(f'{indent}\t0001-01-01T00:00:00')
+ lines.append(f'{indent}\t0001-01-01T00:00:00')
lines.append(f'{indent}')
elif type_str and re.match(r'^date', type_str):
lines.append(f'{indent}{esc_xml(val_str)}')
@@ -692,6 +817,8 @@ def emit_param_value(lines, type_str, val, indent):
lines.append(f'{indent}{esc_xml(val_str)}')
elif val_str == 'true' or val_str == 'false':
lines.append(f'{indent}{esc_xml(val_str)}')
+ elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
+ lines.append(f'{indent}{esc_xml(val_str)}')
else:
lines.append(f'{indent}{esc_xml(val_str)}')
@@ -700,10 +827,15 @@ def emit_single_param(lines, p, parsed):
lines.append('\t')
lines.append(f'\t\t{esc_xml(parsed["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\ttrue')
# Expression
if parsed.get('expression'):
lines.append(f'\t\t{esc_xml(parsed["expression"])}')
+ if parsed.get('hidden'):
+ parsed['availableAsField'] = False
# AvailableAsField
if parsed.get('availableAsField') is False:
lines.append('\t\tfalse')
+ # ValueListAllowed
+ if parsed.get('valueListAllowed'):
+ lines.append('\t\ttrue')
+
+ # 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')
+ lines.append(f'\t\t\t{esc_xml(av_val)}')
+ # `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')
+
+ # 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\ttrue')
+
# Use
+ use_val = None
if p is not None and not isinstance(p, str) and p.get('use'):
- lines.append(f'\t\t')
+ use_val = str(p['use'])
+ elif parsed.get('use'):
+ use_val = str(parsed['use'])
+ if use_val:
+ lines.append(f'\t\t')
lines.append('\t')
+_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}{esc_xml(color)}')
-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')
# 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}')
lines.append(f'{ind}\t\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430')
- lines.append(f'{ind}\t{width}')
+ lines.append(f'{ind}\t{fmt_dec(width)}')
lines.append(f'{ind}')
lines.append(f'{ind}')
lines.append(f'{ind}\t\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430')
- lines.append(f'{ind}\t{width}')
+ lines.append(f'{ind}\t{fmt_dec(width)}')
lines.append(f'{ind}')
# 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\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438')
lines.append(f'{ind}\ttrue')
lines.append(f'{ind}')
+ # Horizontal merge
+ if h_merge:
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u0438')
+ lines.append(f'{ind}\ttrue')
+ lines.append(f'{ind}')
+ # Extra appearance items (e.g. drilldown)
+ if extra_items:
+ for ei in extra_items:
+ lines.append(ei)
lines.append('\t\t\t\t')
@@ -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')
lines.append(f'\t\t{esc_xml(str(t["name"]))}')
lines.append('\t\t')
@@ -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')
- 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')
- lines.append(f'\t\t\t\t\t\t{esc_xml(m.group(1))}')
+ lines.append(f'\t\t\t\t\t\t{esc_xml(param_name)}')
lines.append('\t\t\t\t\t')
+ # 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')
+ cell_extra_items.append(f'\t\t\t\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430')
+ cell_extra_items.append(f'\t\t\t\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{dd_val}')
+ cell_extra_items.append('\t\t\t\t\t')
else:
lines.append('\t\t\t\t\t')
- lines.append('\t\t\t\t\t\t')
- lines.append('\t\t\t\t\t\t\t')
- lines.append('\t\t\t\t\t\t\t\tru')
- lines.append(f'\t\t\t\t\t\t\t\t{esc_xml(cell_str)}')
- lines.append('\t\t\t\t\t\t\t')
- lines.append('\t\t\t\t\t\t')
+ emit_mltext(lines, '\t\t\t\t\t\t', 'dcsat:value', cell_str)
lines.append('\t\t\t\t\t')
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')
lines.append('\t\t\t')
@@ -985,6 +1225,17 @@ def _emit_area_template_dsl(lines, t):
lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
lines.append('\t\t')
+ # Drilldown parameter
+ if tp.get('drilldown'):
+ dd_val = str(tp['drilldown'])
+ lines.append('\t\t')
+ lines.append(f'\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}')
+ lines.append('\t\t\t')
+ lines.append('\t\t\t\t\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430')
+ lines.append(f'\t\t\t\t"{esc_xml(dd_val)}"')
+ lines.append('\t\t\t')
+ lines.append('\t\t\tDrillDown')
+ lines.append('\t\t')
lines.append('\t')
@@ -1007,6 +1258,17 @@ def emit_templates(lines, defn):
lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
lines.append('\t\t')
+ # Drilldown parameter
+ if tp.get('drilldown'):
+ dd_val = str(tp['drilldown'])
+ lines.append('\t\t')
+ lines.append(f'\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}')
+ lines.append('\t\t\t')
+ lines.append('\t\t\t\t\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430')
+ lines.append(f'\t\t\t\t"{esc_xml(dd_val)}"')
+ lines.append('\t\t\t')
+ lines.append('\t\t\tDrillDown')
+ lines.append('\t\t')
lines.append('\t')
@@ -1016,11 +1278,19 @@ def emit_group_templates(lines, defn):
if not defn.get('groupTemplates'):
return
for gt in defn['groupTemplates']:
- lines.append('\t')
- lines.append(f'\t\t{esc_xml(str(gt["groupField"]))}')
- lines.append(f'\t\t{esc_xml(str(gt["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{esc_xml(str(gt["groupName"]))}')
+ elif gt.get('groupField'):
+ lines.append(f'\t\t{esc_xml(str(gt["groupField"]))}')
+ lines.append(f'\t\t{esc_xml(xml_ttype)}')
lines.append(f'\t\t{esc_xml(str(gt["template"]))}')
- lines.append('\t')
+ 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')
lines.append(f'{indent}\t\t{esc_xml(item)}')
lines.append(f'{indent}\t')
+ elif item.get('folder'):
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}\t\t')
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t\t\tru')
+ lines.append(f'{indent}\t\t\t\t{esc_xml(str(item["folder"]))}')
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t')
+ 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')
+ lines.append(f'{indent}\t\t\t{esc_xml(sub_name)}')
+ lines.append(f'{indent}\t\t')
+ lines.append(f'{indent}\t\tAuto')
+ lines.append(f'{indent}\t')
else:
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(str(item["field"]))}')
@@ -1062,6 +1347,19 @@ def emit_filter_item(lines, item, indent):
lines.append(f'{indent}\t{group_type}')
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}')
return
@@ -1097,12 +1395,7 @@ def emit_filter_item(lines, item, indent):
lines.append(f'{indent}\t{v_str}')
if item.get('presentation'):
- lines.append(f'{indent}\t')
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t\t\tru')
- lines.append(f'{indent}\t\t\t{esc_xml(str(item["presentation"]))}')
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t')
+ emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item["presentation"])
if item.get('viewMode'):
lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}')
@@ -1112,12 +1405,7 @@ def emit_filter_item(lines, item, indent):
lines.append(f'{indent}\t{esc_xml(uid)}')
if item.get('userSettingPresentation'):
- lines.append(f'{indent}\t')
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t\t\tru')
- lines.append(f'{indent}\t\t\t{esc_xml(str(item["userSettingPresentation"]))}')
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t')
+ emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item["userSettingPresentation"])
lines.append(f'{indent}')
@@ -1191,13 +1479,8 @@ def emit_appearance_value(lines, key, val, indent):
lines.append(f'{indent}\t{esc_xml(actual_val)}')
elif actual_val == 'true' or actual_val == 'false':
lines.append(f'{indent}\t{actual_val}')
- elif key == '\u0422\u0435\u043a\u0441\u0442' or key == '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a':
- lines.append(f'{indent}\t')
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t\t\tru')
- lines.append(f'{indent}\t\t\t{esc_xml(actual_val)}')
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t')
+ 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{esc_xml(actual_val)}')
lines.append(f'{indent}')
@@ -1262,12 +1545,7 @@ def emit_output_parameters(lines, params, indent):
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(key)}')
if ptype == 'mltext':
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t\t\t')
- lines.append(f'{indent}\t\t\t\tru')
- lines.append(f'{indent}\t\t\t\t{esc_xml(val_str)}')
- lines.append(f'{indent}\t\t\t')
- lines.append(f'{indent}\t\t')
+ emit_mltext(lines, f'{indent}\t\t', 'dcscor:value', val_str)
else:
lines.append(f'{indent}\t\t{esc_xml(val_str)}')
lines.append(f'{indent}\t')
@@ -1303,18 +1581,29 @@ def emit_data_parameters(lines, items, indent):
lines.append(f'{indent}\t\t{esc_xml(str(dp["parameter"]))}')
# Value
- if dp.get('value') is not None:
+ if dp.get('nilValue') is True:
+ lines.append(f'{indent}\t\t')
+ 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')
lines.append(f'{indent}\t\t\t{esc_xml(str(val["variant"]))}')
+ lines.append(f'{indent}\t\t\t0001-01-01T00:00:00')
+ lines.append(f'{indent}\t\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t\t')
- elif isinstance(val, bool):
+ elif vtype == 'boolean' or isinstance(val, bool):
bv = str(val).lower()
lines.append(f'{indent}\t\t{esc_xml(bv)}')
- 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{esc_xml(str(val))}')
+ elif re.match(r'^decimal', vtype):
+ lines.append(f'{indent}\t\t{esc_xml(str(val))}')
+ elif re.match(r'^string', vtype):
+ lines.append(f'{indent}\t\t{esc_xml(str(val))}')
+ 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{esc_xml(str(val))}')
else:
lines.append(f'{indent}\t\t{esc_xml(str(val))}')
@@ -1326,12 +1615,7 @@ def emit_data_parameters(lines, items, indent):
lines.append(f'{indent}\t\t{esc_xml(uid)}')
if dp.get('userSettingPresentation'):
- lines.append(f'{indent}\t\t')
- lines.append(f'{indent}\t\t\t')
- lines.append(f'{indent}\t\t\t\tru')
- lines.append(f'{indent}\t\t\t\t{esc_xml(str(dp["userSettingPresentation"]))}')
- lines.append(f'{indent}\t\t\t')
- lines.append(f'{indent}\t\t')
+ emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp["userSettingPresentation"])
lines.append(f'{indent}\t')
lines.append(f'{indent}')
@@ -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}')
@@ -1396,7 +1686,7 @@ def emit_structure_item(lines, item, indent):
if item.get('name'):
lines.append(f'{indent}\t{esc_xml(str(item["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')
- 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')
if row.get('name'):
lines.append(f'{indent}\t\t{esc_xml(str(row["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')
- 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')
- 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')
lines.append(f'\t\t{esc_xml(str(v["name"]))}')
- pres = str(v.get('presentation', '')) or str(v.get('title', '')) or str(v['name'])
- lines.append('\t\t')
- lines.append('\t\t\t')
- lines.append('\t\t\t\tru')
- lines.append(f'\t\t\t\t{esc_xml(pres)}')
- lines.append('\t\t\t')
- lines.append('\t\t')
+ pres = v.get('presentation') or v.get('title') or v['name']
+ emit_mltext(lines, '\t\t', 'dcsset:presentation', pres)
lines.append('\t\t')
@@ -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 → +
+ 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 ---
diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md
index c62dc28c..1c212ea1 100644
--- a/.claude/skills/skd-edit/SKILL.md
+++ b/.claude/skills/skd-edit/SKILL.md
@@ -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` → `` (аналогично add-field).
+
Также добавляется в selection варианта.
### add-parameter — добавить параметр
```
-"Период: StandardPeriod = LastMonth @autoDates"
+"Период [Отчетный период]: StandardPeriod = LastMonth @autoDates"
"Организация: CatalogRef.Организации"
```
-`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически.
+Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет ``.
+
+`@autoDates` генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра — для БСП-отчётов, чтобы получить пару полей «Начало/Конец» в панели быстрых настроек.
+
+### modify-parameter — изменить существующий параметр
+
+Находит параметр по имени, добавляет/обновляет свойства.
+
+```
+"ПорядокОкругления use=Always"
+"ПорядокОкругления [Округление сумм] denyIncompleteValues=true"
+"ПериодОтчета [Отчетный период]" # только title
+"ПорядокОкругления availableValue=Перечисление.Округления.Окр1 presentation=руб."
+```
+
+`[Заголовок]` опциональный — устанавливает или заменяет ``. Можно вызывать без других 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=Имя` — присваивает имя группировке (``). Используется для привязки шаблонов через `groupName`.
+
### modify-field — изменить существующее поле
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
@@ -200,11 +269,18 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
### modify-filter — изменить существующий фильтр
-Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги.
+Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. См. правило для `
+
+
+ Комментарий
+
+
+ ru
+ Комментарий
+
+
+
+
+ xs:string
+
+ 200
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ DontIndex
+ Use
+
+
diff --git a/tests/skills/cases/meta-compile/snapshots/calculation-register/CalculationRegisters/Начисления.xml b/tests/skills/cases/meta-compile/snapshots/calculation-register/CalculationRegisters/Начисления.xml
index af1347b2..da2c690f 100644
--- a/tests/skills/cases/meta-compile/snapshots/calculation-register/CalculationRegisters/Начисления.xml
+++ b/tests/skills/cases/meta-compile/snapshots/calculation-register/CalculationRegisters/Начисления.xml
@@ -45,8 +45,8 @@
ChartOfCalculationTypes.ВидыНачислений
Month
- true
- true
+ false
+ false
@@ -330,6 +330,46 @@
Use
+
+
+ Комментарий
+
+
+ ru
+ Комментарий
+
+
+
+
+ xs:string
+
+ 200
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ DontIndex
+ Use
+
+
diff --git a/tests/skills/cases/meta-compile/snapshots/chart-of-accounts/ChartsOfAccounts/Хозрасчетный.xml b/tests/skills/cases/meta-compile/snapshots/chart-of-accounts/ChartsOfAccounts/Хозрасчетный.xml
index be8a1acb..6b826fc2 100644
--- a/tests/skills/cases/meta-compile/snapshots/chart-of-accounts/ChartsOfAccounts/Хозрасчетный.xml
+++ b/tests/skills/cases/meta-compile/snapshots/chart-of-accounts/ChartsOfAccounts/Хозрасчетный.xml
@@ -42,7 +42,7 @@
true
- 3
+ 0
4
120
@@ -522,38 +522,6 @@
Auto
-
-
- СуммовойУчет
-
-
- ru
- Суммовой учет
-
-
-
-
- xs:boolean
-
- false
-
-
-
- false
-
- false
- false
-
-
- DontCheck
-
-
- Auto
-
-
- Auto
-
-
diff --git a/tests/skills/cases/meta-compile/snapshots/chart-of-calculation-types/ChartsOfCalculationTypes/ВидыНачислений.xml b/tests/skills/cases/meta-compile/snapshots/chart-of-calculation-types/ChartsOfCalculationTypes/ВидыНачислений.xml
index 1e8b05e6..ebc76ee0 100644
--- a/tests/skills/cases/meta-compile/snapshots/chart-of-calculation-types/ChartsOfCalculationTypes/ВидыНачислений.xml
+++ b/tests/skills/cases/meta-compile/snapshots/chart-of-calculation-types/ChartsOfCalculationTypes/ВидыНачислений.xml
@@ -308,8 +308,6 @@
false
- false
-
DontCheck
Items
@@ -321,7 +319,6 @@
Auto
DontIndex
Use
- Use
diff --git a/tests/skills/cases/meta-compile/snapshots/chart-of-characteristic-types/ChartsOfCharacteristicTypes/ДополнительныеРеквизитыИСведения.xml b/tests/skills/cases/meta-compile/snapshots/chart-of-characteristic-types/ChartsOfCharacteristicTypes/ДополнительныеРеквизитыИСведения.xml
index ad6aa150..da7389fe 100644
--- a/tests/skills/cases/meta-compile/snapshots/chart-of-characteristic-types/ChartsOfCharacteristicTypes/ДополнительныеРеквизитыИСведения.xml
+++ b/tests/skills/cases/meta-compile/snapshots/chart-of-characteristic-types/ChartsOfCharacteristicTypes/ДополнительныеРеквизитыИСведения.xml
@@ -340,8 +340,6 @@
false
- false
-
DontCheck
Items
@@ -353,7 +351,6 @@
Auto
DontIndex
Use
- Use
diff --git a/tests/skills/cases/meta-compile/snapshots/document-journal/Configuration.xml b/tests/skills/cases/meta-compile/snapshots/document-journal/Configuration.xml
index 04679094..646b798a 100644
--- a/tests/skills/cases/meta-compile/snapshots/document-journal/Configuration.xml
+++ b/tests/skills/cases/meta-compile/snapshots/document-journal/Configuration.xml
@@ -246,6 +246,8 @@
Русский
+ ПриходнаяНакладная
+ РасходнаяНакладная
ЖурналСкладскихДокументов
diff --git a/tests/skills/cases/meta-compile/snapshots/document-journal/DocumentJournals/ЖурналСкладскихДокументов.xml b/tests/skills/cases/meta-compile/snapshots/document-journal/DocumentJournals/ЖурналСкладскихДокументов.xml
index aab0324e..8f24d739 100644
--- a/tests/skills/cases/meta-compile/snapshots/document-journal/DocumentJournals/ЖурналСкладскихДокументов.xml
+++ b/tests/skills/cases/meta-compile/snapshots/document-journal/DocumentJournals/ЖурналСкладскихДокументов.xml
@@ -221,7 +221,10 @@
DontIndex
-
+
+ Document.ПриходнаяНакладная.Attribute.Контрагент
+ Document.РасходнаяНакладная.Attribute.Контрагент
+
diff --git a/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/ПриходнаяНакладная.xml b/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/ПриходнаяНакладная.xml
new file mode 100644
index 00000000..5678a0ed
--- /dev/null
+++ b/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/ПриходнаяНакладная.xml
@@ -0,0 +1,301 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+
+ ПриходнаяНакладная
+
+
+ ru
+ Приходная накладная
+
+
+
+ true
+
+ String
+ 11
+ Variable
+ Year
+ true
+ true
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+
+
+ Document.ПриходнаяНакладная.StandardAttribute.Number
+
+ DontUse
+ Begin
+ DontUse
+ Directly
+
+
+
+
+
+
+ Allow
+ Deny
+ AutoDelete
+ WriteModified
+ AutoFill
+
+ true
+ true
+ false
+
+ Automatic
+ Use
+
+
+
+
+
+ Auto
+ DontUse
+ false
+ false
+
+
+
+
+ Склад
+
+
+ ru
+ Склад
+
+
+
+
+ xs:string
+
+ 50
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ DontIndex
+ Use
+ Use
+
+
+
+
+ Контрагент
+
+
+ ru
+ Контрагент
+
+
+
+
+ xs:string
+
+ 100
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ DontIndex
+ Use
+ Use
+
+
+
+
+
diff --git a/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/ПриходнаяНакладная/Ext/ObjectModule.bsl b/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/ПриходнаяНакладная/Ext/ObjectModule.bsl
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/РасходнаяНакладная.xml b/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/РасходнаяНакладная.xml
new file mode 100644
index 00000000..6f874e05
--- /dev/null
+++ b/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/РасходнаяНакладная.xml
@@ -0,0 +1,258 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+
+ РасходнаяНакладная
+
+
+ ru
+ Расходная накладная
+
+
+
+ true
+
+ String
+ 11
+ Variable
+ Year
+ true
+ true
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+ DontCheck
+ false
+ false
+ Auto
+
+
+ false
+
+
+ Auto
+ Auto
+
+ false
+ Use
+ false
+
+
+
+ Use
+
+
+
+
+
+
+
+
+
+ Document.РасходнаяНакладная.StandardAttribute.Number
+
+ DontUse
+ Begin
+ DontUse
+ Directly
+
+
+
+
+
+
+ Allow
+ Deny
+ AutoDelete
+ WriteModified
+ AutoFill
+
+ true
+ true
+ false
+
+ Automatic
+ Use
+
+
+
+
+
+ Auto
+ DontUse
+ false
+ false
+
+
+
+
+ Контрагент
+
+
+ ru
+ Контрагент
+
+
+
+
+ xs:string
+
+ 100
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ DontIndex
+ Use
+ Use
+
+
+
+
+
diff --git a/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/РасходнаяНакладная/Ext/ObjectModule.bsl b/tests/skills/cases/meta-compile/snapshots/document-journal/Documents/РасходнаяНакладная/Ext/ObjectModule.bsl
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/skills/cases/meta-compile/snapshots/document-multiple-tabparts/Documents/РеализацияТоваров.xml b/tests/skills/cases/meta-compile/snapshots/document-multiple-tabparts/Documents/РеализацияТоваров.xml
index fd3986a8..07d01121 100644
--- a/tests/skills/cases/meta-compile/snapshots/document-multiple-tabparts/Documents/РеализацияТоваров.xml
+++ b/tests/skills/cases/meta-compile/snapshots/document-multiple-tabparts/Documents/РеализацияТоваров.xml
@@ -192,9 +192,7 @@
AutoDelete
WriteModified
AutoFill
-
- AccumulationRegister.Продажи
-
+
true
true
false
diff --git a/tests/skills/cases/role-compile/explicit-rights.json b/tests/skills/cases/role-compile/explicit-rights.json
index 60b67ebb..71483723 100644
--- a/tests/skills/cases/role-compile/explicit-rights.json
+++ b/tests/skills/cases/role-compile/explicit-rights.json
@@ -5,7 +5,9 @@
"script": "meta-compile/scripts/meta-compile",
"input": {
"type": "InformationRegister",
- "name": "Цены"
+ "name": "Цены",
+ "dimensions": ["Номенклатура: String(100)"],
+ "resources": ["Цена: Number(15,2)"]
},
"args": {
"-JsonPath": "{inputFile}",
diff --git a/tests/skills/cases/role-compile/snapshots/explicit-rights/InformationRegisters/Цены.xml b/tests/skills/cases/role-compile/snapshots/explicit-rights/InformationRegisters/Цены.xml
index cc5c147c..9abe7a85 100644
--- a/tests/skills/cases/role-compile/snapshots/explicit-rights/InformationRegisters/Цены.xml
+++ b/tests/skills/cases/role-compile/snapshots/explicit-rights/InformationRegisters/Цены.xml
@@ -169,6 +169,97 @@
false
false
-
+
+
+
+ Цена
+
+
+ ru
+ Цена
+
+
+
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ DontIndex
+ Use
+ Use
+
+
+
+
+ Номенклатура
+
+
+ ru
+ Номенклатура
+
+
+
+
+ xs:string
+
+ 100
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+ Items
+
+
+ Auto
+ Auto
+
+
+ Auto
+ false
+ false
+ false
+ DontIndex
+ Use
+ Use
+
+
+
diff --git a/tests/skills/cases/skd-compile/auto-data-parameters.json b/tests/skills/cases/skd-compile/auto-data-parameters.json
new file mode 100644
index 00000000..bcdf23a4
--- /dev/null
+++ b/tests/skills/cases/skd-compile/auto-data-parameters.json
@@ -0,0 +1,31 @@
+{
+ "name": "dataParameters: auto — наследование значений по всем типам",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "query": "ВЫБРАТЬ 1 КАК Поле",
+ "fields": ["Поле: число"]
+ }],
+ "parameters": [
+ "Период: СтандартныйПериод = LastMonth @autoDates",
+ "ПериодБезДефолта: СтандартныйПериод",
+ "Флаг: boolean = true",
+ "Сумма: decimal(15,2) = 0",
+ "Ставка: decimal(5,2) = 13.5",
+ "Метка: string(50) = ТестовоеЗначение",
+ "ПустаяСтрока: string(50)",
+ "Валюта: СправочникСсылка.Валюты = Справочник.Валюты.EmptyRef"
+ ],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "structure": "details",
+ "dataParameters": "auto"
+ }
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/available-values-and-folders.json b/tests/skills/cases/skd-compile/available-values-and-folders.json
new file mode 100644
index 00000000..9605231f
--- /dev/null
+++ b/tests/skills/cases/skd-compile/available-values-and-folders.json
@@ -0,0 +1,48 @@
+{
+ "name": "availableValues, denyIncompleteValues, Folder в selection",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Счет, Т.Остаток, Т.Поступление1, Т.Поступление2, Т.Выбытие1, Т.Выбытие2 ИЗ Регистр КАК Т",
+ "fields": ["Счет: string", "Остаток: decimal(15,2)", "Поступление1: decimal(15,2)", "Поступление2: decimal(15,2)", "Выбытие1: decimal(15,2)", "Выбытие2: decimal(15,2)"]
+ }],
+ "parameters": [{
+ "name": "ПорядокОкругления",
+ "type": "EnumRef.Округления",
+ "value": "Перечисление.Округления.Окр1_00",
+ "use": "Always",
+ "denyIncompleteValues": true,
+ "availableValues": [
+ {"value": "Перечисление.Округления.Окр1_00", "presentation": "руб. коп"},
+ {"value": "Перечисление.Округления.Окр1", "presentation": "руб."},
+ {"value": "Перечисление.Округления.Окр1000", "presentation": "тыс. руб"}
+ ]
+ }],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": [
+ "Auto",
+ "Счет",
+ "Остаток",
+ {"folder": "Поступление", "items": ["Поступление1", "Поступление2"]},
+ {"folder": "Выбытие", "items": ["Выбытие1", "Выбытие2"]}
+ ],
+ "structure": {
+ "type": "group",
+ "name": "ДанныеОтчета",
+ "groupBy": ["Счет"],
+ "selection": [
+ "Auto",
+ {"folder": "Поступление", "items": ["Поступление1", "Поступление2"]}
+ ]
+ }
+ }
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/calc-object-name-restrict-string.json b/tests/skills/cases/skd-compile/calc-object-name-restrict-string.json
new file mode 100644
index 00000000..254d602a
--- /dev/null
+++ b/tests/skills/cases/skd-compile/calc-object-name-restrict-string.json
@@ -0,0 +1,23 @@
+{
+ "name": "calculatedFields объектная форма с name и строковым useRestriction",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Номенклатура, Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Номенклатура", "Сумма: decimal(15,2)"]
+ }],
+ "calculatedFields": [
+ {
+ "name": "ИмяРесурса",
+ "title": "Имя ресурса",
+ "expression": "\"\"",
+ "useRestriction": "#noField #noFilter #noGroup #noOrder"
+ }
+ ]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/calc-shorthand-extended.json b/tests/skills/cases/skd-compile/calc-shorthand-extended.json
new file mode 100644
index 00000000..8570dba9
--- /dev/null
+++ b/tests/skills/cases/skd-compile/calc-shorthand-extended.json
@@ -0,0 +1,19 @@
+{
+ "name": "calculatedFields shorthand с [Title], :type, =expr, #restrict",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Цена, Т.Закупка ИЗ Регистр КАК Т",
+ "fields": ["Цена: decimal(15,2)", "Закупка: decimal(15,2)"]
+ }],
+ "calculatedFields": [
+ "ИмяРесурса [Имя ресурса]: string = \"\" #noField #noFilter #noGroup #noOrder",
+ "Маржа [Маржа]: decimal(15,2) = Цена - Закупка"
+ ]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/field-appearance-and-presentation.json b/tests/skills/cases/skd-compile/field-appearance-and-presentation.json
new file mode 100644
index 00000000..399308ac
--- /dev/null
+++ b/tests/skills/cases/skd-compile/field-appearance-and-presentation.json
@@ -0,0 +1,34 @@
+{
+ "name": "appearance и presentationExpression на поле DataSet",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "ЖурналОшибок",
+ "objectName": "ЖурналОшибок",
+ "fields": [
+ {
+ "field": "ТекстСообщения",
+ "title": "Текст сообщения",
+ "type": "string(150)",
+ "appearance": {
+ "МинимальнаяШирина": "100",
+ "РастягиватьПоГоризонтали": "true"
+ }
+ },
+ {
+ "field": "Расшифровка",
+ "title": "Описание",
+ "type": "CatalogRef.Организации",
+ "presentationExpression": "ТекстСообщения",
+ "appearance": {
+ "ГоризонтальноеПоложение": "Right"
+ }
+ }
+ ]
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/field-multi-type.json b/tests/skills/cases/skd-compile/field-multi-type.json
new file mode 100644
index 00000000..49ad75d9
--- /dev/null
+++ b/tests/skills/cases/skd-compile/field-multi-type.json
@@ -0,0 +1,26 @@
+{
+ "name": "Составной тип поля (multi-type valueType)",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "ЖурналОшибок",
+ "objectName": "ЖурналОшибок",
+ "fields": [
+ { "field": "ТекстСообщения", "title": "Текст сообщения", "type": "string(150)" },
+ {
+ "field": "Расшифровка",
+ "title": "Расшифровка",
+ "type": [
+ "CatalogRef.СтруктураПредприятия",
+ "CatalogRef.ЭтапыПроизводства",
+ "CatalogRef.ВидыРабочихЦентров"
+ ]
+ }
+ ]
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/horizontal-merge.json b/tests/skills/cases/skd-compile/horizontal-merge.json
new file mode 100644
index 00000000..2fdcadf6
--- /dev/null
+++ b/tests/skills/cases/skd-compile/horizontal-merge.json
@@ -0,0 +1,47 @@
+{
+ "name": "Горизонтальное объединение ячеек (>) в шаблонах",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Счет, Т.Остаток, Т.Пост1, Т.Пост2, Т.Пост3, Т.Выб1, Т.Выб2, Т.Итого ИЗ Регистр КАК Т",
+ "fields": ["Счет: string", "Остаток: decimal(15,2)", "Пост1: decimal(15,2)", "Пост2: decimal(15,2)", "Пост3: decimal(15,2)", "Выб1: decimal(15,2)", "Выб2: decimal(15,2)", "Итого: decimal(15,2)"]
+ }],
+ "templates": [
+ {
+ "name": "Макет1",
+ "style": "header",
+ "widths": [30, 16, 16, 16, 16, 16, 16, 16],
+ "minHeight": 24.75,
+ "rows": [
+ ["Счет", "Остаток", "Поступление", ">", ">", "Выбытие", ">", "Итого"],
+ ["|", "|", "из произв.", "из п/ф", "прочее", "Реализ.", "прочее", "|"],
+ ["К1", "К2", "К3", "К4", "К5", "К6", "К7", "К8"]
+ ]
+ },
+ {
+ "name": "Макет2",
+ "style": "data",
+ "widths": [30, 16, 16, 16, 16, 16, 16, 16],
+ "rows": [["{Счет}", "{Остаток}", "{Пост1}", "{Пост2}", "{Пост3}", "{Выб1}", "{Выб2}", "{Итого}"]]
+ }
+ ],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Auto"],
+ "structure": "details"
+ }
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"],
+ "contains": [
+ "ОбъединятьПоГоризонтали",
+ "ОбъединятьПоВертикали",
+ "Поступление",
+ "Выбытие"
+ ]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/multi-lang-title.json b/tests/skills/cases/skd-compile/multi-lang-title.json
new file mode 100644
index 00000000..656384ef
--- /dev/null
+++ b/tests/skills/cases/skd-compile/multi-lang-title.json
@@ -0,0 +1,33 @@
+{
+ "name": "Многоязычные title и presentation (ru + en)",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": [
+ { "field": "Сумма", "title": { "ru": "Сумма продажи", "en": "Sale amount" }, "type": "decimal(15,2)" }
+ ]
+ }],
+ "calculatedFields": [
+ { "name": "Маржа", "title": { "ru": "Маржа", "en": "Margin" }, "expression": "Сумма * 0.2" }
+ ],
+ "totalFields": [
+ { "dataPath": "Сумма", "title": { "ru": "Итого, руб.", "en": "Total, RUB" }, "expression": "Сумма(Сумма)" }
+ ],
+ "parameters": [
+ { "name": "Период", "title": { "ru": "Период", "en": "Period" }, "type": "StandardPeriod" }
+ ],
+ "settingsVariants": [{
+ "name": "Основной",
+ "title": { "ru": "Продажи", "en": "Sales" },
+ "settings": {
+ "selection": ["Сумма", "Маржа", "Auto"]
+ }
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/orgroup-string-items.json b/tests/skills/cases/skd-compile/orgroup-string-items.json
new file mode 100644
index 00000000..368a34ca
--- /dev/null
+++ b/tests/skills/cases/skd-compile/orgroup-string-items.json
@@ -0,0 +1,31 @@
+{
+ "name": "OrGroup в conditionalAppearance со строками-shorthand в items",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Поле1, Т.Поле2, Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Поле1: decimal", "Поле2: decimal", "Сумма: decimal(15,2)"]
+ }],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Поле1", "Поле2", "Сумма"],
+ "conditionalAppearance": [
+ {
+ "filter": [{"group": "Or", "items": [
+ "Поле1 = 1",
+ "Поле2 = 2"
+ ]}],
+ "appearance": { "Формат": "ЧЦ=15; ЧДЦ=0" }
+ }
+ ],
+ "structure": "details"
+ }
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/parameter-title-presentation-synonyms.json b/tests/skills/cases/skd-compile/parameter-title-presentation-synonyms.json
new file mode 100644
index 00000000..9407e6a0
--- /dev/null
+++ b/tests/skills/cases/skd-compile/parameter-title-presentation-synonyms.json
@@ -0,0 +1,27 @@
+{
+ "name": "Parameter.presentation и availableValue.title — синонимы",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": [
+ {
+ "name": "ПорядокОкругления",
+ "presentation": "Округление",
+ "type": "EnumRef.Округления",
+ "value": "Перечисление.Округления.Окр1_00",
+ "availableValues": [
+ { "value": "Перечисление.Округления.Окр1_00", "title": "руб. коп" },
+ { "value": "Перечисление.Округления.Окр1", "presentation": "руб." }
+ ]
+ }
+ ]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/snapshots/auto-data-parameters/Template.xml b/tests/skills/cases/skd-compile/snapshots/auto-data-parameters/Template.xml
new file mode 100644
index 00000000..c1f7b2ca
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/auto-data-parameters/Template.xml
@@ -0,0 +1,211 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ НаборДанных1
+
+ Поле
+ Поле
+
+ decimal
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ 1 КАК Поле
+
+
+ Период
+
+ v8:StandardPeriod
+
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+ true
+ Always
+
+
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
+
+ xs:dateTime
+
+ Date
+
+
+ 0001-01-01T00:00:00
+ true
+ &Период.ДатаНачала
+
+
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
+
+ xs:dateTime
+
+ Date
+
+
+ 0001-01-01T00:00:00
+ true
+ &Период.ДатаОкончания
+
+
+ ПериодБезДефолта
+
+ v8:StandardPeriod
+
+
+
+ Флаг
+
+ xs:boolean
+
+ true
+
+
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+ 0
+
+
+ Ставка
+
+ xs:decimal
+
+ 5
+ 2
+ Any
+
+
+ 13.5
+
+
+ Метка
+
+ xs:string
+
+ 50
+ Variable
+
+
+ ТестовоеЗначение
+
+
+ ПустаяСтрока
+
+ xs:string
+
+ 50
+ Variable
+
+
+
+
+ Валюта
+
+ d5p1:CatalogRef.Валюты
+
+ Справочник.Валюты.EmptyRef
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+ Период
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+ UUID-001
+
+
+ false
+ ПериодБезДефолта
+
+ Custom
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+ UUID-002
+
+
+ Флаг
+ true
+ UUID-003
+
+
+ Сумма
+ 0
+ UUID-004
+
+
+ Ставка
+ 13.5
+ UUID-005
+
+
+ Метка
+ ТестовоеЗначение
+ UUID-006
+
+
+ false
+ ПустаяСтрока
+
+ UUID-007
+
+
+ Валюта
+ Справочник.Валюты.EmptyRef
+ UUID-008
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/available-values-and-folders/Template.xml b/tests/skills/cases/skd-compile/snapshots/available-values-and-folders/Template.xml
new file mode 100644
index 00000000..45bf05ca
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/available-values-and-folders/Template.xml
@@ -0,0 +1,208 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Счет
+ Счет
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Остаток
+ Остаток
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Поступление1
+ Поступление1
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Поступление2
+ Поступление2
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Выбытие1
+ Выбытие1
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Выбытие2
+ Выбытие2
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Счет, Т.Остаток, Т.Поступление1, Т.Поступление2, Т.Выбытие1, Т.Выбытие2 ИЗ Регистр КАК Т
+
+
+ ПорядокОкругления
+
+ d5p1:EnumRef.Округления
+
+ Перечисление.Округления.Окр1_00
+
+ Перечисление.Округления.Окр1_00
+
+
+ ru
+ руб. коп
+
+
+
+
+ Перечисление.Округления.Окр1
+
+
+ ru
+ руб.
+
+
+
+
+ Перечисление.Округления.Окр1000
+
+
+ ru
+ тыс. руб
+
+
+
+ true
+ Always
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+ Счет
+
+
+ Остаток
+
+
+
+
+ ru
+ Поступление
+
+
+
+ Поступление1
+
+
+ Поступление2
+
+ Auto
+
+
+
+
+ ru
+ Выбытие
+
+
+
+ Выбытие1
+
+
+ Выбытие2
+
+ Auto
+
+
+
+ ДанныеОтчета
+
+
+ Счет
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+
+
+
+ ru
+ Поступление
+
+
+
+ Поступление1
+
+
+ Поступление2
+
+ Auto
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/calc-object-name-restrict-string/Template.xml b/tests/skills/cases/skd-compile/snapshots/calc-object-name-restrict-string/Template.xml
new file mode 100644
index 00000000..a3e62e1c
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/calc-object-name-restrict-string/Template.xml
@@ -0,0 +1,72 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Номенклатура
+ Номенклатура
+
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Номенклатура, Т.Сумма ИЗ Регистр КАК Т
+
+
+ ИмяРесурса
+ ""
+
+
+ ru
+ Имя ресурса
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/calc-shorthand-extended/Template.xml b/tests/skills/cases/skd-compile/snapshots/calc-shorthand-extended/Template.xml
new file mode 100644
index 00000000..212f0a72
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/calc-shorthand-extended/Template.xml
@@ -0,0 +1,105 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Цена
+ Цена
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Закупка
+ Закупка
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Цена, Т.Закупка ИЗ Регистр КАК Т
+
+
+ ИмяРесурса
+ ""
+
+
+ ru
+ Имя ресурса
+
+
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+ Маржа
+ Цена - Закупка
+
+
+ ru
+ Маржа
+
+
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml b/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml
new file mode 100644
index 00000000..935cc8ca
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/field-appearance-and-presentation/Template.xml
@@ -0,0 +1,87 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ ЖурналОшибок
+
+ ТекстСообщения
+ ТекстСообщения
+
+
+ ru
+ Текст сообщения
+
+
+
+ xs:string
+
+ 150
+ Variable
+
+
+
+
+ МинимальнаяШирина
+ 100
+
+
+ РастягиватьПоГоризонтали
+ true
+
+
+
+
+ Расшифровка
+ Расшифровка
+
+
+ ru
+ Описание
+
+
+
+ d5p1:CatalogRef.Организации
+
+
+
+ ГоризонтальноеПоложение
+ Right
+
+
+ ТекстСообщения
+
+ ИсточникДанных1
+ ЖурналОшибок
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/field-multi-type/Template.xml b/tests/skills/cases/skd-compile/snapshots/field-multi-type/Template.xml
new file mode 100644
index 00000000..54bbdc54
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/field-multi-type/Template.xml
@@ -0,0 +1,72 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ ЖурналОшибок
+
+ ТекстСообщения
+ ТекстСообщения
+
+
+ ru
+ Текст сообщения
+
+
+
+ xs:string
+
+ 150
+ Variable
+
+
+
+
+ Расшифровка
+ Расшифровка
+
+
+ ru
+ Расшифровка
+
+
+
+ d5p1:CatalogRef.СтруктураПредприятия
+ d5p1:CatalogRef.ЭтапыПроизводства
+ d5p1:CatalogRef.ВидыРабочихЦентров
+
+
+ ИсточникДанных1
+ ЖурналОшибок
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/full-example/Template.xml b/tests/skills/cases/skd-compile/snapshots/full-example/Template.xml
index 72736534..2662700b 100644
--- a/tests/skills/cases/skd-compile/snapshots/full-example/Template.xml
+++ b/tests/skills/cases/skd-compile/snapshots/full-example/Template.xml
@@ -65,29 +65,47 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+ true
+ Always
- ДатаНачала
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
- ДатаОкончания
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Основной
@@ -122,6 +140,8 @@
Период
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
UUID-002
diff --git a/tests/skills/cases/skd-compile/snapshots/horizontal-merge/Template.xml b/tests/skills/cases/skd-compile/snapshots/horizontal-merge/Template.xml
new file mode 100644
index 00000000..c891619f
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/horizontal-merge/Template.xml
@@ -0,0 +1,2285 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Счет
+ Счет
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Остаток
+ Остаток
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Пост1
+ Пост1
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Пост2
+ Пост2
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Пост3
+ Пост3
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Выб1
+ Выб1
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Выб2
+ Выб2
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Итого
+ Итого
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Счет, Т.Остаток, Т.Пост1, Т.Пост2, Т.Пост3, Т.Выб1, Т.Выб2, Т.Итого ИЗ Регистр КАК Т
+
+
+ Макет1
+
+
+
+
+
+
+ ru
+ Счет
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 30
+
+
+ МаксимальнаяШирина
+ 30
+
+
+ МинимальнаяВысота
+ 24.75
+
+
+
+
+
+
+
+ ru
+ Остаток
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ МинимальнаяВысота
+ 24.75
+
+
+
+
+
+
+
+ ru
+ Поступление
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ МинимальнаяВысота
+ 24.75
+
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ ОбъединятьПоГоризонтали
+ true
+
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ ОбъединятьПоГоризонтали
+ true
+
+
+
+
+
+
+
+ ru
+ Выбытие
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ МинимальнаяВысота
+ 24.75
+
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ ОбъединятьПоГоризонтали
+ true
+
+
+
+
+
+
+
+ ru
+ Итого
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ МинимальнаяВысота
+ 24.75
+
+
+
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 30
+
+
+ МаксимальнаяШирина
+ 30
+
+
+ ОбъединятьПоВертикали
+ true
+
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ ОбъединятьПоВертикали
+ true
+
+
+
+
+
+
+
+ ru
+ из произв.
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ из п/ф
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ прочее
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ Реализ.
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ прочее
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+ ОбъединятьПоВертикали
+ true
+
+
+
+
+
+
+
+
+
+ ru
+ К1
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 30
+
+
+ МаксимальнаяШирина
+ 30
+
+
+
+
+
+
+
+ ru
+ К2
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ К3
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ К4
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ К5
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ К6
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ К7
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ ru
+ К8
+
+
+
+
+
+ ЦветФона
+ d8p1:ReportHeaderBackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ ГоризонтальноеПоложение
+ Center
+
+
+ Размещение
+ Wrap
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ Макет2
+
+
+
+
+ Счет
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 30
+
+
+ МаксимальнаяШирина
+ 30
+
+
+
+
+
+ Остаток
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+ Пост1
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+ Пост2
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+ Пост3
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+ Выб1
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+ Выб2
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+ Итого
+
+
+
+ ЦветФона
+ d8p1:ReportGroup1BackColor
+
+
+ ЦветГраницы
+ d8p1:ReportLineColor
+
+
+ СтильГраницы
+
+ None
+
+
+ СтильГраницы.Слева
+
+ Solid
+
+
+
+ СтильГраницы.Сверху
+
+ Solid
+
+
+
+ СтильГраницы.Справа
+
+ Solid
+
+
+
+ СтильГраницы.Снизу
+
+ Solid
+
+
+
+
+ Шрифт
+
+
+
+ МинимальнаяШирина
+ 16
+
+
+ МаксимальнаяШирина
+ 16
+
+
+
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml b/tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml
new file mode 100644
index 00000000..ba89d296
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/multi-lang-title/Template.xml
@@ -0,0 +1,98 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+
+ ru
+ Сумма продажи
+
+
+ en
+ Sale amount
+
+
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ Маржа
+ Сумма * 0.2
+
+
+ ru
+ Маржа
+
+
+ en
+ Margin
+
+
+
+
+ Сумма
+ Сумма(Сумма)
+
+
+ Период
+
+
+ ru
+ Период
+
+
+ en
+ Period
+
+
+
+ v8:StandardPeriod
+
+
+
+ Основной
+
+
+ ru
+ Продажи
+
+
+ en
+ Sales
+
+
+
+
+
+ Сумма
+
+
+ Маржа
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/orgroup-string-items/Template.xml b/tests/skills/cases/skd-compile/snapshots/orgroup-string-items/Template.xml
new file mode 100644
index 00000000..4e89027c
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/orgroup-string-items/Template.xml
@@ -0,0 +1,106 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Поле1
+ Поле1
+
+ decimal
+
+
+
+ Поле2
+ Поле2
+
+ decimal
+
+
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Поле1, Т.Поле2, Т.Сумма ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+ Поле1
+
+
+ Поле2
+
+
+ Сумма
+
+
+
+
+
+
+
+ OrGroup
+
+ Поле1
+ Equal
+ 1
+
+
+ Поле2
+ Equal
+ 2
+
+
+
+
+
+ Формат
+
+
+ ru
+ ЧЦ=15; ЧДЦ=0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/parameter-title-presentation-synonyms/Template.xml b/tests/skills/cases/skd-compile/snapshots/parameter-title-presentation-synonyms/Template.xml
new file mode 100644
index 00000000..4f2ce774
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/parameter-title-presentation-synonyms/Template.xml
@@ -0,0 +1,83 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПорядокОкругления
+
+
+ ru
+ Округление
+
+
+
+ d5p1:EnumRef.Округления
+
+ Перечисление.Округления.Окр1_00
+
+ Перечисление.Округления.Окр1_00
+
+
+ ru
+ руб. коп
+
+
+
+
+ Перечисление.Округления.Окр1
+
+
+ ru
+ руб.
+
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/structure-object-form/Template.xml b/tests/skills/cases/skd-compile/snapshots/structure-object-form/Template.xml
new file mode 100644
index 00000000..c49b5df9
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/structure-object-form/Template.xml
@@ -0,0 +1,110 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Организация
+ Организация
+
+
+ Номенклатура
+ Номенклатура
+
+
+ Количество
+ Количество
+
+ xs:decimal
+
+ 15
+ 3
+ Any
+
+
+
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Организация, Т.Номенклатура, Т.Количество, Т.Сумма ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+ ПоОрганизациям
+
+
+ Организация
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+ Организация
+
+
+ Сумма
+
+
+
+
+
+ Номенклатура
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+ Номенклатура
+
+
+ Количество
+
+
+ Сумма
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/userestriction-object-form/Template.xml b/tests/skills/cases/skd-compile/snapshots/userestriction-object-form/Template.xml
new file mode 100644
index 00000000..a3e62e1c
--- /dev/null
+++ b/tests/skills/cases/skd-compile/snapshots/userestriction-object-form/Template.xml
@@ -0,0 +1,72 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Номенклатура
+ Номенклатура
+
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Номенклатура, Т.Сумма ИЗ Регистр КАК Т
+
+
+ ИмяРесурса
+ ""
+
+
+ ru
+ Имя ресурса
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-compile/snapshots/with-filters/Template.xml b/tests/skills/cases/skd-compile/snapshots/with-filters/Template.xml
index ac039ef0..a1a7f051 100644
--- a/tests/skills/cases/skd-compile/snapshots/with-filters/Template.xml
+++ b/tests/skills/cases/skd-compile/snapshots/with-filters/Template.xml
@@ -60,29 +60,47 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+ true
+ Always
- ДатаНачала
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
- ДатаОкончания
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Основной
@@ -129,6 +147,8 @@
Период
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
UUID-002
diff --git a/tests/skills/cases/skd-compile/snapshots/with-parameters/Template.xml b/tests/skills/cases/skd-compile/snapshots/with-parameters/Template.xml
index 13acb5d5..496f1606 100644
--- a/tests/skills/cases/skd-compile/snapshots/with-parameters/Template.xml
+++ b/tests/skills/cases/skd-compile/snapshots/with-parameters/Template.xml
@@ -57,29 +57,47 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+ true
+ Always
- ДатаНачала
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
- ДатаОкончания
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Организация
diff --git a/tests/skills/cases/skd-compile/structure-object-form.json b/tests/skills/cases/skd-compile/structure-object-form.json
new file mode 100644
index 00000000..ea856c7e
--- /dev/null
+++ b/tests/skills/cases/skd-compile/structure-object-form.json
@@ -0,0 +1,33 @@
+{
+ "name": "Объектная форма structure с name, groupFields, children, selection",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Организация, Т.Номенклатура, Т.Количество, Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Организация", "Номенклатура", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"]
+ }],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "structure": [
+ {
+ "name": "ПоОрганизациям",
+ "groupFields": ["Организация"],
+ "selection": ["Организация", "Сумма"],
+ "children": [
+ {
+ "groupFields": ["Номенклатура"],
+ "selection": ["Номенклатура", "Количество", "Сумма"]
+ }
+ ]
+ }
+ ]
+ }
+ }]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-compile/userestriction-object-form.json b/tests/skills/cases/skd-compile/userestriction-object-form.json
new file mode 100644
index 00000000..447ead4f
--- /dev/null
+++ b/tests/skills/cases/skd-compile/userestriction-object-form.json
@@ -0,0 +1,23 @@
+{
+ "name": "useRestriction в объектной форме calculatedFields",
+ "params": { "outputPath": "Template.xml" },
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Номенклатура, Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Номенклатура", "Сумма: decimal(15,2)"]
+ }],
+ "calculatedFields": [
+ {
+ "field": "ИмяРесурса",
+ "title": "Имя ресурса",
+ "expression": "\"\"",
+ "useRestriction": { "field": true, "condition": true, "group": true, "order": true }
+ }
+ ]
+ },
+ "validatePath": "Template.xml",
+ "expect": {
+ "files": ["Template.xml"]
+ }
+}
diff --git a/tests/skills/cases/skd-edit/add-calculated-field-restrict.json b/tests/skills/cases/skd-edit/add-calculated-field-restrict.json
new file mode 100644
index 00000000..f7ca8be6
--- /dev/null
+++ b/tests/skills/cases/skd-edit/add-calculated-field-restrict.json
@@ -0,0 +1,21 @@
+{
+ "name": "Вычисляемое поле с useRestriction",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Цена, Т.Закупка ИЗ Регистр КАК Т",
+ "fields": ["Цена: decimal(15,2)", "Закупка: decimal(15,2)"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "add-calculated-field",
+ "value": "Служебное: string = \"\" #noFilter #noOrder #noGroup"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/add-drilldown.json b/tests/skills/cases/skd-edit/add-drilldown.json
new file mode 100644
index 00000000..b0c21d63
--- /dev/null
+++ b/tests/skills/cases/skd-edit/add-drilldown.json
@@ -0,0 +1,9 @@
+{
+ "name": "Добавление расшифровки ресурсов в шаблоны",
+ "setup": "fixture:drilldown-base",
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "add-drilldown",
+ "value": "Ресурс1, Ресурс2"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/add-selection-auto-dedup.json b/tests/skills/cases/skd-edit/add-selection-auto-dedup.json
new file mode 100644
index 00000000..2a6e4749
--- /dev/null
+++ b/tests/skills/cases/skd-edit/add-selection-auto-dedup.json
@@ -0,0 +1,31 @@
+{
+ "name": "add-selection Auto не дублируется если уже существует",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Поле1, Т.Поле2 ИЗ Регистр КАК Т",
+ "fields": ["Поле1", "Поле2"]
+ }],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Auto"],
+ "structure": "details"
+ }
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "add-selection",
+ "value": "Auto ;; Поле1 ;; Поле2"
+ },
+ "expect": {
+ "stdout": "WARN.*SelectedItemAuto already exists"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/conditional-appearance-v2.json b/tests/skills/cases/skd-edit/conditional-appearance-v2.json
new file mode 100644
index 00000000..43901275
--- /dev/null
+++ b/tests/skills/cases/skd-edit/conditional-appearance-v2.json
@@ -0,0 +1,22 @@
+{
+ "name": "conditionalAppearance: DesignTimeValue, Format, OrGroup",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": ["ПорядокОкругления: string"]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "add-conditionalAppearance",
+ "value": "Формат = ЧЦ=15; ЧДЦ=2 when ПараметрыДанных.ПорядокОкругления = Перечисление.Округления.Окр1_00 ;; Формат = ЧЦ=15; ЧДЦ=0 when ПараметрыДанных.ПорядокОкругления = Перечисление.Округления.Окр1 or ПараметрыДанных.ПорядокОкругления = Перечисление.Округления.Окр1000"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/fixtures/drilldown-base/Template.xml b/tests/skills/cases/skd-edit/fixtures/drilldown-base/Template.xml
new file mode 100644
index 00000000..b8647895
--- /dev/null
+++ b/tests/skills/cases/skd-edit/fixtures/drilldown-base/Template.xml
@@ -0,0 +1,97 @@
+
+
+
+ Источник
+ Local
+
+
+ Основной
+
+ Ресурс1
+ Ресурс1
+
+
+ Ресурс2
+ Ресурс2
+
+
+ Счет
+ Счет
+
+ Источник
+ ВЫБРАТЬ 1
+
+
+ Макет1
+
+
+
+
+
+
+ ru
+ Заголовок
+
+
+
+
+
+
+
+
+ Макет3
+
+
+
+
+ Счет
+
+
+
+ Шрифт
+
+
+
+
+
+
+ Рес1
+
+
+
+ Шрифт
+
+
+
+
+
+
+ Рес2
+
+
+
+ Шрифт
+
+
+
+
+
+
+
+ Счет
+ Представление(Счет)
+
+
+ Рес1
+ Ресурс1
+
+
+ Рес2
+ Ресурс2
+
+
+
+ Основной
+
+
+
diff --git a/tests/skills/cases/skd-edit/modify-dataParameter-preserves-use.json b/tests/skills/cases/skd-edit/modify-dataParameter-preserves-use.json
new file mode 100644
index 00000000..3cb7843c
--- /dev/null
+++ b/tests/skills/cases/skd-edit/modify-dataParameter-preserves-use.json
@@ -0,0 +1,30 @@
+{
+ "name": "modify-dataParameter: сохраняет существующий false когда @off не задан",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": ["ПериодОтчета: StandardPeriod = LastMonth"],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Auto"],
+ "dataParameters": ["ПериодОтчета = LastMonth @off"],
+ "structure": "details"
+ }
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "modify-dataParameter",
+ "value": "ПериодОтчета = Custom"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/modify-parameter-combined.json b/tests/skills/cases/skd-edit/modify-parameter-combined.json
new file mode 100644
index 00000000..3d393da6
--- /dev/null
+++ b/tests/skills/cases/skd-edit/modify-parameter-combined.json
@@ -0,0 +1,22 @@
+{
+ "name": "modify-parameter: combined kv + availableValue in single entry",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": [{"name": "ПорядокОкругления", "type": "string", "value": "Окр1_00"}]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "modify-parameter",
+ "value": "ПорядокОкругления denyIncompleteValues=true use=Always availableValue=Перечисление.Округления.Окр1_00 presentation=руб. коп ;; ПорядокОкругления availableValue=Перечисление.Округления.Окр1 presentation=руб. ;; ПорядокОкругления availableValue=Перечисление.Округления.Окр1000 presentation=тыс. руб"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/modify-parameter-title.json b/tests/skills/cases/skd-edit/modify-parameter-title.json
new file mode 100644
index 00000000..f8c8ea9d
--- /dev/null
+++ b/tests/skills/cases/skd-edit/modify-parameter-title.json
@@ -0,0 +1,22 @@
+{
+ "name": "modify-parameter: установка title через [Заголовок]",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": ["ПериодОтчета: StandardPeriod = LastMonth"]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "modify-parameter",
+ "value": "ПериодОтчета [Отчетный период] use=Always denyIncompleteValues=true"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/modify-parameter.json b/tests/skills/cases/skd-edit/modify-parameter.json
new file mode 100644
index 00000000..05309342
--- /dev/null
+++ b/tests/skills/cases/skd-edit/modify-parameter.json
@@ -0,0 +1,22 @@
+{
+ "name": "modify-parameter: use, denyIncompleteValues, availableValue",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": [{"name": "ПорядокОкругления", "type": "string", "value": "Окр1_00"}]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "modify-parameter",
+ "value": "ПорядокОкругления use=Always ;; ПорядокОкругления denyIncompleteValues=true ;; ПорядокОкругления availableValue=Перечисление.Округления.Окр1_00 presentation=руб. коп ;; ПорядокОкругления availableValue=Перечисление.Округления.Окр1 presentation=руб."
+ }
+}
diff --git a/tests/skills/cases/skd-edit/rename-parameter.json b/tests/skills/cases/skd-edit/rename-parameter.json
new file mode 100644
index 00000000..8854afe2
--- /dev/null
+++ b/tests/skills/cases/skd-edit/rename-parameter.json
@@ -0,0 +1,33 @@
+{
+ "name": "rename-parameter: переименование с обновлением expressions и dataParameters",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": [
+ "Период: StandardPeriod = LastMonth @autoDates",
+ "Организация: string"
+ ],
+ "settingsVariants": [{
+ "name": "Основной",
+ "settings": {
+ "selection": ["Auto"],
+ "dataParameters": ["Период = LastMonth @user", "Организация @off"],
+ "structure": "details"
+ }
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "rename-parameter",
+ "value": "Период => ПериодОтчета"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/reorder-parameters.json b/tests/skills/cases/skd-edit/reorder-parameters.json
new file mode 100644
index 00000000..8dcdd532
--- /dev/null
+++ b/tests/skills/cases/skd-edit/reorder-parameters.json
@@ -0,0 +1,28 @@
+{
+ "name": "reorder-parameters: явный частичный список + остальные сохраняют порядок",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т",
+ "fields": ["Сумма: decimal(15,2)"]
+ }],
+ "parameters": [
+ "Организация: string",
+ "ПрекращаемаяДеятельность: boolean",
+ "ПериодОтчета: StandardPeriod = LastMonth",
+ "ПланСчетовМеждународный: string",
+ "ПорядокОкругленияСумм: string"
+ ]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "reorder-parameters",
+ "value": "ПериодОтчета, ПланСчетовМеждународный, ПорядокОкругленияСумм"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/set-structure-named.json b/tests/skills/cases/skd-edit/set-structure-named.json
new file mode 100644
index 00000000..d2231ee6
--- /dev/null
+++ b/tests/skills/cases/skd-edit/set-structure-named.json
@@ -0,0 +1,25 @@
+{
+ "name": "set-structure с @name= и Folder в selection + @group=",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Счет, Т.Поступление1, Т.Поступление2, Т.Выбытие1 ИЗ Регистр КАК Т",
+ "fields": ["Счет: string", "Поступление1: decimal(15,2)", "Поступление2: decimal(15,2)", "Выбытие1: decimal(15,2)"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ },
+ {
+ "script": "skd-edit/scripts/skd-edit",
+ "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "set-structure", "-Value": "Счет @name=ДанныеОтчета" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "add-selection",
+ "value": "Auto @group=ДанныеОтчета ;; Счет @group=ДанныеОтчета ;; Folder(Поступление: Поступление1, Поступление2) @group=ДанныеОтчета ;; Выбытие1 @group=ДанныеОтчета"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/snapshots/add-calculated-field-restrict/Template.xml b/tests/skills/cases/skd-edit/snapshots/add-calculated-field-restrict/Template.xml
new file mode 100644
index 00000000..01696c5d
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/add-calculated-field-restrict/Template.xml
@@ -0,0 +1,76 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Цена
+ Цена
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Закупка
+ Закупка
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Цена, Т.Закупка ИЗ Регистр КАК Т
+
+
+ Служебное
+ ""
+
+ true
+ true
+ true
+
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+ Служебное
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/add-drilldown/Template.xml b/tests/skills/cases/skd-edit/snapshots/add-drilldown/Template.xml
new file mode 100644
index 00000000..ac6fb98e
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/add-drilldown/Template.xml
@@ -0,0 +1,121 @@
+
+
+
+ Источник
+ Local
+
+
+ Основной
+
+ Ресурс1
+ Ресурс1
+
+
+ Ресурс2
+ Ресурс2
+
+
+ Счет
+ Счет
+
+ Источник
+ ВЫБРАТЬ 1
+
+
+ Макет1
+
+
+
+
+
+
+ ru
+ Заголовок
+
+
+
+
+
+
+
+
+ Макет3
+
+
+
+
+ Счет
+
+
+
+ Шрифт
+
+
+
+
+
+
+ Рес1
+
+
+
+ Шрифт
+
+
+
+ Расшифровка
+ Расшифровка_Ресурс1
+
+
+
+
+
+ Рес2
+
+
+
+ Шрифт
+
+
+
+ Расшифровка
+ Расшифровка_Ресурс2
+
+
+
+
+
+
+ Счет
+ Представление(Счет)
+
+
+ Рес1
+ Ресурс1
+
+
+ Рес2
+ Ресурс2
+
+
+ Расшифровка_Ресурс1
+
+ ИмяРесурса
+ "Ресурс1"
+
+ DrillDown
+
+
+ Расшифровка_Ресурс2
+
+ ИмяРесурса
+ "Ресурс2"
+
+ DrillDown
+
+
+
+ Основной
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/add-parameter/Template.xml b/tests/skills/cases/skd-edit/snapshots/add-parameter/Template.xml
index 49e00602..c169bb35 100644
--- a/tests/skills/cases/skd-edit/snapshots/add-parameter/Template.xml
+++ b/tests/skills/cases/skd-edit/snapshots/add-parameter/Template.xml
@@ -20,29 +20,45 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
ДатаНачала
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
ДатаОкончания
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Основной
diff --git a/tests/skills/cases/skd-edit/snapshots/add-selection-auto-dedup/Template.xml b/tests/skills/cases/skd-edit/snapshots/add-selection-auto-dedup/Template.xml
new file mode 100644
index 00000000..77da25b7
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/add-selection-auto-dedup/Template.xml
@@ -0,0 +1,48 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Поле1
+ Поле1
+
+
+ Поле2
+ Поле2
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Поле1, Т.Поле2 ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+ Поле1
+
+
+ Поле2
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml b/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml
new file mode 100644
index 00000000..4f068625
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml
@@ -0,0 +1,107 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПорядокОкругления
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+ ПараметрыДанных.ПорядокОкругления
+ Equal
+ Перечисление.Округления.Окр1_00
+
+
+
+
+ Формат
+
+
+ ru
+ ЧЦ=15; ЧДЦ=2
+
+
+
+
+
+
+
+
+
+ OrGroup
+
+ ПараметрыДанных.ПорядокОкругления
+ Equal
+ Перечисление.Округления.Окр1
+
+
+ ПараметрыДанных.ПорядокОкругления
+ Equal
+ Перечисление.Округления.Окр1000
+
+
+
+
+
+ Формат
+
+
+ ru
+ ЧЦ=15; ЧДЦ=0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/modify-dataParameter-preserves-use/Template.xml b/tests/skills/cases/skd-edit/snapshots/modify-dataParameter-preserves-use/Template.xml
new file mode 100644
index 00000000..823e8e10
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/modify-dataParameter-preserves-use/Template.xml
@@ -0,0 +1,67 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПериодОтчета
+
+ v8:StandardPeriod
+
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+ false
+ ПериодОтчета
+
+ Custom
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/modify-parameter-combined/Template.xml b/tests/skills/cases/skd-edit/snapshots/modify-parameter-combined/Template.xml
new file mode 100644
index 00000000..52bd3990
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/modify-parameter-combined/Template.xml
@@ -0,0 +1,85 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПорядокОкругления
+
+ xs:string
+
+ 0
+ Variable
+
+
+ Окр1_00
+
+ Перечисление.Округления.Окр1_00
+
+
+ ru
+ руб. коп
+
+
+
+
+ Перечисление.Округления.Окр1
+
+
+ ru
+ руб.
+
+
+
+
+ Перечисление.Округления.Окр1000
+
+
+ ru
+ тыс. руб
+
+
+
+ true
+ Always
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/modify-parameter-title/Template.xml b/tests/skills/cases/skd-edit/snapshots/modify-parameter-title/Template.xml
new file mode 100644
index 00000000..a6e14541
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/modify-parameter-title/Template.xml
@@ -0,0 +1,64 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПериодОтчета
+
+
+ ru
+ Отчетный период
+
+
+
+ v8:StandardPeriod
+
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+ true
+ Always
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/modify-parameter/Template.xml b/tests/skills/cases/skd-edit/snapshots/modify-parameter/Template.xml
new file mode 100644
index 00000000..35c109ee
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/modify-parameter/Template.xml
@@ -0,0 +1,76 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПорядокОкругления
+
+ xs:string
+
+ 0
+ Variable
+
+
+ Окр1_00
+
+ Перечисление.Округления.Окр1_00
+
+
+ ru
+ руб. коп
+
+
+
+
+ Перечисление.Округления.Окр1
+
+
+ ru
+ руб.
+
+
+
+ true
+ Always
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml b/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml
new file mode 100644
index 00000000..fceae3d8
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml
@@ -0,0 +1,119 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+
+ ПериодОтчета
+
+ v8:StandardPeriod
+
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+ true
+ Always
+
+
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
+
+ xs:dateTime
+
+ Date
+
+
+ 0001-01-01T00:00:00
+ true
+ &ПериодОтчета.ДатаНачала
+
+
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
+
+ xs:dateTime
+
+ Date
+
+
+ 0001-01-01T00:00:00
+ true
+ &ПериодОтчета.ДатаОкончания
+
+
+ Организация
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+ ПериодОтчета
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+ UUID-001
+
+
+ false
+ Организация
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml b/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml
new file mode 100644
index 00000000..76e17c33
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml
@@ -0,0 +1,92 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Сумма ИЗ Регистр КАК Т
+
+ ПериодОтчета
+
+ v8:StandardPeriod
+
+
+ LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+ ПланСчетовМеждународный
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ ПорядокОкругленияСумм
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Организация
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ ПрекращаемаяДеятельность
+
+ xs:boolean
+
+
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/set-structure-named/Template.xml b/tests/skills/cases/skd-edit/snapshots/set-structure-named/Template.xml
new file mode 100644
index 00000000..2be2dea2
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/set-structure-named/Template.xml
@@ -0,0 +1,111 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Счет
+ Счет
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Поступление1
+ Поступление1
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Поступление2
+ Поступление2
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Выбытие1
+ Выбытие1
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Счет, Т.Поступление1, Т.Поступление2, Т.Выбытие1 ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+ ДанныеОтчета
+
+
+ Счет
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+
+ Счет
+
+
+
+
+ ru
+ Поступление
+
+
+
+ Поступление1
+
+
+ Поступление2
+
+ Auto
+
+
+ Выбытие1
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-info/object-only-full.json b/tests/skills/cases/skd-info/object-only-full.json
new file mode 100644
index 00000000..d09aa518
--- /dev/null
+++ b/tests/skills/cases/skd-info/object-only-full.json
@@ -0,0 +1,22 @@
+{
+ "name": "Полный режим на схеме без Query (только DataSetObject)",
+ "args_extra": ["-Mode", "full"],
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "ВнешниеДанные",
+ "objectName": "ЖурналОшибок",
+ "fields": [
+ { "field": "ТекстСообщения", "title": "Текст сообщения", "type": "string(150)" },
+ { "field": "Расшифровка", "title": "Описание", "type": "CatalogRef.Контрагенты" }
+ ]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": { "templatePath": "Template.xml" },
+ "expect": { "stdoutContains": "ВнешниеДанные" }
+}
diff --git a/tests/skills/cases/skd-info/snapshots/overview-with-params/Template.xml b/tests/skills/cases/skd-info/snapshots/overview-with-params/Template.xml
index 14e99316..e6a40946 100644
--- a/tests/skills/cases/skd-info/snapshots/overview-with-params/Template.xml
+++ b/tests/skills/cases/skd-info/snapshots/overview-with-params/Template.xml
@@ -65,29 +65,47 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+ true
+ Always
- ДатаНачала
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
- ДатаОкончания
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Основной
@@ -122,6 +140,8 @@
Период
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
UUID-002
diff --git a/tests/skills/cases/skd-validate/snapshots/valid-full/Template.xml b/tests/skills/cases/skd-validate/snapshots/valid-full/Template.xml
index 066b8386..cacea382 100644
--- a/tests/skills/cases/skd-validate/snapshots/valid-full/Template.xml
+++ b/tests/skills/cases/skd-validate/snapshots/valid-full/Template.xml
@@ -75,29 +75,47 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+ true
+ Always
- ДатаНачала
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
- ДатаОкончания
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Основной
@@ -135,6 +153,8 @@
Период
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
UUID-002
diff --git a/tests/skills/cases/skd-validate/snapshots/valid-with-params/Template.xml b/tests/skills/cases/skd-validate/snapshots/valid-with-params/Template.xml
index 6c4ba7f5..eba5d92d 100644
--- a/tests/skills/cases/skd-validate/snapshots/valid-with-params/Template.xml
+++ b/tests/skills/cases/skd-validate/snapshots/valid-with-params/Template.xml
@@ -65,29 +65,47 @@
LastMonth
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+ true
+ Always
- ДатаНачала
+ НачалоПериода
+
+
+ ru
+ Начало периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаНачала
- false
- ДатаОкончания
+ КонецПериода
+
+
+ ru
+ Конец периода
+
+
xs:dateTime
Date
+ 0001-01-01T00:00:00
+ true
&Период.ДатаОкончания
- false
Основной
diff --git a/tests/skills/cases/subsystem-compile/_skill.json b/tests/skills/cases/subsystem-compile/_skill.json
index 5c9bf7a8..5db9ab8c 100644
--- a/tests/skills/cases/subsystem-compile/_skill.json
+++ b/tests/skills/cases/subsystem-compile/_skill.json
@@ -3,7 +3,8 @@
"setup": "empty-config",
"args": [
{ "flag": "-DefinitionFile", "from": "inputFile" },
- { "flag": "-OutputDir", "from": "workDir" }
+ { "flag": "-OutputDir", "from": "workDir" },
+ { "flag": "-Parent", "from": "workPath", "field": "parent", "optional": true }
],
"snapshot": {
"root": "workDir",
diff --git a/tests/skills/cases/subsystem-compile/nested-parent.json b/tests/skills/cases/subsystem-compile/nested-parent.json
new file mode 100644
index 00000000..d64e2d27
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/nested-parent.json
@@ -0,0 +1,24 @@
+{
+ "name": "Вложенная подсистема через -Parent (bottom-up flow)",
+ "preRun": [
+ {
+ "script": "subsystem-compile/scripts/subsystem-compile",
+ "input": { "name": "Продажи", "synonym": "Продажи" },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputDir": "{workDir}" }
+ }
+ ],
+ "params": { "parent": "Subsystems/Продажи.xml" },
+ "input": {
+ "name": "Настройки",
+ "synonym": "Настройки раздела",
+ "explanation": "Настройки подсистемы продаж",
+ "includeInCommandInterface": true
+ },
+ "validatePath": "Subsystems/Продажи/Subsystems/Настройки",
+ "expect": {
+ "files": [
+ "Subsystems/Продажи.xml",
+ "Subsystems/Продажи/Subsystems/Настройки.xml"
+ ]
+ }
+}
diff --git a/tests/skills/cases/subsystem-compile/snapshots/full/Subsystems/Продажи/Subsystems/Настройки.xml b/tests/skills/cases/subsystem-compile/snapshots/full/Subsystems/Продажи/Subsystems/Настройки.xml
new file mode 100644
index 00000000..7f218906
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/full/Subsystems/Продажи/Subsystems/Настройки.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Настройки
+
+
+ true
+ true
+ false
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml
new file mode 100644
index 00000000..2e6aa527
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Configuration.xml
@@ -0,0 +1,252 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+ UUID-012
+ UUID-013
+
+
+ UUID-014
+ UUID-015
+
+
+
+ TestConfig
+
+
+ ru
+ TestConfig
+
+
+
+
+ Version8_3_24
+ ManagedApplication
+
+ PlatformApplication
+
+ Russian
+
+
+
+
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biometrics
+ true
+
+
+ Location
+ false
+
+
+ BackgroundLocation
+ false
+
+
+ BluetoothPrinters
+ false
+
+
+ WiFiPrinters
+ false
+
+
+ Contacts
+ false
+
+
+ Calendars
+ false
+
+
+ PushNotifications
+ false
+
+
+ LocalNotifications
+ false
+
+
+ InAppPurchases
+ false
+
+
+ PersonalComputerFileExchange
+ false
+
+
+ Ads
+ false
+
+
+ NumberDialing
+ false
+
+
+ CallProcessing
+ false
+
+
+ CallLog
+ false
+
+
+ AutoSendSMS
+ false
+
+
+ ReceiveSMS
+ false
+
+
+ SMSLog
+ false
+
+
+ Camera
+ false
+
+
+ Microphone
+ false
+
+
+ MusicLibrary
+ false
+
+
+ PictureAndVideoLibraries
+ false
+
+
+ AudioPlaybackAndVibration
+ false
+
+
+ BackgroundAudioPlaybackAndVibration
+ false
+
+
+ InstallPackages
+ false
+
+
+ OSBackup
+ true
+
+
+ ApplicationUsageStatistics
+ false
+
+
+ BarcodeScanning
+ false
+
+
+ BackgroundAudioRecording
+ false
+
+
+ AllFilesAccess
+ false
+
+
+ Videoconferences
+ false
+
+
+ NFC
+ false
+
+
+ DocumentScanning
+ false
+
+
+ SpeechToText
+ false
+
+
+ Geofences
+ false
+
+
+ IncomingShareRequests
+ false
+
+
+ AllIncomingShareRequestsTypesProcessing
+ false
+
+
+
+
+
+ Normal
+
+
+ Language.Русский
+
+
+
+
+
+ Managed
+ NotAutoFree
+ DontUse
+ DontUse
+ TaxiEnableVersion8_2
+ DontUse
+ Version8_3_24
+
+
+
+ Русский
+ Продажи
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml
new file mode 100644
index 00000000..37c60d78
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Languages/Русский.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Русский
+
+
+ ru
+ Русский
+
+
+
+ ru
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml
new file mode 100644
index 00000000..e2384567
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ Продажи
+
+
+ ru
+ Продажи
+
+
+
+ true
+ true
+ false
+
+
+
+
+
+ Настройки
+
+
+
diff --git a/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml
new file mode 100644
index 00000000..516eb081
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/nested-parent/Subsystems/Продажи/Subsystems/Настройки.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Настройки
+
+
+ ru
+ Настройки раздела
+
+
+
+ true
+ true
+ false
+
+
+ ru
+ Настройки подсистемы продаж
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/subsystem-compile/snapshots/with-children/Subsystems/Администрирование/Subsystems/Настройки.xml b/tests/skills/cases/subsystem-compile/snapshots/with-children/Subsystems/Администрирование/Subsystems/Настройки.xml
new file mode 100644
index 00000000..7f218906
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/with-children/Subsystems/Администрирование/Subsystems/Настройки.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Настройки
+
+
+ true
+ true
+ false
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/subsystem-compile/snapshots/with-children/Subsystems/Администрирование/Subsystems/Пользователи.xml b/tests/skills/cases/subsystem-compile/snapshots/with-children/Subsystems/Администрирование/Subsystems/Пользователи.xml
new file mode 100644
index 00000000..cdbce883
--- /dev/null
+++ b/tests/skills/cases/subsystem-compile/snapshots/with-children/Subsystems/Администрирование/Subsystems/Пользователи.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Пользователи
+
+
+ true
+ true
+ false
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/subsystem-edit/snapshots/add-child/Subsystems/Продажи/Subsystems/Настройки.xml b/tests/skills/cases/subsystem-edit/snapshots/add-child/Subsystems/Продажи/Subsystems/Настройки.xml
new file mode 100644
index 00000000..7f218906
--- /dev/null
+++ b/tests/skills/cases/subsystem-edit/snapshots/add-child/Subsystems/Продажи/Subsystems/Настройки.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Настройки
+
+
+ true
+ true
+ false
+
+
+
+
+
+
+
diff --git a/tests/skills/integration/build-config.test.mjs b/tests/skills/integration/build-config.test.mjs
index 7f9a772e..81878a1e 100644
--- a/tests/skills/integration/build-config.test.mjs
+++ b/tests/skills/integration/build-config.test.mjs
@@ -152,7 +152,7 @@ export const steps = [
input: {
title: 'Контрагент',
attributes: [
- { name: 'Объект', type: 'FormDataStructure', main: true },
+ { name: 'Объект', type: 'CatalogObject.Контрагенты', main: true },
],
elements: [
{ input: 'Наименование', path: 'Объект.Description', title: 'Наименование' },
@@ -176,7 +176,7 @@ export const steps = [
input: {
title: 'Приходная накладная',
attributes: [
- { name: 'Объект', type: 'FormDataStructure', main: true },
+ { name: 'Объект', type: 'DocumentObject.ПриходнаяНакладная', main: true },
],
elements: [
{ input: 'Контрагент', path: 'Объект.Контрагент', title: 'Контрагент' },
diff --git a/tests/skills/integration/build-epf.test.mjs b/tests/skills/integration/build-epf.test.mjs
index 1a88de26..76421c7d 100644
--- a/tests/skills/integration/build-epf.test.mjs
+++ b/tests/skills/integration/build-epf.test.mjs
@@ -1,5 +1,5 @@
// build-epf.test.mjs — Integration test: build an external data processor (EPF) from scratch
-// Steps: epf-init → epf-add-form → form-compile → template-add → mxl-compile → epf-validate
+// Steps: epf-init → form-add → form-compile → template-add → mxl-compile → epf-validate
export const name = 'Сборка внешней обработки с нуля';
export const setup = 'none';
@@ -15,9 +15,9 @@ export const steps = [
// ── 2. Add form ──
{
- name: 'epf-add-form: Форма к ТестоваяОбработка',
- script: 'epf-add-form/scripts/add-form',
- args: { '-ProcessorName': 'ТестоваяОбработка', '-FormName': 'Форма', '-SrcDir': '{workDir}' },
+ name: 'form-add: Форма к ТестоваяОбработка',
+ script: 'form-add/scripts/form-add',
+ args: { '-ObjectPath': '{workDir}/ТестоваяОбработка.xml', '-FormName': 'Форма' },
validate: { script: 'epf-validate/scripts/epf-validate', flag: '-ObjectPath', path: 'ТестоваяОбработка' },
},
@@ -28,7 +28,7 @@ export const steps = [
input: {
title: 'Тестовая обработка',
attributes: [
- { name: 'Объект', type: 'FormDataStructure', main: true },
+ { name: 'Объект', type: 'DataProcessorObject.ТестоваяОбработка', main: true },
{ name: 'Наименование', type: 'String' },
{ name: 'Количество', type: 'Number' },
],
diff --git a/tests/skills/integration/platform-cfe.test.mjs b/tests/skills/integration/platform-cfe.test.mjs
index a689a62c..de3680ae 100644
--- a/tests/skills/integration/platform-cfe.test.mjs
+++ b/tests/skills/integration/platform-cfe.test.mjs
@@ -46,7 +46,7 @@ export const steps = [
{ id: 'Наименование', type: 'input', path: 'Object.Description', title: 'Наименование' },
],
},
- args: { '-FormPath': '{workDir}/config/Catalogs/Контрагенты/Forms/ФормаЭлемента', '-JsonPath': '{inputFile}' },
+ args: { '-OutputPath': '{workDir}/config/Catalogs/Контрагенты/Forms/ФормаЭлемента/Ext/Form.xml', '-JsonPath': '{inputFile}' },
},
// ── 2. Build extension ──
diff --git a/tests/skills/integration/platform-config.test.mjs b/tests/skills/integration/platform-config.test.mjs
index cc6a2ebd..599d3525 100644
--- a/tests/skills/integration/platform-config.test.mjs
+++ b/tests/skills/integration/platform-config.test.mjs
@@ -52,7 +52,7 @@ export const steps = [
{ id: 'Наименование', type: 'input', path: 'Object.Description', title: 'Наименование' },
],
},
- args: { '-FormPath': '{workDir}/config/Catalogs/Товары/Forms/ФормаЭлемента', '-JsonPath': '{inputFile}' },
+ args: { '-OutputPath': '{workDir}/config/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml', '-JsonPath': '{inputFile}' },
},
{
name: 'form-add: форма документа',
@@ -71,7 +71,7 @@ export const steps = [
{ id: 'Склад', type: 'input', path: 'Object.Склад', title: 'Склад' },
],
},
- args: { '-FormPath': '{workDir}/config/Documents/Приход/Forms/ФормаДокумента', '-JsonPath': '{inputFile}' },
+ args: { '-OutputPath': '{workDir}/config/Documents/Приход/Forms/ФормаДокумента/Ext/Form.xml', '-JsonPath': '{inputFile}' },
},
{
name: 'cf-edit: регистрация объектов',
diff --git a/tests/skills/integration/platform-epf.test.mjs b/tests/skills/integration/platform-epf.test.mjs
index 22296f65..626f9afc 100644
--- a/tests/skills/integration/platform-epf.test.mjs
+++ b/tests/skills/integration/platform-epf.test.mjs
@@ -1,6 +1,6 @@
// platform-epf.test.mjs — Integration test: EPF build/dump roundtrip
// Requires: 1C platform (1cv8.exe) via .v8-project.json
-// Steps: epf-init → epf-add-form → form-compile → epf-build → epf-dump
+// Steps: epf-init → form-add → form-compile → epf-build → epf-dump
export const name = 'Сборка и разборка внешней обработки (roundtrip)';
export const setup = 'none';
@@ -16,8 +16,8 @@ export const steps = [
// ── 2. Add form to EPF ──
{
- name: 'epf-add-form: форма обработки',
- script: 'epf-add-form/scripts/add-form',
+ name: 'form-add: форма обработки',
+ script: 'form-add/scripts/form-add',
args: {
'-ObjectPath': '{workDir}/RoundtripТест.xml',
'-FormName': 'Форма',
@@ -39,7 +39,7 @@ export const steps = [
{ id: 'Загрузить', title: 'Загрузить' },
],
},
- args: { '-FormPath': '{workDir}/RoundtripТест/Forms/Форма', '-JsonPath': '{inputFile}' },
+ args: { '-OutputPath': '{workDir}/RoundtripТест/Forms/Форма/Ext/Form.xml', '-JsonPath': '{inputFile}' },
},
// ── 3. Build EPF binary ──
diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs
index 18ffec03..cafe95df 100644
--- a/tests/skills/runner.mjs
+++ b/tests/skills/runner.mjs
@@ -223,8 +223,16 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) {
case 'workPath':
// workDir + value from case.params or case (specified in mapping.field)
const wpField = mapping.field || 'objectPath';
- const wpVal = caseData.params?.[wpField] ?? caseData[wpField] ?? '';
- args.push(join(workDir, wpVal));
+ const wpVal = caseData.params?.[wpField] ?? caseData[wpField];
+ if (wpVal === undefined || wpVal === null || wpVal === '') {
+ if (mapping.optional) {
+ args.pop(); // remove the flag we pushed at the top of the loop
+ break;
+ }
+ args.push(join(workDir, ''));
+ } else {
+ args.push(join(workDir, wpVal));
+ }
break;
case 'switch':
// flag already pushed, no value needed — remove the flag and re-push conditionally
diff --git a/tests/skills/verify-snapshots.mjs b/tests/skills/verify-snapshots.mjs
new file mode 100644
index 00000000..faf58ba6
--- /dev/null
+++ b/tests/skills/verify-snapshots.mjs
@@ -0,0 +1,1059 @@
+#!/usr/bin/env node
+// verify-snapshots v0.2 — Platform verification of skill test snapshots
+// Reruns skill scripts from test-case DSL, then loads into 1C platform.
+// Usage: node tests/skills/verify-snapshots.mjs [--skill meta-compile] [--case catalog-basic] [--runtime powershell|python] [--keep] [--verbose]
+// Supports: meta-compile, form-compile, form-add, form-edit, skd-compile, skd-edit,
+// role-compile, subsystem-compile, subsystem-edit, mxl-compile, template-add,
+// help-add, cf-init, cf-edit, epf-init, meta-edit, interface-edit,
+// cfe-init, cfe-borrow, cfe-patch-method
+
+import { execFileSync } from 'child_process';
+import { existsSync, mkdirSync, mkdtempSync, rmSync, readFileSync, writeFileSync,
+ readdirSync, statSync, cpSync } from 'fs';
+import { join, resolve, dirname, basename } from 'path';
+import { tmpdir } from 'os';
+
+// ─── Paths ──────────────────────────────────────────────────────────────────
+
+const ROOT = resolve(dirname(new URL(import.meta.url).pathname).replace(/^\/([A-Z]:)/i, '$1'));
+const REPO_ROOT = resolve(ROOT, '../..');
+const SKILLS = resolve(REPO_ROOT, '.claude/skills');
+const CASES = resolve(ROOT, 'cases');
+const REPORT_DIR = resolve(REPO_ROOT, 'debug/snapshot-verify');
+
+// ─── CLI args ───────────────────────────────────────────────────────────────
+
+function parseArgs(argv) {
+ const args = { skill: null, caseName: null, runtime: 'powershell', keep: false, verbose: false };
+ const rest = argv.slice(2);
+ for (let i = 0; i < rest.length; i++) {
+ const a = rest[i];
+ if (a === '--skill' && rest[i + 1]) { args.skill = rest[++i]; continue; }
+ if (a === '--case' && rest[i + 1]) { args.caseName = rest[++i]; continue; }
+ if (a === '--runtime' && rest[i + 1]) { args.runtime = rest[++i]; continue; }
+ if (a === '--keep') { args.keep = true; continue; }
+ if (a === '--verbose' || a === '-v') { args.verbose = true; continue; }
+ }
+ return args;
+}
+
+// ─── Platform context ───────────────────────────────────────────────────────
+
+function loadV8Context() {
+ const projectFile = join(REPO_ROOT, '.v8-project.json');
+ if (!existsSync(projectFile)) return null;
+ try {
+ const proj = JSON.parse(readFileSync(projectFile, 'utf8'));
+ const v8bin = proj.v8path;
+ const v8exe = v8bin ? (existsSync(join(v8bin, '1cv8.exe')) ? join(v8bin, '1cv8.exe') : null) : null;
+ if (!v8exe) return null;
+ return { v8path: v8bin, v8exe };
+ } catch { return null; }
+}
+
+// ─── Script execution ───────────────────────────────────────────────────────
+
+function resolveScript(relPath, runtime) {
+ const ext = runtime === 'python' ? '.py' : '.ps1';
+ const full = join(SKILLS, relPath + ext);
+ if (!existsSync(full)) throw new Error(`Script not found: ${full}`);
+ return full;
+}
+
+function execSkill(runtime, scriptRelPath, args, timeout = 60_000, cwd = REPO_ROOT) {
+ const scriptPath = resolveScript(scriptRelPath, runtime);
+ if (runtime === 'python') {
+ return execFileSync(process.env.PYTHON || 'python', [scriptPath, ...args], {
+ encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'], cwd,
+ });
+ }
+ return execFileSync('powershell.exe', [
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
+ '-File', scriptPath, ...args
+ ], { encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'], cwd });
+}
+
+// ─── Dependency resolution ──────────────────────────────────────────────────
+
+const ID = '[\\w\\u0400-\\u04FF]+';
+
+function extractTypeRefs(input) {
+ const refs = new Map();
+ const json = JSON.stringify(input);
+
+ const refPattern = new RegExp(`(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|ExchangePlan)Ref\\.(${ID})`, 'g');
+ let m;
+ while ((m = refPattern.exec(json)) !== null) {
+ refs.set(`${m[1]}.${m[2]}`, { type: m[1], name: m[2] });
+ }
+
+ const directPattern = new RegExp(`(ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes)\\.(${ID})`, 'g');
+ while ((m = directPattern.exec(json)) !== null) {
+ refs.set(`${m[1]}.${m[2]}`, { type: m[1], name: m[2] });
+ }
+
+ const objPattern = new RegExp(`(Document|Catalog|BusinessProcess|Task|ExchangePlan)Object\\.(${ID})`, 'g');
+ while ((m = objPattern.exec(json)) !== null) {
+ refs.set(`${m[1]}.${m[2]}`, { type: m[1], name: m[2] });
+ }
+
+ const modPattern = new RegExp(`CommonModule\\.(${ID})\\.${ID}`, 'g');
+ while ((m = modPattern.exec(json)) !== null) {
+ refs.set(`CommonModule.${m[1]}`, { type: 'CommonModule', name: m[1] });
+ }
+
+ if (input && input.type === 'ScheduledJob' && input.methodName) {
+ const parts = input.methodName.split('.');
+ if (parts.length >= 2) {
+ refs.set(`CommonModule.${parts[0]}`, { type: 'CommonModule', name: parts[0] });
+ }
+ }
+
+ return refs;
+}
+
+// ─── Structural dependencies ────────────────────────────────────────────────
+
+function getStructuralDeps(input) {
+ const deps = [];
+ const hostEdits = []; // edits applied to the host (the input that triggered the dep)
+ const inputs = Array.isArray(input) ? input : [input];
+ if (!inputs[0] || !inputs[0].type) return { deps, hostEdits };
+
+ for (const inp of inputs) {
+ const regTypePrefix = {
+ AccumulationRegister: 'AccumulationRegister',
+ AccountingRegister: 'AccountingRegister',
+ CalculationRegister: 'CalculationRegister',
+ }[inp.type];
+
+ // InformationRegister needs a registrar only when subordinated to a recorder
+ // (writeMode: 'Subordinate' / RecorderSubordinate).
+ const isSubordinatedInfoReg = inp.type === 'InformationRegister' &&
+ (inp.writeMode === 'Subordinate' || inp.writeMode === 'RecorderSubordinate' ||
+ inp.recorderSubordinate === true);
+ const effectivePrefix = regTypePrefix || (isSubordinatedInfoReg ? 'InformationRegister' : null);
+
+ if (effectivePrefix) {
+ deps.push({
+ type: 'Document', name: 'ТестовыйДокумент',
+ dsl: { type: 'Document', name: 'ТестовыйДокумент' },
+ postEdit: [{ op: 'add-registerRecord', val: `${effectivePrefix}.${inp.name}` }],
+ });
+ }
+
+ switch (inp.type) {
+ case 'BusinessProcess': {
+ const taskRef = inp.task;
+ if (taskRef) {
+ const taskName = taskRef.split('.').pop();
+ deps.push({ type: 'Task', name: taskName, dsl: { type: 'Task', name: taskName, descriptionLength: 100 } });
+ }
+ break;
+ }
+ case 'DocumentJournal':
+ if (inp.registeredDocuments) {
+ for (const docRef of inp.registeredDocuments) {
+ const docName = docRef.split('.').pop();
+ deps.push({ type: 'Document', name: docName, dsl: { type: 'Document', name: docName } });
+ }
+ }
+ break;
+ case 'ChartOfAccounts': {
+ // ExtDimensionTypes (виды субконто) link is required when the chart uses
+ // any ExtDimension at all — otherwise platform rejects forms that bind to
+ // Объект.ExtDimensionTypes.*.
+ const usesExtDim = (inp.maxExtDimensionCount && inp.maxExtDimensionCount > 0)
+ || (Array.isArray(inp.extDimensionAccountingFlags) && inp.extDimensionAccountingFlags.length > 0);
+ if (usesExtDim && !inp.extDimensionTypes) {
+ const stubName = `ВидыСубконтоСтаб${inp.name}`;
+ deps.push({
+ type: 'ChartOfCharacteristicTypes', name: stubName,
+ dsl: { type: 'ChartOfCharacteristicTypes', name: stubName, codeLength: 9, descriptionLength: 100 },
+ });
+ hostEdits.push({
+ hostType: 'ChartOfAccounts', hostName: inp.name,
+ op: 'modify-property', val: `ExtDimensionTypes=ChartOfCharacteristicTypes.${stubName}`,
+ });
+ }
+ break;
+ }
+ }
+ }
+ return { deps, hostEdits };
+}
+
+// ─── Stub creation ──────────────────────────────────────────────────────────
+
+function makeStubDSL(type, name) {
+ switch (type) {
+ case 'Catalog': return { type: 'Catalog', name };
+ case 'Document': return { type: 'Document', name };
+ case 'Enum': return { type: 'Enum', name, values: ['Значение1'] };
+ case 'InformationRegister': return { type: 'InformationRegister', name, dimensions: ['Ключ: String(10)'] };
+ case 'AccumulationRegister': return { type: 'AccumulationRegister', name, dimensions: ['Ключ: String(10)'], resources: ['Значение: Number(15,2)'] };
+ case 'ChartOfAccounts': return { type: 'ChartOfAccounts', name, codeLength: 4, descriptionLength: 100, maxExtDimensionCount: 0 };
+ case 'ChartOfCharacteristicTypes': return { type: 'ChartOfCharacteristicTypes', name, codeLength: 9, descriptionLength: 100 };
+ case 'ChartOfCalculationTypes': return { type: 'ChartOfCalculationTypes', name, codeLength: 9, descriptionLength: 100 };
+ case 'CommonModule': return { type: 'CommonModule', name, server: true };
+ case 'BusinessProcess': return { type: 'BusinessProcess', name };
+ case 'Task': return { type: 'Task', name };
+ case 'ExchangePlan': return { type: 'ExchangePlan', name, codeLength: 9, descriptionLength: 100 };
+ case 'Role': return { type: 'Role', name: name };
+ case 'Subsystem': return null; // Subsystems need special handling
+ default: return null;
+ }
+}
+
+const TYPE_TO_PREFIX = {
+ Catalog: 'Catalog', Document: 'Document', Enum: 'Enum', Constant: 'Constant',
+ CommonModule: 'CommonModule', DataProcessor: 'DataProcessor', Report: 'Report',
+ InformationRegister: 'InformationRegister', AccumulationRegister: 'AccumulationRegister',
+ AccountingRegister: 'AccountingRegister', CalculationRegister: 'CalculationRegister',
+ ChartOfAccounts: 'ChartOfAccounts', ChartOfCharacteristicTypes: 'ChartOfCharacteristicTypes',
+ ChartOfCalculationTypes: 'ChartOfCalculationTypes', BusinessProcess: 'BusinessProcess',
+ Task: 'Task', ExchangePlan: 'ExchangePlan', DocumentJournal: 'DocumentJournal',
+ EventSubscription: 'EventSubscription', ScheduledJob: 'ScheduledJob',
+ DefinedType: 'DefinedType', HTTPService: 'HTTPService', WebService: 'WebService',
+ Subsystem: 'Subsystem', Role: 'Role',
+};
+
+const TYPE_TO_DIR = {
+ Catalog: 'Catalogs', Document: 'Documents', Enum: 'Enums', Constant: 'Constants',
+ CommonModule: 'CommonModules', DataProcessor: 'DataProcessors', Report: 'Reports',
+ InformationRegister: 'InformationRegisters', AccumulationRegister: 'AccumulationRegisters',
+ AccountingRegister: 'AccountingRegisters', CalculationRegister: 'CalculationRegisters',
+ ChartOfAccounts: 'ChartsOfAccounts', ChartOfCharacteristicTypes: 'ChartsOfCharacteristicTypes',
+ ChartOfCalculationTypes: 'ChartsOfCalculationTypes', BusinessProcess: 'BusinessProcesses',
+ Task: 'Tasks', ExchangePlan: 'ExchangePlans', DocumentJournal: 'DocumentJournals',
+ EventSubscription: 'EventSubscriptions', ScheduledJob: 'ScheduledJobs',
+ DefinedType: 'DefinedTypes', HTTPService: 'HTTPServices', WebService: 'WebServices',
+ Subsystem: 'Subsystems', Role: 'Roles',
+};
+
+// ─── Auto-detect objects in config dir for cf-edit ──────────────────────────
+
+function scanConfigObjects(configDir) {
+ const objects = [];
+ // DIR_TO_TYPE: reverse mapping of TYPE_TO_DIR
+ const DIR_TO_TYPE = {};
+ for (const [type, dir] of Object.entries(TYPE_TO_DIR)) DIR_TO_TYPE[dir] = type;
+
+ for (const dir of readdirSync(configDir)) {
+ const type = DIR_TO_TYPE[dir];
+ if (!type) continue;
+ const fullDir = join(configDir, dir);
+ if (!statSync(fullDir).isDirectory()) continue;
+ for (const item of readdirSync(fullDir)) {
+ // Object = either dir or .xml file (for flat objects like DefinedTypes)
+ if (statSync(join(fullDir, item)).isDirectory()) {
+ objects.push({ type, name: item });
+ } else if (item.endsWith('.xml')) {
+ const name = item.replace('.xml', '');
+ // Avoid duplicates: if dir "Foo" exists and "Foo.xml" too, skip the xml
+ if (!existsSync(join(fullDir, name))) {
+ objects.push({ type, name });
+ }
+ }
+ }
+ }
+ return objects;
+}
+
+// ─── Build skill args from _skill.json mapping ─────────────────────────────
+
+function buildSkillArgs(skillConfig, caseData, workDir, inputFile, runtime) {
+ const args = [];
+ const scriptPath = resolveScript(skillConfig.script, runtime);
+
+ for (const mapping of skillConfig.args) {
+ args.push(mapping.flag);
+ switch (mapping.from) {
+ case 'inputFile':
+ args.push(inputFile || '');
+ break;
+ case 'workDir':
+ args.push(workDir);
+ break;
+ case 'workPath': {
+ const field = mapping.field || 'objectPath';
+ const val = caseData.params?.[field] ?? caseData[field];
+ if (val === undefined || val === null || val === '') {
+ if (mapping.optional) {
+ args.pop(); // remove flag pushed above
+ break;
+ }
+ args.push(join(workDir, ''));
+ } else {
+ args.push(join(workDir, val));
+ }
+ break;
+ }
+ case 'switch':
+ args.pop();
+ if (caseData[mapping.flag.replace(/^-/, '')] !== false) args.push(mapping.flag);
+ break;
+ default:
+ if (mapping.from.startsWith('case.')) {
+ const field = mapping.from.slice(5);
+ args.push(String(caseData.params?.[field] ?? caseData[field] ?? ''));
+ } else if (mapping.from === 'literal') {
+ args.push(mapping.value || '');
+ }
+ }
+ }
+ if (caseData.args_extra) args.push(...caseData.args_extra);
+ return { scriptPath, args };
+}
+
+// ─── Execute preRun steps ───────────────────────────────────────────────────
+
+function runPreSteps(preRun, workDir, runtime, log) {
+ if (!preRun) return;
+ for (const step of preRun) {
+ const preArgs = [];
+ for (const [flag, value] of Object.entries(step.args || {})) {
+ preArgs.push(flag);
+ if (value === true || value === '') continue;
+ preArgs.push(String(value).replace('{workDir}', workDir).replace('{inputFile}', ''));
+ }
+ let preInputFile = null;
+ if (step.input) {
+ preInputFile = join(workDir, '__pre_input.json');
+ writeFileSync(preInputFile, JSON.stringify(step.input, null, 2), 'utf8');
+ for (let i = 0; i < preArgs.length; i++) {
+ if (preArgs[i] === '') preArgs[i] = preInputFile;
+ }
+ }
+ const stepName = step.script.split('/').pop();
+ try {
+ execSkill(runtime, step.script, preArgs);
+ log(`preRun: ${stepName}`, true);
+ } catch (e) {
+ log(`preRun: ${stepName}`, false, e.stderr || e.message);
+ throw new Error(`preRun "${step.script}" failed: ${(e.stderr || e.message).substring(0, 500)}`);
+ }
+ if (preInputFile && existsSync(preInputFile)) rmSync(preInputFile);
+ }
+}
+
+// ─── Skills that DON'T produce loadable configs ─────────────────────────────
+// These produce standalone files (SKD templates, MXL templates) that can't be
+// loaded into platform without wrapping in a container object.
+
+// Standalone file skills — produce files (not configs), platform load = just run script
+const STANDALONE_SKILLS = new Set([
+ 'skd-compile', 'skd-edit', 'skd-info', 'skd-validate',
+ 'mxl-decompile', 'mxl-info', 'mxl-validate',
+]);
+
+// Standalone skills that CAN be platform-verified by wrapping their output in
+// an external report (ERF) and running erf-build — the platform parses the
+// schema and we know if it's accepted.
+const SKD_PLATFORM_VERIFY = new Set(['skd-compile', 'skd-edit']);
+
+// MXL: wrap produced Template.xml as a SpreadsheetDocument template inside
+// an EPF source and run epf-build — platform parses the macro layout.
+const MXL_PLATFORM_VERIFY = new Set(['mxl-compile']);
+
+// EPF/ERF skills — verified by epf-build on the produced source.
+// Map skill -> output extension (.epf/.erf).
+const EPF_SKILLS = new Map([
+ ['epf-init', '.epf'],
+ ['erf-init', '.erf'],
+]);
+
+// Skills that produce either an EPF/ERF source or a full Configuration —
+// route is auto-detected after the main script runs.
+const EPF_OR_CONFIG_SKILLS = new Set(['template-add', 'help-add']);
+
+// CFE skills — two-stage load: base config → extension
+const CFE_SKILLS = new Set([
+ 'cfe-init', 'cfe-borrow', 'cfe-patch-method',
+]);
+
+// cf-init produces a config dir — verify by loading the created config
+const CONFIG_INIT_SKILLS = new Set(['cf-init']);
+
+// ─── Main verification pipeline ────────────────────────────────────────────
+
+async function verifyCase(skillName, caseName, skillConfig, caseData, opts) {
+ const result = {
+ skill: skillName, case: caseName, name: caseData.name || caseName,
+ passed: false, steps: [], errors: [], warnings: [], workDir: null,
+ };
+
+ const workDir = mkdtempSync(join(tmpdir(), `verify-${skillName}-${caseName}-`));
+ result.workDir = workDir;
+
+ const log = (step, ok, detail) => {
+ result.steps.push({ step, ok, detail: detail?.substring(0, 2000) });
+ if (opts.verbose) {
+ const icon = ok ? '\u2713' : '\u2717';
+ console.log(` ${icon} ${step}${detail ? ': ' + detail.substring(0, 200) : ''}`);
+ }
+ };
+
+ // Determine config dir
+ const setupType = skillConfig.setup || 'empty-config';
+ const isStandalone = STANDALONE_SKILLS.has(skillName);
+ let epfExt = EPF_SKILLS.get(skillName);
+ let isEpf = !!epfExt;
+ const isCfInit = CONFIG_INIT_SKILLS.has(skillName);
+ // For 'empty-config': workDir is the config (setup creates it)
+ // For cf-init: workDir becomes the config after the script runs
+ // For 'none' + non-special: no config (standalone/EPF)
+ let configDir = (setupType === 'empty-config' || isCfInit) ? workDir : null;
+
+ try {
+ // ── Step 0: Case-level fixture copy (runner.mjs compatibility) ──
+ // A case may declare `"setup": "fixture:"` pointing to
+ // tests/skills/cases//fixtures/ — copy its contents into workDir
+ // so the skill script finds them at the expected relative path.
+ if (typeof caseData.setup === 'string' && caseData.setup.startsWith('fixture:')) {
+ const fixtureName = caseData.setup.slice('fixture:'.length);
+ const fixturePath = join(CASES, skillName, 'fixtures', fixtureName);
+ if (!existsSync(fixturePath)) {
+ result.errors.push(`Fixture not found: ${fixturePath}`);
+ return result;
+ }
+ cpSync(fixturePath, workDir, { recursive: true });
+ log(`fixture: ${fixtureName}`, true);
+ }
+
+ // ── Step 1: Setup (cf-init for empty-config, nothing for 'none') ──
+ // Skip setup for cf-init skill — the test itself creates the config
+ if (configDir && setupType === 'empty-config' && !CONFIG_INIT_SKILLS.has(skillName)) {
+ try {
+ execSkill(opts.runtime, 'cf-init/scripts/cf-init', ['-Name', 'VerifyTest', '-OutputDir', workDir]);
+ log('cf-init', true);
+ } catch (e) {
+ log('cf-init', false, e.stderr || e.message);
+ result.errors.push(`cf-init failed: ${(e.stderr || e.message).substring(0, 500)}`);
+ return result;
+ }
+ }
+
+ // ── Step 2: Dependency stubs ──
+ // Collect all inputs: from caseData.input AND from preRun steps
+ const allInputs = [];
+ if (caseData.input && (caseData.input.type || Array.isArray(caseData.input))) {
+ const inputs = Array.isArray(caseData.input) ? caseData.input : [caseData.input];
+ allInputs.push(...inputs.filter(i => i.type));
+ }
+ // Also scan preRun inputs for type refs (D3 fix)
+ if (caseData.preRun) {
+ for (const step of caseData.preRun) {
+ if (step.input && step.input.type) allInputs.push(step.input);
+ if (Array.isArray(step.input)) allInputs.push(...step.input.filter(i => i && i.type));
+ }
+ }
+
+ if (configDir && allInputs.length > 0) {
+ const mainNames = new Set(allInputs.map(i => `${i.type}.${i.name}`));
+
+ // Structural deps (scanned across both main input and preRun inputs)
+ const { deps: structDeps, hostEdits: structHostEdits } = getStructuralDeps(allInputs);
+ // Stash host edits on the result so we can apply them after preRun.
+ result._structHostEdits = structHostEdits;
+ const structDSLs = new Map();
+ const structPostEdits = new Map();
+ for (const dep of structDeps) {
+ const key = `${dep.type}.${dep.name}`;
+ if (dep.dsl) structDSLs.set(key, dep.dsl);
+ if (dep.postEdit) structPostEdits.set(key, dep.postEdit);
+ }
+
+ // Type refs from ALL inputs (main + preRun)
+ const allRefs = new Map();
+ for (const inp of allInputs) {
+ for (const [key, ref] of extractTypeRefs(inp)) {
+ if (!mainNames.has(key)) allRefs.set(key, ref);
+ }
+ }
+ for (const dep of structDeps) {
+ const key = `${dep.type}.${dep.name}`;
+ if (!mainNames.has(key) && !allRefs.has(key)) allRefs.set(key, { type: dep.type, name: dep.name });
+ }
+
+ // Create stubs
+ for (const [key, ref] of allRefs) {
+ const stubDSL = structDSLs.get(key) || makeStubDSL(ref.type, ref.name);
+ if (!stubDSL) { result.warnings.push(`Cannot create stub for ${key}`); continue; }
+ try {
+ const stubFile = join(workDir, `__stub.json`);
+ writeFileSync(stubFile, JSON.stringify(stubDSL, null, 2), 'utf8');
+ execSkill(opts.runtime, 'meta-compile/scripts/meta-compile', ['-JsonPath', stubFile, '-OutputDir', configDir]);
+ log(`stub: ${key}`, true);
+ } catch (e) {
+ log(`stub: ${key}`, false, e.stderr || e.message);
+ result.warnings.push(`Stub failed: ${key}`);
+ }
+
+ // Post-edit (e.g. add-registerRecord)
+ const edits = structPostEdits.get(key);
+ if (edits) {
+ const dir = TYPE_TO_DIR[ref.type];
+ const objPath = dir ? join(configDir, dir, ref.name) : null;
+ if (objPath && existsSync(objPath)) {
+ for (const edit of edits) {
+ try {
+ execSkill(opts.runtime, 'meta-edit/scripts/meta-edit',
+ ['-ObjectPath', objPath, '-Operation', edit.op, '-Value', edit.val]);
+ log(`postEdit: ${key}`, true, `${edit.op} ${edit.val}`);
+ } catch (e) {
+ log(`postEdit: ${key}`, false, e.stderr || e.message);
+ result.warnings.push(`PostEdit failed: ${key}`);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // ── Step 3: preRun steps ──
+ try {
+ runPreSteps(caseData.preRun, workDir, opts.runtime, log);
+ } catch (e) {
+ result.errors.push(e.message);
+ return result;
+ }
+
+ // ── Step 3.5: host-level structural edits (apply to objects created in preRun) ──
+ if (configDir && result._structHostEdits?.length) {
+ for (const he of result._structHostEdits) {
+ const dir = TYPE_TO_DIR[he.hostType];
+ const objPath = dir ? join(configDir, dir, he.hostName) : null;
+ if (!objPath || !existsSync(objPath)) {
+ result.warnings.push(`HostEdit skipped (object not found): ${he.hostType}.${he.hostName}`);
+ continue;
+ }
+ try {
+ execSkill(opts.runtime, 'meta-edit/scripts/meta-edit',
+ ['-ObjectPath', objPath, '-Operation', he.op, '-Value', he.val]);
+ log(`hostEdit: ${he.hostType}.${he.hostName}`, true, `${he.op} ${he.val}`);
+ } catch (e) {
+ log(`hostEdit: ${he.hostType}.${he.hostName}`, false, e.stderr || e.message);
+ result.warnings.push(`HostEdit failed: ${he.hostType}.${he.hostName}`);
+ }
+ }
+ }
+
+ // ── Step 4: Main skill script ──
+ let inputFile = null;
+ if (caseData.input !== undefined) {
+ inputFile = join(workDir, '__input.json');
+ writeFileSync(inputFile, JSON.stringify(caseData.input, null, 2), 'utf8');
+ }
+
+ try {
+ const { args } = buildSkillArgs(skillConfig, caseData, workDir, inputFile, opts.runtime);
+ const mainCwd = skillConfig.cwd === 'workDir' ? workDir : REPO_ROOT;
+ const output = execSkill(opts.runtime, skillConfig.script, args, 60_000, mainCwd);
+ const lastLine = output.trim().split('\n').pop();
+ if (caseData.expectError) {
+ log(skillName, false, 'expected non-zero exit but got success');
+ result.errors.push(`${skillName}: expected error but got success`);
+ return result;
+ }
+ log(skillName, true, lastLine);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ if (caseData.expectError) {
+ if (typeof caseData.expectError === 'string' && !detail.includes(caseData.expectError)) {
+ log(skillName, false, `expected "${caseData.expectError}" in stderr, got: ${detail.substring(0, 200)}`);
+ result.errors.push(`${skillName}: stderr does not contain "${caseData.expectError}"`);
+ return result;
+ }
+ log(skillName, true, `(expected error) ${detail.substring(0, 100)}`);
+ result.passed = true;
+ return result;
+ }
+ log(skillName, false, detail);
+ result.errors.push(`${skillName} failed: ${detail.substring(0, 500)}`);
+ return result;
+ }
+ if (inputFile && existsSync(inputFile)) rmSync(inputFile);
+
+ // ── Step 5: Determine verification strategy ──
+ if (SKD_PLATFORM_VERIFY.has(skillName)) {
+ // Wrap produced Template.xml in an external report (ERF) and try to build —
+ // platform either accepts the schema or rejects it with an error.
+ if (!opts.v8ctx) {
+ result.passed = true;
+ log('platform-load', true, 'skipped (no v8 context)');
+ return result;
+ }
+ const tplName = caseData.params?.templatePath || caseData.params?.outputPath || 'Template.xml';
+ const tplPath = join(workDir, tplName);
+ if (!existsSync(tplPath)) {
+ result.errors.push(`Output not produced at ${tplPath}`);
+ return result;
+ }
+ const erfDir = join(workDir, 'erf-src');
+ const erfOutDir = join(workDir, 'erf-build');
+ mkdirSync(erfOutDir, { recursive: true });
+ try {
+ execSkill(opts.runtime, 'erf-init/scripts/init', ['-Name', 'TestReport', '-SrcDir', erfDir, '-WithSKD']);
+ log('erf-init', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('erf-init', false, detail);
+ result.errors.push(`erf-init failed: ${detail.substring(0, 500)}`);
+ return result;
+ }
+ const dcsTpl = join(erfDir, 'TestReport', 'Templates', 'ОсновнаяСхемаКомпоновкиДанных', 'Ext', 'Template.xml');
+ cpSync(tplPath, dcsTpl, { force: true });
+ try {
+ execSkill(opts.runtime, 'epf-build/scripts/epf-build', [
+ '-V8Path', opts.v8ctx.v8path,
+ '-SourceFile', join(erfDir, 'TestReport.xml'),
+ '-OutputFile', join(erfOutDir, 'TestReport.erf'),
+ ], 120_000);
+ log('erf-build', true, 'platform accepted schema');
+ result.passed = true;
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('erf-build', false, detail);
+ result.errors.push(`erf-build rejected schema: ${detail.substring(0, 1000)}`);
+ }
+ return result;
+ }
+
+ if (MXL_PLATFORM_VERIFY.has(skillName)) {
+ const tplName = caseData.params?.outputPath || 'Template.xml';
+ const tplPath = join(workDir, tplName);
+ if (!existsSync(tplPath)) {
+ result.errors.push(`Output not produced at ${tplPath}`);
+ return result;
+ }
+ const epfDir = join(workDir, 'epf-src');
+ const epfOutDir = join(workDir, 'epf-build');
+ mkdirSync(epfOutDir, { recursive: true });
+ try {
+ execSkill(opts.runtime, 'epf-init/scripts/init', ['-Name', 'TestProc', '-SrcDir', epfDir]);
+ log('epf-init', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('epf-init', false, detail);
+ result.errors.push(`epf-init failed: ${detail.substring(0, 500)}`);
+ return result;
+ }
+ try {
+ execSkill(opts.runtime, 'template-add/scripts/add-template', [
+ '-ObjectName', 'TestProc',
+ '-TemplateName', 'Макет',
+ '-TemplateType', 'SpreadsheetDocument',
+ '-SrcDir', epfDir,
+ ]);
+ log('template-add', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('template-add', false, detail);
+ result.errors.push(`template-add failed: ${detail.substring(0, 500)}`);
+ return result;
+ }
+ const tplDest = join(epfDir, 'TestProc', 'Templates', 'Макет', 'Ext', 'Template.xml');
+ cpSync(tplPath, tplDest, { force: true });
+ try {
+ execSkill(opts.runtime, 'epf-build/scripts/epf-build', [
+ '-V8Path', opts.v8ctx.v8path,
+ '-SourceFile', join(epfDir, 'TestProc.xml'),
+ '-OutputFile', join(epfOutDir, 'TestProc.epf'),
+ ], 180_000);
+ log('epf-build', true, 'platform accepted MXL');
+ result.passed = true;
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('epf-build', false, detail);
+ result.errors.push(`epf-build rejected MXL: ${detail.substring(0, 1000)}`);
+ }
+ return result;
+ }
+
+ if (isStandalone) {
+ result.passed = true;
+ log('platform-load', true, 'skipped (standalone file, not a config)');
+ return result;
+ }
+
+ // Auto-detect: skills like template-add/help-add can target either an
+ // EPF/ERF source or a full configuration. If Configuration.xml is absent
+ // but a *.xml source for the named object is, route via epf-build.
+ if (!isEpf && EPF_OR_CONFIG_SKILLS.has(skillName)) {
+ const hasConfig = existsSync(join(workDir, 'Configuration.xml'));
+ if (hasConfig) {
+ configDir = workDir;
+ } else {
+ const epfName = caseData.params?.objectName || caseData.params?.name;
+ if (epfName) {
+ const xmlPath = join(workDir, `${epfName}.xml`);
+ if (existsSync(xmlPath)) {
+ const xml = readFileSync(xmlPath, 'utf8');
+ if (/]/.test(xml)) epfExt = '.epf';
+ else if (/]/.test(xml)) epfExt = '.erf';
+ isEpf = !!epfExt;
+ }
+ }
+ }
+ }
+
+ if (isEpf) {
+ const name = caseData.params?.name || caseData.params?.objectName;
+ if (!name) {
+ result.errors.push(`EPF/ERF verify requires params.name or params.objectName`);
+ return result;
+ }
+ const sourceFile = join(workDir, `${name}.xml`);
+ if (!existsSync(sourceFile)) {
+ result.errors.push(`EPF/ERF source not found: ${sourceFile}`);
+ return result;
+ }
+ const outDir = join(workDir, '__build');
+ mkdirSync(outDir, { recursive: true });
+ const outFile = join(outDir, `${name}${epfExt}`);
+ try {
+ execSkill(opts.runtime, 'epf-build/scripts/epf-build', [
+ '-V8Path', opts.v8ctx.v8path,
+ '-SourceFile', sourceFile,
+ '-OutputFile', outFile,
+ ], 180_000);
+ log('epf-build', true, `platform built ${epfExt}`);
+ result.passed = true;
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('epf-build', false, detail);
+ result.errors.push(`epf-build failed: ${detail.substring(0, 1000)}`);
+ }
+ return result;
+ }
+
+ if (CFE_SKILLS.has(skillName)) {
+ // CFE: two-stage load — base config first, then extension
+ const extDir = join(workDir, 'ext');
+ const baseConfigDir = workDir; // preRun puts base config directly in workDir
+ const dbDir = join(workDir, 'testdb');
+
+ // Register base config objects
+ const baseObjects = scanConfigObjects(baseConfigDir);
+ const baseCfEditOps = baseObjects
+ .filter(o => TYPE_TO_PREFIX[o.type])
+ .map(o => ({ operation: 'add-childObject', value: `${TYPE_TO_PREFIX[o.type]}.${o.name}` }));
+ if (baseCfEditOps.length > 0) {
+ try {
+ const editFile = join(workDir, '__cf-edit-base.json');
+ writeFileSync(editFile, JSON.stringify(baseCfEditOps, null, 2), 'utf8');
+ execSkill(opts.runtime, 'cf-edit/scripts/cf-edit', ['-ConfigPath', baseConfigDir, '-DefinitionFile', editFile]);
+ log('cf-edit (base)', true, `${baseCfEditOps.length} objects`);
+ } catch (e) {
+ log('cf-edit (base)', false, e.stderr || e.message);
+ result.errors.push(`cf-edit base failed: ${(e.stderr || e.message).substring(0, 500)}`);
+ return result;
+ }
+ }
+
+ // Create DB + load base config
+ try {
+ execSkill(opts.runtime, 'db-create/scripts/db-create', ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir]);
+ log('db-create', true);
+ } catch (e) {
+ log('db-create', false, e.stderr || e.message);
+ result.errors.push(`db-create failed: ${(e.stderr || e.message).substring(0, 500)}`);
+ return result;
+ }
+
+ try {
+ execSkill(opts.runtime, 'db-load-xml/scripts/db-load-xml',
+ ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir, '-ConfigDir', baseConfigDir, '-StrictLog'], 180_000);
+ log('db-load-xml (config)', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('db-load-xml (config)', false, detail);
+ result.errors.push(`LoadConfig failed: ${detail.substring(0, 1000)}`);
+ return result;
+ }
+
+ try {
+ execSkill(opts.runtime, 'db-update/scripts/db-update',
+ ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir], 180_000);
+ log('db-update (config)', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('db-update (config)', false, detail);
+ result.errors.push(`UpdateDBCfg config failed: ${detail.substring(0, 1000)}`);
+ return result;
+ }
+
+ // Load extension — detect extension name from ext/Configuration.xml
+ let extName = 'Extension';
+ try {
+ const extConfigXml = readFileSync(join(extDir, 'Configuration.xml'), 'utf8');
+ const nameMatch = extConfigXml.match(/([^<]+)<\/Name>/);
+ if (nameMatch) extName = nameMatch[1];
+ } catch {}
+
+ if (existsSync(extDir)) {
+ try {
+ execSkill(opts.runtime, 'db-load-xml/scripts/db-load-xml',
+ ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir, '-ConfigDir', extDir, '-Extension', extName, '-StrictLog'], 180_000);
+ log('db-load-xml (ext)', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('db-load-xml (ext)', false, detail);
+ result.errors.push(`LoadExtension failed: ${detail.substring(0, 1000)}`);
+ return result;
+ }
+
+ try {
+ execSkill(opts.runtime, 'db-update/scripts/db-update',
+ ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir, '-Extension', extName], 180_000);
+ log('db-update (ext)', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('db-update (ext)', false, detail);
+ result.errors.push(`UpdateDBCfg ext failed: ${detail.substring(0, 1000)}`);
+ return result;
+ }
+ }
+
+ result.passed = true;
+ return result;
+ }
+
+ if (CONFIG_INIT_SKILLS.has(skillName)) {
+ // cf-init: the script already created the config in workDir,
+ // but we called cf-init in Step 1 already. For cf-init tests,
+ // the MAIN script IS cf-init, so workDir = the new config.
+ // It should be loadable as-is.
+ }
+
+ if (!configDir) {
+ // No config to load — setup was 'none' and not EPF/standalone
+ result.passed = true;
+ return result;
+ }
+
+ // ── Step 6: Auto-detect and register objects in ChildObjects ──
+ const allObjects = scanConfigObjects(configDir);
+ const cfEditOps = [];
+ for (const obj of allObjects) {
+ const prefix = TYPE_TO_PREFIX[obj.type];
+ if (prefix) cfEditOps.push({ operation: 'add-childObject', value: `${prefix}.${obj.name}` });
+ }
+
+ if (cfEditOps.length > 0) {
+ try {
+ const editFile = join(workDir, '__cf-edit.json');
+ writeFileSync(editFile, JSON.stringify(cfEditOps, null, 2), 'utf8');
+ execSkill(opts.runtime, 'cf-edit/scripts/cf-edit', ['-ConfigPath', configDir, '-DefinitionFile', editFile]);
+ log('cf-edit', true, `${cfEditOps.length} objects`);
+ } catch (e) {
+ log('cf-edit', false, e.stderr || e.message);
+ result.errors.push(`cf-edit failed: ${(e.stderr || e.message).substring(0, 500)}`);
+ return result;
+ }
+ }
+
+ // ── Step 7: Platform load ──
+ const dbDir = join(workDir, 'testdb');
+
+ try {
+ execSkill(opts.runtime, 'db-create/scripts/db-create', ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir]);
+ log('db-create', true);
+ } catch (e) {
+ log('db-create', false, e.stderr || e.message);
+ result.errors.push(`db-create failed: ${(e.stderr || e.message).substring(0, 500)}`);
+ return result;
+ }
+
+ try {
+ execSkill(opts.runtime, 'db-load-xml/scripts/db-load-xml',
+ ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir, '-ConfigDir', configDir, '-StrictLog'], 180_000);
+ log('db-load-xml', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('db-load-xml', false, detail);
+ result.errors.push(`LoadConfigFromFiles failed: ${detail.substring(0, 1000)}`);
+ return result;
+ }
+
+ try {
+ execSkill(opts.runtime, 'db-update/scripts/db-update',
+ ['-V8Path', opts.v8ctx.v8path, '-InfoBasePath', dbDir], 180_000);
+ log('db-update', true);
+ } catch (e) {
+ const detail = (e.stderr || e.stdout || e.message).trim();
+ log('db-update', false, detail);
+ result.errors.push(`UpdateDBCfg failed: ${detail.substring(0, 1000)}`);
+ return result;
+ }
+
+ result.passed = true;
+ } catch (e) {
+ result.errors.push(`Unexpected error: ${e.message}`);
+ } finally {
+ if (!opts.keep) {
+ try { rmSync(workDir, { recursive: true, force: true }); } catch {}
+ result.workDir = '(cleaned)';
+ }
+ }
+
+ return result;
+}
+
+// ─── Discovery ──────────────────────────────────────────────────────────────
+
+// Default skills to verify when no --skill given
+const DEFAULT_SKILLS = [
+ 'meta-compile', 'form-compile', 'form-compile-from-object', 'form-add', 'form-edit',
+ 'role-compile', 'subsystem-compile', 'subsystem-edit',
+ 'cf-init', 'cf-edit', 'meta-edit', 'interface-edit',
+ 'epf-init', 'erf-init', 'template-add', 'help-add',
+ 'cfe-init', 'cfe-borrow', 'cfe-patch-method',
+ 'skd-compile', 'skd-edit', 'mxl-compile',
+];
+
+function discoverCases(skillFilter, caseFilter) {
+ const results = [];
+ const skillDirs = skillFilter ? [skillFilter] : DEFAULT_SKILLS;
+
+ for (const skillDir of skillDirs) {
+ const skillPath = join(CASES, skillDir);
+ if (!existsSync(skillPath)) continue;
+
+ const skillJsonPath = join(skillPath, '_skill.json');
+ if (!existsSync(skillJsonPath)) continue;
+ const skillConfig = JSON.parse(readFileSync(skillJsonPath, 'utf8'));
+
+ // Skip skills that don't have snapshots (read-only, info, validate)
+ if (!existsSync(join(skillPath, 'snapshots'))) continue;
+
+ for (const file of readdirSync(skillPath)) {
+ if (file.startsWith('_') || !file.endsWith('.json')) continue;
+ const caseName = file.replace(/\.json$/, '');
+
+ if (caseFilter && caseName !== caseFilter) continue;
+
+ const caseData = JSON.parse(readFileSync(join(skillPath, file), 'utf8'));
+
+ // Skip error cases
+ if (caseName.startsWith('error-')) continue;
+
+ // Skip cases without input AND without preRun AND without params (truly read-only)
+ if (caseData.input === undefined && !caseData.preRun && !caseData.params) continue;
+
+ results.push({ skill: skillDir, caseName, caseData, skillConfig });
+ }
+ }
+ return results;
+}
+
+// ─── Report ─────────────────────────────────────────────────────────────────
+
+function writeReport(results) {
+ mkdirSync(REPORT_DIR, { recursive: true });
+
+ const lines = [
+ `# Snapshot Verification Report`,
+ ``,
+ `Date: ${new Date().toISOString().split('T')[0]}`,
+ `Total: ${results.length} | Passed: ${results.filter(r => r.passed).length} | Failed: ${results.filter(r => !r.passed).length}`,
+ ``,
+ ];
+
+ lines.push('| Skill | Case | Status | Error |');
+ lines.push('|-------|------|--------|-------|');
+ for (const r of results) {
+ const status = r.passed ? 'OK' : 'FAIL';
+ const error = r.errors.length > 0 ? r.errors[0].substring(0, 100).replace(/\|/g, '\\|').replace(/\n/g, ' ') : '';
+ lines.push(`| ${r.skill} | ${r.case} | ${status} | ${error} |`);
+ }
+
+ const failures = results.filter(r => !r.passed);
+ if (failures.length > 0) {
+ lines.push('', '## Findings', '');
+ for (const r of failures) {
+ lines.push(`### ${r.skill}/${r.case}: ${r.name}`);
+ lines.push('');
+ lines.push('**Steps:**');
+ for (const s of r.steps) {
+ lines.push(`- ${s.ok ? '\u2713' : '\u2717'} ${s.step}${s.detail ? ': ' + s.detail.substring(0, 300) : ''}`);
+ }
+ if (r.warnings.length > 0) {
+ lines.push('', '**Warnings:**');
+ for (const w of r.warnings) lines.push(`- ${w}`);
+ }
+ lines.push('', '**Errors:**');
+ for (const e of r.errors) lines.push('```', e, '```');
+ lines.push('');
+ lines.push('**Classification:** ');
+ lines.push('**Action:** ');
+ lines.push('');
+ }
+ }
+
+ const withWarnings = results.filter(r => r.passed && r.warnings.length > 0);
+ if (withWarnings.length > 0) {
+ lines.push('', '## Warnings (passed with notes)', '');
+ for (const r of withWarnings) {
+ lines.push(`### ${r.skill}/${r.case}`);
+ for (const w of r.warnings) lines.push(`- ${w}`);
+ lines.push('');
+ }
+ }
+
+ const reportPath = join(REPORT_DIR, 'REPORT.md');
+ writeFileSync(reportPath, lines.join('\n'), 'utf8');
+ console.log(`\nReport written to: ${reportPath}`);
+}
+
+// ─── Main ───────────────────────────────────────────────────────────────────
+
+async function main() {
+ const opts = parseArgs(process.argv);
+
+ const v8ctx = loadV8Context();
+ if (!v8ctx) {
+ console.error('ERROR: 1C platform not found. Check .v8-project.json');
+ process.exit(1);
+ }
+ opts.v8ctx = v8ctx;
+ console.log(`Platform: ${v8ctx.v8exe}`);
+
+ const cases = discoverCases(opts.skill, opts.caseName);
+ if (cases.length === 0) {
+ console.error('No cases found.');
+ process.exit(1);
+ }
+ console.log(`Found ${cases.length} case(s) to verify.\n`);
+
+ const results = [];
+ for (const { skill, caseName, caseData, skillConfig } of cases) {
+ const label = `${skill}/${caseName}`;
+ if (opts.verbose) console.log(` ${label}: ${caseData.name || ''}`);
+ else process.stdout.write(` ${label}...`);
+
+ const t0 = performance.now();
+ const result = await verifyCase(skill, caseName, skillConfig, caseData, opts);
+ const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
+
+ if (!opts.verbose) {
+ const icon = result.passed ? '\u2713' : '\u2717';
+ console.log(` ${icon} (${elapsed}s)${result.errors.length ? ' — ' + result.errors[0].substring(0, 80) : ''}`);
+ } else {
+ console.log(` → ${result.passed ? 'PASS' : 'FAIL'} (${elapsed}s)\n`);
+ }
+
+ results.push(result);
+ }
+
+ const passed = results.filter(r => r.passed).length;
+ const failed = results.filter(r => !r.passed).length;
+ console.log(`\n${'='.repeat(60)}`);
+ console.log(`Results: ${passed} passed, ${failed} failed out of ${results.length}`);
+
+ writeReport(results);
+ process.exit(failed > 0 ? 1 : 0);
+}
+
+main().catch(e => { console.error(e); process.exit(1); });