diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1
index 3ce1b65d..81c2139e 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.ps1
+++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1
@@ -1,4 +1,4 @@
-# skd-compile v1.21 — Compile 1C DCS from JSON
+# skd-compile v1.22 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -1130,6 +1130,8 @@ function Emit-ParamValue {
X "$indent$(Esc-Xml $valStr)"
} elseif ($type -match '^string') {
X "$indent$(Esc-Xml $valStr)"
+ } elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') {
+ X "$indent$(Esc-Xml $valStr)"
} else {
# Guess from value
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') {
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py
index 9a32ca18..0586e64b 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.py
+++ b/.claude/skills/skd-compile/scripts/skd-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# skd-compile v1.21 — Compile 1C DCS from JSON
+# skd-compile v1.22 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -811,6 +811,8 @@ def emit_param_value(lines, type_str, val, indent):
lines.append(f'{indent}{esc_xml(val_str)}')
elif type_str and re.match(r'^string', type_str):
lines.append(f'{indent}{esc_xml(val_str)}')
+ elif type_str and re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', type_str):
+ lines.append(f'{indent}{esc_xml(val_str)}')
else:
# Guess from value
if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md
index 88dad8e0..f867dda4 100644
--- a/.claude/skills/skd-edit/SKILL.md
+++ b/.claude/skills/skd-edit/SKILL.md
@@ -249,16 +249,27 @@ Shorthand: `"старое => новое"`. Заменяет все вхожде
### set-structure — установить структуру варианта
-Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
+Shorthand: `"Поле1 > Поле2 > details"`. `>` — вложенный уровень группировки, `,` — несколько полей в одном уровне, `details` — детальные записи. **Заменяет всю структуру полностью** (включая Selection/order/filter/conditionalAppearance каждой группы). Для точечной модификации полей группировки с сохранением настроек — используй `modify-structure`. Не поддерживает пакетный режим.
```
"Организация > Номенклатура > details"
+"Валюта, НаименованиеБанка, ИНН"
"details"
"СчетМеждународногоУчета @name=ДанныеОтчета"
```
`@name=Имя` — присваивает имя группировке (``). Используется для привязки шаблонов через `groupName`.
+### modify-structure — изменить поля группировки существующей группы
+
+Тот же shorthand что и `set-structure`. Находит группу по `@name=`, заменяет только `` (поля группировки). Selection/order/filter/conditionalAppearance/outputParameters группы сохраняются. Без `@name=` — ошибка.
+
+```
+"Валюта @name=ДанныеОтчета"
+"Валюта, НаименованиеБанка @name=ДанныеОтчета"
+"details @name=ДанныеОтчета"
+```
+
### modify-field — изменить существующее поле
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1
index 64e77ed6..ac0dc892 100644
--- a/.claude/skills/skd-edit/scripts/skd-edit.ps1
+++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1
@@ -1,4 +1,4 @@
-# skd-edit v1.11 — Atomic 1C DCS editor
+# skd-edit v1.12 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -11,7 +11,7 @@ param(
"add-dataParameter","add-order","add-selection","add-dataSetLink",
"add-dataSet","add-variant","add-conditionalAppearance","add-drilldown",
"set-query","patch-query","set-outputParameter","set-structure",
- "modify-field","modify-filter","modify-dataParameter","modify-parameter",
+ "modify-field","modify-filter","modify-dataParameter","modify-parameter","modify-structure",
"rename-parameter","reorder-parameters",
"clear-selection","clear-order","clear-filter",
"remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")]
@@ -581,15 +581,17 @@ function Parse-StructureShorthand {
$seg = $segments[$i].Trim()
$group = @{ type = "group" }
- if ($seg -match '@name=(.+)') {
- $group["name"] = $Matches[1].Trim()
- $seg = ($seg -replace '\s*@name=.+', '').Trim()
+ if ($seg -match '@name=(?:"([^"]+)"|''([^'']+)''|(\S+))') {
+ $rawName = if ($Matches[1]) { $Matches[1] } elseif ($Matches[2]) { $Matches[2] } else { $Matches[3] }
+ $group["name"] = $rawName.Trim()
+ $seg = ($seg -replace '\s*@name=(?:"[^"]+"|''[^'']+''|\S+)', '').Trim()
}
if ($seg -match '^(?i)(details|детали)$') {
$group["groupBy"] = @()
} else {
- $group["groupBy"] = @($seg)
+ $fields = @($seg -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
+ $group["groupBy"] = $fields
}
if ($null -ne $innermost) {
@@ -804,6 +806,48 @@ function Build-CalcFieldFragment {
return $lines -join "`r`n"
}
+function Build-ParamValueXml {
+ # Returns array of XML lines for a ... element (or StandardPeriod block).
+ # Selects xsi:type by declared type, then falls back to value pattern.
+ param([string]$type, [string]$value, [string]$indent, [string]$tagName = "value", [string]$tagNs = "")
+
+ $i = $indent
+ $valStr = "$value"
+ $open = if ($tagNs) { "$tagNs`:$tagName" } else { $tagName }
+ $lines = @()
+
+ if ($type -eq "StandardPeriod") {
+ $lines += "$i<$open xsi:type=`"v8:StandardPeriod`">"
+ $lines += "$i`t$(Esc-Xml $valStr)"
+ $lines += "$i`t0001-01-01T00:00:00"
+ $lines += "$i`t0001-01-01T00:00:00"
+ $lines += "$i$open>"
+ return $lines
+ }
+
+ $xsi = $null
+ if ($type -match '^date') { $xsi = "xs:dateTime" }
+ elseif ($type -eq "boolean") { $xsi = "xs:boolean" }
+ elseif ($type -match '^decimal') { $xsi = "xs:decimal" }
+ elseif ($type -match '^string') { $xsi = "xs:string" }
+ elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') {
+ $xsi = "dcscor:DesignTimeValue"
+ }
+ else {
+ # Type unknown or empty — guess from value
+ if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $xsi = "xs:dateTime" }
+ elseif ($valStr -eq "true" -or $valStr -eq "false") { $xsi = "xs:boolean" }
+ elseif ($valStr -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or
+ $valStr -match '^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
+ $xsi = "dcscor:DesignTimeValue"
+ }
+ else { $xsi = "xs:string" }
+ }
+
+ $lines += "$i<$open xsi:type=`"$xsi`">$(Esc-Xml $valStr)$open>"
+ return $lines
+}
+
function Build-ParamFragment {
param($parsed, [string]$indent)
@@ -825,22 +869,8 @@ function Build-ParamFragment {
}
if ($null -ne $parsed.value) {
- $valStr = "$($parsed.value)"
- if ($parsed.type -eq "StandardPeriod") {
- $lines += "$i`t"
- $lines += "$i`t`t$(Esc-Xml $valStr)"
- $lines += "$i`t`t0001-01-01T00:00:00"
- $lines += "$i`t`t0001-01-01T00:00:00"
- $lines += "$i`t"
- } elseif ($parsed.type -match '^date') {
- $lines += "$i`t$(Esc-Xml $valStr)"
- } elseif ($parsed.type -eq "boolean") {
- $lines += "$i`t$(Esc-Xml $valStr)"
- } elseif ($parsed.type -match '^decimal') {
- $lines += "$i`t$(Esc-Xml $valStr)"
- } else {
- $lines += "$i`t$(Esc-Xml $valStr)"
- }
+ $valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t"
+ foreach ($vl in $valueLines) { $lines += $vl }
}
$lines += "$i"
@@ -1565,10 +1595,10 @@ $corNs = "http://v8.1c.ru/8.1/data-composition-system/core"
# --- 7. Batch value splitting ---
-if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "add-dataSet") {
+if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "modify-structure" -or $Operation -eq "add-dataSet") {
$values = @($Value)
} elseif ($Operation -eq "patch-query") {
- $values = @($Value -split ';;' | Where-Object { $_.Trim() })
+ $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} elseif ($Operation -eq "add-drilldown") {
if ($Value.Contains(';;')) {
$values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
@@ -1810,15 +1840,63 @@ switch ($Operation) {
$avPart = $rest.Substring($avIdx)
}
- # Process simple key=value pairs (use, denyIncompleteValues, etc.)
+ # Process simple key=value pairs (use, denyIncompleteValues, value, etc.)
if ($simpleRest) {
$kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)')
foreach ($kv in $kvPairs) {
$key = $kv.Groups[1].Value
$value = $kv.Groups[2].Value
- $existing = $paramEl.SelectSingleNode($key)
- if ($existing) {
+ # Namespace-aware lookup (children live in $schNs)
+ $existing = $null
+ foreach ($ch in $paramEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $key -and $ch.NamespaceURI -eq $schNs) {
+ $existing = $ch; break
+ }
+ }
+
+ if ($key -eq "value") {
+ # Special-case: rebuild with correct xsi:type from
+ $declaredType = ""
+ $vtEl = $null
+ foreach ($ch in $paramEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break }
+ }
+ if ($vtEl) {
+ foreach ($tnode in $vtEl.ChildNodes) {
+ if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') {
+ $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', ''
+ break
+ }
+ }
+ }
+ $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent
+ $fragXml = $valueLines -join "`r`n"
+
+ $wasExisting = ($null -ne $existing)
+ if ($existing) {
+ # Capture position by next-element sibling, then remove existing
+ $refNode = $existing.NextSibling
+ while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
+ $refNode = $refNode.NextSibling
+ }
+ Remove-NodeWithWhitespace $existing
+ } else {
+ # Insert before useRestriction/availableValue/denyIncompleteValues/use
+ $refNode = $null
+ foreach ($child in $paramEl.ChildNodes) {
+ if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) {
+ $refNode = $child; break
+ }
+ }
+ }
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $paramEl $node $refNode $childIndent
+ }
+ $verb = if ($wasExisting) { "updated" } else { "added" }
+ Write-Host "[OK] Parameter `"$paramName`": value $verb to $value"
+ } elseif ($existing) {
$existing.InnerText = $value
Write-Host "[OK] Parameter `"$paramName`": $key updated to $value"
} else {
@@ -1849,15 +1927,25 @@ switch ($Operation) {
$avValue = $avParts[0].Trim()
$avPresentation = if ($avParts.Count -gt 1) { $avParts[1].Trim() } else { "" }
- # Detect value type
- $avType = "xs:string"
- if ($avValue -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
- $avType = "dcscor:DesignTimeValue"
+ # Detect value type: prefer declared of the parameter, else guess from value
+ $declaredType = ""
+ $vtEl = $null
+ foreach ($ch in $paramEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break }
+ }
+ if ($vtEl) {
+ foreach ($tnode in $vtEl.ChildNodes) {
+ if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') {
+ $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', ''
+ break
+ }
+ }
}
$avLines = @()
$avLines += "$childIndent"
- $avLines += "$childIndent`t$(Esc-Xml $avValue)"
+ $valueLines = Build-ParamValueXml -type $declaredType -value $avValue -indent "$childIndent`t"
+ foreach ($vl in $valueLines) { $avLines += $vl }
if ($avPresentation) {
$avLines += "$childIndent`t"
$avLines += "$childIndent`t`t"
@@ -2289,6 +2377,102 @@ switch ($Operation) {
Write-Host "[OK] Structure set in variant `"$varName`": $Value"
}
+ "modify-structure" {
+ $settings = Resolve-VariantSettings
+ $varName = Get-VariantName
+
+ $structItems = Parse-StructureShorthand $Value
+
+ # Flatten parsed tree into (name, groupBy) targets
+ $targets = @()
+ $stack = New-Object System.Collections.Stack
+ foreach ($it in $structItems) { $stack.Push($it) }
+ while ($stack.Count -gt 0) {
+ $it = $stack.Pop()
+ if ($it["name"]) {
+ $targets += @{ name = $it["name"]; groupBy = $it["groupBy"] }
+ }
+ if ($it["children"]) {
+ foreach ($ch in $it["children"]) { $stack.Push($ch) }
+ }
+ }
+
+ if ($targets.Count -eq 0) {
+ Write-Error "modify-structure requires @name= for at least one group: $Value"
+ exit 1
+ }
+
+ $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
+ $nsMgr.AddNamespace("dcsset", $setNs)
+ $nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
+
+ foreach ($t in $targets) {
+ $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$($t.name)']", $nsMgr)
+ if (-not $groupEl) {
+ Write-Host "[WARN] Group with @name=`"$($t.name)`" not found — skipped"
+ continue
+ }
+
+ $giEl = $null
+ foreach ($ch in $groupEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) {
+ $giEl = $ch; break
+ }
+ }
+ $groupIndent = Get-ChildIndent $groupEl
+ if (-not $giEl) {
+ # Create after , before //...
+ $nameEl = $null
+ $refAfterName = $null
+ foreach ($ch in $groupEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $setNs) {
+ $nameEl = $ch
+ } elseif ($ch.NodeType -eq 'Element' -and $nameEl -and -not $refAfterName) {
+ $refAfterName = $ch; break
+ }
+ }
+ $giFrag = "$groupIndent"
+ $nodes = Import-Fragment $xmlDoc $giFrag
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $groupEl $node $refAfterName $groupIndent
+ }
+ # Re-find
+ foreach ($ch in $groupEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) {
+ $giEl = $ch; break
+ }
+ }
+ }
+
+ $toRemove = @()
+ foreach ($ch in $giEl.ChildNodes) {
+ if ($ch.NodeType -eq 'Element') { $toRemove += $ch }
+ }
+ foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el }
+
+ $itemIndent = "$groupIndent`t"
+
+ foreach ($field in $t.groupBy) {
+ $lines = @()
+ $lines += "$itemIndent"
+ $lines += "$itemIndent`t$(Esc-Xml $field)"
+ $lines += "$itemIndent`tItems"
+ $lines += "$itemIndent`tNone"
+ $lines += "$itemIndent`t0001-01-01T00:00:00"
+ $lines += "$itemIndent`t0001-01-01T00:00:00"
+ $lines += "$itemIndent"
+ $fragXml = $lines -join "`r`n"
+ $nodes = Import-Fragment $xmlDoc $fragXml
+ foreach ($node in $nodes) {
+ Insert-BeforeElement $giEl $node $null $itemIndent
+ }
+ }
+
+ $desc = if ($t.groupBy.Count -eq 0) { "details" } else { $t.groupBy -join ', ' }
+ Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc"
+ }
+ }
+
"add-dataSetLink" {
foreach ($val in $values) {
$parsed = Parse-DataSetLinkShorthand $val
diff --git a/.claude/skills/skd-edit/scripts/skd-edit.py b/.claude/skills/skd-edit/scripts/skd-edit.py
index 91b2bb73..4218581b 100644
--- a/.claude/skills/skd-edit/scripts/skd-edit.py
+++ b/.claude/skills/skd-edit/scripts/skd-edit.py
@@ -1,4 +1,4 @@
-# skd-edit v1.11 — Atomic 1C DCS editor (Python port)
+# skd-edit v1.12 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -18,7 +18,7 @@ VALID_OPS = [
"add-dataParameter", "add-order", "add-selection", "add-dataSetLink",
"add-dataSet", "add-variant", "add-conditionalAppearance", "add-drilldown",
"set-query", "patch-query", "set-outputParameter", "set-structure",
- "modify-field", "modify-filter", "modify-dataParameter", "modify-parameter",
+ "modify-field", "modify-filter", "modify-dataParameter", "modify-parameter", "modify-structure",
"rename-parameter", "reorder-parameters",
"clear-selection", "clear-order", "clear-filter",
"remove-field", "remove-total", "remove-calculated-field", "remove-parameter", "remove-filter",
@@ -545,15 +545,17 @@ def parse_structure_shorthand(s):
seg = segments[i].strip()
group = {"type": "group"}
- name_m = re.search(r'\s*@name=(.+)', seg)
+ name_m = re.search(r'@name=(?:"([^"]+)"|\'([^\']+)\'|(\S+))', seg)
if name_m:
- group["name"] = name_m.group(1).strip()
- seg = re.sub(r'\s*@name=.+', '', seg).strip()
+ raw_name = name_m.group(1) or name_m.group(2) or name_m.group(3)
+ group["name"] = raw_name.strip()
+ seg = re.sub(r'\s*@name=(?:"[^"]+"|\'[^\']+\'|\S+)', '', seg).strip()
if re.match(r'^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg, re.IGNORECASE):
group["groupBy"] = []
else:
- group["groupBy"] = [seg]
+ fields = [f.strip() for f in re.split(r'\s*,\s*', seg) if f.strip()]
+ group["groupBy"] = fields
if innermost is not None:
group["children"] = [innermost]
@@ -724,6 +726,47 @@ def build_calc_field_fragment(parsed, indent):
return "\r\n".join(lines)
+def build_param_value_xml(type_str, value, indent, tag_name="value", tag_ns=""):
+ """Return list of XML lines for ...."""
+ val_str = "" if value is None else str(value)
+ open_tag = f"{tag_ns}:{tag_name}" if tag_ns else tag_name
+ lines = []
+
+ if type_str == "StandardPeriod":
+ lines.append(f'{indent}<{open_tag} xsi:type="v8:StandardPeriod">')
+ lines.append(f'{indent}\t{esc_xml(val_str)}')
+ lines.append(f"{indent}\t0001-01-01T00:00:00")
+ lines.append(f"{indent}\t0001-01-01T00:00:00")
+ lines.append(f"{indent}{open_tag}>")
+ return lines
+
+ t = type_str or ""
+ xsi = None
+ if t.startswith("date"):
+ xsi = "xs:dateTime"
+ elif t == "boolean":
+ xsi = "xs:boolean"
+ elif t.startswith("decimal"):
+ xsi = "xs:decimal"
+ elif t.startswith("string"):
+ xsi = "xs:string"
+ elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t):
+ xsi = "dcscor:DesignTimeValue"
+ else:
+ if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
+ xsi = "xs:dateTime"
+ elif val_str in ("true", "false"):
+ xsi = "xs:boolean"
+ elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or \
+ re.match(r'^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
+ xsi = "dcscor:DesignTimeValue"
+ else:
+ xsi = "xs:string"
+
+ lines.append(f'{indent}<{open_tag} xsi:type="{xsi}">{esc_xml(val_str)}{open_tag}>')
+ return lines
+
+
def build_param_fragment(parsed, indent):
i = indent
fragments = []
@@ -739,21 +782,8 @@ def build_param_fragment(parsed, indent):
lines.append(f"{i}\t")
if parsed["value"] is not None:
- val_str = str(parsed["value"])
- if parsed.get("type") == "StandardPeriod":
- lines.append(f'{i}\t')
- lines.append(f'{i}\t\t{esc_xml(val_str)}')
- lines.append(f"{i}\t\t0001-01-01T00:00:00")
- lines.append(f"{i}\t\t0001-01-01T00:00:00")
- lines.append(f"{i}\t")
- elif parsed.get("type", "").startswith("date"):
- lines.append(f'{i}\t{esc_xml(val_str)}')
- elif parsed.get("type") == "boolean":
- lines.append(f'{i}\t{esc_xml(val_str)}')
- elif parsed.get("type", "").startswith("decimal"):
- lines.append(f'{i}\t{esc_xml(val_str)}')
- else:
- lines.append(f'{i}\t{esc_xml(val_str)}')
+ for vl in build_param_value_xml(parsed.get("type", ""), parsed["value"], f"{i}\t"):
+ lines.append(vl)
lines.append(f"{i}")
fragments.append("\r\n".join(lines))
@@ -1361,10 +1391,10 @@ xml_doc = tree.getroot()
# ── 7. Batch value splitting ────────────────────────────────
-if operation in ("set-query", "set-structure", "add-dataSet"):
+if operation in ("set-query", "set-structure", "modify-structure", "add-dataSet"):
values = [value_arg]
elif operation == "patch-query":
- values = [v for v in value_arg.split(";;") if v.strip()]
+ values = [v.strip() for v in value_arg.split(";;") if v.strip()]
elif operation == "add-drilldown":
if ";;" in value_arg:
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
@@ -1568,8 +1598,33 @@ elif operation == "modify-parameter":
if simple_rest:
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
key, value = m.group(1), m.group(2)
- existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key), None)
- if existing is not None:
+ existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key and etree.QName(ch.tag).namespace == SCH_NS), None)
+
+ if key == "value":
+ # Rebuild with correct xsi:type from
+ declared_type = ""
+ vt_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS), None)
+ if vt_el is not None:
+ for tnode in vt_el:
+ if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
+ declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
+ break
+ value_lines = build_param_value_xml(declared_type, value, child_indent)
+ frag_xml = "\r\n".join(value_lines)
+ was_existing = existing is not None
+ if existing is not None:
+ # Find next-element sibling as ref before removing
+ idx = list(param_el).index(existing)
+ ref_node = param_el[idx + 1] if idx + 1 < len(param_el) else None
+ remove_node_with_whitespace(existing)
+ else:
+ ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("useRestriction", "availableValue", "denyIncompleteValues", "use")), None)
+ nodes = import_fragment(xml_doc, frag_xml)
+ for node in nodes:
+ insert_before_element(param_el, node, ref_node, child_indent)
+ verb = "updated" if was_existing else "added"
+ print(f'[OK] Parameter "{param_name}": value {verb} to {value}')
+ elif existing is not None:
existing.text = value
print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
else:
@@ -1591,12 +1646,22 @@ elif operation == "modify-parameter":
av_value = av_parts[0].strip()
av_presentation = av_parts[1].strip() if len(av_parts) > 1 else ""
- av_type = "xs:string"
- if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_value):
- av_type = "dcscor:DesignTimeValue"
+ # Prefer declared of the parameter; fall back to value pattern
+ declared_type = ""
+ vt_el = None
+ for ch in param_el:
+ if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS:
+ vt_el = ch
+ break
+ if vt_el is not None:
+ for tnode in vt_el:
+ if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
+ declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
+ break
av_lines = [f"{child_indent}"]
- av_lines.append(f'{child_indent}\t{esc_xml(av_value)}')
+ for vl in build_param_value_xml(declared_type, av_value, f"{child_indent}\t"):
+ av_lines.append(vl)
if av_presentation:
av_lines.append(f'{child_indent}\t')
av_lines.append(f"{child_indent}\t\t")
@@ -1922,6 +1987,85 @@ elif operation == "set-structure":
print(f'[OK] Structure set in variant "{var_name}": {value_arg}')
+elif operation == "modify-structure":
+ settings = resolve_variant_settings()
+ var_name = get_variant_name()
+
+ struct_items = parse_structure_shorthand(value_arg)
+
+ # Flatten parsed tree into (name, groupBy) targets
+ targets = []
+ stack = list(struct_items)
+ while stack:
+ it = stack.pop()
+ if it.get("name"):
+ targets.append({"name": it["name"], "groupBy": it.get("groupBy", [])})
+ for ch in it.get("children", []) or []:
+ stack.append(ch)
+
+ if not targets:
+ print(f"modify-structure requires @name= for at least one group: {value_arg}", file=sys.stderr)
+ sys.exit(1)
+
+ ns = {"dcsset": SET_NS, "xsi": XSI_NS}
+
+ for t in targets:
+ xpath = f".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='{t['name']}']"
+ group_el = settings.find(xpath, ns)
+ if group_el is None:
+ print(f'[WARN] Group with @name="{t["name"]}" not found — skipped')
+ continue
+
+ gi_el = None
+ for ch in group_el:
+ if isinstance(ch.tag, str) and local_name(ch) == "groupItems" and etree.QName(ch.tag).namespace == SET_NS:
+ gi_el = ch
+ break
+
+ group_indent = get_child_indent(group_el)
+ if gi_el is None:
+ # Insert after , before first non-name sibling
+ ref_after_name = None
+ saw_name = False
+ for ch in group_el:
+ if not isinstance(ch.tag, str):
+ continue
+ if local_name(ch) == "name" and etree.QName(ch.tag).namespace == SET_NS:
+ saw_name = True
+ elif saw_name:
+ ref_after_name = ch
+ break
+ gi_frag = f"{group_indent}"
+ for node in import_fragment(xml_doc, gi_frag):
+ insert_before_element(group_el, node, ref_after_name, group_indent)
+ for ch in group_el:
+ if isinstance(ch.tag, str) and local_name(ch) == "groupItems" and etree.QName(ch.tag).namespace == SET_NS:
+ gi_el = ch
+ break
+
+ to_remove = [ch for ch in gi_el if isinstance(ch.tag, str)]
+ for el in to_remove:
+ remove_node_with_whitespace(el)
+
+ item_indent = group_indent + "\t"
+
+ for field in t["groupBy"]:
+ lines = [
+ f'{item_indent}',
+ f'{item_indent}\t{esc_xml(field)}',
+ f'{item_indent}\tItems',
+ f'{item_indent}\tNone',
+ f'{item_indent}\t0001-01-01T00:00:00',
+ f'{item_indent}\t0001-01-01T00:00:00',
+ f'{item_indent}',
+ ]
+ frag_xml = "\r\n".join(lines)
+ for node in import_fragment(xml_doc, frag_xml):
+ insert_before_element(gi_el, node, None, item_indent)
+
+ desc = "details" if not t["groupBy"] else ", ".join(t["groupBy"])
+ print(f'[OK] Group "{t["name"]}" groupItems updated: {desc}')
+
elif operation == "add-dataSetLink":
for val in values:
parsed = parse_data_set_link_shorthand(val)
diff --git a/tests/skills/cases/skd-edit/add-parameter-ref-type.json b/tests/skills/cases/skd-edit/add-parameter-ref-type.json
new file mode 100644
index 00000000..4e6f0710
--- /dev/null
+++ b/tests/skills/cases/skd-edit/add-parameter-ref-type.json
@@ -0,0 +1,21 @@
+{
+ "name": "add-parameter: ссылочный тип → xsi:type=dcscor:DesignTimeValue",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Счет ИЗ Регистр КАК Т",
+ "fields": ["Счет: string"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "add-parameter",
+ "value": "Контрагент: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/modify-parameter-value.json b/tests/skills/cases/skd-edit/modify-parameter-value.json
new file mode 100644
index 00000000..0bb4354b
--- /dev/null
+++ b/tests/skills/cases/skd-edit/modify-parameter-value.json
@@ -0,0 +1,25 @@
+{
+ "name": "modify-parameter value=... обновляет существующий (не дублирует)",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Счет ИЗ Регистр КАК Т",
+ "fields": ["Счет: string"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ },
+ {
+ "script": "skd-edit/scripts/skd-edit",
+ "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "add-parameter", "-Value": "Контрагент: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "modify-parameter",
+ "value": "Контрагент value=Справочник.Контрагенты.НашаОрганизация"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/modify-structure-preserves-selection.json b/tests/skills/cases/skd-edit/modify-structure-preserves-selection.json
new file mode 100644
index 00000000..03cb2c92
--- /dev/null
+++ b/tests/skills/cases/skd-edit/modify-structure-preserves-selection.json
@@ -0,0 +1,29 @@
+{
+ "name": "modify-structure меняет groupItems, сохраняет Selection",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Счет, Т.Сумма, Т.Валюта ИЗ Регистр КАК Т",
+ "fields": ["Счет: string", "Сумма: decimal(15,2)", "Валюта: string"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ },
+ {
+ "script": "skd-edit/scripts/skd-edit",
+ "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "set-structure", "-Value": "Счет @name=ДанныеОтчета" }
+ },
+ {
+ "script": "skd-edit/scripts/skd-edit",
+ "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "add-selection", "-Value": "Счет @group=ДанныеОтчета ;; Сумма @group=ДанныеОтчета" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "modify-structure",
+ "value": "Валюта @name=ДанныеОтчета"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/patch-query-batch.json b/tests/skills/cases/skd-edit/patch-query-batch.json
new file mode 100644
index 00000000..6e045694
--- /dev/null
+++ b/tests/skills/cases/skd-edit/patch-query-batch.json
@@ -0,0 +1,21 @@
+{
+ "name": "patch-query ;;-batch: сегменты триммятся по краям",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.СтароеИмя1, Т.СтароеИмя2 ИЗ Регистр КАК Т",
+ "fields": ["СтароеИмя1: string", "СтароеИмя2: string"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "patch-query",
+ "value": "СтароеИмя1 => НовоеИмя1 ;; СтароеИмя2 => НовоеИмя2"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/set-structure-multi-field.json b/tests/skills/cases/skd-edit/set-structure-multi-field.json
new file mode 100644
index 00000000..78a6a7b7
--- /dev/null
+++ b/tests/skills/cases/skd-edit/set-structure-multi-field.json
@@ -0,0 +1,21 @@
+{
+ "name": "set-structure: запятая в shorthand → несколько GroupItemField",
+ "preRun": [
+ {
+ "script": "skd-compile/scripts/skd-compile",
+ "input": {
+ "dataSets": [{
+ "name": "Основной",
+ "query": "ВЫБРАТЬ Т.Валюта, Т.Банк, Т.ИНН ИЗ Регистр КАК Т",
+ "fields": ["Валюта: string", "Банк: string", "ИНН: string"]
+ }]
+ },
+ "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
+ }
+ ],
+ "params": {
+ "templatePath": "Template.xml",
+ "operation": "set-structure",
+ "value": "Валюта, Банк, ИНН @name=ДанныеОтчета"
+ }
+}
diff --git a/tests/skills/cases/skd-edit/snapshots/add-parameter-ref-type/Template.xml b/tests/skills/cases/skd-edit/snapshots/add-parameter-ref-type/Template.xml
new file mode 100644
index 00000000..db6b77df
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/add-parameter-ref-type/Template.xml
@@ -0,0 +1,51 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Счет
+ Счет
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Счет ИЗ Регистр КАК Т
+
+
+ Контрагент
+
+ d5p1:CatalogRef.Контрагенты
+
+ Справочник.Контрагенты.ПустаяСсылка
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/modify-parameter-value/Template.xml b/tests/skills/cases/skd-edit/snapshots/modify-parameter-value/Template.xml
new file mode 100644
index 00000000..08a8314e
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/modify-parameter-value/Template.xml
@@ -0,0 +1,51 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Счет
+ Счет
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Счет ИЗ Регистр КАК Т
+
+
+ Контрагент
+
+ d5p1:CatalogRef.Контрагенты
+
+ Справочник.Контрагенты.НашаОрганизация
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/modify-structure-preserves-selection/Template.xml b/tests/skills/cases/skd-edit/snapshots/modify-structure-preserves-selection/Template.xml
new file mode 100644
index 00000000..4f314edf
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/modify-structure-preserves-selection/Template.xml
@@ -0,0 +1,83 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Счет
+ Счет
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Сумма
+ Сумма
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+
+
+ Валюта
+ Валюта
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Счет, Т.Сумма, Т.Валюта ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+ ДанныеОтчета
+
+
+ Валюта
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+
+ Счет
+
+
+ Сумма
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/patch-query-batch/Template.xml b/tests/skills/cases/skd-edit/snapshots/patch-query-batch/Template.xml
new file mode 100644
index 00000000..93e673a5
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/patch-query-batch/Template.xml
@@ -0,0 +1,55 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ СтароеИмя1
+ СтароеИмя1
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ СтароеИмя2
+ СтароеИмя2
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.НовоеИмя1, Т.НовоеИмя2 ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/skills/cases/skd-edit/snapshots/set-structure-multi-field/Template.xml b/tests/skills/cases/skd-edit/snapshots/set-structure-multi-field/Template.xml
new file mode 100644
index 00000000..2378f2b7
--- /dev/null
+++ b/tests/skills/cases/skd-edit/snapshots/set-structure-multi-field/Template.xml
@@ -0,0 +1,90 @@
+
+
+
+ ИсточникДанных1
+ Local
+
+
+ Основной
+
+ Валюта
+ Валюта
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ Банк
+ Банк
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+
+ ИНН
+ ИНН
+
+ xs:string
+
+ 0
+ Variable
+
+
+
+ ИсточникДанных1
+ ВЫБРАТЬ Т.Валюта, Т.Банк, Т.ИНН ИЗ Регистр КАК Т
+
+
+ Основной
+
+
+ ru
+ Основной
+
+
+
+
+ ДанныеОтчета
+
+
+ Валюта
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+ Банк
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+ ИНН
+ Items
+ None
+ 0001-01-01T00:00:00
+ 0001-01-01T00:00:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+