diff --git a/.claude/skills/epf-add-form/scripts/add-form.ps1 b/.claude/skills/epf-add-form/scripts/add-form.ps1 index c8a90b1f..15266f7d 100644 --- a/.claude/skills/epf-add-form/scripts/add-form.ps1 +++ b/.claude/skills/epf-add-form/scripts/add-form.ps1 @@ -1,4 +1,6 @@ -param( +# epf-add-form v1.0 — Add managed form to 1C processor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$ProcessorName, diff --git a/.claude/skills/epf-add-help/scripts/add-help.ps1 b/.claude/skills/epf-add-help/scripts/add-help.ps1 index c1081f39..36ce90f1 100644 --- a/.claude/skills/epf-add-help/scripts/add-help.ps1 +++ b/.claude/skills/epf-add-help/scripts/add-help.ps1 @@ -1,4 +1,6 @@ -param( +# epf-add-help v1.0 — Add built-in help to 1C processor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$ProcessorName, diff --git a/.claude/skills/epf-add-template/SKILL.md b/.claude/skills/epf-add-template/SKILL.md index 55100184..d005dfc4 100644 --- a/.claude/skills/epf-add-template/SKILL.md +++ b/.claude/skills/epf-add-template/SKILL.md @@ -25,7 +25,7 @@ allowed-tools: |---------------|:------------:|-----------------|--------------------------------------------------| | ProcessorName | да | — | Имя обработки | | TemplateName | да | — | Имя макета | -| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData | +| TemplateType | да | — | Тип: HTML, Text, SpreadsheetDocument, BinaryData, DataCompositionSchema | | Synonym | нет | = TemplateName | Синоним макета | | SrcDir | нет | `src` | Каталог исходников | @@ -45,6 +45,7 @@ pwsh -NoProfile -File .claude/skills/epf-add-template/scripts/add-template.ps1 - | Text, текстовый документ, текст | TextDocument | `.txt` | Пустой файл | | SpreadsheetDocument, табличный документ, MXL | SpreadsheetDocument | `.xml` | Минимальный spreadsheet | | BinaryData, двоичные данные | BinaryData | `.bin` | Пустой файл | +| DataCompositionSchema, СКД, схема компоновки | DataCompositionSchema | `.xml` | Минимальная DCS-схема | ## Конвенция именования diff --git a/.claude/skills/epf-add-template/scripts/add-template.ps1 b/.claude/skills/epf-add-template/scripts/add-template.ps1 index a63ac008..ce29c0d5 100644 --- a/.claude/skills/epf-add-template/scripts/add-template.ps1 +++ b/.claude/skills/epf-add-template/scripts/add-template.ps1 @@ -1,4 +1,6 @@ -param( +# epf-add-template v1.0 — Add template to 1C processor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$ProcessorName, @@ -6,7 +8,7 @@ [string]$TemplateName, [Parameter(Mandatory)] - [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData")] + [ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")] [string]$TemplateType, [string]$Synonym = $TemplateName, @@ -23,6 +25,7 @@ $typeMap = @{ "Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" } "SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" } "BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" } + "DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" } } $tmpl = $typeMap[$TemplateType] @@ -111,6 +114,25 @@ switch ($TemplateType) { "BinaryData" { [System.IO.File]::WriteAllBytes($templateFilePath, @()) } + "DataCompositionSchema" { + $content = @" + + + + ИсточникДанных1 + Local + + +"@ + [System.IO.File]::WriteAllText($templateFilePath, $content, $encBom) + } } # --- 3. Модификация корневого XML --- diff --git a/.claude/skills/epf-init/scripts/init.ps1 b/.claude/skills/epf-init/scripts/init.ps1 index 68b55d77..76283c42 100644 --- a/.claude/skills/epf-init/scripts/init.ps1 +++ b/.claude/skills/epf-init/scripts/init.ps1 @@ -1,4 +1,6 @@ -param( +# epf-init v1.0 — Init 1C external data processor scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$Name, diff --git a/.claude/skills/epf-remove-form/scripts/remove-form.ps1 b/.claude/skills/epf-remove-form/scripts/remove-form.ps1 index 80c573a8..2fcd5046 100644 --- a/.claude/skills/epf-remove-form/scripts/remove-form.ps1 +++ b/.claude/skills/epf-remove-form/scripts/remove-form.ps1 @@ -1,4 +1,6 @@ -param( +# epf-remove-form v1.0 — Remove form from 1C processor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$ProcessorName, diff --git a/.claude/skills/epf-remove-template/scripts/remove-template.ps1 b/.claude/skills/epf-remove-template/scripts/remove-template.ps1 index c3b4fe52..65945b24 100644 --- a/.claude/skills/epf-remove-template/scripts/remove-template.ps1 +++ b/.claude/skills/epf-remove-template/scripts/remove-template.ps1 @@ -1,4 +1,6 @@ -param( +# epf-remove-template v1.0 — Remove template from 1C processor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$ProcessorName, diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index 5cdc84c1..0b036d44 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -1,4 +1,6 @@ -param( +# form-add v1.0 — Add managed form to 1C config object +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$ObjectPath, diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 496551e0..14a918ba 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,6 @@ -param( +# form-compile v1.0 — Compile 1C managed form from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$JsonPath, diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index ebc4366f..65add737 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1,4 +1,6 @@ -param( +# form-edit v1.0 — Edit 1C managed form elements +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$FormPath, diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1 index 61ef72da..0604f887 100644 --- a/.claude/skills/form-info/scripts/form-info.ps1 +++ b/.claude/skills/form-info/scripts/form-info.ps1 @@ -1,4 +1,6 @@ -param( +# form-info v1.0 — Analyze 1C managed form structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory=$true)] [string]$FormPath, [int]$Limit = 150, diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index 696dd310..33df9299 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -1,4 +1,6 @@ -param( +# form-validate v1.0 — Validate 1C managed form +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$FormPath, diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 5159cd6c..3f80d4c4 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -1,4 +1,6 @@ -param( +# mxl-compile v1.0 — Compile 1C spreadsheet from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$JsonPath, diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 70df68d2..9899981c 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -1,4 +1,6 @@ -param( +# mxl-decompile v1.0 — Decompile 1C spreadsheet to JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$TemplatePath, diff --git a/.claude/skills/mxl-info/scripts/mxl-info.ps1 b/.claude/skills/mxl-info/scripts/mxl-info.ps1 index 77327827..d857dd0f 100644 --- a/.claude/skills/mxl-info/scripts/mxl-info.ps1 +++ b/.claude/skills/mxl-info/scripts/mxl-info.ps1 @@ -1,4 +1,6 @@ -param( +# mxl-info v1.0 — Analyze 1C spreadsheet structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [string]$TemplatePath, [string]$ProcessorName, [string]$TemplateName, diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 index 2fc6006c..b44a125e 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 @@ -1,4 +1,6 @@ -param( +# mxl-validate v1.0 — Validate 1C spreadsheet +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [string]$TemplatePath, [string]$ProcessorName, [string]$TemplateName, diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1 index 83e08de9..d5332874 100644 --- a/.claude/skills/role-compile/scripts/role-compile.ps1 +++ b/.claude/skills/role-compile/scripts/role-compile.ps1 @@ -1,4 +1,6 @@ -param( +# role-compile v1.0 — Compile 1C role from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$JsonPath, diff --git a/.claude/skills/role-info/scripts/role-info.ps1 b/.claude/skills/role-info/scripts/role-info.ps1 index 9de9c9f1..d127714c 100644 --- a/.claude/skills/role-info/scripts/role-info.ps1 +++ b/.claude/skills/role-info/scripts/role-info.ps1 @@ -1,4 +1,6 @@ -param( +# role-info v1.0 — Analyze 1C role rights +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory=$true)][string]$RightsPath, [switch]$ShowDenied, [int]$Limit = 150, diff --git a/.claude/skills/role-validate/scripts/role-validate.ps1 b/.claude/skills/role-validate/scripts/role-validate.ps1 index ccad6b59..be8022b5 100644 --- a/.claude/skills/role-validate/scripts/role-validate.ps1 +++ b/.claude/skills/role-validate/scripts/role-validate.ps1 @@ -1,4 +1,6 @@ -param( +# role-validate v1.0 — Validate 1C role structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( [Parameter(Mandatory)] [string]$RightsPath, diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md new file mode 100644 index 00000000..d4d7f191 --- /dev/null +++ b/.claude/skills/skd-compile/SKILL.md @@ -0,0 +1,225 @@ +--- +name: skd-compile +description: Компиляция схемы компоновки данных 1С (СКД) — Template.xml из компактного JSON-определения +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /skd-compile — генерация СКД из JSON DSL + +Принимает JSON-определение схемы компоновки данных → генерирует Template.xml (DataCompositionSchema). + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `JsonPath` | Путь к JSON-определению СКД | +| `OutputPath` | Путь к выходному Template.xml | + +```powershell +powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.ps1 -JsonPath "" -OutputPath "" +``` + +## JSON DSL — краткий справочник + +Полная спецификация: `docs/skd-dsl-spec.md`. + +### Корневая структура + +```json +{ + "dataSets": [...], + "calculatedFields": [...], + "totalFields": [...], + "parameters": [...], + "dataSetLinks": [...], + "settingsVariants": [...] +} +``` + +Умолчания: `dataSources` → авто `ИсточникДанных1/Local`; `settingsVariants` → авто "Основной" с деталями. + +### Наборы данных + +Тип по ключу: `query` → DataSetQuery, `objectName` → DataSetObject, `items` → DataSetUnion. + +```json +{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] } +``` + +### Поля — shorthand + +``` +"Наименование" — просто имя +"Количество: decimal(15,2)" — имя + тип +"Организация: CatalogRef.Организации @dimension" — + роль +"Служебное: string #noFilter #noOrder" — + ограничения +``` + +Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией. + +**Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые. + +Роли: `@dimension`, `@account`, `@balance`, `@period`. + +Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`. + +### Итоги (shorthand) + +```json +"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] +``` + +### Параметры (shorthand + @autoDates) + +```json +"parameters": [ + "Период: StandardPeriod = LastMonth @autoDates" +] +``` + +`@autoDates` — автоматически генерирует параметры `ДатаНачала` и `ДатаОкончания` с выражениями `&Период.ДатаНачала` / `&Период.ДатаОкончания` и `availableAsField=false`. Заменяет 5 строк на 1. + +### Фильтры — shorthand + +```json +"filter": [ + "Организация = _ @off @user", + "Дата >= 2024-01-01T00:00:00", + "Статус filled" +] +``` + +Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`. + +В объектной форме доступны: `viewMode`, `userSettingID`, `userSettingPresentation`. + +Группы фильтров (Or/And/Not): +```json +{ "group": "Or", "items": [ + { "group": "And", "items": [ + { "field": "Статус", "op": "=", "value": "Активен" }, + { "field": "Сумма", "op": ">", "value": 1000 } + ]}, + { "field": "Количество", "op": "filled" } +]} +``` + +### Параметры данных — shorthand + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. + +### Структура — string shorthand + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +``` + +`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне. + +Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма. + +### Варианты настроек + +```json +"settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Auto"], + "filter": ["Организация = _ @off @user"], + "order": ["Количество desc", "Auto"], + "conditionalAppearance": [ + { + "filter": ["Просрочено = true"], + "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" }, + "presentation": "Выделять просроченные", + "viewMode": "Normal", + "userSettingID": "auto" + } + ], + "outputParameters": { "Заголовок": "Мой отчёт" }, + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } +}] +``` + +### Условное оформление (conditionalAppearance) + +```json +"conditionalAppearance": [ + { + "selection": ["Поле1"], + "filter": ["Поле1 notFilled"], + "appearance": { "Текст": "Не указано", "ЦветТекста": "style:XXX" }, + "presentation": "Описание", + "viewMode": "Normal", + "userSettingID": "auto" + } +] +``` + +Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Текст` → LocalStringType, прочее → String. + +### Итоги с привязкой к группировкам + +```json +"totalFields": [ + { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["Группа1", "Группа1 Иерархия", "ОбщийИтог"] } +] +``` + +## Примеры + +### Минимальный + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": ["Наименование"] + }] +} +``` + +### С ресурсами, параметрами и @autoDates + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи", + "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"] + }], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], + "parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "filter": ["Организация = _ @off @user"], + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } + }] +} +``` + +## Верификация + +``` +/skd-validate — валидация структуры XML +/skd-info — визуальная сводка +/skd-info -Mode variant -Name 1 — проверка варианта настроек +``` diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 new file mode 100644 index 00000000..6df434ed --- /dev/null +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -0,0 +1,1636 @@ +# skd-compile v1.0 — Compile 1C DCS from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$JsonPath, + + [Parameter(Mandatory)] + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Load and validate JSON --- + +if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 +} + +$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$def = $json | ConvertFrom-Json + +if (-not $def.dataSets -or $def.dataSets.Count -eq 0) { + Write-Error "JSON must have at least one entry in 'dataSets'" + exit 1 +} + +# --- 2. XML helpers --- + +$script:xml = New-Object System.Text.StringBuilder 16384 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +function Emit-MLText { + param([string]$tag, [string]$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" + X "$indent" +} + +function New-Guid-String { + return [System.Guid]::NewGuid().ToString() +} + +# --- 3. Resolve defaults --- + +# DataSources +$dataSources = @() +if ($def.dataSources) { + foreach ($ds in $def.dataSources) { + $dataSources += @{ + name = "$($ds.name)" + type = if ($ds.type) { "$($ds.type)" } else { "Local" } + } + } +} else { + $dataSources += @{ name = "ИсточникДанных1"; type = "Local" } +} + +$defaultSource = $dataSources[0].name + +# Auto-name dataSets +$dsIndex = 1 +foreach ($ds in $def.dataSets) { + if (-not $ds.name) { + $ds | Add-Member -NotePropertyName "name" -NotePropertyValue "НаборДанных$dsIndex" -Force + } + $dsIndex++ +} + +# --- 4. Type system --- + +# Type synonyms — normalize Russian/common names to canonical DSL types +# Use case-sensitive hashtable to avoid PS 5.1 DuplicateKeyInHashLiteral +$script:typeSynonyms = New-Object System.Collections.Hashtable +# Russian names (case doesn't matter — we'll also do case-insensitive lookup) +$script:typeSynonyms["число"] = "decimal" +$script:typeSynonyms["строка"] = "string" +$script:typeSynonyms["булево"] = "boolean" +$script:typeSynonyms["дата"] = "date" +$script:typeSynonyms["датавремя"] = "dateTime" +$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" +# English canonical (lowercase for lookup) +$script:typeSynonyms["bool"] = "boolean" +$script:typeSynonyms["str"] = "string" +$script:typeSynonyms["int"] = "decimal" +$script:typeSynonyms["integer"] = "decimal" +$script:typeSynonyms["number"] = "decimal" +$script:typeSynonyms["num"] = "decimal" +# Reference synonyms (Russian, lowercase) +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + # Check for parameterized types: число(15,2), строка(100), etc. + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + + # Resolve base name (case-insensitive via .ToLower()) + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + + return $typeStr + } + + # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) # includes the dot + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + # Simple name lookup (case-insensitive) + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + + return $typeStr +} + +function Emit-ValueType { + param([string]$typeStr, [string]$indent) + + if (-not $typeStr) { return } + + # Resolve synonyms first + $typeStr = Resolve-TypeStr $typeStr + + # boolean + if ($typeStr -eq "boolean") { + X "$indentxs:boolean" + return + } + + # string or string(N) + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + X "$indentxs:string" + X "$indent" + X "$indent`t$len" + X "$indent`tVariable" + X "$indent" + return + } + + # decimal(D,F) or decimal(D,F,nonneg) + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indentxs:decimal" + X "$indent" + X "$indent`t$digits" + X "$indent`t$fraction" + X "$indent`t$sign" + X "$indent" + return + } + + # date / dateTime + if ($typeStr -match '^(date|dateTime)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + } + X "$indentxs:dateTime" + X "$indent" + X "$indent`t$fractions" + X "$indent" + return + } + + # StandardPeriod + if ($typeStr -eq "StandardPeriod") { + X "$indentv8:StandardPeriod" + return + } + + # Reference types: CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc. + # Real DCS files use inline namespace d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + X "$indentd5p1:$(Esc-Xml $typeStr)" + return + } + + # Fallback — assume dot-qualified types are also config references + if ($typeStr.Contains('.')) { + X "$indentd5p1:$(Esc-Xml $typeStr)" + return + } + + X "$indent$(Esc-Xml $typeStr)" +} + +# --- 5. Field shorthand parser --- + +function Parse-FieldShorthand { + param([string]$s) + + $result = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @(); appearance = @{} + } + + # Extract @roles + $roleMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $roleMatches) { + $result.roles += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*@\w+', '') + + # Extract #restrictions + $restrictMatches = [regex]::Matches($s, '#(\w+)') + foreach ($m in $restrictMatches) { + $result.restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*#\w+', '') + + # Split name: type + $s = $s.Trim() + if ($s.Contains(':')) { + $parts = $s -split ':', 2 + $result.dataPath = $parts[0].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) + } else { + $result.dataPath = $s + } + + $result.field = $result.dataPath + return $result +} + +# --- 6. Total field shorthand parser --- + +function Parse-TotalShorthand { + param([string]$s) + + # "DataPath: Func" or "DataPath: Func(expr)" + $parts = $s -split ':', 2 + $dataPath = $parts[0].Trim() + $funcPart = $parts[1].Trim() + + if ($funcPart -match '^\w+\(') { + # Already has expression form: Func(expr) + return @{ dataPath = $dataPath; expression = $funcPart } + } else { + # Short: Func → Func(DataPath) + return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } + } +} + +# --- 7. Parameter shorthand parser --- + +function Parse-ParamShorthand { + param([string]$s) + + $result = @{ name = ""; type = ""; value = $null; autoDates = $false } + + # Extract @autoDates flag + if ($s -match '@autoDates') { + $result.autoDates = $true + $s = $s -replace '\s*@autoDates', '' + } + + # Split "Name: Type = Value" + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') { + $result.name = $Matches[1].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) + if ($Matches[4]) { + $result.value = $Matches[4].Trim() + } + } else { + $result.name = $s.Trim() + } + + return $result +} + +# --- 8. Calculated field shorthand parser --- + +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() + } + } + return @{ dataPath = $s.Trim(); expression = "" } +} + +# --- 8b. DataParameter shorthand parser --- +# Formats: "Период = LastMonth @user", "Организация @off @user", "Период @user" +function Parse-DataParamShorthand { + param([string]$s) + + $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } + + # Extract @flags + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + + $s = $s.Trim() + + # Split "Name = Value" + if ($s -match '^([^=]+)=\s*(.+)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + + # Detect StandardPeriod variants + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + if ($periodVariants -contains $valStr) { + $result.value = @{ variant = $valStr } + } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valStr + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + $result.value = [bool]($valStr -eq "true") + } else { + $result.value = $valStr + } + } else { + $result.parameter = $s + } + + return $result +} + +# --- 8c. Filter item shorthand parser --- +# Formats: "Организация = _ @off @user", "Дата >= 2024-01-01T00:00:00", "Статус filled" +function Parse-FilterShorthand { + param([string]$s) + + $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } + + # Extract @flags + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + if ($s -match '@inaccessible') { + $result.viewMode = "Inaccessible" + $s = $s -replace '\s*@inaccessible', '' + } + + $s = $s.Trim() + + # Try to match: Field op Value, or Field op (no value for filled/notFilled) + # Operators sorted longest first to match >= before > + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $opRaw = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + + # Map op + $opMap = @{ + "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" + } + $mapped = $opMap[$opRaw] + if ($mapped) { $result.op = $opRaw } else { $result.op = $opRaw } + + # Parse value (skip "_" which means empty/placeholder) + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { + $result.value = [bool]($valPart -eq "true") + $result["valueType"] = "xs:boolean" + } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valPart + $result["valueType"] = "xs:dateTime" + } elseif ($valPart -match '^\d+(\.\d+)?$') { + $result.value = $valPart + $result["valueType"] = "xs:decimal" + } else { + $result.value = $valPart + $result["valueType"] = "xs:string" + } + } + } else { + # No operator found — just a field name + $result.field = $s + } + + return $result +} + +# --- 9. Comparison type mapper --- + +$script:comparisonTypes = @{ + "=" = "Equal"; "<>" = "NotEqual" + ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual" + "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" +} + +# --- 10. Output parameter type detection --- + +$script:outputParamTypes = @{ + "Заголовок" = "mltext" + "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" + "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" + "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" + "МакетОформления" = "xs:string" + "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" + "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" + "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" + "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" +} + +# --- 11. Emit sections --- + +# === DataSources === +function Emit-DataSources { + foreach ($ds in $dataSources) { + X "`t" + X "`t`t$(Esc-Xml $ds.name)" + X "`t`t$(Esc-Xml $ds.type)" + X "`t" + } +} + +# === Fields === +function Emit-Field { + param($fieldDef, [string]$indent) + + if ($fieldDef -is [string]) { + $f = Parse-FieldShorthand $fieldDef + } else { + $f = @{ + dataPath = "$($fieldDef.dataPath)" + 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 { "" } + roles = @() + restrict = @() + appearance = @{} + } + # Parse role + if ($fieldDef.role) { + if ($fieldDef.role -is [string]) { + $f.roles = @($fieldDef.role) + } else { + # Object form — collect truthy keys + $roleObj = $fieldDef.role + foreach ($prop in $roleObj.PSObject.Properties) { + if ($prop.Value -eq $true) { $f.roles += $prop.Name } + } + } + } + # Parse restrictions + if ($fieldDef.restrict) { + $f.restrict = @($fieldDef.restrict) + } + # Parse appearance + if ($fieldDef.appearance) { + foreach ($prop in $fieldDef.appearance.PSObject.Properties) { + $f.appearance[$prop.Name] = "$($prop.Value)" + } + } + if ($fieldDef.presentationExpression) { + $f["presentationExpression"] = "$($fieldDef.presentationExpression)" + } + # attrRestrict + if ($fieldDef.attrRestrict) { + $f["attrRestrict"] = @($fieldDef.attrRestrict) + } + # role object extras + if ($fieldDef.role -and $fieldDef.role -isnot [string]) { + $f["roleObj"] = $fieldDef.role + } + } + + X "$indent" + X "$indent`t$(Esc-Xml $f.dataPath)" + X "$indent`t$(Esc-Xml $f.field)" + + # Title + if ($f.title) { + Emit-MLText -tag "title" -text $f.title -indent "$indent`t" + } + + # UseRestriction + $restrictMap = @{ + "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" + "noGroup" = "group"; "noOrder" = "order" + } + if ($f.restrict.Count -gt 0) { + X "$indent`t" + foreach ($r in $f.restrict) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + X "$indent`t`t<$xmlName>true" + } + } + X "$indent`t" + } + + # AttributeUseRestriction + if ($f["attrRestrict"] -and $f["attrRestrict"].Count -gt 0) { + X "$indent`t" + foreach ($r in $f["attrRestrict"]) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + X "$indent`t`t<$xmlName>true" + } + } + X "$indent`t" + } + + # Role + if ($f.roles.Count -gt 0 -or $f["roleObj"]) { + X "$indent`t" + foreach ($role in $f.roles) { + if ($role -eq "period") { + # @period -> periodNumber + periodType (not ) + X "$indent`t`t1" + X "$indent`t`tMain" + } else { + X "$indent`t`ttrue" + } + } + if ($f["roleObj"]) { + $ro = $f["roleObj"] + if ($ro.accountTypeExpression) { + X "$indent`t`t$(Esc-Xml "$($ro.accountTypeExpression)")" + } + if ($ro.balanceGroup) { + X "$indent`t`t$(Esc-Xml "$($ro.balanceGroup)")" + } + } + X "$indent`t" + } + + # ValueType + if ($f.type) { + X "$indent`t" + Emit-ValueType -typeStr $f.type -indent "$indent`t`t" + X "$indent`t" + } + + # Appearance + if ($f.appearance -and $f.appearance.Count -gt 0) { + X "$indent`t" + foreach ($key in $f.appearance.Keys) { + $val = $f.appearance[$key] + X "$indent`t`t" + X "$indent`t`t`t$(Esc-Xml $key)" + if ($key -eq "ГоризонтальноеПоложение") { + X "$indent`t`t`t$(Esc-Xml $val)" + } else { + X "$indent`t`t`t$(Esc-Xml $val)" + } + X "$indent`t`t" + } + X "$indent`t" + } + + # PresentationExpression + if ($f["presentationExpression"]) { + X "$indent`t$(Esc-Xml $f["presentationExpression"])" + } + + X "$indent" +} + +# === DataSets === +function Emit-DataSet { + param($ds, [string]$indent) + + # Determine type + if ($ds.items) { + $dsType = "DataSetUnion" + } elseif ($ds.objectName) { + $dsType = "DataSetObject" + } else { + $dsType = "DataSetQuery" + } + + X "$indent" + X "$indent`t$(Esc-Xml "$($ds.name)")" + + # Fields + if ($ds.fields) { + foreach ($f in $ds.fields) { + Emit-Field -fieldDef $f -indent "$indent`t" + } + } + + # DataSource (not for Union) + if ($dsType -ne "DataSetUnion") { + $src = if ($ds.source) { "$($ds.source)" } else { $defaultSource } + X "$indent`t$(Esc-Xml $src)" + } + + # Type-specific content + if ($dsType -eq "DataSetQuery") { + X "$indent`t$(Esc-Xml "$($ds.query)")" + if ($ds.autoFillFields -eq $false) { + X "$indent`tfalse" + } + } elseif ($dsType -eq "DataSetObject") { + X "$indent`t$(Esc-Xml "$($ds.objectName)")" + } elseif ($dsType -eq "DataSetUnion") { + foreach ($item in $ds.items) { + # Union items are nested dataSets + Emit-DataSet -ds $item -indent "$indent`t" | Out-Null + } + } + + X "$indent" +} + +function Emit-DataSets { + foreach ($ds in $def.dataSets) { + Emit-DataSet -ds $ds -indent "`t" + } +} + +# === DataSetLinks === +function Emit-DataSetLinks { + if (-not $def.dataSetLinks) { return } + foreach ($link in $def.dataSetLinks) { + X "`t" + X "`t`t$(Esc-Xml "$($link.source)")" + X "`t`t$(Esc-Xml "$($link.dest)")" + X "`t`t$(Esc-Xml "$($link.sourceExpr)")" + X "`t`t$(Esc-Xml "$($link.destExpr)")" + if ($link.parameter) { + X "`t`t$(Esc-Xml "$($link.parameter)")" + } + X "`t" + } +} + +# === CalculatedFields === +function Emit-CalcFields { + if (-not $def.calculatedFields) { return } + foreach ($cf in $def.calculatedFields) { + if ($cf -is [string]) { + $parsed = Parse-CalcShorthand $cf + } else { + $parsed = @{ + dataPath = "$($cf.dataPath)" + expression = "$($cf.expression)" + } + } + + X "`t" + X "`t`t$(Esc-Xml $parsed.dataPath)" + X "`t`t$(Esc-Xml $parsed.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" + } + X "`t`t" + foreach ($r in $cf.restrict) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { X "`t`t`t<$xmlName>true" } + } + 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" + } +} + +# === TotalFields === +function Emit-TotalFields { + if (-not $def.totalFields) { return } + foreach ($tf in $def.totalFields) { + if ($tf -is [string]) { + $parsed = Parse-TotalShorthand $tf + } else { + $parsed = @{ + dataPath = "$($tf.dataPath)" + expression = "$($tf.expression)" + } + if ($tf.group) { $parsed.groups = @($tf.group) } + } + + X "`t" + X "`t`t$(Esc-Xml $parsed.dataPath)" + X "`t`t$(Esc-Xml $parsed.expression)" + if ($parsed.groups) { + foreach ($g in $parsed.groups) { + X "`t`t$(Esc-Xml "$g")" + } + } + X "`t" + } +} + +# === Parameters === + +function Emit-SingleParam { + param($p, $parsed) + + X "`t" + X "`t`t$(Esc-Xml $parsed.name)" + + # Title + $title = if ($p -isnot [string] -and $p.title) { "$($p.title)" } else { "" } + if ($title) { + Emit-MLText -tag "title" -text $title -indent "`t`t" + } + + # ValueType + if ($parsed.type) { + X "`t`t" + Emit-ValueType -typeStr $parsed.type -indent "`t`t`t" + X "`t`t" + } + + # Value + Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" + + # UseRestriction + if ($p -isnot [string] -and $p.useRestriction -eq $true) { + X "`t`ttrue" + } + + # Expression + if ($parsed.expression) { + X "`t`t$(Esc-Xml $parsed.expression)" + } + + # AvailableAsField + if ($parsed.availableAsField -eq $false) { + X "`t`tfalse" + } + + # Use + if ($p -isnot [string] -and $p.use) { + X "`t`t$(Esc-Xml "$($p.use)")" + } + + X "`t" +} + +function Emit-Parameters { + if (-not $def.parameters) { return } + foreach ($p in $def.parameters) { + if ($p -is [string]) { + $parsed = Parse-ParamShorthand $p + } else { + $parsed = @{ + name = "$($p.name)" + type = if ($p.type) { Resolve-TypeStr "$($p.type)" } else { "" } + value = $p.value + autoDates = $false + } + if ($p.expression) { $parsed.expression = "$($p.expression)" } + if ($p.availableAsField -eq $false) { $parsed.availableAsField = $false } + if ($p.autoDates -eq $true) { $parsed.autoDates = $true } + } + + Emit-SingleParam -p $p -parsed $parsed + + # @autoDates: auto-generate ДатаНачала and ДатаОкончания + if ($parsed.autoDates) { + $paramName = $parsed.name + $beginParsed = @{ + name = "ДатаНачала"; type = "date"; value = $null + expression = "&$paramName.ДатаНачала"; availableAsField = $false + } + Emit-SingleParam -p $null -parsed $beginParsed + $endParsed = @{ + name = "ДатаОкончания"; type = "date"; value = $null + expression = "&$paramName.ДатаОкончания"; availableAsField = $false + } + Emit-SingleParam -p $null -parsed $endParsed + } + } +} + +function Emit-ParamValue { + param([string]$type, $val, [string]$indent) + + if ($null -eq $val) { return } + + $valStr = "$val" + + if ($type -eq "StandardPeriod") { + # val is a period variant string like "LastMonth" + X "$indent" + X "$indent`t$(Esc-Xml $valStr)" + X "$indent" + } elseif ($type -match '^date') { + X "$indent$(Esc-Xml $valStr)" + } elseif ($type -eq "boolean") { + X "$indent$(Esc-Xml $valStr)" + } elseif ($type -match '^decimal') { + X "$indent$(Esc-Xml $valStr)" + } elseif ($type -match '^string') { + X "$indent$(Esc-Xml $valStr)" + } else { + # Guess from value + if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent$(Esc-Xml $valStr)" + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + X "$indent$(Esc-Xml $valStr)" + } else { + X "$indent$(Esc-Xml $valStr)" + } + } +} + +# === Templates === +function Emit-Templates { + if (-not $def.templates) { return } + foreach ($t in $def.templates) { + X "`t" + } +} + +# === GroupTemplates === +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)")" + X "`t`t" + X "`t" + } +} + +# === Settings Variants === + +function Emit-Selection { + param($items, [string]$indent, [switch]$skipAuto) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + if (-not $skipAuto) { + X "$indent`t" + } + } else { + X "$indent`t" + X "$indent`t`t$(Esc-Xml $item)" + X "$indent`t" + } + } else { + X "$indent`t" + X "$indent`t`t$(Esc-Xml "$($item.field)")" + if ($item.title) { + X "$indent`t`t" + X "$indent`t`t`t" + X "$indent`t`t`t`tru" + X "$indent`t`t`t`t$(Esc-Xml "$($item.title)")" + X "$indent`t`t`t" + X "$indent`t`t" + } + X "$indent`t" + } + } + X "$indent" +} + +function Emit-FilterItem { + param($item, [string]$indent) + + if ($item.group) { + # FilterItemGroup + $groupType = switch ("$($item.group)") { + "And" { "AndGroup" } + "Or" { "OrGroup" } + "Not" { "NotGroup" } + default { "$($item.group)Group" } + } + X "$indent" + X "$indent`t$groupType" + if ($item.items) { + foreach ($sub in $item.items) { + Emit-FilterItem -item $sub -indent "$indent`t" + } + } + X "$indent" + return + } + + # FilterItemComparison + X "$indent" + + if ($item.use -eq $false) { + X "$indent`tfalse" + } + + X "$indent`t$(Esc-Xml "$($item.field)")" + + $compType = $script:comparisonTypes["$($item.op)"] + if (-not $compType) { $compType = "$($item.op)" } + X "$indent`t$(Esc-Xml $compType)" + + # Right value + if ($null -ne $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + $v = $item.value + if ($v -is [bool]) { + $vt = "xs:boolean" + } elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { + $vt = "xs:decimal" + } elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { + $vt = "xs:dateTime" + } else { + $vt = "xs:string" + } + } + $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } + X "$indent`t$vStr" + } + + 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" + } + + if ($item.viewMode) { + X "$indent`t$(Esc-Xml "$($item.viewMode)")" + } + + if ($item.userSettingID) { + $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t$(Esc-Xml $uid)" + } + + 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" + } + + X "$indent" +} + +function Emit-Filter { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + # Parse shorthand: "Организация = _ @off @user" + $parsed = Parse-FilterShorthand $item + $filterObj = New-Object PSObject + $filterObj | Add-Member -NotePropertyName "field" -NotePropertyValue $parsed.field + $filterObj | Add-Member -NotePropertyName "op" -NotePropertyValue $parsed.op + if ($parsed.use -eq $false) { + $filterObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false + } + if ($null -ne $parsed.value) { + $filterObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value + } + if ($parsed["valueType"]) { + $filterObj | Add-Member -NotePropertyName "valueType" -NotePropertyValue $parsed["valueType"] + } + if ($parsed.userSettingID) { + $filterObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID + } + if ($parsed.viewMode) { + $filterObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode + } + Emit-FilterItem -item $filterObj -indent "$indent`t" + } else { + Emit-FilterItem -item $item -indent "$indent`t" + } + } + X "$indent" +} + +function Emit-Order { + param($items, [string]$indent, [switch]$skipAuto) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + if (-not $skipAuto) { + X "$indent`t" + } + } else { + $parts = $item -split '\s+' + $field = $parts[0] + $dir = "Asc" + if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)desc$') { $dir = "Desc" } + elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)asc$') { $dir = "Asc" } + X "$indent`t" + X "$indent`t`t$(Esc-Xml $field)" + X "$indent`t`t$dir" + X "$indent`t" + } + } + } + X "$indent" +} + +function Emit-AppearanceValue { + param([string]$key, $val, [string]$indent) + + X "$indent" + if ($val -is [PSCustomObject] -and $val.use -ne $null -and $val.use -eq $false) { + X "$indent`tfalse" + X "$indent`t$(Esc-Xml $key)" + $actualVal = "$($val.value)" + } else { + X "$indent`t$(Esc-Xml $key)" + $actualVal = "$val" + } + + # Auto-detect value type + if ($actualVal -match '^(style|web|win):') { + 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" + } else { + X "$indent`t$(Esc-Xml $actualVal)" + } + X "$indent" +} + +function Emit-ConditionalAppearance { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($ca in $items) { + X "$indent`t" + + # Selection (which fields to apply to; empty = all) + if ($ca.selection -and $ca.selection.Count -gt 0) { + X "$indent`t`t" + foreach ($sel in $ca.selection) { + X "$indent`t`t`t" + X "$indent`t`t`t`t$(Esc-Xml "$sel")" + X "$indent`t`t`t" + } + X "$indent`t`t" + } else { + X "$indent`t`t" + } + + # Filter (reuse existing Emit-Filter logic) + if ($ca.filter) { + Emit-Filter -items $ca.filter -indent "$indent`t`t" + } + + # Appearance (parameter-value pairs) + if ($ca.appearance) { + X "$indent`t`t" + foreach ($prop in $ca.appearance.PSObject.Properties) { + Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" + } + X "$indent`t`t" + } + + # Presentation + if ($ca.presentation) { + X "$indent`t`t$(Esc-Xml "$($ca.presentation)")" + } + + # ViewMode + if ($ca.viewMode) { + X "$indent`t`t$(Esc-Xml "$($ca.viewMode)")" + } + + # UserSettingID + if ($ca.userSettingID) { + $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } + X "$indent`t`t$(Esc-Xml $uid)" + } + + X "$indent`t" + } + X "$indent" +} + +function Emit-OutputParameters { + param($params, [string]$indent) + + if (-not $params) { return } + + X "$indent" + foreach ($prop in $params.PSObject.Properties) { + $key = $prop.Name + $val = "$($prop.Value)" + $ptype = $script:outputParamTypes[$key] + if (-not $ptype) { $ptype = "xs:string" } + + 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" + } else { + X "$indent`t`t$(Esc-Xml $val)" + } + X "$indent`t" + } + X "$indent" +} + +function Emit-DataParameters { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($dp in $items) { + # Support string shorthand + if ($dp -is [string]) { + $parsed = Parse-DataParamShorthand $dp + $dpObj = New-Object PSObject + $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter + if ($null -ne $parsed.value) { + $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value + } + if ($parsed.use -eq $false) { + $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false + } + if ($parsed.userSettingID) { + $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID + } + if ($parsed.viewMode) { + $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode + } + $dp = $dpObj + } + + X "$indent`t" + + if ($dp.use -eq $false) { + X "$indent`t`tfalse" + } + + X "$indent`t`t$(Esc-Xml "$($dp.parameter)")" + + # Value + if ($null -ne $dp.value) { + 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" + } 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" + } elseif ($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') { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } else { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } + } + + if ($dp.viewMode) { + X "$indent`t`t$(Esc-Xml "$($dp.viewMode)")" + } + + if ($dp.userSettingID) { + $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" } + X "$indent`t`t$(Esc-Xml $uid)" + } + + 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" + } + + X "$indent`t" + } + X "$indent" +} + +# === Structure items (recursive) === + +function Emit-GroupItems { + param($groupBy, [string]$indent) + + if (-not $groupBy -or $groupBy.Count -eq 0) { return } + + X "$indent" + foreach ($field in $groupBy) { + if ($field -is [string]) { + X "$indent`t" + X "$indent`t`t$(Esc-Xml $field)" + X "$indent`t`tItems" + X "$indent`t`tNone" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t" + } else { + # Object form + X "$indent`t" + X "$indent`t`t$(Esc-Xml "$($field.field)")" + $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } + X "$indent`t`t$(Esc-Xml $gt)" + $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } + X "$indent`t`t$(Esc-Xml $pat)" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t" + } + } + X "$indent" +} + +# Parse structure string shorthand: "Организация > Номенклатура > details" +function Parse-StructureShorthand { + param([string]$s) + + $segments = $s -split '\s*>\s*' + $result = @() + + # Build nested groups from right to left + $innermost = $null + for ($i = $segments.Count - 1; $i -ge 0; $i--) { + $seg = $segments[$i].Trim() + $group = New-Object PSObject + $group | Add-Member -NotePropertyName "type" -NotePropertyValue "group" + + if ($seg -match '^(?i)(details|детали)$') { + # Empty groupBy = detailed records + $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @() + } else { + $group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($seg) + } + + if ($null -ne $innermost) { + $group | Add-Member -NotePropertyName "children" -NotePropertyValue @($innermost) + } + $innermost = $group + } + + if ($innermost) { $result += $innermost } + return ,$result +} + +function Emit-StructureItem { + param($item, [string]$indent) + + $type = "$($item.type)" + + if ($type -eq "group") { + X "$indent" + + if ($item.name) { + X "$indent`t$(Esc-Xml "$($item.name)")" + } + + Emit-GroupItems -groupBy $item.groupBy -indent "$indent`t" + + # Default order to ["Auto"] if not specified + $orderItems = $item.order + if (-not $orderItems) { $orderItems = @("Auto") } + Emit-Order -items $orderItems -indent "$indent`t" + + # Default selection to ["Auto"] if not specified + $selItems = $item.selection + if (-not $selItems) { $selItems = @("Auto") } + Emit-Selection -items $selItems -indent "$indent`t" + + Emit-Filter -items $item.filter -indent "$indent`t" + + if ($item.outputParameters) { + Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" + } + + # Nested children + if ($item.children) { + foreach ($child in $item.children) { + Emit-StructureItem -item $child -indent "$indent`t" + } + } + + X "$indent" + } + elseif ($type -eq "table") { + X "$indent" + + if ($item.name) { + X "$indent`t$(Esc-Xml "$($item.name)")" + } + + # Columns + if ($item.columns) { + foreach ($col in $item.columns) { + X "$indent`t" + Emit-GroupItems -groupBy $col.groupBy -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") } + Emit-Selection -items $colSel -indent "$indent`t`t" + X "$indent`t" + } + } + + # Rows + if ($item.rows) { + foreach ($row in $item.rows) { + X "$indent`t" + if ($row.name) { + X "$indent`t`t$(Esc-Xml "$($row.name)")" + } + Emit-GroupItems -groupBy $row.groupBy -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") } + Emit-Selection -items $rowSel -indent "$indent`t`t" + X "$indent`t" + } + } + + X "$indent" + } + elseif ($type -eq "chart") { + X "$indent" + + if ($item.name) { + X "$indent`t$(Esc-Xml "$($item.name)")" + } + + # Points + if ($item.points) { + X "$indent`t" + Emit-GroupItems -groupBy $item.points.groupBy -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") } + Emit-Selection -items $ptSel -indent "$indent`t`t" + X "$indent`t" + } + + # Series + if ($item.series) { + X "$indent`t" + Emit-GroupItems -groupBy $item.series.groupBy -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") } + Emit-Selection -items $srSel -indent "$indent`t`t" + X "$indent`t" + } + + # Selection (chart values) + Emit-Selection -items $item.selection -indent "$indent`t" + + if ($item.outputParameters) { + Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" + } + + X "$indent" + } +} + +function Emit-SettingsVariants { + $variants = $def.settingsVariants + + # Default variant if none specified + if (-not $variants -or $variants.Count -eq 0) { + $variants = @(@{ + name = "Основной" + presentation = "Основной" + settings = @{ + selection = @("Auto") + structure = @(@{ + type = "group" + order = @("Auto") + selection = @("Auto") + }) + } + }) + # Convert to PSCustomObject-like structure + $variants = @($variants | ForEach-Object { + $v = New-Object PSObject + $v | Add-Member -NotePropertyName "name" -NotePropertyValue $_.name + $v | Add-Member -NotePropertyName "presentation" -NotePropertyValue $_.presentation + $settingsObj = New-Object PSObject + $settingsObj | Add-Member -NotePropertyName "selection" -NotePropertyValue $_.settings.selection + $structItem = New-Object PSObject + $structItem | Add-Member -NotePropertyName "type" -NotePropertyValue "group" + $structItem | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto") + $structItem | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto") + $settingsObj | Add-Member -NotePropertyName "structure" -NotePropertyValue @($structItem) + $v | Add-Member -NotePropertyName "settings" -NotePropertyValue $settingsObj + $v + }) + } + + foreach ($v in $variants) { + X "`t" + X "`t`t$(Esc-Xml "$($v.name)")" + + $pres = if ($v.presentation) { "$($v.presentation)" } 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" + + X "`t`t" + + $s = $v.settings + + # Selection (Auto items only belong at group level, not top-level settings) + if ($s.selection) { + Emit-Selection -items $s.selection -indent "`t`t`t" -skipAuto + } + + # Filter + if ($s.filter) { + Emit-Filter -items $s.filter -indent "`t`t`t" + } + + # Order (Auto items only belong at group level, not top-level settings) + if ($s.order) { + Emit-Order -items $s.order -indent "`t`t`t" -skipAuto + } + + # ConditionalAppearance + if ($s.conditionalAppearance) { + Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "`t`t`t" + } + + # OutputParameters + if ($s.outputParameters) { + Emit-OutputParameters -params $s.outputParameters -indent "`t`t`t" + } + + # DataParameters + if ($s.dataParameters) { + Emit-DataParameters -items $s.dataParameters -indent "`t`t`t" + } + + # Structure (supports string shorthand: "Организация > details") + if ($s.structure) { + $structItems = $s.structure + if ($structItems -is [string]) { + $structItems = Parse-StructureShorthand $structItems + } + foreach ($item in $structItems) { + Emit-StructureItem -item $item -indent "`t`t`t" + } + } + + X "`t`t" + X "`t" + } +} + +# --- 12. Assemble XML --- + +X "" +X "" + +Emit-DataSources +Emit-DataSets +Emit-DataSetLinks +Emit-CalcFields +Emit-TotalFields +Emit-Parameters +Emit-Templates +Emit-GroupTemplates +Emit-SettingsVariants + +X '' + +# --- 13. Write output --- + +$parentDir = [System.IO.Path]::GetDirectoryName($OutputPath) +if ($parentDir -and -not (Test-Path $parentDir)) { + New-Item -ItemType Directory -Force $parentDir | Out-Null +} + +$content = $script:xml.ToString() +$utf8Bom = New-Object System.Text.UTF8Encoding $true +[System.IO.File]::WriteAllText($OutputPath, $content, $utf8Bom) + +# --- 14. Statistics --- + +$dsCount = $def.dataSets.Count +$fieldCount = 0 +foreach ($ds in $def.dataSets) { + if ($ds.fields) { $fieldCount += $ds.fields.Count } +} +$calcCount = if ($def.calculatedFields) { $def.calculatedFields.Count } else { 0 } +$totalCount = if ($def.totalFields) { $def.totalFields.Count } else { 0 } +$paramCount = if ($def.parameters) { $def.parameters.Count } else { 0 } +$variantCount = if ($def.settingsVariants) { $def.settingsVariants.Count } else { 1 } +$fileSize = (Get-Item $OutputPath).Length + +Write-Host "OK $OutputPath" +Write-Host " DataSets: $dsCount Fields: $fieldCount Calculated: $calcCount Totals: $totalCount Params: $paramCount Variants: $variantCount" +Write-Host " Size: $fileSize bytes" diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md new file mode 100644 index 00000000..26eb1d2a --- /dev/null +++ b/.claude/skills/skd-edit/SKILL.md @@ -0,0 +1,217 @@ +--- +name: skd-edit +description: Точечное редактирование схемы компоновки данных 1С (СКД) — добавление/удаление полей, итогов, фильтров, параметров, вычисляемых полей +argument-hint: -Operation -Value +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /skd-edit — точечное редактирование СКД (Template.xml) + +Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) | +| `Operation` | Операция (см. список ниже) | +| `Value` | Значение операции (shorthand-строка или текст запроса) | +| `DataSet` | (опц.) Имя набора данных (умолч. первый) | +| `Variant` | (опц.) Имя варианта настроек (умолч. первый) | +| `NoSelection` | (опц.) Не добавлять поле в selection варианта | + +```powershell +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -TemplatePath "" -Operation -Value "" +``` + +## Пакетный режим (batch) + +Несколько значений в одном вызове через разделитель `;;`: + +```powershell +-Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)" +``` + +Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`. + +## Операции + +### add-field — добавить поле в набор данных + +Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`. + +``` +"Цена: decimal(15,2)" +"Организация [Орг-ция]: CatalogRef.Организации @dimension" +"Служебное: string #noFilter #noOrder" +``` + +Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск. + +### add-total — добавить итог + +``` +"Цена: Среднее" +"Стоимость: Сумма(Кол * Цена)" +``` + +### add-calculated-field — добавить вычисляемое поле + +Shorthand: `"Имя [Заголовок]: тип = Выражение"`. + +``` +"Маржа = Продажа - Закупка" +"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100" +``` + +Также добавляется в selection варианта. + +### add-parameter — добавить параметр + +``` +"Период: StandardPeriod = LastMonth @autoDates" +"Организация: CatalogRef.Организации" +``` + +`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически. + +### add-filter — добавить фильтр в вариант + +Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`. + +``` +"Номенклатура = _ @off @user" +"Дата >= 2024-01-01T00:00:00" +"Статус filled" +``` + +### add-dataParameter — добавить параметр данных в вариант + +Shorthand: `"Имя [= значение] @флаги"`. + +``` +"Период = LastMonth @user" +"Организация @off @user" +``` + +### add-order — добавить сортировку + +Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент. + +``` +"Количество desc" +"Auto" +``` + +### add-selection — добавить элемент выборки + +``` +"Номенклатура" +"Auto" +``` + +### add-dataSetLink — добавить связь наборов данных + +Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`. + +``` +"Набор1 > Набор2 on Поле1 = Поле2" +"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]" +``` + +### add-dataSet — добавить набор данных + +Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРОСА"` (авто-имя `НаборДанныхN`). + +``` +"Доп: ВЫБРАТЬ 1 КАК Тест" +"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура" +``` + +`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`). + +### add-variant — добавить вариант настроек + +Shorthand: `"Имя [Представление]"`. Представление опционально, по умолчанию = имя. + +``` +"Детальный" +"Детальный [Детальный отчёт]" +``` + +Создаёт вариант с Auto selection + detail group. Дубликат имени — предупреждение, пропуск. + +### add-conditionalAppearance — добавить условное оформление + +Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение). + +``` +"ЦветТекста = web:Red when Сумма < 0" +"ЦветФона = web:LightGreen when Статус = Одобрен for Статус" +"МинимальнаяШирина = 50 for Организация" +"Формат = ЧДЦ=2 for Цена, Сумма" +``` + +Типы значений (автодетект): `web:*`/`style:*`/`win:*` → цвет, `true`/`false` → boolean, иначе строка. + +### set-query — заменить текст запроса + +Не поддерживает пакетный режим. Value — полный текст запроса. + +### set-outputParameter — установить параметр вывода + +``` +"Заголовок = Мой отчёт" +"ВыводитьЗаголовок = true" +``` + +Если параметр уже существует — заменяет значение. + +### set-structure — установить структуру варианта + +Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим. + +``` +"Организация > Номенклатура > details" +"details" +``` + +### modify-field — изменить существующее поле + +Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию. + +``` +"Цена [Цена USD]: decimal(10,4) @dimension" +``` + +### modify-filter — изменить существующий фильтр + +Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. + +### modify-dataParameter — изменить параметр данных + +Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги. + +### remove-* и clear-* + +| Операция | Value | Действие | +|----------|-------|----------| +| `remove-field` | dataPath | Удаляет поле из набора + из selection варианта | +| `remove-total` | dataPath | Удаляет итог | +| `remove-calculated-field` | dataPath | Удаляет вычисляемое поле + из selection | +| `remove-parameter` | name | Удаляет параметр | +| `remove-filter` | поле | Удаляет первый фильтр с указанным полем | +| `clear-selection` | `*` | Очищает все элементы selection | +| `clear-order` | `*` | Очищает все элементы order | +| `clear-filter` | `*` | Очищает все элементы filter | + +## Верификация + +``` +/skd-validate — валидация структуры после редактирования +/skd-info — визуальная сводка +``` diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 new file mode 100644 index 00000000..6d92c3e3 --- /dev/null +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -0,0 +1,2291 @@ +# skd-edit v1.1 — Atomic 1C DCS editor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$TemplatePath, + + [Parameter(Mandatory)] + [ValidateSet( + "add-field","add-total","add-calculated-field","add-parameter","add-filter", + "add-dataParameter","add-order","add-selection","add-dataSetLink", + "add-dataSet","add-variant","add-conditionalAppearance", + "set-query","set-outputParameter","set-structure", + "modify-field","modify-filter","modify-dataParameter", + "clear-selection","clear-order","clear-filter", + "remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")] + [string]$Operation, + + [Parameter(Mandatory)] + [string]$Value, + + [string]$DataSet, + [string]$Variant, + [switch]$NoSelection +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Resolve path --- + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +# --- 2. Type system (copied from skd-compile) --- + +$script:typeSynonyms = New-Object System.Collections.Hashtable +$script:typeSynonyms["число"] = "decimal" +$script:typeSynonyms["строка"] = "string" +$script:typeSynonyms["булево"] = "boolean" +$script:typeSynonyms["дата"] = "date" +$script:typeSynonyms["датавремя"] = "dateTime" +$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" +$script:typeSynonyms["bool"] = "boolean" +$script:typeSynonyms["str"] = "string" +$script:typeSynonyms["int"] = "decimal" +$script:typeSynonyms["integer"] = "decimal" +$script:typeSynonyms["number"] = "decimal" +$script:typeSynonyms["num"] = "decimal" +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" + +$script:outputParamTypes = @{ + "Заголовок" = "mltext" + "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" + "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" + "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" + "МакетОформления" = "xs:string" + "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" + "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" + "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" + "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + return $typeStr + } + + if ($typeStr.Contains('.')) { + $dotIdx = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $dotIdx) + $suffix = $typeStr.Substring($dotIdx) + $resolved = $script:typeSynonyms[$prefix.ToLower()] + if ($resolved) { return "$resolved$suffix" } + return $typeStr + } + + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + return $typeStr +} + +# --- 3. Parsers --- + +function Parse-FieldShorthand { + param([string]$s) + + $result = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @() + } + + # Extract [Title] + if ($s -match '\[([^\]]+)\]') { + $result.title = $Matches[1] + $s = $s -replace '\s*\[[^\]]+\]', '' + } + + # Extract @roles + $roleMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $roleMatches) { + $result.roles += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*@\w+', '') + + # Extract #restrictions + $restrictMatches = [regex]::Matches($s, '#(\w+)') + foreach ($m in $restrictMatches) { + $result.restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*#\w+', '') + + # Split name: type + $s = $s.Trim() + if ($s.Contains(':')) { + $parts = $s -split ':', 2 + $result.dataPath = $parts[0].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) + } else { + $result.dataPath = $s + } + + $result.field = $result.dataPath + return $result +} + +function Read-FieldProperties($fieldEl) { + $props = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @() + } + + foreach ($ch in $fieldEl.ChildNodes) { + if ($ch.NodeType -ne 'Element') { continue } + switch ($ch.LocalName) { + "dataPath" { $props.dataPath = $ch.InnerText.Trim() } + "field" { $props.field = $ch.InnerText.Trim() } + "title" { + # Extract text from LocalStringType + foreach ($item in $ch.ChildNodes) { + if ($item.NodeType -eq 'Element' -and $item.LocalName -eq 'item') { + foreach ($gc in $item.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') { + $props.title = $gc.InnerText.Trim() + } + } + } + } + } + "valueType" { + # Read type info — store the raw element for now, we'll use type from parsed if overridden + $typeEl = $null + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') { + $typeEl = $gc; break + } + } + if ($typeEl) { + $props["_rawTypeText"] = $typeEl.InnerText.Trim() + } + } + "role" { + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element') { + if ($gc.LocalName -eq 'periodNumber') { + $props.roles += "period" + } elseif ($gc.InnerText.Trim() -eq 'true') { + $props.roles += $gc.LocalName + } + } + } + } + "useRestriction" { + $revMap = @{ "field" = "noField"; "condition" = "noFilter"; "group" = "noGroup"; "order" = "noOrder" } + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.InnerText.Trim() -eq 'true') { + $mapped = $revMap[$gc.LocalName] + if ($mapped) { $props.restrict += $mapped } + } + } + } + } + } + return $props +} + +function Parse-TotalShorthand { + param([string]$s) + + $parts = $s -split ':', 2 + $dataPath = $parts[0].Trim() + $funcPart = $parts[1].Trim() + + if ($funcPart -match '^\w+\(') { + return @{ dataPath = $dataPath; expression = $funcPart } + } else { + return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } + } +} + +function Parse-CalcShorthand { + param([string]$s) + + $title = "" + # Extract [Title] first + if ($s -match '\[([^\]]+)\]') { + $title = $Matches[1] + $s = $s -replace '\s*\[[^\]]+\]', '' + } + + # Support "Name: Type = Expression" and "Name = Expression" + $eqIdx = $s.IndexOf('=') + if ($eqIdx -gt 0) { + $left = $s.Substring(0, $eqIdx).Trim() + $expression = $s.Substring($eqIdx + 1).Trim() + + if ($left.Contains(':')) { + $colonIdx = $left.IndexOf(':') + $dataPath = $left.Substring(0, $colonIdx).Trim() + $type = Resolve-TypeStr ($left.Substring($colonIdx + 1).Trim()) + return @{ dataPath = $dataPath; expression = $expression; type = $type; title = $title } + } + return @{ dataPath = $left; expression = $expression; type = ""; title = $title } + } + return @{ dataPath = $s.Trim(); expression = ""; type = ""; title = $title } +} + +function Parse-ParamShorthand { + param([string]$s) + + $result = @{ name = ""; type = ""; value = $null; autoDates = $false } + + if ($s -match '@autoDates') { + $result.autoDates = $true + $s = $s -replace '\s*@autoDates', '' + } + + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') { + $result.name = $Matches[1].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) + if ($Matches[4]) { + $result.value = $Matches[4].Trim() + } + } else { + $result.name = $s.Trim() + } + + return $result +} + +function Parse-FilterShorthand { + param([string]$s) + + $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null } + + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + if ($s -match '@inaccessible') { + $result.viewMode = "Inaccessible" + $s = $s -replace '\s*@inaccessible', '' + } + + $s = $s.Trim() + + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $opRaw = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + + $opMap = @{ + "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" + } + $mapped = $opMap[$opRaw] + if ($mapped) { $result.op = $mapped } else { $result.op = $opRaw } + + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { + $result.value = $valPart + $result["valueType"] = "xs:boolean" + } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valPart + $result["valueType"] = "xs:dateTime" + } elseif ($valPart -match '^\d+(\.\d+)?$') { + $result.value = $valPart + $result["valueType"] = "xs:decimal" + } else { + $result.value = $valPart + $result["valueType"] = "xs:string" + } + } + } else { + $result.field = $s + } + + return $result +} + +function Parse-DataParamShorthand { + param([string]$s) + + $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } + + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + + $s = $s.Trim() + + if ($s -match '^([^=]+)=\s*(.+)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + if ($periodVariants -contains $valStr) { + $result.value = @{ variant = $valStr } + } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valStr + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + $result.value = $valStr + } else { + $result.value = $valStr + } + } else { + $result.parameter = $s + } + + return $result +} + +function Parse-OrderShorthand { + param([string]$s) + $s = $s.Trim() + if ($s -eq "Auto") { + return @{ field = "Auto"; direction = "" } + } + $parts = $s -split '\s+', 2 + $field = $parts[0] + $dir = "Asc" + if ($parts.Count -gt 1 -and $parts[1] -match '(?i)^desc$') { $dir = "Desc" } + return @{ field = $field; direction = $dir } +} + +function Parse-DataSetLinkShorthand { + param([string]$s) + + $result = @{ source = ""; dest = ""; sourceExpr = ""; destExpr = ""; parameter = "" } + + # Extract optional [param ParamName] + if ($s -match '\[param\s+([^\]]+)\]') { + $result.parameter = $Matches[1].Trim() + $s = $s -replace '\s*\[param\s+[^\]]+\]', '' + } + + # Pattern: "Source > Dest on FieldA = FieldB" + if ($s -match '^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$') { + $result.source = $Matches[1].Trim() + $result.dest = $Matches[2].Trim() + $result.sourceExpr = $Matches[3].Trim() + $result.destExpr = $Matches[4].Trim() + } else { + Write-Error "Invalid dataSetLink shorthand: $s. Expected: 'Source > Dest on FieldA = FieldB [param Name]'" + exit 1 + } + + return $result +} + +function Parse-DataSetShorthand { + param([string]$s) + + $s = $s.Trim() + # "Name: QUERY" — split on first ": " only if prefix is a single word (no spaces) + if ($s -match '^(\S+):\s(.+)$') { + return @{ name = $Matches[1]; query = $Matches[2] } + } + return @{ name = ""; query = $s } +} + +function Parse-VariantShorthand { + param([string]$s) + + $presentation = "" + if ($s -match '\[([^\]]+)\]') { + $presentation = $Matches[1] + $s = $s -replace '\s*\[[^\]]+\]', '' + } + $name = $s.Trim() + if (-not $presentation) { $presentation = $name } + return @{ name = $name; presentation = $presentation } +} + +function Parse-ConditionalAppearanceShorthand { + param([string]$s) + + $result = @{ param = ""; value = ""; filter = $null; fields = @() } + + # Extract " when ..." — condition part + $whenIdx = $s.IndexOf(' when ') + $forIdx = $s.IndexOf(' for ') + + # Determine boundaries + $mainEnd = $s.Length + if ($whenIdx -ge 0 -and $forIdx -ge 0) { + $mainEnd = [Math]::Min($whenIdx, $forIdx) + } elseif ($whenIdx -ge 0) { + $mainEnd = $whenIdx + } elseif ($forIdx -ge 0) { + $mainEnd = $forIdx + } + + # Parse "for" fields + if ($forIdx -ge 0) { + $forEnd = $s.Length + if ($whenIdx -gt $forIdx) { $forEnd = $whenIdx } + $forPart = $s.Substring($forIdx + 5, $forEnd - $forIdx - 5).Trim() + $result.fields = @($forPart -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + + # Parse "when" filter + if ($whenIdx -ge 0) { + $whenEnd = $s.Length + if ($forIdx -gt $whenIdx) { $whenEnd = $forIdx } + $whenPart = $s.Substring($whenIdx + 6, $whenEnd - $whenIdx - 6).Trim() + $result.filter = Parse-FilterShorthand $whenPart + } + + # Parse main part: "Param = Value" + $mainPart = $s.Substring(0, $mainEnd).Trim() + $eqIdx = $mainPart.IndexOf('=') + if ($eqIdx -gt 0) { + $result.param = $mainPart.Substring(0, $eqIdx).Trim() + $result.value = $mainPart.Substring($eqIdx + 1).Trim() + } else { + $result.param = $mainPart + } + + return $result +} + +function Parse-StructureShorthand { + param([string]$s) + + $segments = $s -split '\s*>\s*' + $result = @() + + $innermost = $null + for ($i = $segments.Count - 1; $i -ge 0; $i--) { + $seg = $segments[$i].Trim() + $group = @{ type = "group" } + + if ($seg -match '^(?i)(details|детали)$') { + $group["groupBy"] = @() + } else { + $group["groupBy"] = @($seg) + } + + if ($null -ne $innermost) { + $group["children"] = @($innermost) + } + $innermost = $group + } + + if ($innermost) { $result += $innermost } + return ,$result +} + +function Parse-OutputParamShorthand { + param([string]$s) + $idx = $s.IndexOf('=') + if ($idx -gt 0) { + return @{ + key = $s.Substring(0, $idx).Trim() + value = $s.Substring($idx + 1).Trim() + } + } + return @{ key = $s.Trim(); value = "" } +} + +# --- 4. Build-* functions (XML fragment generators) --- + +function Build-ValueTypeXml { + param([string]$typeStr, [string]$indent) + + if (-not $typeStr) { return "" } + $typeStr = Resolve-TypeStr $typeStr + $lines = @() + + if ($typeStr -eq "boolean") { + $lines += "$indentxs:boolean" + return $lines -join "`r`n" + } + + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $lines += "$indentxs:string" + $lines += "$indent" + $lines += "$indent`t$len" + $lines += "$indent`tVariable" + $lines += "$indent" + return $lines -join "`r`n" + } + + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + $lines += "$indentxs:decimal" + $lines += "$indent" + $lines += "$indent`t$digits" + $lines += "$indent`t$fraction" + $lines += "$indent`t$sign" + $lines += "$indent" + return $lines -join "`r`n" + } + + if ($typeStr -match '^(date|dateTime)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + } + $lines += "$indentxs:dateTime" + $lines += "$indent" + $lines += "$indent`t$fractions" + $lines += "$indent" + return $lines -join "`r`n" + } + + if ($typeStr -eq "StandardPeriod") { + $lines += "$indentv8:StandardPeriod" + return $lines -join "`r`n" + } + + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + $lines += "$indentd5p1:$(Esc-Xml $typeStr)" + return $lines -join "`r`n" + } + + if ($typeStr.Contains('.')) { + $lines += "$indentd5p1:$(Esc-Xml $typeStr)" + return $lines -join "`r`n" + } + + $lines += "$indent$(Esc-Xml $typeStr)" + return $lines -join "`r`n" +} + +function Build-MLTextXml { + param([string]$tag, [string]$text, [string]$indent) + $lines = @() + $lines += "$indent<$tag xsi:type=`"v8:LocalStringType`">" + $lines += "$indent`t" + $lines += "$indent`t`tru" + $lines += "$indent`t`t$(Esc-Xml $text)" + $lines += "$indent`t" + $lines += "$indent" + return $lines -join "`r`n" +} + +function Build-RoleXml { + param([string[]]$roles, [string]$indent) + + if (-not $roles -or $roles.Count -eq 0) { return "" } + + $lines = @() + $lines += "$indent" + foreach ($role in $roles) { + if ($role -eq "period") { + $lines += "$indent`t1" + $lines += "$indent`tMain" + } else { + $lines += "$indent`ttrue" + } + } + $lines += "$indent" + return $lines -join "`r`n" +} + +function Build-RestrictionXml { + param([string[]]$restrict, [string]$indent) + + if (-not $restrict -or $restrict.Count -eq 0) { return "" } + + $restrictMap = @{ + "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" + "noGroup" = "group"; "noOrder" = "order" + } + + $lines = @() + $lines += "$indent" + foreach ($r in $restrict) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + $lines += "$indent`t<$xmlName>true" + } + } + $lines += "$indent" + return $lines -join "`r`n" +} + +function Build-FieldFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.dataPath)" + $lines += "$i`t$(Esc-Xml $parsed.field)" + + if ($parsed.title) { + $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") + } + + if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { + $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") + } + + $roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t" + if ($roleXml) { $lines += $roleXml } + + if ($parsed.type) { + $lines += "$i`t" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-TotalFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.dataPath)" + $lines += "$i`t$(Esc-Xml $parsed.expression)" + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-CalcFieldFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.dataPath)" + $lines += "$i`t$(Esc-Xml $parsed.expression)" + + if ($parsed.title) { + $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") + } + + if ($parsed.type) { + $lines += "$i`t" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-ParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $fragments = @() + + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.name)" + + if ($parsed.type) { + $lines += "$i`t" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t" + } + + if ($null -ne $parsed.value) { + $valStr = "$($parsed.value)" + if ($parsed.type -eq "StandardPeriod") { + $lines += "$i`t" + $lines += "$i`t`t$(Esc-Xml $valStr)" + $lines += "$i`t" + } elseif ($parsed.type -match '^date') { + $lines += "$i`t$(Esc-Xml $valStr)" + } elseif ($parsed.type -eq "boolean") { + $lines += "$i`t$(Esc-Xml $valStr)" + } elseif ($parsed.type -match '^decimal') { + $lines += "$i`t$(Esc-Xml $valStr)" + } else { + $lines += "$i`t$(Esc-Xml $valStr)" + } + } + + $lines += "$i" + $fragments += ($lines -join "`r`n") + + if ($parsed.autoDates) { + $paramName = $parsed.name + + $bLines = @() + $bLines += "$i" + $bLines += "$i`tДатаНачала" + $bLines += "$i`t" + $bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") + $bLines += "$i`t" + $bLines += "$i`t$(Esc-Xml "&$paramName.ДатаНачала")" + $bLines += "$i`tfalse" + $bLines += "$i" + $fragments += ($bLines -join "`r`n") + + $eLines = @() + $eLines += "$i" + $eLines += "$i`tДатаОкончания" + $eLines += "$i`t" + $eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") + $eLines += "$i`t" + $eLines += "$i`t$(Esc-Xml "&$paramName.ДатаОкончания")" + $eLines += "$i`tfalse" + $eLines += "$i" + $fragments += ($eLines -join "`r`n") + } + + return ,$fragments +} + +function Build-FilterItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + + if ($parsed.use -eq $false) { + $lines += "$i`tfalse" + } + + $lines += "$i`t$(Esc-Xml $parsed.field)" + $lines += "$i`t$(Esc-Xml $parsed.op)" + + if ($null -ne $parsed.value) { + $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } + $lines += "$i`t$(Esc-Xml "$($parsed.value)")" + } + + if ($parsed.viewMode) { + $lines += "$i`t$(Esc-Xml $parsed.viewMode)" + } + + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + $lines += "$i`t$(Esc-Xml $uid)" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-SelectionItemFragment { + param([string]$fieldName, [string]$indent) + + $i = $indent + $lines = @() + if ($fieldName -eq "Auto") { + $lines += "$i" + } else { + $lines += "$i" + $lines += "$i`t$(Esc-Xml $fieldName)" + $lines += "$i" + } + return $lines -join "`r`n" +} + +function Build-DataParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + + if ($parsed.use -eq $false) { + $lines += "$i`tfalse" + } + + $lines += "$i`t$(Esc-Xml $parsed.parameter)" + + if ($null -ne $parsed.value) { + if ($parsed.value -is [hashtable] -and $parsed.value.variant) { + $lines += "$i`t" + $lines += "$i`t`t$(Esc-Xml $parsed.value.variant)" + $lines += "$i`t" + } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { + $lines += "$i`t$(Esc-Xml "$($parsed.value)")" + } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { + $lines += "$i`t$(Esc-Xml "$($parsed.value)")" + } else { + $lines += "$i`t$(Esc-Xml "$($parsed.value)")" + } + } + + if ($parsed.viewMode) { + $lines += "$i`t$(Esc-Xml $parsed.viewMode)" + } + + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + $lines += "$i`t$(Esc-Xml $uid)" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-OrderItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + if ($parsed.field -eq "Auto") { + $lines += "$i" + } else { + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.field)" + $lines += "$i`t$($parsed.direction)" + $lines += "$i" + } + return $lines -join "`r`n" +} + +function Build-DataSetLinkFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.source)" + $lines += "$i`t$(Esc-Xml $parsed.dest)" + $lines += "$i`t$(Esc-Xml $parsed.sourceExpr)" + $lines += "$i`t$(Esc-Xml $parsed.destExpr)" + if ($parsed.parameter) { + $lines += "$i`t$(Esc-Xml $parsed.parameter)" + } + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-DataSetQueryFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.name)" + $lines += "$i`t$(Esc-Xml $parsed.dataSource)" + $lines += "$i`t$(Esc-Xml $parsed.query)" + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-VariantFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.name)" + $lines += (Build-MLTextXml -tag "dcsset:presentation" -text $parsed.presentation -indent "$i`t") + $lines += "$i`t" + $lines += "$i`t`t" + $lines += "$i`t`t`t" + $lines += "$i`t`t" + $lines += "$i`t`t" + $lines += "$i`t`t`t" + $lines += "$i`t`t`t" + $lines += "$i`t`t`t`t" + $lines += "$i`t`t`t" + $lines += "$i`t`t`t" + $lines += "$i`t`t`t`t" + $lines += "$i`t`t`t" + $lines += "$i`t`t" + $lines += "$i`t" + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-ConditionalAppearanceItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + + # selection + if ($parsed.fields -and $parsed.fields.Count -gt 0) { + $lines += "$i`t" + foreach ($fld in $parsed.fields) { + $lines += "$i`t`t" + $lines += "$i`t`t`t$(Esc-Xml $fld)" + $lines += "$i`t`t" + } + $lines += "$i`t" + } else { + $lines += "$i`t" + } + + # filter + if ($parsed.filter) { + $lines += "$i`t" + $f = $parsed.filter + $lines += "$i`t`t" + $lines += "$i`t`t`t$(Esc-Xml $f.field)" + $lines += "$i`t`t`t$(Esc-Xml $f.op)" + if ($null -ne $f.value) { + $vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" } + $lines += "$i`t`t`t$(Esc-Xml "$($f.value)")" + } + $lines += "$i`t`t" + $lines += "$i`t" + } else { + $lines += "$i`t" + } + + # appearance + $lines += "$i`t" + + # Auto-detect value type + $val = $parsed.value + $valType = "xs:string" + if ($val -match '^(web|style|win):') { + $valType = "v8ui:Color" + } elseif ($val -eq "true" -or $val -eq "false") { + $valType = "xs:boolean" + } + + $lines += "$i`t`t" + $lines += "$i`t`t`t$(Esc-Xml $parsed.param)" + $lines += "$i`t`t`t$(Esc-Xml $val)" + $lines += "$i`t`t" + $lines += "$i`t" + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-StructureItemFragment { + param($item, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + + # groupItems + $groupBy = $item["groupBy"] + if (-not $groupBy -or $groupBy.Count -eq 0) { + $lines += "$i`t" + } else { + $lines += "$i`t" + foreach ($field in $groupBy) { + $lines += "$i`t`t" + $lines += "$i`t`t`t$(Esc-Xml $field)" + $lines += "$i`t`t`tItems" + $lines += "$i`t`t`tNone" + $lines += "$i`t`t`t0001-01-01T00:00:00" + $lines += "$i`t`t`t0001-01-01T00:00:00" + $lines += "$i`t`t" + } + $lines += "$i`t" + } + + # order (Auto) + $lines += "$i`t" + $lines += "$i`t`t" + $lines += "$i`t" + + # selection (Auto) + $lines += "$i`t" + $lines += "$i`t`t" + $lines += "$i`t" + + # Recursive children + if ($item["children"]) { + foreach ($child in $item["children"]) { + $childXml = Build-StructureItemFragment -item $child -indent "$i`t" + $lines += $childXml + } + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-OutputParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $key = $parsed.key + $val = $parsed.value + $ptype = $script:outputParamTypes[$key] + if (-not $ptype) { $ptype = "xs:string" } + + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $key)" + + if ($ptype -eq "mltext") { + $lines += "$i`t" + $lines += "$i`t`t" + $lines += "$i`t`t`tru" + $lines += "$i`t`t`t$(Esc-Xml $val)" + $lines += "$i`t`t" + $lines += "$i`t" + } else { + $lines += "$i`t$(Esc-Xml $val)" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +# --- 5. XML helpers --- + +function Import-Fragment($doc, [string]$xmlString) { + $wrapper = @" +<_W xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:v8="http://v8.1c.ru/8.1/data/core" + xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" + xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" + xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" + xmlns:v8ui="http://v8.1c.ru/8.1/data/ui">$xmlString +"@ + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $doc.ImportNode($child, $true) + } + } + return ,$nodes +} + +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + $text = $child.Value + if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($text -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0 + $current = $container + while ($current -and $current -ne $xmlDoc.DocumentElement) { + $depth++ + $current = $current.ParentNode + } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Clear-ContainerChildren($container) { + $toRemove = @() + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $toRemove += $child + } + } + foreach ($el in $toRemove) { + Remove-NodeWithWhitespace $el + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Find-FirstElement($container, [string[]]$localNames, [string]$nsUri) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element') { + foreach ($name in $localNames) { + if ($child.LocalName -eq $name) { + if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { + return $child + } + } + } + } + } + return $null +} + +function Find-LastElement($container, [string]$localName, [string]$nsUri) { + $last = $null + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { + if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { + $last = $child + } + } + } + return $last +} + +function Find-ElementByChildValue($container, [string]$elemName, [string]$childName, [string]$childValue, [string]$nsUri) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -ne $elemName) { continue } + if ($nsUri -and $child.NamespaceURI -ne $nsUri) { continue } + + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $childName -and $gc.InnerText.Trim() -eq $childValue) { + return $child + } + } + } + return $null +} + +function Set-OrCreateChildElement($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$indent) { + $existing = $null + foreach ($ch in $parent.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { + $existing = $ch + break + } + } + if ($existing) { + $existing.InnerText = $value + } else { + $prefix = $parent.GetPrefixOfNamespace($nsUri) + $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } + $fragXml = "$indent<$qualName>$(Esc-Xml $value)" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $parent $node $null $indent + } + } +} + +function Set-OrCreateChildElementWithAttr($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$xsiType, [string]$indent) { + $existing = $null + foreach ($ch in $parent.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { + $existing = $ch + break + } + } + if ($existing) { + $existing.InnerText = $value + if ($xsiType) { + $existing.SetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance", $xsiType) | Out-Null + } + } else { + $prefix = $parent.GetPrefixOfNamespace($nsUri) + $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } + $typeAttr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } + $fragXml = "$indent<$qualName$typeAttr>$(Esc-Xml $value)" + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $parent $node $null $indent + } + } +} + +function Resolve-DataSet { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $root = $xmlDoc.DocumentElement + + if ($DataSet) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $DataSet) { + return $child + } + } + } + Write-Error "DataSet '$DataSet' not found" + exit 1 + } + + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + return $child + } + } + Write-Error "No dataSet found in DCS" + exit 1 +} + +function Resolve-VariantSettings { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $root = $xmlDoc.DocumentElement + + $sv = $null + if ($Variant) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $Variant) { + $sv = $child + break + } + } + } + if (-not $sv) { + Write-Error "Variant '$Variant' not found" + exit 1 + } + } else { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $sv = $child + break + } + } + if (-not $sv) { + Write-Error "No settingsVariant found in DCS" + exit 1 + } + } + + foreach ($gc in $sv.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'settings' -and $gc.NamespaceURI -eq $setNs) { + return $gc + } + } + + Write-Error "No found in variant" + exit 1 +} + +function Ensure-SettingsChild($settings, [string]$childName, [string[]]$afterSiblings) { + $el = Find-FirstElement $settings @($childName) $setNs + if ($el) { return $el } + + $indent = Get-ChildIndent $settings + $fragXml = "$indent" + $nodes = Import-Fragment $xmlDoc $fragXml + + $refNode = $null + foreach ($sibName in $afterSiblings) { + $sib = Find-FirstElement $settings @($sibName) $setNs + if ($sib) { + $refNode = $sib.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + break + } + } + + foreach ($node in $nodes) { + Insert-BeforeElement $settings $node $refNode $indent + } + + return Find-FirstElement $settings @($childName) $setNs +} + +function Get-VariantName { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $root = $xmlDoc.DocumentElement + + if ($Variant) { return $Variant } + + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + return $gc.InnerText + } + } + } + } + return "(unknown)" +} + +function Get-DataSetName($dsNode) { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + foreach ($gc in $dsNode.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + return $gc.InnerText + } + } + return "(unknown)" +} + +function Get-ContainerChildIndent($container) { + $hasElements = $false + foreach ($ch in $container.ChildNodes) { + if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } + } + if ($hasElements) { + return Get-ChildIndent $container + } else { + $parentIndent = Get-ChildIndent $container.ParentNode + return $parentIndent + "`t" + } +} + +# --- 6. Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +$xmlDoc.Load($resolvedPath) + +$schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" +$setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" +$corNs = "http://v8.1c.ru/8.1/data-composition-system/core" + +# --- 7. Batch value splitting --- + +if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "add-dataSet") { + $values = @($Value) +} else { + $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) +} + +# --- 8. Main logic --- + +switch ($Operation) { + "add-field" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + $parsed = Parse-FieldShorthand $val + $childIndent = Get-ChildIndent $dsNode + + # Duplicate check + $existing = Find-ElementByChildValue $dsNode "field" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] Field `"$($parsed.dataPath)`" already exists in dataset `"$dsName`" — skipped" + continue + } + + $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $refNode = Find-FirstElement $dsNode @("dataSource") $schNs + foreach ($node in $nodes) { + Insert-BeforeElement $dsNode $node $refNode $childIndent + } + + Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`"" + + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Ensure-SettingsChild $settings "selection" @() + $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs + if ($existingSel) { + Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" + } else { + $selIndent = Get-ContainerChildIndent $selection + $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" + } + } + } + } + + "add-total" { + foreach ($val in $values) { + $parsed = Parse-TotalShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "totalField" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] TotalField `"$($parsed.dataPath)`" already exists — skipped" + continue + } + + $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $root = $xmlDoc.DocumentElement + $lastTotal = Find-LastElement $root "totalField" $schNs + if ($lastTotal) { + $refNode = $lastTotal.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added" + } + } + + "add-calculated-field" { + foreach ($val in $values) { + $parsed = Parse-CalcShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "calculatedField" "dataPath" $parsed.dataPath $schNs + if ($existing) { + Write-Host "[WARN] CalculatedField `"$($parsed.dataPath)`" already exists — skipped" + continue + } + + $fragXml = Build-CalcFieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $root = $xmlDoc.DocumentElement + $lastCalc = Find-LastElement $root "calculatedField" $schNs + if ($lastCalc) { + $refNode = $lastCalc.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added" + + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Ensure-SettingsChild $settings "selection" @() + $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs + if ($existingSel) { + Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" + } else { + $selIndent = Get-ContainerChildIndent $selection + $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" + } + } + } + } + + "add-parameter" { + foreach ($val in $values) { + $parsed = Parse-ParamShorthand $val + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + # Duplicate check + $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $parsed.name $schNs + if ($existing) { + Write-Host "[WARN] Parameter `"$($parsed.name)`" already exists — skipped" + continue + } + + $fragments = Build-ParamFragment -parsed $parsed -indent $childIndent + + $root = $xmlDoc.DocumentElement + $lastParam = Find-LastElement $root "parameter" $schNs + if ($lastParam) { + $refNode = $lastParam.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("template","groupTemplate","settingsVariant") $schNs + } + + foreach ($fragXml in $fragments) { + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + } + + Write-Host "[OK] Parameter `"$($parsed.name)`" added" + if ($parsed.autoDates) { + Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added" + } + } + } + + "add-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-FilterShorthand $val + + $filterEl = Ensure-SettingsChild $settings "filter" @("selection") + $filterIndent = Get-ContainerChildIndent $filterEl + + $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $filterEl $node $null $filterIndent + } + + Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`"" + } + } + + "add-dataParameter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-DataParamShorthand $val + + $dpEl = Ensure-SettingsChild $settings "dataParameters" @("outputParameters","conditionalAppearance","order","filter","selection") + $dpIndent = Get-ContainerChildIndent $dpEl + + $fragXml = Build-DataParamFragment -parsed $parsed -indent $dpIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $dpEl $node $null $dpIndent + } + + Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`"" + } + } + + "add-order" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-OrderShorthand $val + + $orderEl = Ensure-SettingsChild $settings "order" @("filter","selection") + $orderIndent = Get-ContainerChildIndent $orderEl + + # Duplicate check + if ($parsed.field -eq "Auto") { + $isDup = $false + foreach ($ch in $orderEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { + $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($typeAttr -and $typeAttr.Contains("OrderItemAuto")) { $isDup = $true; break } + } + } + if ($isDup) { + Write-Host "[WARN] OrderItemAuto already exists in variant `"$varName`" — skipped" + continue + } + } else { + $existingOrd = Find-ElementByChildValue $orderEl "item" "field" $parsed.field $setNs + if ($existingOrd) { + Write-Host "[WARN] Order `"$($parsed.field)`" already exists in variant `"$varName`" — skipped" + continue + } + } + + $fragXml = Build-OrderItemFragment -parsed $parsed -indent $orderIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $orderEl $node $null $orderIndent + } + + $desc = if ($parsed.field -eq "Auto") { "Auto" } else { "$($parsed.field) $($parsed.direction)" } + Write-Host "[OK] Order `"$desc`" added to variant `"$varName`"" + } + } + + "add-selection" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $fieldName = $val.Trim() + + $selection = Ensure-SettingsChild $settings "selection" @() + $selIndent = Get-ContainerChildIndent $selection + + $selXml = Build-SelectionItemFragment -fieldName $fieldName -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + + Write-Host "[OK] Selection `"$fieldName`" added to variant `"$varName`"" + } + } + + "set-query" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + $queryEl = Find-FirstElement $dsNode @("query") $schNs + if (-not $queryEl) { + Write-Error "No element found in dataset '$dsName'" + exit 1 + } + + # InnerText setter handles XML escaping automatically + $queryEl.InnerText = $Value + + Write-Host "[OK] Query replaced in dataset `"$dsName`"" + } + + "set-outputParameter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-OutputParamShorthand $val + + $outputEl = Ensure-SettingsChild $settings "outputParameters" @("conditionalAppearance","order","filter","selection") + $outputIndent = Get-ContainerChildIndent $outputEl + + # Remove existing parameter with same key if present + $existingParam = Find-ElementByChildValue $outputEl "item" "parameter" $parsed.key $corNs + if ($existingParam) { + Remove-NodeWithWhitespace $existingParam + Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`"" + } else { + Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`"" + } + + $fragXml = Build-OutputParamFragment -parsed $parsed -indent $outputIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $outputEl $node $null $outputIndent + } + } + } + + "set-structure" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + # Remove all existing structure items (dcsset:item elements) + $toRemove = @() + foreach ($ch in $settings.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item' -and $ch.NamespaceURI -eq $setNs) { + $toRemove += $ch + } + } + foreach ($el in $toRemove) { + Remove-NodeWithWhitespace $el + } + + # Parse structure shorthand + $structItems = Parse-StructureShorthand $Value + $settingsIndent = Get-ChildIndent $settings + + # Find insertion point — before outputParameters/dataParameters/conditionalAppearance/order/filter/selection or at end + $refNode = Find-FirstElement $settings @("outputParameters","dataParameters","conditionalAppearance","order","filter","selection","item") $setNs + if (-not $refNode) { $refNode = $null } + + foreach ($structItem in $structItems) { + $fragXml = Build-StructureItemFragment -item $structItem -indent $settingsIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $settings $node $refNode $settingsIndent + } + } + + Write-Host "[OK] Structure set in variant `"$varName`": $Value" + } + + "add-dataSetLink" { + foreach ($val in $values) { + $parsed = Parse-DataSetLinkShorthand $val + $root = $xmlDoc.DocumentElement + $childIndent = Get-ChildIndent $root + + $fragXml = Build-DataSetLinkFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last dataSetLink, or before calculatedField/totalField/parameter/... + $lastLink = Find-LastElement $root "dataSetLink" $schNs + if ($lastLink) { + $refNode = $lastLink.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + $desc = "$($parsed.source) > $($parsed.dest) on $($parsed.sourceExpr) = $($parsed.destExpr)" + if ($parsed.parameter) { $desc += " [param $($parsed.parameter)]" } + Write-Host "[OK] DataSetLink `"$desc`" added" + } + } + + "add-dataSet" { + $root = $xmlDoc.DocumentElement + $childIndent = Get-ChildIndent $root + + $parsed = Parse-DataSetShorthand $Value + + # Auto-name if empty + if (-not $parsed.name) { + $count = 0 + foreach ($ch in $root.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'dataSet' -and $ch.NamespaceURI -eq $schNs) { $count++ } + } + $parsed.name = "НаборДанных$($count + 1)" + } + + # Duplicate check + $existing = Find-ElementByChildValue $root "dataSet" "name" $parsed.name $schNs + if ($existing) { + Write-Host "[WARN] DataSet `"$($parsed.name)`" already exists — skipped" + } else { + # Get dataSource name from first existing + $dsSourceEl = Find-FirstElement $root @("dataSource") $schNs + $dsSourceName = "ИсточникДанных1" + if ($dsSourceEl) { + $nameEl = Find-FirstElement $dsSourceEl @("name") $schNs + if ($nameEl) { $dsSourceName = $nameEl.InnerText.Trim() } + } + $parsed["dataSource"] = $dsSourceName + + $fragXml = Build-DataSetQueryFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last , or after if none + $lastDS = Find-LastElement $root "dataSet" $schNs + if ($lastDS) { + $refNode = $lastDS.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("dataSetLink","calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)" + } + } + + "add-variant" { + $root = $xmlDoc.DocumentElement + $childIndent = Get-ChildIndent $root + + foreach ($val in $values) { + $parsed = Parse-VariantShorthand $val + + # Duplicate check — search for settingsVariant with matching dcsset:name + $isDup = $false + foreach ($ch in $root.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'settingsVariant' -and $ch.NamespaceURI -eq $schNs) { + foreach ($gc in $ch.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs -and $gc.InnerText -eq $parsed.name) { + $isDup = $true; break + } + } + if ($isDup) { break } + } + } + if ($isDup) { + Write-Host "[WARN] Variant `"$($parsed.name)`" already exists — skipped" + continue + } + + $fragXml = Build-VariantFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last + $lastSV = Find-LastElement $root "settingsVariant" $schNs + if ($lastSV) { + $refNode = $lastSV.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = $null + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added" + } + } + + "add-conditionalAppearance" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-ConditionalAppearanceShorthand $val + + $caEl = Ensure-SettingsChild $settings "conditionalAppearance" @("outputParameters","order","filter","selection") + $caIndent = Get-ContainerChildIndent $caEl + + $fragXml = Build-ConditionalAppearanceItemFragment -parsed $parsed -indent $caIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $caEl $node $null $caIndent + } + + $desc = "$($parsed.param) = $($parsed.value)" + if ($parsed.filter) { $desc += " when $($parsed.filter.field) $($parsed.filter.op)" } + if ($parsed.fields -and $parsed.fields.Count -gt 0) { $desc += " for $($parsed.fields -join ', ')" } + Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`"" + } + } + + "clear-selection" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + Clear-ContainerChildren $selection + Write-Host "[OK] Selection cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No selection section in variant `"$varName`"" + } + } + + "clear-order" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $orderEl = Find-FirstElement $settings @("order") $setNs + if ($orderEl) { + Clear-ContainerChildren $orderEl + Write-Host "[OK] Order cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No order section in variant `"$varName`"" + } + } + + "clear-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $filterEl = Find-FirstElement $settings @("filter") $setNs + if ($filterEl) { + Clear-ContainerChildren $filterEl + Write-Host "[OK] Filter cleared in variant `"$varName`"" + } else { + Write-Host "[INFO] No filter section in variant `"$varName`"" + } + } + + "modify-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-FilterShorthand $val + + $filterEl = Find-FirstElement $settings @("filter") $setNs + if (-not $filterEl) { + Write-Host "[WARN] No filter section in variant `"$varName`"" + continue + } + + $filterItem = Find-ElementByChildValue $filterEl "item" "left" $parsed.field $setNs + if (-not $filterItem) { + Write-Host "[WARN] Filter for `"$($parsed.field)`" not found in variant `"$varName`"" + continue + } + + $itemIndent = Get-ChildIndent $filterItem + + # Update comparisonType + Set-OrCreateChildElement $filterItem "comparisonType" $setNs $parsed.op $itemIndent + + # Update right value + if ($null -ne $parsed.value) { + $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } + Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent + } + + # Update use + if ($parsed.use -eq $false) { + Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent + } else { + # If explicitly not @off, remove use=false if exists + $useEl = $null + foreach ($ch in $filterItem.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) { + $useEl = $ch; break + } + } + if ($useEl -and $useEl.InnerText -eq 'false') { + Remove-NodeWithWhitespace $useEl + } + } + + # Update viewMode + if ($parsed.viewMode) { + Set-OrCreateChildElement $filterItem "viewMode" $setNs $parsed.viewMode $itemIndent + } + + # Update userSettingID + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + Set-OrCreateChildElement $filterItem "userSettingID" $setNs $uid $itemIndent + } + + Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`"" + } + } + + "modify-dataParameter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $parsed = Parse-DataParamShorthand $val + + $dpEl = Find-FirstElement $settings @("dataParameters") $setNs + if (-not $dpEl) { + Write-Host "[WARN] No dataParameters section in variant `"$varName`"" + continue + } + + $dpItem = Find-ElementByChildValue $dpEl "item" "parameter" $parsed.parameter $corNs + if (-not $dpItem) { + Write-Host "[WARN] DataParameter `"$($parsed.parameter)`" not found in variant `"$varName`"" + continue + } + + $itemIndent = Get-ChildIndent $dpItem + + # Update value + if ($null -ne $parsed.value) { + # Remove existing value element first + $existingVal = $null + foreach ($ch in $dpItem.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $corNs) { + $existingVal = $ch; break + } + } + if ($existingVal) { + Remove-NodeWithWhitespace $existingVal + } + + # Build new value fragment + $valLines = @() + if ($parsed.value -is [hashtable] -and $parsed.value.variant) { + $valLines += "$itemIndent" + $valLines += "$itemIndent`t$(Esc-Xml $parsed.value.variant)" + $valLines += "$itemIndent" + } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { + $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" + } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { + $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" + } else { + $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" + } + $valXml = $valLines -join "`r`n" + $valNodes = Import-Fragment $xmlDoc $valXml + foreach ($node in $valNodes) { + Insert-BeforeElement $dpItem $node $null $itemIndent + } + } + + # Update use + if ($parsed.use -eq $false) { + Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent + } else { + $useEl = $null + foreach ($ch in $dpItem.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) { + $useEl = $ch; break + } + } + if ($useEl -and $useEl.InnerText -eq 'false') { + Remove-NodeWithWhitespace $useEl + } + } + + # Update viewMode + if ($parsed.viewMode) { + Set-OrCreateChildElement $dpItem "viewMode" $setNs $parsed.viewMode $itemIndent + } + + # Update userSettingID + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + Set-OrCreateChildElement $dpItem "userSettingID" $setNs $uid $itemIndent + } + + Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`"" + } + } + + "modify-field" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + $parsed = Parse-FieldShorthand $val + $fieldName = $parsed.dataPath + + # Find existing field + $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs + if (-not $fieldEl) { + Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" + continue + } + + # Read existing properties + $existing = Read-FieldProperties $fieldEl + + # Merge: parsed overrides existing for non-empty values + $merged = @{ + dataPath = $existing.dataPath + field = $existing.field + title = if ($parsed.title) { $parsed.title } else { $existing.title } + type = if ($parsed.type) { $parsed.type } else { $existing.type } + roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles } + restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict } + } + + # Remember position (NextSibling after whitespace) + $nextSib = $fieldEl.NextSibling + while ($nextSib -and ($nextSib.NodeType -eq 'Whitespace' -or $nextSib.NodeType -eq 'SignificantWhitespace')) { + $nextSib = $nextSib.NextSibling + } + + # Remove old field + $childIndent = Get-ChildIndent $dsNode + Remove-NodeWithWhitespace $fieldEl + + # Build new field fragment with merged data + $fragXml = Build-FieldFragment -parsed $merged -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert at saved position + foreach ($node in $nodes) { + Insert-BeforeElement $dsNode $node $nextSib $childIndent + } + + Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`"" + } + } + + "remove-field" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + foreach ($val in $values) { + $fieldName = $val.Trim() + + $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs + if (-not $fieldEl) { + Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" + continue + } + + Remove-NodeWithWhitespace $fieldEl + Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`"" + + # Also remove from selection in variant + try { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + $selItem = Find-ElementByChildValue $selection "item" "field" $fieldName $setNs + if ($selItem) { + Remove-NodeWithWhitespace $selItem + Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`"" + } + } + } catch { + # No variant — that's fine + } + } + } + + "remove-total" { + foreach ($val in $values) { + $dataPath = $val.Trim() + $root = $xmlDoc.DocumentElement + + $totalEl = Find-ElementByChildValue $root "totalField" "dataPath" $dataPath $schNs + if (-not $totalEl) { + Write-Host "[WARN] TotalField `"$dataPath`" not found" + continue + } + + Remove-NodeWithWhitespace $totalEl + Write-Host "[OK] TotalField `"$dataPath`" removed" + } + } + + "remove-calculated-field" { + foreach ($val in $values) { + $dataPath = $val.Trim() + $root = $xmlDoc.DocumentElement + + $calcEl = Find-ElementByChildValue $root "calculatedField" "dataPath" $dataPath $schNs + if (-not $calcEl) { + Write-Host "[WARN] CalculatedField `"$dataPath`" not found" + continue + } + + Remove-NodeWithWhitespace $calcEl + Write-Host "[OK] CalculatedField `"$dataPath`" removed" + + # Also remove from selection + try { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + $selItem = Find-ElementByChildValue $selection "item" "field" $dataPath $setNs + if ($selItem) { + Remove-NodeWithWhitespace $selItem + Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`"" + } + } + } catch { } + } + } + + "remove-parameter" { + foreach ($val in $values) { + $paramName = $val.Trim() + $root = $xmlDoc.DocumentElement + + $paramEl = Find-ElementByChildValue $root "parameter" "name" $paramName $schNs + if (-not $paramEl) { + Write-Host "[WARN] Parameter `"$paramName`" not found" + continue + } + + Remove-NodeWithWhitespace $paramEl + Write-Host "[OK] Parameter `"$paramName`" removed" + } + } + + "remove-filter" { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + foreach ($val in $values) { + $fieldName = $val.Trim() + + $filterEl = Find-FirstElement $settings @("filter") $setNs + if (-not $filterEl) { + Write-Host "[WARN] No filter section in variant `"$varName`"" + continue + } + + $filterItem = Find-ElementByChildValue $filterEl "item" "left" $fieldName $setNs + if (-not $filterItem) { + Write-Host "[WARN] Filter for `"$fieldName`" not found in variant `"$varName`"" + continue + } + + Remove-NodeWithWhitespace $filterItem + Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`"" + } + } +} + +# --- 9. Save --- + +$content = $xmlDoc.OuterXml +$content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8' +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $content, $enc) + +Write-Host "[OK] Saved $resolvedPath" diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md new file mode 100644 index 00000000..4acc6019 --- /dev/null +++ b/.claude/skills/skd-info/SKILL.md @@ -0,0 +1,74 @@ +--- +name: skd-info +description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты +argument-hint: [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace] [-Name ] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /skd-info — Анализ схемы компоновки данных + +Читает Template.xml схемы компоновки данных (СКД) и выводит компактную сводку. Заменяет необходимость читать тысячи строк XML. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) | +| `Mode` | Режим анализа (по умолчанию `overview`) | +| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) | +| `Batch` | Номер пакета запроса, 0 = все (только query) | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -TemplatePath "<путь>" +``` + +С указанием режима: +```powershell +... -Mode query -Name НоменклатураСЦенами +... -Mode query -Name ДанныеТ13 -Batch 3 +... -Mode fields -Name КадастроваяСтоимость +... -Mode calculated -Name КоэффициентКи +... -Mode resources -Name СуммаНалога +... -Mode trace -Name "Коэффициент Ки" +... -Mode variant -Name 1 +... -Mode templates +... -Mode templates -Name ВидНалоговойБазы +``` + +## Режимы + +| Режим | Без `-Name` | С `-Name` | +|-------|-------------|-----------| +| `overview` | Навигационная карта схемы + подсказки Next | — | +| `query` | — | Текст запроса набора (с оглавлением батчей) | +| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат | +| `links` | Все связи наборов | — | +| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения | +| `resources` | Карта: имена ресурсов (`*` = групповые формулы) | Формулы агрегации по группировкам | +| `params` | Таблица параметров: тип, значение, видимость | — | +| `variant` | Список вариантов | Структура группировок + фильтры + вывод | +| `templates` | Карта привязок шаблонов (field/group) | Содержимое шаблона: строки, ячейки, выражения | +| `trace` | — | Полная цепочка: набор → вычисление → ресурс | + +Паттерн: без `-Name` — карта/индекс, с `-Name` — деталь конкретного элемента. + +## Типичный workflow + +1. `overview` — понять структуру, увидеть подсказки +2. `trace -Name <поле>` — узнать как считается колонка отчёта (от заголовка до запроса за один вызов) +3. `query -Name <набор>` — посмотреть текст SQL-запроса +4. `variant -Name ` — посмотреть группировки и фильтры варианта + +Подробные примеры вывода каждого режима — в `modes-reference.md`. + +## Верификация + +``` +/skd-info — overview (точка входа) +/skd-info -Mode trace -Name — трассировка поля +``` diff --git a/.claude/skills/skd-info/modes-reference.md b/.claude/skills/skd-info/modes-reference.md new file mode 100644 index 00000000..eb6bd983 --- /dev/null +++ b/.claude/skills/skd-info/modes-reference.md @@ -0,0 +1,246 @@ +# /skd-info — полная справка по режимам + +Компактное описание — в [SKILL.md](SKILL.md). + +## overview (по умолчанию) — карта схемы + +Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги: + +``` +=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) === + +Sources: ИсточникДанных1 (Local) + +Datasets: + [Query] НоменклатураСЦенами 7 fields, query 40 lines +Calculated: 1 +Resources: 1 +Templates: 1 templates, 1 group bindings +Params: (none) + +Variants: + [1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters + [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters + +Next: + -Mode query query text + -Mode fields field tables by dataset + -Mode calculated calculated field expressions + -Mode resources resource aggregation + -Mode variant -Name variant structure (1..2) +``` + +Для DataSetUnion — дерево наборов + связи: +``` +Datasets: + [Union] РасчетНалогаНаИмущество 52 fields + ├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines + ├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines + ├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines +Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields) +``` + +Параметры разделяются на видимые/скрытые: +``` +Params: 18 (7 visible, 11 hidden): Период, Ответственный, ... +``` + +## query — текст запроса + +`-Name <набор>` — имя DataSet (обязателен если наборов > 1). + +Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей: + +``` +=== Query: ДанныеТ13 (334 lines, 13 batches) === + Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды + Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации + ... +--- Batch 1 --- +ВЫБРАТЬ + ДАТАВРЕМЯ(1, 1, 1) КАК Период +ПОМЕСТИТЬ Представления_Периоды +... +``` + +Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет. + +## fields — поля наборов данных + +Без `-Name` — карта: имена полей по наборам: +``` +=== Fields map === +СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния +РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ... + РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ... +``` + +С `-Name <поле>` — детали конкретного поля: +``` +=== Field: ДатаСостояния "Дата ввода в эксплуатацию" === + +Dataset: СостояниеОС [Query] +Format: ДФ=dd.MM.yyyy +``` + +Показывает: dataset, title, type, role, useRestriction, format, presentationExpression. + +## links — связи наборов данных + +``` +=== Links (4) === + +РасчетНалогаНаИмущество -> СостояниеОС : + Организация -> Организация + ОсновноеСредство -> ОсновноеСредство +``` + +Группирует по парам наборов. Показывает поля связи и параметры. + +## calculated — вычисляемые поля + +Без `-Name` — карта: имена и заголовки: +``` +=== Calculated fields (23) === + ДоляСтоимости "Доля стоимости" + КоэффициентКи "Коэффициент Ки" + ... +``` + +С `-Name <поле>` — полное выражение: +``` +=== Calculated: ДоляСтоимости === + +Expression: + ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ +Title: Доля стоимости +Restrict: condition +``` + +## resources — ресурсы (итоги по группировкам) + +Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам: +``` +=== Resources (51) === + НалоговаяБаза + КоэффициентКи * + ... + * = has group-level formulas +``` + +С `-Name <поле>` — формулы агрегации: +``` +=== Resource: ДатаСостояния === + + [ОсновноеСредство] ЕстьNull(ДатаСостояния, "") +``` + +## params — параметры схемы + +``` +=== Parameters (16) === + Name Type Default Visible Expression + Период StandardPeriod LastMonth yes - + НачалоПериода DateTime - hidden &Период.ДатаНачала + Организация CatalogRef.Организации null yes - +``` + +## variant — варианты отчёта + +Без `-Name` — список вариантов: +``` +=== Variants (2) === + [1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters + [2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters +``` + +С `-Name ` — структура конкретного варианта: +``` +=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" === + +Structure: + Table "Таблица" + ├── Columns: [ТипЦен Items] + │ Selection: Auto, Цена + └── Rows: [Номенклатура Items] + Selection: Номенклатура, УИД, Auto + +Filter: + [ ] Номенклатура InHierarchy [user] + [ ] ТипЦен Equal + [x] ВАрхиве = false "Исключая скрытые товары" + +DataParams: КлючВарианта="НоменклатураИЦены" +Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None +``` + +## templates — привязки шаблонов вывода + +Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы). + +Без `-Name` — карта привязок: +``` +=== Templates (70 defined: 49 field, 37 group) === + +Field bindings (49): (all trivial) + ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ... + +Group bindings (37): + ВидНалоговойБазы + Header -> Макет3 (1 rows, 1 params) + СреднегодоваяСтоимость2019 + Footer -> Макет50 (1 rows) spacer + GroupHeader -> Макет40 (3 rows) +``` + +С `-Name <группировка|поле>` — содержимое шаблонов: +``` +=== Templates: СреднегодоваяСтоимость2019 === + +Footer -> Макет50 [1 rows, 1 cells]: + Row 1: (empty) + +GroupHeader -> Макет40 [3 rows, 78 cells]: + Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ... + Row 2: "01.01" | "01.02" | ... | "31.12" + Row 3: "1" | "2" | ... | "26" +``` + +Для field-привязок: +``` +=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 === +[1 rows, 1 cells] + Row 1: {ОстаточнаяСтоимостьНа0101} + (all params trivial) +``` + +**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д. + +## trace — трассировка поля от заголовка до запроса + +Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов: + +``` +=== Trace: КоэффициентКи "Коэффициент Ки" === + +Dataset: (schema-level only, not in dataset fields) + +Calculated: + ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ + Operands: + КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query] + КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query] + +Resource: + [ОсновноеСредство] Сумма(КоэффициентКи) +``` + +Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс. + +## Что не выводится + +- XML namespace-декларации +- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст) +- userSettingID (GUID-ы пользовательских настроек) +- Дефолтные periodAdditionBegin/End = 0001-01-01 +- viewMode diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 new file mode 100644 index 00000000..6dfc8a60 --- /dev/null +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -0,0 +1,1792 @@ +# skd-info v1.0 — Analyze 1C DCS structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)] + [string]$TemplatePath, + [ValidateSet("overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates")] + [string]$Mode = "overview", + [string]$Name, + [int]$Batch = 0, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path + +# --- Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load($resolvedPath) + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") +$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") +$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") +$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") + +$root = $xmlDoc.DocumentElement + +# --- Helpers --- + +function Get-MLText($node) { + if (-not $node) { return "" } + $content = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($content) { return $content.InnerText } + $text = $node.InnerText.Trim() + if ($text) { return $text } + return "" +} + +function Unescape-Xml([string]$text) { + if (-not $text) { return $text } + $text = $text.Replace("&", "&") + $text = $text.Replace(">", ">") + $text = $text.Replace("<", "<") + $text = $text.Replace(""", '"') + $text = $text.Replace("'", "'") + return $text +} + +function Get-CompactType($valueTypeNode) { + if (-not $valueTypeNode) { return "" } + $types = @() + foreach ($t in $valueTypeNode.SelectNodes("v8:Type", $ns)) { + $raw = $t.InnerText + switch -Wildcard ($raw) { + "xs:string" { $types += "String" } + "xs:decimal" { $types += "Number" } + "xs:boolean" { $types += "Boolean" } + "xs:dateTime" { $types += "DateTime" } + "v8:StandardPeriod" { $types += "StandardPeriod" } + "v8:StandardBeginningDate" { $types += "StandardBeginningDate" } + "v8:AccountType" { $types += "AccountType" } + "v8:Null" { $types += "Null" } + default { + # Strip namespace prefixes like d4p1: cfg: + $clean = $raw -replace '^[a-zA-Z0-9]+:', '' + $types += $clean + } + } + } + if ($types.Count -eq 0) { return "" } + return ($types -join " | ") +} + +function Get-DataSetType($dsNode) { + $xsiType = $dsNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*DataSetQuery*") { return "Query" } + if ($xsiType -like "*DataSetObject*") { return "Object" } + if ($xsiType -like "*DataSetUnion*") { return "Union" } + return "Unknown" +} + +function Get-FieldCount($dsNode) { + return $dsNode.SelectNodes("s:field", $ns).Count +} + +function Get-QueryLineCount($dsNode) { + $queryNode = $dsNode.SelectSingleNode("s:query", $ns) + if (-not $queryNode) { return 0 } + $text = $queryNode.InnerText + return ($text -split "`n").Count +} + +function Get-StructureItemType($itemNode) { + $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*StructureItemGroup*") { return "Group" } + if ($xsiType -like "*StructureItemTable*") { return "Table" } + if ($xsiType -like "*StructureItemChart*") { return "Chart" } + return "Unknown" +} + +function Get-GroupFields($itemNode) { + $fields = @() + foreach ($gi in $itemNode.SelectNodes("dcsset:groupItems/dcsset:item", $ns)) { + $fieldNode = $gi.SelectSingleNode("dcsset:field", $ns) + $groupType = $gi.SelectSingleNode("dcsset:groupType", $ns) + if ($fieldNode) { + $f = $fieldNode.InnerText + $gt = if ($groupType) { $groupType.InnerText } else { "" } + if ($gt -and $gt -ne "Items") { $f += "($gt)" } + $fields += $f + } + } + return $fields +} + +function Get-SelectionFields($itemNode) { + $fields = @() + foreach ($si in $itemNode.SelectNodes("dcsset:selection/dcsset:item", $ns)) { + $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*SelectedItemAuto*") { + $fields += "Auto" + } elseif ($xsiType -like "*SelectedItemField*") { + $f = $si.SelectSingleNode("dcsset:field", $ns) + if ($f) { $fields += $f.InnerText } + } elseif ($xsiType -like "*SelectedItemFolder*") { + $fields += "Folder" + } + } + return $fields +} + +function Get-FilterSummary($settingsNode) { + $filters = @() + foreach ($fi in $settingsNode.SelectNodes("dcsset:filter/dcsset:item", $ns)) { + $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + + if ($xsiType -like "*FilterItemGroup*") { + $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns) + $gt = if ($groupType) { $groupType.InnerText } else { "And" } + $subCount = $fi.SelectNodes("dcsset:item", $ns).Count + $filters += "[Group:$gt $subCount items]" + continue + } + + $use = $fi.SelectSingleNode("dcsset:use", $ns) + $isActive = if ($use -and $use.InnerText -eq "false") { "[ ]" } else { "[x]" } + + $left = $fi.SelectSingleNode("dcsset:left", $ns) + $comp = $fi.SelectSingleNode("dcsset:comparisonType", $ns) + $right = $fi.SelectSingleNode("dcsset:right", $ns) + $pres = $fi.SelectSingleNode("dcsset:presentation", $ns) + $userSetting = $fi.SelectSingleNode("dcsset:userSettingID", $ns) + + $leftStr = if ($left) { $left.InnerText } else { "?" } + $compStr = if ($comp) { $comp.InnerText } else { "?" } + $rightStr = "" + if ($right) { + $rightStr = " $($right.InnerText)" + } + + $presStr = "" + if ($pres) { + $pt = Get-MLText $pres + if ($pt) { $presStr = " `"$pt`"" } + } + + $userStr = "" + if ($userSetting) { $userStr = " [user]" } + + $filters += "$isActive $leftStr $compStr$rightStr$presStr$userStr" + } + return $filters +} + +function Build-StructureTree { + param($itemNode, [string]$prefix, [bool]$isLast, [System.Collections.Generic.List[string]]$outLines) + + $itemType = Get-StructureItemType $itemNode + $nameNode = $itemNode.SelectSingleNode("dcsset:name", $ns) + $itemName = if ($nameNode) { $nameNode.InnerText } else { "" } + + $groupFields = Get-GroupFields $itemNode + $groupStr = if ($groupFields.Count -gt 0) { "[" + ($groupFields -join ", ") + "]" } else { "(detail)" } + + $selFields = Get-SelectionFields $itemNode + $selStr = if ($selFields.Count -gt 0) { "Selection: " + ($selFields -join ", ") } else { "" } + + $line = "" + switch ($itemType) { + "Group" { + $line = "$itemType $groupStr" + if ($itemName) { $line = "$itemType `"$itemName`" $groupStr" } + } + "Table" { + $line = "Table" + if ($itemName) { $line = "Table `"$itemName`"" } + } + "Chart" { + $line = "Chart" + if ($itemName) { $line = "Chart `"$itemName`"" } + } + } + + $outLines.Add("$prefix$line") + if ($selStr -and $itemType -eq "Group") { + $outLines.Add("$prefix $selStr") + } + + # For Table, show columns and rows + if ($itemType -eq "Table") { + $columns = $itemNode.SelectNodes("dcsset:column", $ns) + $rows = $itemNode.SelectNodes("dcsset:row", $ns) + + foreach ($col in $columns) { + $colGroup = Get-GroupFields $col + $colGroupStr = if ($colGroup.Count -gt 0) { "[" + ($colGroup -join ", ") + "]" } else { "(detail)" } + $colSel = Get-SelectionFields $col + $colSelStr = if ($colSel.Count -gt 0) { "Selection: " + ($colSel -join ", ") } else { "" } + $connC = if ($rows.Count -gt 0) { [string][char]0x251C + [string][char]0x2500 + [string][char]0x2500 } else { [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 } + $contC = if ($rows.Count -gt 0) { [string][char]0x2502 + " " } else { " " } + $outLines.Add("$prefix$connC Columns: $colGroupStr") + if ($colSelStr) { $outLines.Add("$prefix$contC $colSelStr") } + } + + foreach ($row in $rows) { + $rowGroup = Get-GroupFields $row + $rowGroupStr = if ($rowGroup.Count -gt 0) { "[" + ($rowGroup -join ", ") + "]" } else { "(detail)" } + $rowSel = Get-SelectionFields $row + $rowSelStr = if ($rowSel.Count -gt 0) { "Selection: " + ($rowSel -join ", ") } else { "" } + $outLines.Add("$prefix" + [string][char]0x2514 + [string][char]0x2500 + [string][char]0x2500 + " Rows: $rowGroupStr") + if ($rowSelStr) { $outLines.Add("$prefix $rowSelStr") } + } + } + + # Recurse into nested structure items (for Group) + if ($itemType -eq "Group") { + $children = $itemNode.SelectNodes("dcsset:item", $ns) + for ($i = 0; $i -lt $children.Count; $i++) { + $child = $children[$i] + $childType = $child.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($childType -like "*StructureItem*") { + $last = ($i -eq $children.Count - 1) + $connector = if ($last) { [string][char]0x2514 + [string][char]0x2500 + " " } else { [string][char]0x251C + [string][char]0x2500 + " " } + $continuation = if ($last) { " " } else { [string][char]0x2502 + " " } + Build-StructureTree -itemNode $child -prefix "$prefix$continuation" -isLast $last -outLines $outLines + } + } + } +} + +# --- Output collector --- + +$lines = [System.Collections.Generic.List[string]]::new() + +# Determine template name from path +$pathParts = $resolvedPath -split '[/\\]' +$templateName = $resolvedPath +for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { + if ($pathParts[$i] -eq "Ext" -and $i -ge 1) { + $templateName = $pathParts[$i - 1] + break + } +} + +$totalXmlLines = (Get-Content $resolvedPath).Count + +# ============================================================ +# MODE: overview +# ============================================================ +if ($Mode -eq "overview") { + + $lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===") + $lines.Add("") + + # Sources + $sources = @() + foreach ($ds in $root.SelectNodes("s:dataSource", $ns)) { + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $dsType = $ds.SelectSingleNode("s:dataSourceType", $ns).InnerText + $sources += "$dsName ($dsType)" + } + $lines.Add("Sources: " + ($sources -join ", ")) + $lines.Add("") + + # Datasets (recursive for Union) + $lines.Add("Datasets:") + foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) { + $dsType = Get-DataSetType $ds + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $fieldCount = Get-FieldCount $ds + + switch ($dsType) { + "Query" { + $queryLines = Get-QueryLineCount $ds + $lines.Add(" [Query] $dsName $fieldCount fields, query $queryLines lines") + } + "Object" { + $objName = $ds.SelectSingleNode("s:objectName", $ns) + $objStr = if ($objName) { " objectName=$($objName.InnerText)" } else { "" } + $lines.Add(" [Object] $dsName$objStr $fieldCount fields") + } + "Union" { + $lines.Add(" [Union] $dsName $fieldCount fields") + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subType = Get-DataSetType $subDs + $subName = $subDs.SelectSingleNode("s:name", $ns) + $subNameStr = if ($subName) { $subName.InnerText } else { "?" } + $subFields = Get-FieldCount $subDs + switch ($subType) { + "Query" { + $subQueryLines = Get-QueryLineCount $subDs + $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Query] $subNameStr $subFields fields, query $subQueryLines lines") + } + "Object" { + $subObjName = $subDs.SelectSingleNode("s:objectName", $ns) + $subObjStr = if ($subObjName) { " objectName=$($subObjName.InnerText)" } else { "" } + $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [Object] $subNameStr$subObjStr $subFields fields") + } + default { + $lines.Add(" " + [string][char]0x251C + [string][char]0x2500 + " [$subType] $subNameStr $subFields fields") + } + } + } + } + } + } + + # Links — only dataset pairs (not field-level) + $links = $root.SelectNodes("s:dataSetLink", $ns) + if ($links.Count -gt 0) { + $linkPairs = [ordered]@{} + foreach ($lnk in $links) { + $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText + $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText + $key = "$srcDs -> $dstDs" + if (-not $linkPairs.Contains($key)) { $linkPairs[$key] = 0 } + $linkPairs[$key] = $linkPairs[$key] + 1 + } + $linkStrs = @() + foreach ($key in $linkPairs.Keys) { + $cnt = $linkPairs[$key] + if ($cnt -gt 1) { $linkStrs += "$key (${cnt} fields)" } + else { $linkStrs += $key } + } + $lines.Add("Links: " + ($linkStrs -join ", ")) + } + + # Calculated fields — count only + $calcFields = $root.SelectNodes("s:calculatedField", $ns) + if ($calcFields.Count -gt 0) { + $lines.Add("Calculated: $($calcFields.Count)") + } + + # Totals — count + group flag + $totalFields = $root.SelectNodes("s:totalField", $ns) + if ($totalFields.Count -gt 0) { + $hasGrouped = $false + $uniquePaths = @{} + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $uniquePaths[$tfPath] = $true + if ($tf.SelectSingleNode("s:group", $ns)) { $hasGrouped = $true } + } + $groupNote = if ($hasGrouped) { ", with group formulas" } else { "" } + if ($uniquePaths.Count -eq $totalFields.Count) { + $lines.Add("Resources: $($totalFields.Count)$groupNote") + } else { + $lines.Add("Resources: $($totalFields.Count) ($($uniquePaths.Count) fields$groupNote)") + } + } + + # Templates — count with binding types + $tplDefs = $root.SelectNodes("s:template", $ns) + $fieldTpls = $root.SelectNodes("s:fieldTemplate", $ns) + $groupTpls = $root.SelectNodes("s:groupTemplate", $ns) + $groupHeaderTpls = $root.SelectNodes("s:groupHeaderTemplate", $ns) + $groupFooterTpls = $root.SelectNodes("s:groupFooterTemplate", $ns) + $totalBindings = $fieldTpls.Count + $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count + if ($tplDefs.Count -gt 0) { + $parts = @() + if ($fieldTpls.Count -gt 0) { $parts += "$($fieldTpls.Count) field" } + $grpCount = $groupTpls.Count + $groupHeaderTpls.Count + $groupFooterTpls.Count + if ($grpCount -gt 0) { $parts += "$grpCount group" } + $lines.Add("Templates: $($tplDefs.Count) defined ($($parts -join ', ') bindings)") + } + + # Parameters — split visible/hidden + $params = $root.SelectNodes("s:parameter", $ns) + if ($params.Count -gt 0) { + $visibleNames = @() + $hiddenCount = 0 + foreach ($p in $params) { + $pName = $p.SelectSingleNode("s:name", $ns).InnerText + $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) + $isHidden = ($useRestrict -and $useRestrict.InnerText -eq "true") + if ($isHidden) { $hiddenCount++ } else { $visibleNames += $pName } + } + $paramLine = "Params: $($params.Count)" + if ($hiddenCount -gt 0 -and $visibleNames.Count -gt 0) { + $paramLine += " ($($visibleNames.Count) visible, $hiddenCount hidden)" + } elseif ($hiddenCount -eq $params.Count) { + $paramLine += " (all hidden)" + } + if ($visibleNames.Count -gt 0 -and $visibleNames.Count -le 8) { + $paramLine += ": " + ($visibleNames -join ", ") + } + $lines.Add($paramLine) + } else { + $lines.Add("Params: (none)") + } + + $lines.Add("") + + # Variants + $variants = $root.SelectNodes("s:settingsVariant", $ns) + if ($variants.Count -gt 0) { + $lines.Add("Variants:") + $varIdx = 0 + foreach ($v in $variants) { + $varIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText + $vPres = $v.SelectSingleNode("dcsset:presentation", $ns) + $vPresStr = "" + if ($vPres) { + $pt = Get-MLText $vPres + if ($pt) { $vPresStr = " `"$pt`"" } + } + + $settings = $v.SelectSingleNode("dcsset:settings", $ns) + $structItems = @() + if ($settings) { + foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) { + $siType = Get-StructureItemType $si + $groupFields = Get-GroupFields $si + $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" } + $structItems += "$siType$groupStr" + } + } + # Compact: if many identical items, show count + if ($structItems.Count -gt 3) { + $grouped = $structItems | Group-Object | Sort-Object Count -Descending + $compactParts = @() + foreach ($g in $grouped) { + if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" } + else { $compactParts += $g.Name } + } + $structItems = $compactParts + } + $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" } + + $filterCount = 0 + if ($settings) { + $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count + } + $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" } + + $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr") + } + } + + # Hints — suggest next commands + $lines.Add("") + $hints = @() + # Collect query dataset names for hint + $queryDsNames = @() + foreach ($ds in $root.SelectNodes("s:dataSet", $ns)) { + $dsType = Get-DataSetType $ds + if ($dsType -eq "Query") { + $queryDsNames += $ds.SelectSingleNode("s:name", $ns).InnerText + } elseif ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + if ((Get-DataSetType $subDs) -eq "Query") { + $sn = $subDs.SelectSingleNode("s:name", $ns) + if ($sn) { $queryDsNames += $sn.InnerText } + } + } + } + } + if ($queryDsNames.Count -eq 1) { + $hints += "-Mode query query text" + } elseif ($queryDsNames.Count -gt 1) { + $hints += "-Mode query -Name query text ($($queryDsNames -join ', '))" + } + $hints += "-Mode fields field tables by dataset" + $linkCount = $root.SelectNodes("s:dataSetLink", $ns).Count + if ($linkCount -gt 0) { + $hints += "-Mode links dataset connections ($linkCount)" + } + $calcCount = $root.SelectNodes("s:calculatedField", $ns).Count + $totalCount = $root.SelectNodes("s:totalField", $ns).Count + if ($calcCount -gt 0) { + $hints += "-Mode calculated calculated field expressions ($calcCount)" + } + if ($totalCount -gt 0) { + $hints += "-Mode resources resource aggregation ($totalCount)" + } + if ($params.Count -gt 0) { + $hints += "-Mode params parameter details" + } + if ($variants.Count -eq 1) { + $hints += "-Mode variant variant structure" + } elseif ($variants.Count -gt 1) { + $hints += "-Mode variant -Name variant structure (1..$($variants.Count))" + } + if ($tplDefs.Count -gt 0) { + $hints += "-Mode templates template bindings and expressions" + } + $hints += "-Mode trace -Name trace field origin (by name or title)" + $lines.Add("Next:") + foreach ($h in $hints) { $lines.Add(" $h") } +} + +# ============================================================ +# MODE: query +# ============================================================ +elseif ($Mode -eq "query") { + + # Find dataset + $dataSets = $root.SelectNodes("s:dataSet", $ns) + $targetDs = $null + + if ($Name) { + # Search by name: prefer nested Query items over parent Union + # Pass 1: search nested items first + foreach ($ds in $dataSets) { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subNameNode = $subDs.SelectSingleNode("s:name", $ns) + if ($subNameNode -and $subNameNode.InnerText -eq $Name) { $targetDs = $subDs; break } + } + if ($targetDs) { break } + } + # Pass 2: search top-level + if (-not $targetDs) { + foreach ($ds in $dataSets) { + $dsNameNode = $ds.SelectSingleNode("s:name", $ns) + if ($dsNameNode -and $dsNameNode.InnerText -eq $Name) { $targetDs = $ds; break } + } + } + if (-not $targetDs) { + Write-Error "Dataset '$Name' not found" + exit 1 + } + } else { + # Take first Query dataset + foreach ($ds in $dataSets) { + $dsType = Get-DataSetType $ds + if ($dsType -eq "Query") { $targetDs = $ds; break } + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + if ((Get-DataSetType $subDs) -eq "Query") { $targetDs = $subDs; break } + } + if ($targetDs) { break } + } + } + if (-not $targetDs) { + Write-Error "No Query dataset found" + exit 1 + } + } + + $queryNode = $targetDs.SelectSingleNode("s:query", $ns) + if (-not $queryNode) { + # If this is a Union, list nested query datasets + $dsType = Get-DataSetType $targetDs + if ($dsType -eq "Union") { + $subNames = @() + foreach ($subDs in $targetDs.SelectNodes("s:item", $ns)) { + $sn = $subDs.SelectSingleNode("s:name", $ns) + if ($sn) { $subNames += $sn.InnerText } + } + Write-Error "Dataset '$($targetDs.SelectSingleNode("s:name", $ns).InnerText)' is a Union. Specify nested: $($subNames -join ', ')" + } else { + Write-Error "Dataset has no query element" + } + exit 1 + } + + $rawQuery = Unescape-Xml $queryNode.InnerText + $dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText + + # Split into batches + $batches = @() + $batchTexts = $rawQuery -split ';\s*\r?\n\s*/{16,}\s*\r?\n' + foreach ($bt in $batchTexts) { + $trimmed = $bt.Trim() + if ($trimmed) { $batches += $trimmed } + } + + $totalQueryLines = ($rawQuery -split "`n").Count + + if ($batches.Count -le 1) { + # Single query + $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines) ===") + $lines.Add("") + foreach ($ql in ($rawQuery.Trim() -split "`n")) { + $lines.Add($ql.TrimEnd()) + } + } else { + $lines.Add("=== Query: $dsNameStr ($totalQueryLines lines, $($batches.Count) batches) ===") + + if ($Batch -eq 0) { + # Show TOC + $lineNum = 1 + for ($bi = 0; $bi -lt $batches.Count; $bi++) { + $batchLines = ($batches[$bi] -split "`n") + $endLine = $lineNum + $batchLines.Count - 1 + # Detect ПОМЕСТИТЬ target + $target = "" + foreach ($bl in $batchLines) { + if ($bl -match '^\s*(?:ПОМЕСТИТЬ|INTO)\s+(\S+)') { + $target = [char]0x2192 + " " + $Matches[1] + break + } + } + $lines.Add(" Batch $($bi + 1): lines $lineNum-$endLine $target") + $lineNum = $endLine + 3 # +separator + } + $lines.Add("") + + # Show all batches + for ($bi = 0; $bi -lt $batches.Count; $bi++) { + $lines.Add("--- Batch $($bi + 1) ---") + foreach ($ql in ($batches[$bi] -split "`n")) { + $lines.Add($ql.TrimEnd()) + } + $lines.Add("") + } + } else { + # Show specific batch + if ($Batch -gt $batches.Count) { + Write-Error "Batch $Batch not found (total: $($batches.Count))" + exit 1 + } + $lines.Add("") + $lines.Add("--- Batch $Batch ---") + foreach ($ql in ($batches[$Batch - 1] -split "`n")) { + $lines.Add($ql.TrimEnd()) + } + } + } +} + +# ============================================================ +# MODE: fields +# ============================================================ +elseif ($Mode -eq "fields") { + + $dataSets = $root.SelectNodes("s:dataSet", $ns) + + function Show-DataSetFields($dsNode) { + $dsType = Get-DataSetType $dsNode + $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText + $fields = $dsNode.SelectNodes("s:field", $ns) + + $lines.Add("=== Fields: $dsNameStr [$dsType] ($($fields.Count)) ===") + $lines.Add(" dataPath title role restrict format") + + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + $dpStr = if ($dp) { $dp.InnerText } else { "-" } + + $titleNode = $f.SelectSingleNode("s:title", $ns) + $titleStr = if ($titleNode) { Get-MLText $titleNode } else { "" } + if (-not $titleStr) { $titleStr = "-" } + + # Role + $role = $f.SelectSingleNode("s:role", $ns) + $roleStr = "-" + if ($role) { + $roleParts = @() + foreach ($child in $role.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $roleParts += $child.LocalName + } + } + if ($roleParts.Count -gt 0) { $roleStr = $roleParts -join "," } + } + + # UseRestriction + $restrict = $f.SelectSingleNode("s:useRestriction", $ns) + $restrictStr = "-" + if ($restrict) { + $restrictParts = @() + foreach ($child in $restrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $restrictParts += $child.LocalName.Substring(0, [Math]::Min(4, $child.LocalName.Length)) + } + } + if ($restrictParts.Count -gt 0) { $restrictStr = $restrictParts -join "," } + } + + # Appearance format + $formatStr = "-" + $appearance = $f.SelectSingleNode("s:appearance", $ns) + if ($appearance) { + foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) { + $paramNode = $appItem.SelectSingleNode("dcscor:parameter", $ns) + $valNode = $appItem.SelectSingleNode("dcscor:value", $ns) + if ($paramNode -and ($paramNode.InnerText -eq "Формат" -or $paramNode.InnerText -eq "Format") -and $valNode) { + $formatStr = $valNode.InnerText + } + } + } + + # presentationExpression + $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns) + $presStr = "" + if ($presExpr) { $presStr = " presExpr" } + + $dpPad = $dpStr.PadRight(35) + $titlePad = $titleStr.PadRight(22) + $rolePad = $roleStr.PadRight(10) + $restrictPad = $restrictStr.PadRight(12) + + $lines.Add(" $dpPad $titlePad $rolePad $restrictPad $formatStr$presStr") + } + } + + if ($Name) { + # Detail for specific field by dataPath — search all datasets + $found = $false + $matchedIn = @() + + function Collect-FieldInfo($dsNode) { + $dsType = Get-DataSetType $dsNode + $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText + foreach ($f in $dsNode.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp -or $dp.InnerText -ne $Name) { continue } + + $info = @{ dataset = "$dsNameStr [$dsType]" } + + $titleNode = $f.SelectSingleNode("s:title", $ns) + $info.title = if ($titleNode) { Get-MLText $titleNode } else { "" } + + # ValueType + $vt = $f.SelectSingleNode("s:valueType", $ns) + $info.type = if ($vt) { Get-CompactType $vt } else { "" } + + # Role + $role = $f.SelectSingleNode("s:role", $ns) + $roleParts = @() + if ($role) { + foreach ($child in $role.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $roleParts += $child.LocalName + } + } + } + $info.role = $roleParts -join ", " + + # UseRestriction + $restrict = $f.SelectSingleNode("s:useRestriction", $ns) + $restrictParts = @() + if ($restrict) { + foreach ($child in $restrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $restrictParts += $child.LocalName + } + } + } + $info.restrict = $restrictParts -join ", " + + # Format + $formatStr = "" + $appearance = $f.SelectSingleNode("s:appearance", $ns) + if ($appearance) { + foreach ($appItem in $appearance.SelectNodes("dcscor:item", $ns)) { + $pn = $appItem.SelectSingleNode("dcscor:parameter", $ns) + $vn = $appItem.SelectSingleNode("dcscor:value", $ns) + if ($pn -and ($pn.InnerText -eq "Формат" -or $pn.InnerText -eq "Format") -and $vn) { + $formatStr = $vn.InnerText + } + } + } + $info.format = $formatStr + + # PresentationExpression + $presExpr = $f.SelectSingleNode("s:presentationExpression", $ns) + $info.presExpr = if ($presExpr) { $presExpr.InnerText } else { "" } + + return $info + } + return $null + } + + # Search all datasets and nested items + $fieldInfos = @() + foreach ($ds in $dataSets) { + $info = Collect-FieldInfo $ds + if ($info) { $fieldInfos += $info } + $dsType = Get-DataSetType $ds + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $info = Collect-FieldInfo $subDs + if ($info) { $fieldInfos += $info } + } + } + } + + if ($fieldInfos.Count -eq 0) { + Write-Error "Field '$Name' not found in any dataset" + exit 1 + } + + # Use first match for detail (they usually share the same properties) + $first = $fieldInfos[0] + $titleStr = if ($first.title) { " `"$($first.title)`"" } else { "" } + $lines.Add("=== Field: $Name$titleStr ===") + $lines.Add("") + + # Datasets + $dsList = ($fieldInfos | ForEach-Object { $_.dataset }) -join ", " + $lines.Add("Dataset: $dsList") + + if ($first.type) { $lines.Add("Type: $($first.type)") } + if ($first.role) { $lines.Add("Role: $($first.role)") } + if ($first.restrict) { $lines.Add("Restrict: $($first.restrict)") } + if ($first.format) { $lines.Add("Format: $($first.format)") } + if ($first.presExpr) { + $lines.Add("PresentationExpression:") + foreach ($el in ($first.presExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + } + } else { + # Compact map: field names per dataset + $lines.Add("=== Fields map ===") + + function Show-DataSetFieldMap($dsNode, $indent) { + $dsType = Get-DataSetType $dsNode + $dsNameStr = $dsNode.SelectSingleNode("s:name", $ns).InnerText + $fields = $dsNode.SelectNodes("s:field", $ns) + $fieldNames = @() + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if ($dp) { $fieldNames += $dp.InnerText } + } + $nameList = $fieldNames -join ", " + if ($nameList.Length -gt 100) { + $nameList = $nameList.Substring(0, 97) + "..." + } + $lines.Add("$indent$dsNameStr [$dsType] ($($fields.Count)): $nameList") + } + + foreach ($ds in $dataSets) { + Show-DataSetFieldMap $ds "" + $dsType = Get-DataSetType $ds + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + Show-DataSetFieldMap $subDs " " + } + } + } + + $lines.Add("") + $lines.Add("Use -Name for details.") + } +} + +# ============================================================ +# MODE: links +# ============================================================ +elseif ($Mode -eq "links") { + + $links = $root.SelectNodes("s:dataSetLink", $ns) + if ($links.Count -eq 0) { + $lines.Add("(no links)") + } else { + $lines.Add("=== Links ($($links.Count)) ===") + $lines.Add("") + # Group by source->dest pair + $currentPair = "" + foreach ($lnk in $links) { + $srcDs = $lnk.SelectSingleNode("s:sourceDataSet", $ns).InnerText + $dstDs = $lnk.SelectSingleNode("s:destinationDataSet", $ns).InnerText + $srcExpr = $lnk.SelectSingleNode("s:sourceExpression", $ns).InnerText + $dstExpr = $lnk.SelectSingleNode("s:destinationExpression", $ns).InnerText + $paramNode = $lnk.SelectSingleNode("s:parameter", $ns) + $paramListNode = $lnk.SelectSingleNode("s:parameterListAllowed", $ns) + + $pair = "$srcDs -> $dstDs" + if ($pair -ne $currentPair) { + if ($currentPair) { $lines.Add("") } + $lines.Add("$pair :") + $currentPair = $pair + } + + $paramStr = "" + if ($paramNode) { $paramStr = " param=$($paramNode.InnerText)" } + + $lines.Add(" $srcExpr -> $dstExpr$paramStr") + } + } +} + +# ============================================================ +# MODE: calculated +# ============================================================ +elseif ($Mode -eq "calculated") { + + $calcFields = $root.SelectNodes("s:calculatedField", $ns) + if ($calcFields.Count -eq 0) { + $lines.Add("(no calculated fields)") + } elseif ($Name) { + $found = $false + foreach ($cf in $calcFields) { + $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + if ($cfPath -eq $Name) { + $lines.Add("=== Calculated: $cfPath ===") + $lines.Add("") + + $cfExpr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $lines.Add("Expression:") + foreach ($el in ($cfExpr -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + if ($cfTitle) { + $t = Get-MLText $cfTitle + if ($t) { $lines.Add("Title: $t") } + } + + $cfRestrict = $cf.SelectSingleNode("s:useRestriction", $ns) + if ($cfRestrict) { + $parts = @() + foreach ($child in $cfRestrict.ChildNodes) { + if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") { + $parts += $child.LocalName + } + } + if ($parts.Count -gt 0) { $lines.Add("Restrict: $($parts -join ', ')") } + } + + $found = $true + break + } + } + if (-not $found) { + Write-Error "Calculated field '$Name' not found" + exit 1 + } + } else { + # Map + $lines.Add("=== Calculated fields ($($calcFields.Count)) ===") + foreach ($cf in $calcFields) { + $cfPath = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + $titleStr = "" + if ($cfTitle) { + $t = Get-MLText $cfTitle + if ($t -and $t -ne $cfPath) { $titleStr = " `"$t`"" } + } + $lines.Add(" $cfPath$titleStr") + } + $lines.Add("") + $lines.Add("Use -Name for full expression.") + } +} + +# ============================================================ +# MODE: resources +# ============================================================ +elseif ($Mode -eq "resources") { + + $totalFields = $root.SelectNodes("s:totalField", $ns) + if ($totalFields.Count -eq 0) { + $lines.Add("(no resources)") + } elseif ($Name) { + $matched = @() + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + if ($tfPath -eq $Name) { $matched += $tf } + } + if ($matched.Count -eq 0) { + Write-Error "Resource '$Name' not found" + exit 1 + } + $lines.Add("=== Resource: $Name ===") + $lines.Add("") + foreach ($tf in $matched) { + $tfExpr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "(overall)" + if ($tfGroup) { $groupStr = $tfGroup.InnerText } + $lines.Add(" [$groupStr] $tfExpr") + } + } else { + # Map + $lines.Add("=== Resources ($($totalFields.Count)) ===") + $resMap = [ordered]@{} + foreach ($tf in $totalFields) { + $tfPath = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $tfGroup = $tf.SelectSingleNode("s:group", $ns) + if (-not $resMap.Contains($tfPath)) { + $resMap[$tfPath] = @{ hasGroup = $false } + } + if ($tfGroup) { $resMap[$tfPath].hasGroup = $true } + } + foreach ($key in $resMap.Keys) { + $groupMark = if ($resMap[$key].hasGroup) { " *" } else { "" } + $lines.Add(" $key$groupMark") + } + $lines.Add("") + $lines.Add(" * = has group-level formulas") + $lines.Add("") + $lines.Add("Use -Name for full formula.") + } +} + +# ============================================================ +# MODE: params +# ============================================================ +elseif ($Mode -eq "params") { + + $params = $root.SelectNodes("s:parameter", $ns) + $lines.Add("=== Parameters ($($params.Count)) ===") + $lines.Add(" Name Type Default Visible Expression") + + foreach ($p in $params) { + $pName = $p.SelectSingleNode("s:name", $ns).InnerText + $pType = Get-CompactType $p.SelectSingleNode("s:valueType", $ns) + if (-not $pType) { $pType = "-" } + + # Default value + $valNode = $p.SelectSingleNode("s:value", $ns) + $valStr = "-" + if ($valNode) { + $nilAttr = $valNode.GetAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance") + if ($nilAttr -eq "true") { + $valStr = "null" + } else { + $raw = $valNode.InnerText.Trim() + if ($raw -eq "0001-01-01T00:00:00") { + $valStr = "-" + } elseif ($raw) { + # Check for StandardPeriod variant + $variant = $valNode.SelectSingleNode("v8:variant", $ns) + if ($variant) { + $valStr = $variant.InnerText + } else { + $valStr = $raw + if ($valStr.Length -gt 15) { $valStr = $valStr.Substring(0, 12) + "..." } + } + } + } + } + + # Visibility + $useRestrict = $p.SelectSingleNode("s:useRestriction", $ns) + $visStr = "yes" + if ($useRestrict -and $useRestrict.InnerText -eq "true") { $visStr = "hidden" } + if ($useRestrict -and $useRestrict.InnerText -eq "false") { $visStr = "yes" } + + # Expression + $exprNode = $p.SelectSingleNode("s:expression", $ns) + $exprStr = "-" + if ($exprNode -and $exprNode.InnerText.Trim()) { + $exprStr = Unescape-Xml $exprNode.InnerText.Trim() + } + + # availableAsField + $availField = $p.SelectSingleNode("s:availableAsField", $ns) + $availStr = "" + if ($availField -and $availField.InnerText -eq "false") { $availStr = " [noField]" } + + $namePad = $pName.PadRight(33) + $typePad = $pType.PadRight(22) + $valPad = $valStr.PadRight(16) + $visPad = $visStr.PadRight(8) + + $lines.Add(" $namePad $typePad $valPad $visPad $exprStr$availStr") + } +} + +# ============================================================ +# MODE: variant +# ============================================================ +elseif ($Mode -eq "variant") { + + $variants = $root.SelectNodes("s:settingsVariant", $ns) + + if (-not $Name) { + # --- Variant list (map) --- + if ($variants.Count -eq 0) { + $lines.Add("=== Variants: (none) ===") + } else { + $lines.Add("=== Variants ($($variants.Count)) ===") + $varIdx = 0 + foreach ($v in $variants) { + $varIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText + $vPres = $v.SelectSingleNode("dcsset:presentation", $ns) + $vPresStr = "" + if ($vPres) { + $pt = Get-MLText $vPres + if ($pt) { $vPresStr = " `"$pt`"" } + } + + $settings = $v.SelectSingleNode("dcsset:settings", $ns) + $structItems = @() + if ($settings) { + foreach ($si in $settings.SelectNodes("dcsset:item", $ns)) { + $siType = Get-StructureItemType $si + $groupFields = Get-GroupFields $si + $groupStr = if ($groupFields.Count -gt 0) { "(" + ($groupFields -join ",") + ")" } else { "(detail)" } + $structItems += "$siType$groupStr" + } + } + if ($structItems.Count -gt 3) { + $grouped = $structItems | Group-Object | Sort-Object Count -Descending + $compactParts = @() + foreach ($g in $grouped) { + if ($g.Count -gt 1) { $compactParts += "$($g.Count)x $($g.Name)" } + else { $compactParts += $g.Name } + } + $structItems = $compactParts + } + $structStr = if ($structItems.Count -gt 0) { " " + ($structItems -join ", ") } else { "" } + + $filterCount = 0 + if ($settings) { + $filterCount = $settings.SelectNodes("dcsset:filter/dcsset:item", $ns).Count + } + $filterStr = if ($filterCount -gt 0) { " $filterCount filters" } else { "" } + + # Selection fields + $selFields = @() + if ($settings) { $selFields = Get-SelectionFields $settings } + $selStr = if ($selFields.Count -gt 0) { " sel: " + ($selFields -join ", ") } else { "" } + + $lines.Add(" [$varIdx] $vName$vPresStr$structStr$filterStr") + if ($selStr) { $lines.Add(" $selStr") } + } + } + } else { + # --- Variant detail --- + + $targetVariant = $null + $varIdx = 0 + foreach ($v in $variants) { + $varIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns).InnerText + if ($vName -eq $Name -or "$varIdx" -eq $Name) { + $targetVariant = $v + $matchIdx = $varIdx + break + } + } + if (-not $targetVariant) { + Write-Error "Variant '$Name' not found. Use -Mode variant without -Name to see list." + exit 1 + } + + $vName = $targetVariant.SelectSingleNode("dcsset:name", $ns).InnerText + $vPres = $targetVariant.SelectSingleNode("dcsset:presentation", $ns) + $vPresStr = "" + if ($vPres) { + $pt = Get-MLText $vPres + if ($pt) { $vPresStr = " `"$pt`"" } + } + + $lines.Add("=== Variant [$matchIdx]: $vName$vPresStr ===") + + $settings = $targetVariant.SelectSingleNode("dcsset:settings", $ns) + if (-not $settings) { + $lines.Add(" (empty settings)") + } else { + # Selection at settings level + $topSel = Get-SelectionFields $settings + if ($topSel.Count -gt 0) { + $lines.Add("") + $lines.Add("Selection: " + ($topSel -join ", ")) + } + + # Structure + $structItems = $settings.SelectNodes("dcsset:item", $ns) + $hasStruct = $false + foreach ($si in $structItems) { + $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($siXsiType -like "*StructureItem*") { $hasStruct = $true; break } + } + + if ($hasStruct) { + $lines.Add("") + $lines.Add("Structure:") + foreach ($si in $structItems) { + $siXsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($siXsiType -like "*StructureItem*") { + Build-StructureTree -itemNode $si -prefix " " -isLast $false -outLines $lines + } + } + } + + # Filter + $filters = Get-FilterSummary $settings + if ($filters.Count -gt 0) { + $lines.Add("") + $lines.Add("Filter:") + foreach ($f in $filters) { + $lines.Add(" $f") + } + } + + # Data parameters + $dataParams = $settings.SelectNodes("dcsset:dataParameters/dcsset:item", $ns) + if ($dataParams.Count -gt 0) { + $dpStrs = @() + foreach ($dp in $dataParams) { + $dpParam = $dp.SelectSingleNode("dcscor:parameter", $ns) + $dpVal = $dp.SelectSingleNode("dcscor:value", $ns) + if ($dpParam -and $dpVal) { + $dpStrs += "$($dpParam.InnerText)=`"$($dpVal.InnerText)`"" + } + } + if ($dpStrs.Count -gt 0) { + $lines.Add("") + $lines.Add("DataParams: " + ($dpStrs -join ", ")) + } + } + + # Output parameters + $outParams = $settings.SelectNodes("dcsset:outputParameters/dcscor:item", $ns) + if ($outParams.Count -gt 0) { + $opStrs = @() + foreach ($op in $outParams) { + $opParam = $op.SelectSingleNode("dcscor:parameter", $ns) + $opVal = $op.SelectSingleNode("dcscor:value", $ns) + if ($opParam -and $opVal) { + $paramName = $opParam.InnerText + $paramVal = $opVal.InnerText + # Shorten known long names + switch ($paramName) { + "МакетОформления" { $opStrs += "style=$paramVal" } + "РасположениеПолейГруппировки" { $opStrs += "groups=$paramVal" } + "ГоризонтальноеРасположениеОбщихИтогов" { $opStrs += "totalsH=$paramVal" } + "ВертикальноеРасположениеОбщихИтогов" { $opStrs += "totalsV=$paramVal" } + "ВыводитьЗаголовок" { $opStrs += "header=$paramVal" } + "ВыводитьОтбор" { $opStrs += "filter=$paramVal" } + "ВыводитьПараметрыДанных" { $opStrs += "dataParams=$paramVal" } + "РасположениеРеквизитов" { $opStrs += "attrs=$paramVal" } + default { $opStrs += "$paramName=$paramVal" } + } + } + } + if ($opStrs.Count -gt 0) { + $lines.Add("") + $lines.Add("Output: " + ($opStrs -join " ")) + } + } + } + } # end else (variant detail) +} + +# ============================================================ +# MODE: trace +# ============================================================ +elseif ($Mode -eq "trace") { + + if (-not $Name) { + Write-Error "Trace mode requires -Name " + exit 1 + } + + # --- Build field index --- + + $dsFields = @{} # dataPath -> @{ datasets=@(); title="" } + $calcFields = @{} # dataPath -> @{ expression=""; title="" } + $resFields = @{} # dataPath -> @(@{ expression=""; group="" }) + $titleMap = @{} # title -> dataPath + + # Scan dataset fields (including nested Union items) + $dataSets = $root.SelectNodes("s:dataSet", $ns) + foreach ($ds in $dataSets) { + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $dsType = Get-DataSetType $ds + + foreach ($f in $ds.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp) { continue } + $dpStr = $dp.InnerText + if (-not $dsFields.ContainsKey($dpStr)) { + $dsFields[$dpStr] = @{ datasets = @(); title = "" } + } + $dsFields[$dpStr].datasets += "$dsName [$dsType]" + $titleNode = $f.SelectSingleNode("s:title", $ns) + if ($titleNode) { + $t = Get-MLText $titleNode + if ($t) { + if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } + if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + } + } + + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subName = $subDs.SelectSingleNode("s:name", $ns).InnerText + $subType = Get-DataSetType $subDs + foreach ($f in $subDs.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp) { continue } + $dpStr = $dp.InnerText + if (-not $dsFields.ContainsKey($dpStr)) { + $dsFields[$dpStr] = @{ datasets = @(); title = "" } + } + $dsFields[$dpStr].datasets += "$subName [$subType]" + $titleNode = $f.SelectSingleNode("s:title", $ns) + if ($titleNode) { + $t = Get-MLText $titleNode + if ($t) { + if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } + if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + } + } + } + } + } + + # Scan calculated fields + foreach ($cf in $root.SelectNodes("s:calculatedField", $ns)) { + $dpStr = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + $expr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + $t = "" + if ($cfTitle) { $t = Get-MLText $cfTitle } + $calcFields[$dpStr] = @{ expression = $expr; title = $t } + if ($t -and -not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + + # Scan resources + foreach ($tf in $root.SelectNodes("s:totalField", $ns)) { + $dpStr = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $expr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $grp = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "(overall)" + if ($grp) { $groupStr = $grp.InnerText } + if (-not $resFields.ContainsKey($dpStr)) { $resFields[$dpStr] = @() } + $resFields[$dpStr] += @{ expression = $expr; group = $groupStr } + } + + # --- Resolve name: try dataPath, then exact title, then substring title --- + $targetPath = $Name + $knownPaths = @() + $knownPaths += $dsFields.Keys + $knownPaths += $calcFields.Keys + $knownPaths += $resFields.Keys + $isKnown = $knownPaths -contains $Name + + if (-not $isKnown) { + if ($titleMap.ContainsKey($Name)) { + $targetPath = $titleMap[$Name] + } else { + # Substring match in titles + $matchedTitle = $null + foreach ($key in $titleMap.Keys) { + if ($key -like "*$Name*") { + $matchedTitle = $key + break + } + } + if ($matchedTitle) { + $targetPath = $titleMap[$matchedTitle] + } else { + Write-Error "Field '$Name' not found by dataPath or title" + exit 1 + } + } + } + + # --- Build output --- + $title = "" + if ($calcFields.ContainsKey($targetPath) -and $calcFields[$targetPath].title) { + $title = $calcFields[$targetPath].title + } elseif ($dsFields.ContainsKey($targetPath) -and $dsFields[$targetPath].title) { + $title = $dsFields[$targetPath].title + } + $titleStr = if ($title) { " `"$title`"" } else { "" } + + $lines.Add("=== Trace: $targetPath$titleStr ===") + $lines.Add("") + + # Dataset origin + if ($dsFields.ContainsKey($targetPath)) { + $uniqueDs = $dsFields[$targetPath].datasets | Select-Object -Unique + $lines.Add("Dataset: $($uniqueDs -join ', ')") + } else { + $lines.Add("Dataset: (schema-level only, not in dataset fields)") + } + + # Calculated field + if ($calcFields.ContainsKey($targetPath)) { + $cf = $calcFields[$targetPath] + $lines.Add("") + $lines.Add("Calculated:") + foreach ($el in ($cf.expression -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + + # Extract operands: find known field names in expression + $operands = @() + $allKnown = @() + $allKnown += $dsFields.Keys + $allKnown += $calcFields.Keys + $allKnown = $allKnown | Select-Object -Unique | Where-Object { $_ -ne $targetPath } + # Sort by length descending to match longer names first + $allKnown = $allKnown | Sort-Object -Property Length -Descending + + foreach ($fieldName in $allKnown) { + $escaped = [regex]::Escape($fieldName) + if ($cf.expression -match "(? calculated") + } elseif ($dsFields.ContainsKey($op)) { + $opDs = ($dsFields[$op].datasets | Select-Object -Unique) -join ", " + $lines.Add(" $op -> $opDs") + } else { + $lines.Add(" $op") + } + } + } + } + + # Resource + if ($resFields.ContainsKey($targetPath)) { + $lines.Add("") + $lines.Add("Resource:") + foreach ($r in $resFields[$targetPath]) { + $lines.Add(" [$($r.group)] $($r.expression)") + } + } + + # Simple dataset field, no calc/resource + if (-not $calcFields.ContainsKey($targetPath) -and -not $resFields.ContainsKey($targetPath)) { + if ($dsFields.ContainsKey($targetPath)) { + $lines.Add("") + $lines.Add("(direct dataset field, no calculated expression or resource)") + } + } +} + +# ============================================================ +# MODE: templates +# ============================================================ +elseif ($Mode -eq "templates") { + + # --- Helper: check if expression is trivial --- + function Is-TrivialExpr([string]$paramName, [string]$expr) { + $e = $expr.Trim() + $n = $paramName.Trim() + if ($e -eq $n) { return $true } + if ($e -eq "Представление($n)") { return $true } + return $false + } + + # --- Helper: parse template content (rows/cells) --- + function Get-TemplateContent([System.Xml.XmlNode]$tplNode) { + $innerT = $tplNode.SelectSingleNode("s:template", $ns) + if (-not $innerT) { return @{ rows = 0; cells = @(); params = @(); nonTrivial = @() } } + + $rows = $innerT.SelectNodes("dcsat:item", $ns) + $rowCount = $rows.Count + $cellData = [System.Collections.ArrayList]::new() + $rowIdx = 0 + foreach ($row in $rows) { + $rowIdx++ + $rowCells = [System.Collections.ArrayList]::new() + foreach ($cell in $row.SelectNodes("dcsat:tableCell", $ns)) { + $field = $cell.SelectSingleNode("dcsat:item", $ns) + if (-not $field) { + [void]$rowCells.Add("(empty)") + continue + } + $val = $field.SelectSingleNode("dcsat:value", $ns) + if (-not $val) { + [void]$rowCells.Add("(empty)") + continue + } + $xsiType = $val.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -like "*LocalStringType*") { + $text = Get-MLText $val + if ($text) { [void]$rowCells.Add("`"$text`"") } + else { [void]$rowCells.Add("(empty)") } + } elseif ($xsiType -like "*Parameter*") { + [void]$rowCells.Add("{$($val.InnerText)}") + } else { + [void]$rowCells.Add("(?)") + } + } + [void]$cellData.Add(@{ row = $rowIdx; cells = $rowCells.ToArray() }) + } + + # Parameters + $paramNodes = $tplNode.SelectNodes("s:parameter", $ns) + $paramList = [System.Collections.ArrayList]::new() + $nonTrivialList = [System.Collections.ArrayList]::new() + foreach ($p in $paramNodes) { + $pn = $p.SelectSingleNode("dcsat:name", $ns) + $pe = $p.SelectSingleNode("dcsat:expression", $ns) + if ($pn -and $pe) { + $pName = $pn.InnerText + $pExpr = $pe.InnerText + [void]$paramList.Add(@{ name = $pName; expression = $pExpr }) + if (-not (Is-TrivialExpr $pName $pExpr)) { + [void]$nonTrivialList.Add(@{ name = $pName; expression = $pExpr }) + } + } + } + + return @{ + rows = $rowCount + cells = $cellData.ToArray() + params = $paramList.ToArray() + nonTrivial = $nonTrivialList.ToArray() + } + } + + # --- Build template name -> node index --- + $tplIndex = @{} + foreach ($t in $root.SelectNodes("s:template", $ns)) { + $tn = $t.SelectSingleNode("s:name", $ns) + if ($tn) { $tplIndex[$tn.InnerText] = $t } + } + + # --- Parse bindings --- + # Group bindings: groupTemplate + groupHeaderTemplate + $groupBindings = [ordered]@{} # groupName -> @{ bindings = @(@{type; tplName; tplNode}) } + + foreach ($gt in $root.SelectNodes("s:groupTemplate", $ns)) { + $gn = $gt.SelectSingleNode("s:groupName", $ns) + $gf = $gt.SelectSingleNode("s:groupField", $ns) + $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } + $tt = $gt.SelectSingleNode("s:templateType", $ns) + $tn = $gt.SelectSingleNode("s:template", $ns) + $ttStr = if ($tt) { $tt.InnerText } else { "-" } + $tnStr = if ($tn) { $tn.InnerText } else { "-" } + + if (-not $groupBindings.Contains($gnStr)) { + $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() + } + [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr }) + } + + foreach ($ght in $root.SelectNodes("s:groupHeaderTemplate", $ns)) { + $gn = $ght.SelectSingleNode("s:groupName", $ns) + $gf = $ght.SelectSingleNode("s:groupField", $ns) + $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } + $tt = $ght.SelectSingleNode("s:templateType", $ns) + $tn = $ght.SelectSingleNode("s:template", $ns) + $ttStr = if ($tt) { "GroupHeader" } else { "GroupHeader" } + $tnStr = if ($tn) { $tn.InnerText } else { "-" } + + if (-not $groupBindings.Contains($gnStr)) { + $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() + } + [void]$groupBindings[$gnStr].Add(@{ type = $ttStr; tplName = $tnStr }) + } + + foreach ($gft in $root.SelectNodes("s:groupFooterTemplate", $ns)) { + $gn = $gft.SelectSingleNode("s:groupName", $ns) + $gf = $gft.SelectSingleNode("s:groupField", $ns) + $gnStr = if ($gn) { $gn.InnerText } elseif ($gf) { $gf.InnerText } else { "(default)" } + $tn = $gft.SelectSingleNode("s:template", $ns) + $tnStr = if ($tn) { $tn.InnerText } else { "-" } + + if (-not $groupBindings.Contains($gnStr)) { + $groupBindings[$gnStr] = [System.Collections.ArrayList]::new() + } + [void]$groupBindings[$gnStr].Add(@{ type = "GroupFooter"; tplName = $tnStr }) + } + + # Field bindings: fieldTemplate + $fieldBindings = [ordered]@{} # fieldName -> tplName + $fieldNonTrivial = [System.Collections.ArrayList]::new() + + foreach ($ft in $root.SelectNodes("s:fieldTemplate", $ns)) { + $fn = $ft.SelectSingleNode("s:field", $ns) + $tn = $ft.SelectSingleNode("s:template", $ns) + if ($fn -and $tn) { + $fName = $fn.InnerText + $tName = $tn.InnerText + $fieldBindings[$fName] = $tName + # Check params for non-trivial expressions + if ($tplIndex.ContainsKey($tName)) { + $content = Get-TemplateContent $tplIndex[$tName] + foreach ($nt in $content.nonTrivial) { + [void]$fieldNonTrivial.Add(@{ field = $fName; template = $tName; name = $nt.name; expression = $nt.expression }) + } + } + } + } + + $totalTpl = $tplIndex.Count + $fieldCount = $fieldBindings.Count + $groupBindCount = 0 + foreach ($k in $groupBindings.Keys) { $groupBindCount += $groupBindings[$k].Count } + + if (-not $Name) { + # --- MAP mode --- + $lines.Add("=== Templates ($totalTpl defined: $fieldCount field, $groupBindCount group) ===") + + # Field bindings + if ($fieldBindings.Count -gt 0) { + $lines.Add("") + if ($fieldNonTrivial.Count -eq 0) { + $fieldNames = @($fieldBindings.Keys) + if ($fieldNames.Count -le 8) { + $lines.Add("Field bindings ($fieldCount): $($fieldNames -join ', ') (all trivial)") + } else { + $lines.Add("Field bindings ($fieldCount): (all trivial)") + $lines.Add(" $($fieldNames[0..7] -join ', '), ...") + } + } else { + $trivialCount = $fieldBindings.Count - ($fieldNonTrivial | Select-Object -ExpandProperty field -Unique).Count + $lines.Add("Field bindings ($fieldCount, $trivialCount trivial):") + foreach ($nt in $fieldNonTrivial) { + $lines.Add(" $($nt.field): $($nt.name) = $($nt.expression)") + } + } + } + + # Group bindings + if ($groupBindings.Count -gt 0) { + $lines.Add("") + $lines.Add("Group bindings ($groupBindCount):") + foreach ($gName in $groupBindings.Keys) { + $bindings = $groupBindings[$gName] + $parts = @() + foreach ($b in $bindings) { + $info = "$($b.type) -> $($b.tplName)" + if ($tplIndex.ContainsKey($b.tplName)) { + $content = Get-TemplateContent $tplIndex[$b.tplName] + # Check if any cell has content + $hasContent = $false + foreach ($r in $content.cells) { + foreach ($c in $r.cells) { + if ($c -ne "(empty)") { $hasContent = $true; break } + } + if ($hasContent) { break } + } + $info += " ($($content.rows) rows" + if ($content.params.Count -gt 0) { $info += ", $($content.params.Count) params" } + $info += ")" + if (-not $hasContent -and $content.params.Count -eq 0) { + $info += " spacer" + } + if ($content.nonTrivial.Count -gt 0) { + $ntNames = ($content.nonTrivial | ForEach-Object { $_.name }) -join ', ' + $info += " *$ntNames" + } + } + $parts += $info + } + $lines.Add(" $gName") + foreach ($p in $parts) { + $lines.Add(" $p") + } + } + } + + if ($fieldBindings.Count -gt 0 -or $groupBindings.Count -gt 0) { + $lines.Add("") + $lines.Add("Use -Name for template details.") + } + } else { + # --- DETAIL mode --- + $found = $false + + # Check group bindings first + if ($groupBindings.Contains($Name)) { + $found = $true + $bindings = $groupBindings[$Name] + $lines.Add("=== Templates: $Name ===") + foreach ($b in $bindings) { + $lines.Add("") + $tName = $b.tplName + if (-not $tplIndex.ContainsKey($tName)) { + $lines.Add("$($b.type) -> $tName (template not found)") + continue + } + $content = Get-TemplateContent $tplIndex[$tName] + $cellCount = 0 + foreach ($r in $content.cells) { $cellCount += $r.cells.Count } + $lines.Add("$($b.type) -> $tName [$($content.rows) rows, $cellCount cells]:") + + foreach ($r in $content.cells) { + $cellStr = $r.cells -join " | " + $lines.Add(" Row $($r.row): $cellStr") + } + + if ($content.nonTrivial.Count -gt 0) { + $lines.Add(" Params:") + foreach ($nt in $content.nonTrivial) { + $lines.Add(" $($nt.name) = $($nt.expression)") + } + } + } + } + + # Check field bindings + if ($fieldBindings.Contains($Name)) { + if ($found) { $lines.Add("") } + $found = $true + $tName = $fieldBindings[$Name] + $lines.Add("=== Field template: $Name -> $tName ===") + if ($tplIndex.ContainsKey($tName)) { + $content = Get-TemplateContent $tplIndex[$tName] + $cellCount = 0 + foreach ($r in $content.cells) { $cellCount += $r.cells.Count } + $lines.Add("[$($content.rows) rows, $cellCount cells]") + + foreach ($r in $content.cells) { + $cellStr = $r.cells -join " | " + $lines.Add(" Row $($r.row): $cellStr") + } + + if ($content.nonTrivial.Count -gt 0) { + $lines.Add(" Non-trivial params:") + foreach ($nt in $content.nonTrivial) { + $lines.Add(" $($nt.name) = $($nt.expression)") + } + } else { + $lines.Add(" (all params trivial)") + } + } + } + + if (-not $found) { + Write-Error "Group or field '$Name' not found in template bindings" + exit 1 + } + } +} + +# --- Output --- + +$result = $lines.ToArray() +$totalLines = $result.Count + +# OutFile +if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines((Join-Path (Get-Location) $OutFile), $result, $utf8Bom) + Write-Host "Written $totalLines lines to $OutFile" + exit 0 +} + +# Pagination +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $result = $result[$Offset..($totalLines - 1)] +} + +if ($result.Count -gt $Limit) { + $shown = $result[0..($Limit - 1)] + foreach ($l in $shown) { Write-Host $l } + Write-Host "" + Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." +} else { + foreach ($l in $result) { Write-Host $l } +} diff --git a/.claude/skills/skd-validate/SKILL.md b/.claude/skills/skd-validate/SKILL.md new file mode 100644 index 00000000..eeba9eb7 --- /dev/null +++ b/.claude/skills/skd-validate/SKILL.md @@ -0,0 +1,74 @@ +--- +name: skd-validate +description: Валидация структурной корректности схемы компоновки данных 1С (СКД) — Template.xml +argument-hint: [-MaxErrors 20] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /skd-validate — валидация СКД (DataCompositionSchema) + +Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml или каталогу макета (авто-резолв в `Ext/Template.xml`) | +| `MaxErrors` | Макс. ошибок до остановки (по умолчанию 20) | +| `OutFile` | Записать результат в файл | + +```powershell +powershell.exe -NoProfile -File .claude\skills\skd-validate\scripts\skd-validate.ps1 -TemplatePath "<путь>" +``` + +## Проверки (~30) + +| Группа | Что проверяется | +|--------|-----------------| +| **Root** | XML parse, корневой элемент `DataCompositionSchema`, default namespace, ns-префиксы | +| **DataSource** | Наличие, name не пуст, type валиден (Local/External), уникальность имён | +| **DataSet** | Наличие, xsi:type валиден, name не пуст, уникальность, ссылка на dataSource, query не пуст | +| **Fields** | dataPath не пуст, field не пуст, уникальность dataPath в наборе | +| **Links** | source/dest ссылаются на существующие наборы, expressions не пусты | +| **CalcFields** | dataPath не пуст, expression не пуст, уникальность, коллизии с полями наборов | +| **TotalFields** | dataPath не пуст, expression не пуст | +| **Parameters** | name не пуст, уникальность | +| **Templates** | name не пуст, уникальность | +| **GroupTemplates** | template ссылается на существующий template, templateType валиден | +| **Variants** | Наличие, name не пуст, settings element присутствует | +| **Settings** | selection/filter/order ссылаются на известные поля, comparisonType валиден, structure items типизированы | + +## Коды выхода + +| Код | Значение | +|-----|----------| +| 0 | Ошибок нет (могут быть предупреждения) | +| 1 | Есть ошибки | + +## Пример вывода + +``` +=== Validation: Template.xml === + +[OK] XML parsed successfully +[OK] Root element: DataCompositionSchema +[OK] Default namespace correct +[OK] 1 dataSource(s) found, names unique +[OK] 1 dataSet(s) found, names unique +[OK] DataSet "НаборДанных1": 2 fields, dataPath unique +[OK] 1 totalField(s): dataPath and expression present +[OK] 1 settingsVariant(s) found + +=== Result: 0 errors, 0 warnings === +``` + +## Верификация + +``` +/skd-compile — генерация XML +/skd-validate — проверка результата +/skd-info — визуальная сводка +``` diff --git a/.claude/skills/skd-validate/scripts/skd-validate.ps1 b/.claude/skills/skd-validate/scripts/skd-validate.ps1 new file mode 100644 index 00000000..2baa86c0 --- /dev/null +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -0,0 +1,719 @@ +# skd-validate v1.0 — Validate 1C DCS structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$TemplatePath, + + [int]$MaxErrors = 20, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path +$fileName = [System.IO.Path]::GetFileName($resolvedPath) + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 4096 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + Out-Line "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ===" + + $result = $script:output.ToString() + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +Out-Line "=== Validation: $fileName ===" +Out-Line "" + +# --- 1. Parse XML --- + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) + Report-OK "XML parsed successfully" +} catch { + Report-Error "XML parse failed: $($_.Exception.Message)" + # Cannot continue + $result = $script:output.ToString() + Write-Host $result + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + } + exit 1 +} + +# --- 2. Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("s", "http://v8.1c.ru/8.1/data-composition-system/schema") +$ns.AddNamespace("dcscom", "http://v8.1c.ru/8.1/data-composition-system/common") +$ns.AddNamespace("dcscor", "http://v8.1c.ru/8.1/data-composition-system/core") +$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("dcsat", "http://v8.1c.ru/8.1/data-composition-system/area-template") + +$root = $xmlDoc.DocumentElement + +# --- 3. Root element checks --- + +if ($root.LocalName -ne "DataCompositionSchema") { + Report-Error "Root element is '$($root.LocalName)', expected 'DataCompositionSchema'" +} else { + Report-OK "Root element: DataCompositionSchema" +} + +$expectedNs = "http://v8.1c.ru/8.1/data-composition-system/schema" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "Default namespace is '$($root.NamespaceURI)', expected '$expectedNs'" +} else { + Report-OK "Default namespace correct" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 4. Collect inventories --- + +# DataSources +$dataSourceNodes = $root.SelectNodes("s:dataSource", $ns) +$dataSourceNames = @{} +foreach ($dsn in $dataSourceNodes) { + $name = $dsn.SelectSingleNode("s:name", $ns) + if ($name) { $dataSourceNames[$name.InnerText] = $true } +} + +# DataSets (recursive for unions) +$dataSetNodes = $root.SelectNodes("s:dataSet", $ns) +$dataSetNames = @{} +$allFieldPaths = @{} # Global: dataPath → dataSet name + +function Collect-DataSetFields { + param($dsNode, [string]$dsName) + + $fields = $dsNode.SelectNodes("s:field", $ns) + $localPaths = @{} + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if ($dp) { + $path = $dp.InnerText + $localPaths[$path] = $true + $allFieldPaths[$path] = $dsName + } + } + + # Union items + $items = $dsNode.SelectNodes("s:item", $ns) + foreach ($item in $items) { + $itemName = $item.SelectSingleNode("s:name", $ns) + if ($itemName) { + Collect-DataSetFields -dsNode $item -dsName $itemName.InnerText + } + } + + return $localPaths +} + +$dataSetFieldMap = @{} # dsName → hashtable of dataPath +foreach ($ds in $dataSetNodes) { + $nameNode = $ds.SelectSingleNode("s:name", $ns) + if ($nameNode) { + $dsName = $nameNode.InnerText + $dataSetNames[$dsName] = $true + $dataSetFieldMap[$dsName] = Collect-DataSetFields -dsNode $ds -dsName $dsName + } +} + +# CalculatedFields +$calcFieldNodes = $root.SelectNodes("s:calculatedField", $ns) +$calcFieldPaths = @{} +foreach ($cf in $calcFieldNodes) { + $dp = $cf.SelectSingleNode("s:dataPath", $ns) + if ($dp) { $calcFieldPaths[$dp.InnerText] = $true } +} + +# TotalFields +$totalFieldNodes = $root.SelectNodes("s:totalField", $ns) + +# Parameters +$paramNodes = $root.SelectNodes("s:parameter", $ns) +$paramNames = @{} +foreach ($p in $paramNodes) { + $nameNode = $p.SelectSingleNode("s:name", $ns) + if ($nameNode) { $paramNames[$nameNode.InnerText] = $true } +} + +# Templates +$templateNodes = $root.SelectNodes("s:template", $ns) +$templateNames = @{} +foreach ($t in $templateNodes) { + $nameNode = $t.SelectSingleNode("s:name", $ns) + if ($nameNode) { $templateNames[$nameNode.InnerText] = $true } +} + +# GroupTemplates +$groupTemplateNodes = $root.SelectNodes("s:groupTemplate", $ns) + +# SettingsVariants +$variantNodes = $root.SelectNodes("s:settingsVariant", $ns) + +# Known fields = dataset fields + calculated fields +$knownFields = @{} +foreach ($key in $allFieldPaths.Keys) { $knownFields[$key] = $true } +foreach ($key in $calcFieldPaths.Keys) { $knownFields[$key] = $true } + +# --- 5. DataSource checks --- + +if ($dataSourceNodes.Count -eq 0) { + Report-Warn "No dataSource elements found (settings-only DCS?)" +} else { + $dsNamesSeen = @{} + $dsOk = $true + foreach ($dsn in $dataSourceNodes) { + $name = $dsn.SelectSingleNode("s:name", $ns) + $type = $dsn.SelectSingleNode("s:dataSourceType", $ns) + if (-not $name -or -not $name.InnerText) { + Report-Error "DataSource has empty name" + $dsOk = $false + } elseif ($dsNamesSeen.ContainsKey($name.InnerText)) { + Report-Error "Duplicate dataSource name: $($name.InnerText)" + $dsOk = $false + } else { + $dsNamesSeen[$name.InnerText] = $true + } + if ($type) { + $tv = $type.InnerText + if ($tv -ne "Local" -and $tv -ne "External") { + Report-Warn "DataSource '$($name.InnerText)' has unusual type: $tv" + } + } + } + if ($dsOk) { + Report-OK "$($dataSourceNodes.Count) dataSource(s) found, names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 6. DataSet checks --- + +$validDsTypes = @("DataSetQuery", "DataSetObject", "DataSetUnion") + +if ($dataSetNodes.Count -eq 0) { + Report-Warn "No dataSet elements found (settings-only DCS?)" +} else { + $dsNamesSeen = @{} + $dsOk = $true + foreach ($ds in $dataSetNodes) { + $xsiType = $ds.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $nameNode = $ds.SelectSingleNode("s:name", $ns) + $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } + + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "DataSet has empty name" + $dsOk = $false + } elseif ($dsNamesSeen.ContainsKey($dsName)) { + Report-Error "Duplicate dataSet name: $dsName" + $dsOk = $false + } else { + $dsNamesSeen[$dsName] = $true + } + + if (-not $xsiType) { + Report-Error "DataSet '$dsName' missing xsi:type" + $dsOk = $false + } elseif ($validDsTypes -notcontains $xsiType) { + Report-Warn "DataSet '$dsName' has unusual xsi:type: $xsiType" + } + + # Check dataSource reference + if ($xsiType -ne "DataSetUnion") { + $srcNode = $ds.SelectSingleNode("s:dataSource", $ns) + if ($srcNode -and $srcNode.InnerText) { + if (-not $dataSourceNames.ContainsKey($srcNode.InnerText)) { + Report-Error "DataSet '$dsName' references unknown dataSource: $($srcNode.InnerText)" + $dsOk = $false + } + } + } + + # Check query not empty for Query type + if ($xsiType -eq "DataSetQuery") { + $queryNode = $ds.SelectSingleNode("s:query", $ns) + if (-not $queryNode -or -not $queryNode.InnerText.Trim()) { + Report-Warn "DataSet '$dsName' (Query) has empty query" + } + } + + # Check objectName for Object type + if ($xsiType -eq "DataSetObject") { + $objNode = $ds.SelectSingleNode("s:objectName", $ns) + if (-not $objNode -or -not $objNode.InnerText.Trim()) { + Report-Error "DataSet '$dsName' (Object) has empty objectName" + $dsOk = $false + } + } + } + + if ($dsOk) { + Report-OK "$($dataSetNodes.Count) dataSet(s) found, names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 7. Field checks --- + +function Check-DataSetFields { + param($dsNode, [string]$dsName) + + $fields = $dsNode.SelectNodes("s:field", $ns) + if ($fields.Count -eq 0) { return } + + $pathsSeen = @{} + $fieldOk = $true + + foreach ($f in $fields) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + $fn = $f.SelectSingleNode("s:field", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "DataSet '$dsName': field has empty dataPath" + $fieldOk = $false + continue + } + + $path = $dp.InnerText + if ($pathsSeen.ContainsKey($path)) { + Report-Warn "DataSet '$dsName': duplicate dataPath '$path'" + } else { + $pathsSeen[$path] = $true + } + + if (-not $fn -or -not $fn.InnerText) { + Report-Warn "DataSet '$dsName': field '$path' has empty element" + } + } + + if ($fieldOk) { + Report-OK "DataSet `"$dsName`": $($fields.Count) fields, dataPath unique" + } + + # Check union items recursively + $items = $dsNode.SelectNodes("s:item", $ns) + foreach ($item in $items) { + $itemName = $item.SelectSingleNode("s:name", $ns) + $iName = if ($itemName) { $itemName.InnerText } else { "(unnamed item)" } + Check-DataSetFields -dsNode $item -dsName $iName + } +} + +foreach ($ds in $dataSetNodes) { + $nameNode = $ds.SelectSingleNode("s:name", $ns) + $dsName = if ($nameNode) { $nameNode.InnerText } else { "(unnamed)" } + Check-DataSetFields -dsNode $ds -dsName $dsName +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 8. DataSetLink checks --- + +$linkNodes = $root.SelectNodes("s:dataSetLink", $ns) +if ($linkNodes.Count -gt 0) { + $linkOk = $true + foreach ($link in $linkNodes) { + $src = $link.SelectSingleNode("s:sourceDataSet", $ns) + $dst = $link.SelectSingleNode("s:destinationDataSet", $ns) + $srcExpr = $link.SelectSingleNode("s:sourceExpression", $ns) + $dstExpr = $link.SelectSingleNode("s:destinationExpression", $ns) + + if ($src -and $src.InnerText -and -not $dataSetNames.ContainsKey($src.InnerText)) { + Report-Error "DataSetLink: sourceDataSet '$($src.InnerText)' not found" + $linkOk = $false + } + if ($dst -and $dst.InnerText -and -not $dataSetNames.ContainsKey($dst.InnerText)) { + Report-Error "DataSetLink: destinationDataSet '$($dst.InnerText)' not found" + $linkOk = $false + } + if (-not $srcExpr -or -not $srcExpr.InnerText.Trim()) { + Report-Error "DataSetLink: empty sourceExpression" + $linkOk = $false + } + if (-not $dstExpr -or -not $dstExpr.InnerText.Trim()) { + Report-Error "DataSetLink: empty destinationExpression" + $linkOk = $false + } + } + if ($linkOk) { + Report-OK "$($linkNodes.Count) dataSetLink(s): references valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 9. CalculatedField checks --- + +if ($calcFieldNodes.Count -gt 0) { + $cfOk = $true + $cfSeen = @{} + foreach ($cf in $calcFieldNodes) { + $dp = $cf.SelectSingleNode("s:dataPath", $ns) + $expr = $cf.SelectSingleNode("s:expression", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "CalculatedField has empty dataPath" + $cfOk = $false + continue + } + + $path = $dp.InnerText + if ($cfSeen.ContainsKey($path)) { + Report-Error "Duplicate calculatedField dataPath: $path" + $cfOk = $false + } else { + $cfSeen[$path] = $true + } + + if (-not $expr -or -not $expr.InnerText.Trim()) { + Report-Error "CalculatedField '$path' has empty expression" + $cfOk = $false + } + + # Warn if collides with a dataset field + if ($allFieldPaths.ContainsKey($path)) { + Report-Warn "CalculatedField '$path' shadows dataSet field in '$($allFieldPaths[$path])'" + } + } + + if ($cfOk) { + Report-OK "$($calcFieldNodes.Count) calculatedField(s): dataPath and expression valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 10. TotalField checks --- + +if ($totalFieldNodes.Count -gt 0) { + $tfOk = $true + foreach ($tf in $totalFieldNodes) { + $dp = $tf.SelectSingleNode("s:dataPath", $ns) + $expr = $tf.SelectSingleNode("s:expression", $ns) + + if (-not $dp -or -not $dp.InnerText) { + Report-Error "TotalField has empty dataPath" + $tfOk = $false + continue + } + + if (-not $expr -or -not $expr.InnerText.Trim()) { + Report-Error "TotalField '$($dp.InnerText)' has empty expression" + $tfOk = $false + } + } + + if ($tfOk) { + Report-OK "$($totalFieldNodes.Count) totalField(s): dataPath and expression present" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 11. Parameter checks --- + +if ($paramNodes.Count -gt 0) { + $paramOk = $true + $paramSeen = @{} + foreach ($p in $paramNodes) { + $nameNode = $p.SelectSingleNode("s:name", $ns) + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "Parameter has empty name" + $paramOk = $false + continue + } + $pName = $nameNode.InnerText + if ($paramSeen.ContainsKey($pName)) { + Report-Error "Duplicate parameter name: $pName" + $paramOk = $false + } else { + $paramSeen[$pName] = $true + } + } + if ($paramOk) { + Report-OK "$($paramNodes.Count) parameter(s): names unique" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 12. Template checks --- + +if ($templateNodes.Count -gt 0) { + $tplOk = $true + $tplSeen = @{} + foreach ($t in $templateNodes) { + $nameNode = $t.SelectSingleNode("s:name", $ns) + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "Template has empty name" + $tplOk = $false + continue + } + $tName = $nameNode.InnerText + if ($tplSeen.ContainsKey($tName)) { + Report-Error "Duplicate template name: $tName" + $tplOk = $false + } else { + $tplSeen[$tName] = $true + } + } + if ($tplOk) { + Report-OK "$($templateNodes.Count) template(s): names unique" + } +} + +# --- 13. GroupTemplate checks --- + +if ($groupTemplateNodes.Count -gt 0) { + $gtOk = $true + $validTplTypes = @("Header", "Footer", "Overall", "OverallHeader", "OverallFooter") + foreach ($gt in $groupTemplateNodes) { + $tplRef = $gt.SelectSingleNode("s:template", $ns) + $tplType = $gt.SelectSingleNode("s:templateType", $ns) + + if ($tplRef -and $tplRef.InnerText -and -not $templateNames.ContainsKey($tplRef.InnerText)) { + Report-Error "GroupTemplate references unknown template: $($tplRef.InnerText)" + $gtOk = $false + } + if ($tplType -and $validTplTypes -notcontains $tplType.InnerText) { + Report-Warn "GroupTemplate has unusual templateType: $($tplType.InnerText)" + } + } + if ($gtOk) { + Report-OK "$($groupTemplateNodes.Count) groupTemplate(s): references valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- 14. Settings helper functions --- + +$validComparisonTypes = @( + "Equal","NotEqual","Greater","GreaterOrEqual","Less","LessOrEqual", + "InList","NotInList","InHierarchy","InListByHierarchy", + "Contains","NotContains","BeginsWith","NotBeginsWith", + "Filled","NotFilled" +) + +$validStructureTypes = @( + "dcsset:StructureItemGroup", + "dcsset:StructureItemTable", + "dcsset:StructureItemChart", + "dcsset:StructureItemNestedObject" +) + +function Check-FilterItems { + param($parentNode, [string]$variantName) + + $filterItems = $parentNode.SelectNodes("dcsset:filter/dcsset:item", $ns) + foreach ($fi in $filterItems) { + if ($script:stopped) { return } + $xsiType = $fi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -eq "dcsset:FilterItemComparison") { + $compType = $fi.SelectSingleNode("dcsset:comparisonType", $ns) + if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) { + Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'" + } + } elseif ($xsiType -eq "dcsset:FilterItemGroup") { + $groupType = $fi.SelectSingleNode("dcsset:groupType", $ns) + if ($groupType) { + $validGroupTypes = @("AndGroup","OrGroup","NotGroup") + if ($validGroupTypes -notcontains $groupType.InnerText) { + Report-Warn "Variant '$variantName' filter group: unusual groupType '$($groupType.InnerText)'" + } + } + # Recurse into nested items + $nestedItems = $fi.SelectNodes("dcsset:item", $ns) + foreach ($ni in $nestedItems) { + $niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($niType -eq "dcsset:FilterItemComparison") { + $compType = $ni.SelectSingleNode("dcsset:comparisonType", $ns) + if ($compType -and $validComparisonTypes -notcontains $compType.InnerText) { + Report-Error "Variant '$variantName' filter: invalid comparisonType '$($compType.InnerText)'" + } + } + } + } + } +} + +function Check-StructureItem { + param($itemNode, [string]$variantName) + + if ($script:stopped) { return } + + $xsiType = $itemNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if (-not $xsiType) { + Report-Error "Variant '$variantName': structure item missing xsi:type" + return + } + if ($validStructureTypes -notcontains $xsiType) { + Report-Warn "Variant '$variantName': unusual structure item type '$xsiType'" + } + + # Recurse into nested items (groups can contain groups) + $nestedItems = $itemNode.SelectNodes("dcsset:item", $ns) + foreach ($ni in $nestedItems) { + Check-StructureItem -itemNode $ni -variantName $variantName + } + + # Check column/row in tables + if ($xsiType -eq "dcsset:StructureItemTable") { + $columns = $itemNode.SelectNodes("dcsset:column", $ns) + $rows = $itemNode.SelectNodes("dcsset:row", $ns) + if ($columns.Count -eq 0) { + Report-Warn "Variant '$variantName': table has no columns" + } + if ($rows.Count -eq 0) { + Report-Warn "Variant '$variantName': table has no rows" + } + } +} + +function Check-Settings { + param($settingsNode, [string]$variantName) + + if ($script:stopped) { return } + + # Selection + $selItems = $settingsNode.SelectNodes("dcsset:selection/dcsset:item", $ns) + foreach ($si in $selItems) { + $xsiType = $si.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -eq "dcsset:SelectedItemField") { + $field = $si.SelectSingleNode("dcsset:field", $ns) + if ($field -and $field.InnerText -and $field.InnerText -ne "SystemFields.Number") { + $basePath = ($field.InnerText -split '\.')[0] + if (-not $knownFields.ContainsKey($field.InnerText) -and -not $knownFields.ContainsKey($basePath)) { + # Soft check — autoFillFields may add fields not listed explicitly + } + } + } + } + + # Filter + Check-FilterItems -parentNode $settingsNode -variantName $variantName + + # Order + $orderItems = $settingsNode.SelectNodes("dcsset:order/dcsset:item", $ns) + foreach ($oi in $orderItems) { + $xsiType = $oi.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + if ($xsiType -eq "dcsset:OrderItemField") { + $orderType = $oi.SelectSingleNode("dcsset:orderType", $ns) + if ($orderType -and $orderType.InnerText -ne "Asc" -and $orderType.InnerText -ne "Desc") { + Report-Warn "Variant '$variantName' order: invalid orderType '$($orderType.InnerText)'" + } + } + } + + # Structure items + $structItems = $settingsNode.SelectNodes("dcsset:item", $ns) + foreach ($si in $structItems) { + Check-StructureItem -itemNode $si -variantName $variantName + } +} + +# --- 15. SettingsVariant checks --- + +if ($variantNodes.Count -eq 0) { + Report-Warn "No settingsVariant elements found" +} else { + $vOk = $true + $vIdx = 0 + foreach ($v in $variantNodes) { + $vIdx++ + $vName = $v.SelectSingleNode("dcsset:name", $ns) + if (-not $vName -or -not $vName.InnerText) { + Report-Error "SettingsVariant #$vIdx has empty name" + $vOk = $false + } + + $settings = $v.SelectSingleNode("dcsset:settings", $ns) + if (-not $settings) { + Report-Error "SettingsVariant '$($vName.InnerText)' has no settings element" + $vOk = $false + continue + } + + # Check settings internals + Check-Settings -settingsNode $settings -variantName "$($vName.InnerText)" + } + + if ($vOk) { + Report-OK "$($variantNodes.Count) settingsVariant(s) found" + } +} + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..24af5d17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 Nick Shirokov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a1adf052..e29da5fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 1C Skills for Claude Code +> **Work in progress** — проект в стадии активной разработки. Набор навыков и операций расширяется. + Набор [Claude Code Skills](https://docs.anthropic.com/en/docs/claude-code/skills) для работы с артефактами 1С:Предприятия 8.3. Позволяет создавать и модифицировать обработки, макеты печатных форм и другие объекты из XML-исходников, не запоминая детали формата. ## Быстрый старт @@ -23,6 +25,7 @@ | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | | Управляемые формы (Form) | 6 навыков `/form-*` | Создание, анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) | +| Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -41,6 +44,8 @@ - [Form DSL](docs/form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` - [Роли (Rights.xml)](docs/1c-role-spec.md) — XML-формат прав роли, типы объектов, RLS - [Role DSL](docs/role-dsl-spec.md) — JSON-формат описания ролей для `/role-compile` +- [Схема компоновки данных (DCS)](docs/1c-dcs-spec.md) — XML-формат DataCompositionSchema, 930 схем проанализировано +- [SKD DSL](docs/skd-dsl-spec.md) — JSON-формат описания СКД для `/skd-compile` ## Структура репозитория @@ -69,12 +74,17 @@ ├── role-info/ # Анализ прав роли ├── role-compile/ # Создание роли из JSON DSL ├── role-validate/ # Валидация роли +├── skd-info/ # Анализ схемы компоновки данных +├── skd-compile/ # Компиляция СКД из JSON DSL +├── skd-edit/ # Точечное редактирование СКД (25 операций) +├── skd-validate/ # Валидация СКД └── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки ├── mxl-guide.md # Гайд: табличный документ ├── form-guide.md # Гайд: управляемые формы ├── role-guide.md # Гайд: роли +├── skd-guide.md # Гайд: схема компоновки данных ├── 1c-xml-format-spec.md # Спецификация XML-формата ├── 1c-form-spec.md # Спецификация управляемых форм ├── 1c-help-spec.md # Спецификация встроенной справки @@ -82,5 +92,8 @@ docs/ ├── 1c-spreadsheet-spec.md # Спецификация табличного документа ├── mxl-dsl-spec.md # Спецификация MXL DSL ├── form-dsl-spec.md # Спецификация Form DSL -└── 1c-role-spec.md # Спецификация ролей (Rights.xml) +├── 1c-role-spec.md # Спецификация ролей (Rights.xml) +├── 1c-dcs-spec.md # Спецификация СКД (DataCompositionSchema) +├── skd-dsl-spec.md # Спецификация SKD DSL +└── role-dsl-spec.md # Спецификация Role DSL ``` diff --git a/docs/1c-dcs-spec.md b/docs/1c-dcs-spec.md index 6c07c0a3..cb0ca398 100644 --- a/docs/1c-dcs-spec.md +++ b/docs/1c-dcs-spec.md @@ -299,7 +299,8 @@ DataCompositionSchema | `dcscom:accountTypeExpression` | Выражение для определения типа счёта | | `dcscom:balance` | Поле является остатком | | `dcscom:balanceGroup` | Группа остатка | -| `dcscom:period` | Поле — период | +| `dcscom:periodNumber` | Номер периода (обычно `1`) | +| `dcscom:periodType` | Тип периода (`Main`, `Additional`) | ### 4.7. Тип значения (valueType) @@ -313,7 +314,15 @@ DataCompositionSchema ``` -Типы: `xs:string`, `xs:dateTime`, `xs:decimal`, `xs:boolean`, ссылочные (`d4p1:CatalogRef.Номенклатура`). +Типы: `xs:string`, `xs:dateTime`, `xs:decimal`, `xs:boolean`, ссылочные типы конфигурации. + +Ссылочные типы объявляются с inline namespace на элементе ``: + +```xml +d5p1:CatalogRef.Номенклатура +``` + +Префикс (`d5p1`, `d4p1` и т.д.) — автогенерируемый, суть в URI `http://v8.1c.ru/8.1/data/enterprise/current-config`. Поддерживаются: `CatalogRef`, `DocumentRef`, `EnumRef`, `ChartOfAccountsRef`, `ChartOfCharacteristicTypesRef` и др. Квалификаторы: - `v8:StringQualifiers` → `v8:Length`, `v8:AllowedLength` (Fixed/Variable) @@ -446,7 +455,28 @@ DataCompositionSchema |---|---|---| | `dataPath` | да | Путь к полю | | `expression` | да | Агрегатная функция: `Сумма(...)`, `Количество(...)`, `Максимум(...)`, `Минимум(...)`, `Среднее(...)` | -| `group` | нет | Для какой группировки считать итоги | +| `group` | нет | Имя группировки, для которой считать итоги. Без `group` — для всех группировок | + +### Разные формулы для разных группировок + +Одно поле может иметь несколько записей `totalField` с разными формулами для разных группировок: + +```xml + + + ПравоИнтерактивное + Максимум(ПравоИнтерактивное) + ОбъектМетаданных + + + + ПравоИнтерактивное + Максимум(ПравоОтчета) + Отчет + +``` + +Это позволяет вычислять ресурс по-разному в зависимости от контекста группировки. --- @@ -494,7 +524,7 @@ DataCompositionSchema | Дата | `xs:dateTime` | `0001-01-01T00:00:00` | | Строка | `xs:string` | `Т13` | | Стандартный период | `v8:StandardPeriod` | `LastMonth` | -| Ссылка | `d4p1:CatalogRef.ИмяСправочника` | `xsi:nil="true"` | +| Ссылка | `d5p1:CatalogRef.ИмяСправочника` (с `xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config"`) | `xsi:nil="true"` | | null | — | `xsi:nil="true"` | Стандартные варианты периодов (`v8:StandardPeriodVariant`): `Custom`, `Today`, `ThisWeek`, `ThisMonth`, `ThisQuarter`, `ThisYear`, `LastMonth`, `LastQuarter`, `LastYear` и др. diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md new file mode 100644 index 00000000..9a970d1d --- /dev/null +++ b/docs/skd-dsl-spec.md @@ -0,0 +1,790 @@ +# JSON DSL для схемы компоновки данных (СКД) + +Компактный JSON-формат для описания `DataCompositionSchema` (Template.xml). +Компилируется навыком `/skd-compile` в XML, валидируется `/skd-validate`. + +--- + +## 1. Корневая структура + +```json +{ + "dataSources": [...], + "dataSets": [...], + "dataSetLinks": [...], + "calculatedFields": [...], + "totalFields": [...], + "parameters": [...], + "templates": [...], + "groupTemplates": [...], + "settingsVariants": [...] +} +``` + +**Умолчания:** +- `dataSources` опущен → авто-создаётся `{ "name": "ИсточникДанных1", "type": "Local" }` +- `source` в наборе опущен → первый dataSource +- `name` набора опущен → "НаборДанных1", "НаборДанных2"... +- `settingsVariants` опущен → один вариант "Основной" с детальной группировкой и `selection: ["Auto"]` + +--- + +## 2. Источники данных (dataSources) + +```json +"dataSources": [ + { "name": "ИсточникДанных1", "type": "Local" } +] +``` + +| Поле | Обязат. | Умолчание | XML-маппинг | +|------|---------|-----------|-------------| +| `name` | да | — | `` | +| `type` | нет | `"Local"` | `` | + +Значения `type`: `"Local"`, `"External"`. + +--- + +## 3. Наборы данных (dataSets) + +Тип определяется по ключу-дискриминатору: + +| Ключ | Тип | xsi:type | +|------|-----|----------| +| `query` | Запрос | `DataSetQuery` | +| `objectName` | Объект | `DataSetObject` | +| `items` | Объединение | `DataSetUnion` | + +### DataSetQuery (самый частый) + +```json +{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...], "autoFillFields": false } +``` + +### DataSetObject + +```json +{ "name": "ТаблицаПроверки", "objectName": "ТаблицаПроверки", "fields": [...] } +``` + +### DataSetUnion + +```json +{ + "name": "Объединение", + "items": [ + { "name": "Набор1", "query": "...", "fields": [...] }, + { "name": "Набор2", "query": "...", "fields": [...] } + ], + "fields": [...] +} +``` + +| Поле | Обязат. | Описание | +|------|---------|----------| +| `name` | нет | Авто: "НаборДанных1"... | +| `source` | нет | Имя dataSource (авто: первый) | +| `query` | да* | Текст запроса (DataSetQuery) | +| `objectName` | да* | Имя объекта (DataSetObject) | +| `items` | да* | Вложенные наборы (DataSetUnion) | +| `fields` | нет | Массив полей | +| `autoFillFields` | нет | `false` — отключить автозаполнение (по умолчанию не выводится = true) | + +--- + +## 4. Поля — shorthand и объектная форма + +### Shorthand-строка + +``` +"[: ] [@role...] [#restrict...]" +``` + +Примеры: + +```json +"fields": [ + "Наименование", + "Количество: decimal(15,2)", + "Организация: CatalogRef.Организации @dimension", + "Служебное: string #noFilter #noOrder", + "Счёт: CatalogRef.Хозрасчетный @account", + "Сумма: decimal(15,2) @balance" +] +``` + +### Объектная форма + +```json +{ + "dataPath": "Сумма", + "field": "Сумма", + "title": "Сумма продаж", + "type": "decimal(15,2)", + "role": { "dimension": true }, + "restrict": ["noFilter", "noGroup"], + "attrRestrict": ["noFilter"], + "appearance": { "Формат": "ЧДЦ=2" }, + "presentationExpression": "Формат(Сумма, \"ЧДЦ=2\")" +} +``` + +### Парсинг shorthand + +1. Разделить по пробелам; найти `@`-роли и `#`-ограничения +2. Остаток до первого `:` — `dataPath` (и `field` по умолчанию) +3. После `:` до `@`/`#` — тип + +### Типы + +| DSL | XML v8:Type | Квалификатор | +|-----|-------------|--------------| +| `string` | `xs:string` | Length=0, AllowedLength=Variable | +| `string(N)` | `xs:string` | Length=N, AllowedLength=Variable | +| `decimal(D,F)` | `xs:decimal` | Digits=D, FractionDigits=F, AllowedSign=Any | +| `decimal(D,F,nonneg)` | `xs:decimal` | Digits=D, FractionDigits=F, AllowedSign=Nonnegative | +| `boolean` | `xs:boolean` | — | +| `date` | `xs:dateTime` | DateFractions=Date | +| `dateTime` | `xs:dateTime` | DateFractions=DateTime | +| `CatalogRef.XXX` | `d5p1:CatalogRef.XXX` | inline xmlns:d5p1 | +| `DocumentRef.XXX` | `d5p1:DocumentRef.XXX` | inline xmlns:d5p1 | +| `EnumRef.XXX` | `d5p1:EnumRef.XXX` | inline xmlns:d5p1 | +| `ChartOfAccountsRef.XXX` | `d5p1:ChartOfAccountsRef.XXX` | inline xmlns:d5p1 | +| `StandardPeriod` | `v8:StandardPeriod` | — | + +> **Ссылочные типы** (`CatalogRef.XXX`, `DocumentRef.XXX` и др.) эмитируются с inline namespace declaration: `d5p1:CatalogRef.XXX`. Использование префикса `cfg:` вместо `d5p1:` с объявлением namespace приводит к ошибке XDTO. Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией (не пустую). + +### Синонимы типов + +Все имена типов регистронезависимые. Поддерживаются русские и альтернативные имена: + +| Синоним | Канонический тип | +|---------|-----------------| +| `число`, `Число` | `decimal` | +| `строка`, `Строка` | `string` | +| `булево`, `Булево`, `bool` | `boolean` | +| `дата`, `Дата` | `date` | +| `датаВремя`, `ДатаВремя` | `dateTime` | +| `СтандартныйПериод` | `StandardPeriod` | +| `int`, `integer`, `number`, `num` | `decimal` | +| `СправочникСсылка.XXX` | `CatalogRef.XXX` | +| `ДокументСсылка.XXX` | `DocumentRef.XXX` | +| `ПеречислениеСсылка.XXX` | `EnumRef.XXX` | +| `ПланСчетовСсылка.XXX` | `ChartOfAccountsRef.XXX` | +| `ПланВидовХарактеристикСсылка.XXX` | `ChartOfCharacteristicTypesRef.XXX` | + +Параметризованные: `число(15,2)` → `decimal(15,2)`, `строка(100)` → `string(100)`. + +### Роли + +| DSL shorthand | Объектная форма | XML | +|---------------|----------------|-----| +| `@dimension` | `"role": "dimension"` или `{"dimension": true}` | `true` | +| `@account` | `"role": "account"` или `{"account": true}` | `true` | +| `@balance` | `"role": "balance"` или `{"balance": true}` | `true` | +| `@period` | `"role": "period"` или `{"period": true}` | `1` + `Main` | + +Объектная форма с доп. полями: +```json +"role": { + "account": true, + "accountTypeExpression": "Счёт.ВидСчёта", + "balanceGroup": "/Остатки" +} +``` + +### Ограничения + +| DSL shorthand | Объектная форма | XML useRestriction | +|---------------|----------------|-----| +| `#noField` | `"noField"` | `true` | +| `#noFilter` / `#noCondition` | `"noFilter"` | `true` | +| `#noGroup` | `"noGroup"` | `true` | +| `#noOrder` | `"noOrder"` | `true` | + +### Оформление (appearance) + +```json +"appearance": { + "Формат": "ЧДЦ=2", + "ГоризонтальноеПоложение": "Center" +} +``` + +Маппинг на XML: +```xml + + + Формат + ЧДЦ=2 + + +``` + +Значения `ГоризонтальноеПоложение` → `xsi:type="v8ui:HorizontalAlign"`. + +--- + +## 5. Итоговые поля (totalFields) + +### Shorthand + +``` +": <Функция>" +": <Функция>(<выражение>)" +``` + +Примеры: + +```json +"totalFields": [ + "Количество: Сумма", + "Цена: Максимум", + "Стоимость: Сумма(Кол * Цена)" +] +``` + +**Парсинг:** `"A: Func"` → `dataPath=A`, `expression=Func(A)`. `"A: Func(expr)"` → `dataPath=A`, `expression=Func(expr)`. + +Функции (русские): `Сумма`, `Количество`, `Максимум`, `Минимум`, `Среднее`. + +### Объектная форма + +```json +{ "dataPath": "X", "expression": "Максимум(X)", "group": "Группа1" } +``` + +### Привязка к группировкам (group) + +В объектной форме поле `group` может быть строкой или массивом строк. Каждая строка задаёт имя группировки, для которой вычисляется итог: + +```json +"totalFields": [ + { "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["ГруппаПользователей", "ГруппаПользователей Иерархия", "ОбщийИтог"] } +] +``` + +XML-маппинг — по `` на каждый элемент: +```xml + + Кол + Сумма(Кол) + ГруппаПользователей + ГруппаПользователей Иерархия + ОбщийИтог + +``` + +--- + +## 6. Параметры (parameters) + +### Shorthand + +``` +": [= ] [@autoDates]" +``` + +Примеры: + +```json +"parameters": [ + "Период: StandardPeriod = LastMonth @autoDates", + "Организация: CatalogRef.Организации", + "ДатаОтчета: date" +] +``` + +**Парсинг:** `"A: T = V"` → `name=A`, `type=T`, `value=V`. Значение `LastMonth` и другие варианты периодов → `v8:StandardPeriod` с `v8:variant`. + +### @autoDates + +Флаг `@autoDates` в shorthand параметра автоматически генерирует два дополнительных параметра: +- `ДатаНачала` (date, expression=`&<Имя>.ДатаНачала`, availableAsField=false) +- `ДатаОкончания` (date, expression=`&<Имя>.ДатаОкончания`, availableAsField=false) + +Заменяет типовой бойлерплейт из 5 строк на 1: + +```json +// Было: +"parameters": [ + "Период: StandardPeriod = LastMonth", + { "name": "ДатаНачала", "type": "date", "expression": "&Период.ДатаНачала", "availableAsField": false }, + { "name": "ДатаОкончания", "type": "date", "expression": "&Период.ДатаОкончания", "availableAsField": false } +] + +// Стало: +"parameters": ["Период: StandardPeriod = LastMonth @autoDates"] +``` + +### Объектная форма + +```json +{ + "name": "ДатаНач", + "title": "Дата начала", + "type": "date", + "value": "0001-01-01T00:00:00", + "expression": "&Период.ДатаНачала", + "availableAsField": false, + "useRestriction": true, + "use": "Always" +} +``` + +| Поле | Описание | +|------|----------| +| `name` | Имя параметра | +| `title` | Заголовок (умолч. = name) | +| `type` | Тип (см. таблицу типов) | +| `value` | Значение по умолчанию | +| `expression` | Выражение для вычисления | +| `availableAsField` | `false` — скрыть из полей | +| `useRestriction` | `true` — скрыть от пользователя | +| `use` | `"Always"`, `"Auto"` | + +### Значения параметров по типу + +| Тип | value | XML | +|-----|-------|-----| +| `StandardPeriod` | `"LastMonth"`, `"ThisYear"` и др. | `LastMonth` | +| `date` | `"0001-01-01T00:00:00"` | `xsi:type="xs:dateTime"` | +| `string` | `"текст"` | `xsi:type="xs:string"` | +| `boolean` | `true`/`false` | `xsi:type="xs:boolean"` | + +Стандартные варианты периодов: `Custom`, `Today`, `ThisWeek`, `ThisMonth`, `ThisQuarter`, `ThisYear`, `LastMonth`, `LastQuarter`, `LastYear`. + +--- + +## 7. Вычисляемые поля (calculatedFields) + +### Shorthand + +``` +" = " +``` + +```json +"calculatedFields": [ + "УИД = Строка(Ссылка.УникальныйИдентификатор())", + "Итого = Количество * Цена" +] +``` + +### Объектная форма + +```json +{ + "dataPath": "Итого", + "expression": "Количество * Цена", + "title": "Итого", + "type": "decimal(15,2)", + "restrict": ["noGroup"], + "appearance": { "Формат": "ЧДЦ=2" } +} +``` + +--- + +## 8. Связи наборов (dataSetLinks) + +Только объектная форма: + +```json +"dataSetLinks": [ + { + "source": "Периоды", + "dest": "Данные", + "sourceExpr": "Месяц", + "destExpr": "Месяц", + "parameter": "НачалоМесяца" + } +] +``` + +| Поле | XML | +|------|-----| +| `source` | `` | +| `dest` | `` | +| `sourceExpr` | `` | +| `destExpr` | `` | +| `parameter` | `` (опц.) | + +--- + +## 9. Варианты настроек (settingsVariants) + +```json +"settingsVariants": [{ + "name": "Основной", + "presentation": "Основной вариант", + "settings": { + "selection": [...], + "filter": [...], + "order": [...], + "conditionalAppearance": [...], + "outputParameters": {...}, + "dataParameters": [...], + "structure": [...] + } +}] +``` + +### selection + +```json +"selection": [ + "Наименование", + { "field": "Количество", "title": "Кол-во" }, + "Auto" +] +``` + +- Строка → `SelectedItemField` +- `"Auto"` → `SelectedItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется) +- Объект с `field`/`title` → `SelectedItemField` с `lwsTitle` + +### filter + +#### Shorthand-строка + +```json +"filter": [ + "Организация = _ @off @user", + "Дата >= 2024-01-01T00:00:00", + "Статус filled", + "Количество > 0" +] +``` + +Формат: `"<Поле> <оператор> [<значение>] [@off] [@user] [@quickAccess] [@normal] [@inaccessible]"`. + +- Значение `_` — пустое (placeholder, не выводится в XML) +- `@off` → `use=false` +- `@user` → `userSettingID=auto` (генерировать GUID) +- `@quickAccess` → `viewMode=QuickAccess` +- `@normal` → `viewMode=Normal` +- `@inaccessible` → `viewMode=Inaccessible` +- Типы значений автоопределяются: `true`/`false` → boolean, `2024-01-01T00:00:00` → dateTime, числа → decimal, прочее → string + +#### Объектная форма + +```json +"filter": [ + { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }, + { "field": "Дата", "op": ">=", "value": "0001-01-01T00:00:00", "valueType": "xs:dateTime" }, + { "group": "Or", "items": [ + { "field": "Статус", "op": "=", "value": true, "valueType": "xs:boolean" }, + { "field": "Пометка", "op": "filled" } + ]} +] +``` + +| Поле | Описание | +|------|----------| +| `field` | Имя поля | +| `op` | Оператор (см. таблицу) | +| `value` | Правая часть (опц.) | +| `valueType` | xsi:type для значения (опц.) | +| `use` | Включён (`true` по умолчанию) | +| `presentation` | Текст подсказки | +| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` | +| `userSettingID` | `"auto"` → генерировать GUID | +| `userSettingPresentation` | Отображаемое имя настройки (LocalStringType) | + +Операторы: + +| DSL | XML comparisonType | +|-----|--------------------| +| `=` | `Equal` | +| `<>` | `NotEqual` | +| `>` | `Greater` | +| `>=` | `GreaterOrEqual` | +| `<` | `Less` | +| `<=` | `LessOrEqual` | +| `in` | `InList` | +| `notIn` | `NotInList` | +| `inHierarchy` | `InHierarchy` | +| `contains` | `Contains` | +| `notContains` | `NotContains` | +| `beginsWith` | `BeginsWith` | +| `filled` | `Filled` | +| `notFilled` | `NotFilled` | + +Группа условий: `{ "group": "And"|"Or"|"Not", "items": [...] }` → `FilterItemGroup` с `groupType`. + +### order + +```json +"order": ["Количество desc", "Наименование", "Auto"] +``` + +- `"Field"` → `OrderItemField`, `orderType=Asc` +- `"Field desc"` → `OrderItemField`, `orderType=Desc` +- `"Field asc"` → `OrderItemField`, `orderType=Asc` +- `"Auto"` → `OrderItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется) + +### conditionalAppearance + +Условное оформление — массив правил, каждое задаёт набор полей (selection), условия (filter), параметры оформления (appearance) и мета-атрибуты. + +```json +"conditionalAppearance": [ + { + "selection": ["Сумма"], + "filter": ["Сумма > 1000"], + "appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" }, + "presentation": "Выделять крупные суммы", + "viewMode": "Normal", + "userSettingID": "auto" + }, + { + "filter": ["Статус notFilled"], + "appearance": { "Текст": "Не указано", "ЦветТекста": "web:Gray" }, + "presentation": "Скрывать пустые статусы" + } +] +``` + +| Поле | Описание | +|------|----------| +| `selection` | Массив полей, к которым применяется. Пусто/опущено = все поля | +| `filter` | Условия (shorthand-строки или объекты, как в settings filter) | +| `appearance` | Объект `{ "Параметр": "Значение" }` | +| `presentation` | Описание правила | +| `use` | Включено (`true` по умолчанию) | +| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` | +| `userSettingID` | `"auto"` → генерировать GUID | + +**Типы значений appearance** определяются автоматически: +- `style:XXX`, `web:XXX`, `win:XXX` → `v8ui:Color` +- `true`/`false` → `xs:boolean` +- Параметр `Текст` или `Заголовок` → `v8:LocalStringType` +- Прочее → `xs:string` + +Поддержка `use=false` на уровне параметра: +```json +"appearance": { + "ЦветФона": { "value": "web:LightGray", "use": false } +} +``` + +### outputParameters + +```json +"outputParameters": { + "Заголовок": "Мой отчёт", + "ВыводитьЗаголовок": "Output", + "МакетОформления": "ОформлениеОтчетовЧерноБелый" +} +``` + +Ключ → `dcscor:parameter`, значение → `dcscor:value`. + +Типы значений определяются автоматически: +- `"Заголовок"` → `v8:LocalStringType` +- `"ВыводитьЗаголовок"`, `"ВыводитьПараметрыДанных"`, `"ВыводитьОтбор"` → `dcsset:DataCompositionTextOutputType` +- `"РасположениеПолейГруппировки"` → `dcsset:DataCompositionGroupFieldsPlacement` +- `"РасположениеРеквизитов"` → `dcsset:DataCompositionAttributesPlacement` +- `"ГоризонтальноеРасположениеОбщихИтогов"`, `"ВертикальноеРасположениеОбщихИтогов"` → `dcscor:DataCompositionTotalPlacement` +- Прочие → `xs:string` + +### dataParameters + +#### Shorthand-строка + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"<Имя> [= <значение>] [@off] [@user] [@quickAccess] [@normal] [@inaccessible]"`. + +- Значения-варианты периодов (`LastMonth`, `ThisYear` и др.) автоматически оборачиваются в `v8:StandardPeriod` +- `@off` → `use=false`, `@user` → `userSettingID=auto` + +#### Объектная форма + +```json +"dataParameters": [ + { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" }, + { "parameter": "Организация", "use": false, "viewMode": "Normal", "userSettingID": "auto", "userSettingPresentation": "Организация отчёта" } +] +``` + +| Поле | Описание | +|------|----------| +| `parameter` | Имя параметра | +| `value` | Значение (объект `{ "variant": "LastMonth" }` для StandardPeriod, или скаляр) | +| `use` | Включён (`true` по умолчанию) | +| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` | +| `userSettingID` | `"auto"` → генерировать GUID | +| `userSettingPresentation` | Отображаемое имя настройки (LocalStringType) | + +### structure + +#### String shorthand (рекомендуется для типичных случаев) + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +"structure": "Период > Организация > Номенклатура > details" +``` + +`>` разделяет уровни вложенности. Каждый сегмент — группировка по указанному полю. `details` (или `детали`) — детальные записи (пустой `groupBy`). Для каждого уровня `selection` и `order` автоматически `["Auto"]`. + +#### Массив объектов + +```json +"structure": [ + { + "type": "group", + "groupBy": ["Организация"], + "children": [ + { "type": "group" } + ] + } +] +``` + +**Умолчания:** `selection` и `order` по умолчанию `["Auto"]` на каждом уровне (в группировках, строках/колонках таблиц, точках/сериях диаграмм). Указывать явно нужно только если требуется другой набор полей. + +#### Группировка (group) + +| Поле | Описание | +|------|----------| +| `type` | `"group"` | +| `name` | Имя группировки (опц.) | +| `groupBy` | Массив полей. Пусто/опущено = детальные записи | +| `groupType` | `"Items"` (умолч.), `"Hierarchy"`, `"HierarchyOnly"` | +| `selection` | Выборка (умолч. `["Auto"]`) | +| `filter` | Отборы (как в settings) | +| `order` | Сортировка (умолч. `["Auto"]`) | +| `outputParameters` | Параметры вывода (как в settings) | +| `children` | Вложенные элементы структуры | + +Пустой `groupBy` (или `[]`) = детальные записи (без `groupItems` в XML). + +#### Таблица (table) + +```json +{ + "type": "table", + "name": "Таблица", + "rows": [ + { "groupBy": ["Номенклатура"], "selection": ["Auto"], "order": ["Auto"] } + ], + "columns": [ + { "groupBy": ["Период"], "selection": ["Auto"], "order": ["Auto"] } + ] +} +``` + +#### Диаграмма (chart) + +```json +{ + "type": "chart", + "points": { "groupBy": ["Организация"], "order": ["Auto"] }, + "series": { "groupBy": ["Месяц"], "order": ["Auto"] }, + "selection": ["Сумма"] +} +``` + +--- + +## 10. Макеты и привязки (templates, groupTemplates) + +Редко используются. Поддерживаются в объектной форме, близкой к XML. + +### templates + +```json +"templates": [ + { + "name": "Макет1", + "template": "", + "parameters": [ + { "name": "ТипЦены", "expression": "Представление(ТипЦен)" } + ] + } +] +``` + +### groupTemplates + +```json +"groupTemplates": [ + { "groupField": "ТипЦен", "templateType": "Header", "template": "Макет1" } +] +``` + +--- + +## 11. Полный пример — минимальный + +```json +{ + "dataSets": [ + { + "name": "НаборДанных1", + "query": "ВЫБРАТЬ\n\tНоменклатура.Наименование КАК Наименование,\n\tКОЛИЧЕСТВО(1) КАК Количество\nИЗ\n\tСправочник.Номенклатура КАК Номенклатура\nСГРУППИРОВАТЬ ПО\n\tНоменклатура.Наименование", + "fields": [ + { "dataPath": "Наименование", "title": "Наименование" }, + "Количество" + ] + } + ], + "totalFields": ["Количество: Сумма"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Наименование", "Количество"], + "structure": [{ "type": "group" }] + } + }] +} +``` + +## 12. Полный пример — средний (с shorthand v2) + +```json +{ + "dataSets": [ + { + "name": "Продажи", + "query": "ВЫБРАТЬ\n\tПродажи.Организация,\n\tПродажи.Номенклатура,\n\tПродажи.Количество,\n\tПродажи.Сумма\nИЗ\n\tРегистрНакопления.Продажи КАК Продажи\n{ГДЕ\n\tПродажи.Период >= &ДатаНачала\n\tИ Продажи.Период < &ДатаОкончания}", + "fields": [ + "Организация: СправочникСсылка.Организации @dimension", + "Номенклатура: СправочникСсылка.Номенклатура @dimension", + "Количество: число(15,3)", + "Сумма: число(15,2)" + ] + } + ], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], + "parameters": [ + "Период: СтандартныйПериод = LastMonth @autoDates" + ], + "settingsVariants": [{ + "name": "Основной", + "presentation": "Продажи по организациям", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "filter": ["Организация = _ @off @user"], + "order": ["Сумма desc", "Auto"], + "outputParameters": { + "Заголовок": "Анализ продаж", + "ВыводитьЗаголовок": "Output" + }, + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } + }] +} +``` + +**Сравнение с v1:** средний пример сократился с 58 до 33 строк (−43%). Основная экономия: `@autoDates` (−4 строки), structure shorthand (−9 строк), filter/dataParam shorthand (−4 строки). diff --git a/docs/skd-guide.md b/docs/skd-guide.md new file mode 100644 index 00000000..7da5f855 --- /dev/null +++ b/docs/skd-guide.md @@ -0,0 +1,177 @@ +# Схема компоновки данных (СКД) + +Навыки группы `/skd-*` позволяют анализировать, создавать, редактировать и проверять схемы компоновки данных 1С — XML-файлы DataCompositionSchema (Template.xml). + +## Навыки + +| Навык | Параметры | Описание | +|-------|-----------|----------| +| `/skd-info` | ` [-Mode] [-Name]` | Анализ структуры СКД: наборы, поля, параметры, ресурсы, варианты (10 режимов) | +| `/skd-compile` | ` ` | Генерация Template.xml из JSON DSL: наборы, поля, итоги, параметры, варианты | +| `/skd-edit` | ` -Operation -Value ""` | Точечное редактирование: 25 атомарных операций (add/set/modify/clear/remove) | +| `/skd-validate` | ` [-MaxErrors 20]` | Валидация структурной корректности: ~30 проверок | + +## Рабочий цикл + +``` +Описание отчёта (текст) → JSON DSL → /skd-compile → Template.xml → /skd-validate + ↕ /skd-edit → /skd-info +``` + +1. Claude формирует JSON-определение СКД (shorthand-поля, параметры, итоги, варианты) +2. `/skd-compile` генерирует Template.xml с корректными namespace, типами, группировками +3. `/skd-edit` вносит точечные изменения: добавление полей, фильтров, наборов данных, вариантов, условного оформления и т.д. +4. `/skd-validate` проверяет корректность XML +5. `/skd-info` выводит компактную сводку для визуальной проверки + +## JSON DSL — компактный формат + +СКД описываются в JSON с двумя уровнями детализации для каждой секции: + +### Минимальный пример + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Номенклатура.Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": ["Наименование"] + }] +} +``` + +Умолчания: dataSource создаётся автоматически (`ИсточникДанных1/Local`), набор получает имя `НаборДанных1`, вариант настроек "Основной" с деталями. + +### Поля — shorthand + +```json +"fields": [ + "Наименование", + "Количество: decimal(15,2)", + "Организация: CatalogRef.Организации @dimension", + "Служебное: string #noFilter #noOrder" +] +``` + +Формат: `Имя[: Тип] [@роль...] [#ограничение...]`. Роли: `@dimension`, `@account`, `@balance`, `@period`. Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`. + +### Итоги — shorthand + +```json +"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] +``` + +Формат: `Поле: Функция` или `Поле: Функция(выражение)`. Объектная форма поддерживает привязку к группировкам: `{ "dataPath": "X", "expression": "Сумма(X)", "group": ["Группа1", "ОбщийИтог"] }`. + +### Параметры — shorthand + @autoDates + +```json +"parameters": [ + "Период: StandardPeriod = LastMonth @autoDates", + "Организация: CatalogRef.Организации" +] +``` + +`@autoDates` автоматически генерирует параметры `ДатаНачала`/`ДатаОкончания` (заменяет 5 строк на 1). + +### Вычисляемые поля — shorthand + +```json +"calculatedFields": ["Итого = Количество * Цена"] +``` + +### Варианты настроек — shorthand + +```json +"settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма"], + "filter": ["Организация = _ @off @user"], + "order": ["Сумма desc"], + "dataParameters": ["Период = LastMonth @user"], + "outputParameters": { "Заголовок": "Мой отчёт" }, + "structure": "Организация > details" + } +}] +``` + +- **filter shorthand**: `"Поле оператор значение @флаги"` — флаги `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible` +- **dataParameters shorthand**: `"Имя = значение @флаги"` +- **structure shorthand**: `"Поле1 > Поле2 > details"` — `>` разделяет уровни группировки +- **conditionalAppearance**: условное оформление с автоопределением типов значений (Color, Boolean, LocalStringType) + +### Объектная форма + +Все секции поддерживают полную объектную форму для сложных случаев (title, appearance, role с выражениями, userSettingID, userSettingPresentation, conditionalAppearance, группы фильтров And/Or/Not и т.д.). Подробности — в [спецификации SKD DSL](skd-dsl-spec.md). + +## Сценарии использования + +### Анализ существующей СКД + +``` +> Проанализируй схему компоновки отчёта Reports/АнализНДФЛ/Templates/ОсновнаяСхемаКомпоновкиДанных +``` + +Claude вызовет `/skd-info` (overview → trace → query → variant) и опишет: +- наборы данных и их поля +- параметры и значения по умолчанию +- ресурсы и формулы агрегации +- структуру группировок в вариантах настроек + +### Создание СКД по описанию + +``` +> Создай СКД для отчёта по продажам: группировка по организациям, +> поля Номенклатура, Количество, Сумма. Период — параметр. +``` + +Claude сформирует JSON: +```json +{ + "dataSets": [{ + "name": "Продажи", + "query": "ВЫБРАТЬ ...", + "fields": [ + "Организация: CatalogRef.Организации @dimension", + "Номенклатура: CatalogRef.Номенклатура @dimension", + "Количество: decimal(15,3)", + "Сумма: decimal(15,2)" + ] + }], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], + "parameters": ["Период: StandardPeriod = LastMonth @autoDates"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма"], + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } + }] +} +``` + +И вызовет `/skd-compile` → `/skd-validate` → `/skd-info`. + +### Проверка существующей СКД + +``` +> Проверь корректность СКД Reports/МойОтчёт/Templates/ОсновнаяСхемаКомпоновкиДанных/Ext/Template.xml +``` + +Claude вызовет `/skd-validate` и покажет результат: ошибки (битые ссылки, дубликаты, невалидные типы) и предупреждения. + +## Структура файлов СКД + +``` +<Объект>/Templates/ +├── ИмяМакета.xml # Метаданные (UUID, TemplateType=DataCompositionSchema) +└── ИмяМакета/ + └── Ext/ + └── Template.xml # Тело схемы (DataCompositionSchema) +``` + +## Спецификации + +- [1c-dcs-spec.md](1c-dcs-spec.md) — XML-формат DataCompositionSchema, namespace, элементы, типы +- [skd-dsl-spec.md](skd-dsl-spec.md) — JSON DSL для описания СКД (формат входных данных `/skd-compile`)