diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md new file mode 100644 index 00000000..38a21d6d --- /dev/null +++ b/.claude/skills/skd-compile/SKILL.md @@ -0,0 +1,131 @@ +--- +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`. + +Роли: `@dimension`, `@account`, `@balance`, `@period`. + +Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`. + +### Итоги (shorthand) + +```json +"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] +``` + +### Параметры (shorthand) + +```json +"parameters": ["Период: StandardPeriod = LastMonth", "Организация: CatalogRef.Организации"] +``` + +### Варианты настроек + +```json +"settingsVariants": [{ + "name": "Основной", + "presentation": "Основной", + "settings": { + "selection": ["Наименование", "Количество", "Auto"], + "filter": [{ "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }], + "order": ["Количество desc", "Auto"], + "outputParameters": { "Заголовок": "Мой отчёт" }, + "structure": [{ "type": "group", "groupBy": ["Организация"], "selection": ["Auto"], "order": ["Auto"], + "children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }] + }] + } +}] +``` + +## Примеры + +### Минимальный + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": ["Наименование"] + }] +} +``` + +### С ресурсами и параметрами + +```json +{ + "dataSets": [{ + "query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи", + "fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"] + }], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], + "parameters": ["Период: StandardPeriod = LastMonth"] +} +``` + +## Верификация + +``` +/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..72fd6e0e --- /dev/null +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -0,0 +1,1185 @@ +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 --- + +function Emit-ValueType { + param([string]$typeStr, [string]$indent) + + if (-not $typeStr) { return } + + # 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 + } + + # cfg: references (CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc.) + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + X "$indentcfg:$typeStr" + return + } + + # Fallback + if ($typeStr.Contains('.')) { + X "$indentcfg:$typeStr" + } else { + X "$indent$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 = $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 } + + # Split "Name: Type = Value" + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') { + $result.name = $Matches[1].Trim() + $result.type = $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 = "" } +} + +# --- 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$($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) { "$($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) { + 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$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) { + X "`t`t" + Emit-ValueType -typeStr "$($cf.type)" -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.group = "$($tf.group)" } + } + + X "`t" + X "`t`t$(Esc-Xml $parsed.dataPath)" + X "`t`t$(Esc-Xml $parsed.expression)" + if ($parsed.group) { + X "`t`t$(Esc-Xml $parsed.group)" + } + X "`t" + } +} + +# === Parameters === +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) { "$($p.type)" } else { "" } + value = $p.value + } + } + + 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 ($p -isnot [string] -and $p.expression) { + X "`t`t$(Esc-Xml "$($p.expression)")" + } + + # AvailableAsField + if ($p -isnot [string] -and $p.availableAsField -eq $false) { + X "`t`tfalse" + } + + # Use + if ($p -isnot [string] -and $p.use) { + X "`t`t$($p.use)" + } + + X "`t" + } +} + +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$valStr" + X "$indent" + } elseif ($type -match '^date') { + X "$indent$valStr" + } elseif ($type -eq "boolean") { + X "$indent$valStr" + } elseif ($type -match '^decimal') { + X "$indent$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$valStr" + } elseif ($valStr -eq "true" -or $valStr -eq "false") { + X "$indent$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$($gt.templateType)" + X "`t`t" + X "`t" + } +} + +# === Settings Variants === + +function Emit-Selection { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + X "$indent`t" + } else { + X "$indent`t" + X "$indent`t`t$item" + X "$indent`t" + } + } else { + X "$indent`t" + X "$indent`t`t$($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$($item.field)" + + $compType = $script:comparisonTypes["$($item.op)"] + if (-not $compType) { $compType = "$($item.op)" } + X "$indent`t$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$($item.viewMode)" + } + + if ($item.userSettingID) { + $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t$uid" + } + + X "$indent" +} + +function Emit-Filter { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + Emit-FilterItem -item $item -indent "$indent`t" + } + X "$indent" +} + +function Emit-Order { + param($items, [string]$indent) + + if (-not $items -or $items.Count -eq 0) { return } + + X "$indent" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { + 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$field" + X "$indent`t`t$dir" + 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) { + 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.variant) { + # StandardPeriod + X "$indent`t`t" + X "$indent`t`t`t$($dp.value.variant)" + X "$indent`t`t" + } elseif ($dp.value -is [bool]) { + $bv = "$($dp.value)".ToLower() + X "$indent`t`t$bv" + } elseif ("$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent`t`t$($dp.value)" + } else { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } + } + + if ($dp.viewMode) { + X "$indent`t`t$($dp.viewMode)" + } + + if ($dp.userSettingID) { + $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" } + X "$indent`t`t$uid" + } + + 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$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$($field.field)" + $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } + X "$indent`t`t$gt" + $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } + X "$indent`t`t$pat" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t`t0001-01-01T00:00:00" + X "$indent`t" + } + } + X "$indent" +} + +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" + Emit-Order -items $item.order -indent "$indent`t" + Emit-Selection -items $item.selection -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" + Emit-Order -items $col.order -indent "$indent`t`t" + Emit-Selection -items $col.selection -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" + Emit-Order -items $row.order -indent "$indent`t`t" + Emit-Selection -items $row.selection -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" + Emit-Order -items $item.points.order -indent "$indent`t`t" + Emit-Selection -items $item.points.selection -indent "$indent`t`t" + X "$indent`t" + } + + # Series + if ($item.series) { + X "$indent`t" + Emit-GroupItems -groupBy $item.series.groupBy -indent "$indent`t`t" + Emit-Order -items $item.series.order -indent "$indent`t`t" + Emit-Selection -items $item.series.selection -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 + if ($s.selection) { + Emit-Selection -items $s.selection -indent "`t`t`t" + } + + # Filter + if ($s.filter) { + Emit-Filter -items $s.filter -indent "`t`t`t" + } + + # Order + if ($s.order) { + Emit-Order -items $s.order -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 + if ($s.structure) { + foreach ($item in $s.structure) { + 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-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..1a886ca6 --- /dev/null +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -0,0 +1,717 @@ +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/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md new file mode 100644 index 00000000..52c3eb6c --- /dev/null +++ b/docs/skd-dsl-spec.md @@ -0,0 +1,639 @@ +# 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` | `cfg:CatalogRef.XXX` | — | +| `DocumentRef.XXX` | `cfg:DocumentRef.XXX` | — | +| `EnumRef.XXX` | `cfg:EnumRef.XXX` | — | +| `ChartOfAccountsRef.XXX` | `cfg:ChartOfAccountsRef.XXX` | — | +| `StandardPeriod` | `v8:StandardPeriod` | — | + +### Роли + +| 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}` | `true` | + +Объектная форма с доп. полями: +```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" } +``` + +--- + +## 6. Параметры (parameters) + +### Shorthand + +``` +": [= ]" +``` + +Примеры: + +```json +"parameters": [ + "Период: StandardPeriod = LastMonth", + "Организация: CatalogRef.Организации", + "ДатаОтчета: date" +] +``` + +**Парсинг:** `"A: T = V"` → `name=A`, `type=T`, `value=V`. Значение `LastMonth` и другие варианты периодов → `v8:StandardPeriod` с `v8:variant`. + +### Объектная форма + +```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": [...], + "outputParameters": {...}, + "dataParameters": [...], + "structure": [...] + } +}] +``` + +### selection + +```json +"selection": [ + "Наименование", + { "field": "Количество", "title": "Кол-во" }, + "Auto" +] +``` + +- Строка → `SelectedItemField` +- `"Auto"` → `SelectedItemAuto` +- Объект с `field`/`title` → `SelectedItemField` с `lwsTitle` + +### filter + +```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 | + +Операторы: + +| 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` + +### outputParameters + +```json +"outputParameters": { + "Заголовок": "Мой отчёт", + "ВыводитьЗаголовок": "Output", + "МакетОформления": "ОформлениеОтчетовЧерноБелый" +} +``` + +Ключ → `dcscor:parameter`, значение → `dcscor:value`. + +Типы значений определяются автоматически: +- `"Заголовок"` → `v8:LocalStringType` +- `"ВыводитьЗаголовок"`, `"ВыводитьПараметрыДанных"`, `"ВыводитьОтбор"` → `dcsset:DataCompositionTextOutputType` +- `"РасположениеПолейГруппировки"` → `dcsset:DataCompositionGroupFieldsPlacement` +- `"РасположениеРеквизитов"` → `dcsset:DataCompositionAttributesPlacement` +- `"ГоризонтальноеРасположениеОбщихИтогов"`, `"ВертикальноеРасположениеОбщихИтогов"` → `dcscor:DataCompositionTotalPlacement` +- Прочие → `xs:string` + +### dataParameters + +```json +"dataParameters": [ + { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" }, + { "parameter": "Организация", "use": false, "viewMode": "Normal", "userSettingID": "auto" } +] +``` + +### structure + +```json +"structure": [ + { + "type": "group", + "groupBy": ["Организация"], + "selection": ["Auto"], + "order": ["Auto"], + "children": [ + { "type": "group", "selection": ["Auto"], "order": ["Auto"] } + ] + } +] +``` + +#### Группировка (group) + +| Поле | Описание | +|------|----------| +| `type` | `"group"` | +| `name` | Имя группировки (опц.) | +| `groupBy` | Массив полей. Пусто/опущено = детальные записи | +| `groupType` | `"Items"` (умолч.), `"Hierarchy"`, `"HierarchyOnly"` | +| `selection` | Выборка (как в settings) | +| `filter` | Отборы (как в settings) | +| `order` | Сортировка (как в settings) | +| `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": "Основной", + "presentation": "Основной", + "settings": { + "selection": ["Наименование", "Количество"], + "structure": [ + { "type": "group", "order": ["Auto"], "selection": ["Auto"] } + ] + } + }] +} +``` + +## 12. Полный пример — средний + +```json +{ + "dataSets": [ + { + "name": "Продажи", + "query": "ВЫБРАТЬ\n\tПродажи.Организация,\n\tПродажи.Номенклатура,\n\tПродажи.Количество,\n\tПродажи.Сумма\nИЗ\n\tРегистрНакопления.Продажи КАК Продажи\n{ГДЕ\n\tПродажи.Период >= &ДатаНачала\n\tИ Продажи.Период < &ДатаОкончания}", + "fields": [ + "Организация: CatalogRef.Организации @dimension", + "Номенклатура: CatalogRef.Номенклатура @dimension", + "Количество: decimal(15,3)", + "Сумма: decimal(15,2)" + ] + } + ], + "totalFields": [ + "Количество: Сумма", + "Сумма: Сумма" + ], + "parameters": [ + "Период: StandardPeriod = LastMonth", + { "name": "ДатаНачала", "type": "date", "expression": "&Период.ДатаНачала", "availableAsField": false }, + { "name": "ДатаОкончания", "type": "date", "expression": "&Период.ДатаОкончания", "availableAsField": false } + ], + "settingsVariants": [{ + "name": "Основной", + "presentation": "Продажи по организациям", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "filter": [ + { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" } + ], + "order": ["Сумма desc", "Auto"], + "outputParameters": { + "Заголовок": "Анализ продаж", + "ВыводитьЗаголовок": "Output" + }, + "dataParameters": [ + { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" } + ], + "structure": [ + { + "type": "group", + "groupBy": ["Организация"], + "selection": ["Auto"], + "order": ["Auto"], + "children": [ + { "type": "group", "selection": ["Auto"], "order": ["Auto"] } + ] + } + ] + } + }] +} +```