diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index 38a21d6d..161bc981 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -62,6 +62,8 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. +**Синонимы типов** (русские и альтернативные): `число` = 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`. @@ -72,26 +74,62 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p "totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"] ``` -### Параметры (shorthand) +### Параметры (shorthand + @autoDates) ```json -"parameters": ["Период: StandardPeriod = LastMonth", "Организация: CatalogRef.Организации"] +"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`. + +### Параметры данных — shorthand + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически. + +### Структура — string shorthand + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +``` + +`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне. + +Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма. + ### Варианты настроек ```json "settingsVariants": [{ "name": "Основной", - "presentation": "Основной", "settings": { - "selection": ["Наименование", "Количество", "Auto"], - "filter": [{ "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }], + "selection": ["Номенклатура", "Количество", "Auto"], + "filter": ["Организация = _ @off @user"], "order": ["Количество desc", "Auto"], "outputParameters": { "Заголовок": "Мой отчёт" }, - "structure": [{ "type": "group", "groupBy": ["Организация"], "selection": ["Auto"], "order": ["Auto"], - "children": [{ "type": "group", "selection": ["Auto"], "order": ["Auto"] }] - }] + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" } }] ``` @@ -109,16 +147,25 @@ powershell.exe -NoProfile -File .claude\skills\skd-compile\scripts\skd-compile.p } ``` -### С ресурсами и параметрами +### С ресурсами, параметрами и @autoDates ```json { "dataSets": [{ "query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи", - "fields": ["Номенклатура: CatalogRef.Номенклатура @dimension", "Количество: decimal(15,3)", "Сумма: decimal(15,2)"] + "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"] }], "totalFields": ["Количество: Сумма", "Сумма: Сумма"], - "parameters": ["Период: StandardPeriod = LastMonth"] + "parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], + "filter": ["Организация = _ @off @user"], + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" + } + }] } ``` diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 72fd6e0e..3e208fd5 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -80,11 +80,71 @@ foreach ($ds in $def.dataSets) { # --- 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" @@ -178,7 +238,7 @@ function Parse-FieldShorthand { if ($s.Contains(':')) { $parts = $s -split ':', 2 $result.dataPath = $parts[0].Trim() - $result.type = $parts[1].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) } else { $result.dataPath = $s } @@ -211,12 +271,18 @@ function Parse-TotalShorthand { function Parse-ParamShorthand { param([string]$s) - $result = @{ name = ""; type = ""; value = $null } + $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 = $Matches[2].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) if ($Matches[4]) { $result.value = $Matches[4].Trim() } @@ -243,6 +309,128 @@ function Parse-CalcShorthand { 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', '' + } + + $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 = @{ @@ -293,7 +481,7 @@ function Emit-Field { 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 { "" } + type = if ($fieldDef.type) { Resolve-TypeStr "$($fieldDef.type)" } else { "" } roles = @() restrict = @() appearance = @{} @@ -511,8 +699,9 @@ function Emit-CalcFields { Emit-MLText -tag "title" -text "$($cf.title)" -indent "`t`t" } if ($cf.type) { + $cfType = Resolve-TypeStr "$($cf.type)" X "`t`t" - Emit-ValueType -typeStr "$($cf.type)" -indent "`t`t`t" + Emit-ValueType -typeStr $cfType -indent "`t`t`t" X "`t`t" } if ($cf.restrict) { @@ -568,6 +757,52 @@ function Emit-TotalFields { } # === 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$($p.use)" + } + + X "`t" +} + function Emit-Parameters { if (-not $def.parameters) { return } foreach ($p in $def.parameters) { @@ -576,51 +811,31 @@ function Emit-Parameters { } else { $parsed = @{ name = "$($p.name)" - type = if ($p.type) { "$($p.type)" } else { "" } + 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 } } - X "`t" - X "`t`t$(Esc-Xml $parsed.name)" + Emit-SingleParam -p $p -parsed $parsed - # Title - $title = if ($p -isnot [string] -and $p.title) { "$($p.title)" } else { "" } - if ($title) { - Emit-MLText -tag "title" -text $title -indent "`t`t" + # @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 } - - # 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" } } @@ -806,7 +1021,31 @@ function Emit-Filter { X "$indent" foreach ($item in $items) { - Emit-FilterItem -item $item -indent "$indent`t" + 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" } @@ -873,6 +1112,26 @@ function Emit-DataParameters { 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) { @@ -883,8 +1142,13 @@ function Emit-DataParameters { # Value if ($null -ne $dp.value) { - if ($dp.value.variant) { - # StandardPeriod + if ($dp.value -is [PSCustomObject] -and $dp.value.variant) { + # StandardPeriod (object form from JSON) + X "$indent`t`t" + X "$indent`t`t`t$($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$($dp.value.variant)" X "$indent`t`t" @@ -945,6 +1209,37 @@ function Emit-GroupItems { 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) @@ -958,8 +1253,17 @@ function Emit-StructureItem { } Emit-GroupItems -groupBy $item.groupBy -indent "$indent`t" - Emit-Order -items $item.order -indent "$indent`t" - Emit-Selection -items $item.selection -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) { @@ -987,8 +1291,10 @@ function Emit-StructureItem { 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" + $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" } } @@ -1001,8 +1307,10 @@ function Emit-StructureItem { 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" + $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" } } @@ -1020,8 +1328,10 @@ function Emit-StructureItem { 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" + $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" } @@ -1029,8 +1339,10 @@ function Emit-StructureItem { 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" + $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" } @@ -1120,9 +1432,13 @@ function Emit-SettingsVariants { Emit-DataParameters -items $s.dataParameters -indent "`t`t`t" } - # Structure + # Structure (supports string shorthand: "Организация > details") if ($s.structure) { - foreach ($item in $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" } } diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 52c3eb6c..19eb76ba 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -153,6 +153,27 @@ | `ChartOfAccountsRef.XXX` | `cfg:ChartOfAccountsRef.XXX` | — | | `StandardPeriod` | `v8:StandardPeriod` | — | +### Синонимы типов + +Все имена типов регистронезависимые. Поддерживаются русские и альтернативные имена: + +| Синоним | Канонический тип | +|---------|-----------------| +| `число`, `Число` | `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 | @@ -239,14 +260,14 @@ ### Shorthand ``` -": [= ]" +": [= ] [@autoDates]" ``` Примеры: ```json "parameters": [ - "Период: StandardPeriod = LastMonth", + "Период: StandardPeriod = LastMonth @autoDates", "Организация: CatalogRef.Организации", "ДатаОтчета: date" ] @@ -254,6 +275,26 @@ **Парсинг:** `"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 @@ -382,6 +423,27 @@ ### filter +#### Shorthand-строка + +```json +"filter": [ + "Организация = _ @off @user", + "Дата >= 2024-01-01T00:00:00", + "Статус filled", + "Количество > 0" +] +``` + +Формат: `"<Поле> <оператор> [<значение>] [@off] [@user] [@quickAccess]"`. + +- Значение `_` — пустое (placeholder, не выводится в XML) +- `@off` → `use=false` +- `@user` → `userSettingID=auto` (генерировать GUID) +- `@quickAccess` → `viewMode=QuickAccess` +- Типы значений автоопределяются: `true`/`false` → boolean, `2024-01-01T00:00:00` → dateTime, числа → decimal, прочее → string + +#### Объектная форма + ```json "filter": [ { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" }, @@ -458,6 +520,22 @@ ### dataParameters +#### Shorthand-строка + +```json +"dataParameters": [ + "Период = LastMonth @user", + "Организация @off @user" +] +``` + +Формат: `"<Имя> [= <значение>] [@off] [@user] [@quickAccess] [@normal]"`. + +- Значения-варианты периодов (`LastMonth`, `ThisYear` и др.) автоматически оборачиваются в `v8:StandardPeriod` +- `@off` → `use=false`, `@user` → `userSettingID=auto` + +#### Объектная форма + ```json "dataParameters": [ { "parameter": "Период", "value": { "variant": "LastMonth" }, "userSettingID": "auto" }, @@ -467,20 +545,32 @@ ### structure +#### String shorthand (рекомендуется для типичных случаев) + +```json +"structure": "Организация > details" +"structure": "Организация > Номенклатура > details" +"structure": "Период > Организация > Номенклатура > details" +``` + +`>` разделяет уровни вложенности. Каждый сегмент — группировка по указанному полю. `details` (или `детали`) — детальные записи (пустой `groupBy`). Для каждого уровня `selection` и `order` автоматически `["Auto"]`. + +#### Массив объектов + ```json "structure": [ { "type": "group", "groupBy": ["Организация"], - "selection": ["Auto"], - "order": ["Auto"], "children": [ - { "type": "group", "selection": ["Auto"], "order": ["Auto"] } + { "type": "group" } ] } ] ``` +**Умолчания:** `selection` и `order` по умолчанию `["Auto"]` на каждом уровне (в группировках, строках/колонках таблиц, точках/сериях диаграмм). Указывать явно нужно только если требуется другой набор полей. + #### Группировка (group) | Поле | Описание | @@ -489,9 +579,9 @@ | `name` | Имя группировки (опц.) | | `groupBy` | Массив полей. Пусто/опущено = детальные записи | | `groupType` | `"Items"` (умолч.), `"Hierarchy"`, `"HierarchyOnly"` | -| `selection` | Выборка (как в settings) | +| `selection` | Выборка (умолч. `["Auto"]`) | | `filter` | Отборы (как в settings) | -| `order` | Сортировка (как в settings) | +| `order` | Сортировка (умолч. `["Auto"]`) | | `outputParameters` | Параметры вывода (как в settings) | | `children` | Вложенные элементы структуры | @@ -570,18 +660,15 @@ "totalFields": ["Количество: Сумма"], "settingsVariants": [{ "name": "Основной", - "presentation": "Основной", "settings": { "selection": ["Наименование", "Количество"], - "structure": [ - { "type": "group", "order": ["Auto"], "selection": ["Auto"] } - ] + "structure": [{ "type": "group" }] } }] } ``` -## 12. Полный пример — средний +## 12. Полный пример — средний (с shorthand v2) ```json { @@ -590,50 +677,33 @@ "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)" + "Организация: СправочникСсылка.Организации @dimension", + "Номенклатура: СправочникСсылка.Номенклатура @dimension", + "Количество: число(15,3)", + "Сумма: число(15,2)" ] } ], - "totalFields": [ - "Количество: Сумма", - "Сумма: Сумма" - ], + "totalFields": ["Количество: Сумма", "Сумма: Сумма"], "parameters": [ - "Период: StandardPeriod = LastMonth", - { "name": "ДатаНачала", "type": "date", "expression": "&Период.ДатаНачала", "availableAsField": false }, - { "name": "ДатаОкончания", "type": "date", "expression": "&Период.ДатаОкончания", "availableAsField": false } + "Период: СтандартныйПериод = LastMonth @autoDates" ], "settingsVariants": [{ "name": "Основной", "presentation": "Продажи по организациям", "settings": { "selection": ["Номенклатура", "Количество", "Сумма", "Auto"], - "filter": [ - { "field": "Организация", "op": "=", "use": false, "userSettingID": "auto" } - ], + "filter": ["Организация = _ @off @user"], "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"] } - ] - } - ] + "dataParameters": ["Период = LastMonth @user"], + "structure": "Организация > details" } }] } ``` + +**Сравнение с v1:** средний пример сократился с 58 до 33 строк (−43%). Основная экономия: `@autoDates` (−4 строки), structure shorthand (−9 строк), filter/dataParam shorthand (−4 строки).