From 529a5cacaebb4eb4c581bf4d747efe1dc27104b5 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 15 May 2026 13:40:49 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd-edit):=20modify-structure=20+=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=D1=8B=20set-structure/parameter/patch-query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - modify-structure: новая операция, меняет groupItems группы по @name=, сохраняя Selection/order/filter/conditionalAppearance (Bug 1) - set-structure: shorthand поддерживает запятую для нескольких полей в одном уровне группировки (Bug 2) - set-structure: @name= с обрамляющими кавычками (двойными/одинарными) снимает их при записи в (Bug 3) - add-parameter: ссылочные типы (CatalogRef, ChartOfAccountsRef, …) пишут , не xs:string (Bug 4a) - modify-parameter: namespace-aware lookup существующих свойств — обновляет inplace, не плодит дубли (Bug 4b) - modify-parameter value=…: пересборка с корректным xsi:type из (попутно лечит ранее битый XML) - patch-query: батч ;;-сегментов триммится по краям (Bug 5) - skd-compile: симметричный фикс ссылочных типов в emit_value Регресс: 23/23 PS, 23/23 PY (skd-edit), 21/21 PS+PY (skd-compile), 23/23 платформенный verify-snapshots. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skd-compile/scripts/skd-compile.ps1 | 4 +- .../skills/skd-compile/scripts/skd-compile.py | 4 +- .claude/skills/skd-edit/SKILL.md | 13 +- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 248 +++++++++++++++--- .claude/skills/skd-edit/scripts/skd-edit.py | 202 ++++++++++++-- .../skd-edit/add-parameter-ref-type.json | 21 ++ .../skd-edit/modify-parameter-value.json | 25 ++ .../modify-structure-preserves-selection.json | 29 ++ .../cases/skd-edit/patch-query-batch.json | 21 ++ .../skd-edit/set-structure-multi-field.json | 21 ++ .../add-parameter-ref-type/Template.xml | 51 ++++ .../modify-parameter-value/Template.xml | 51 ++++ .../Template.xml | 83 ++++++ .../snapshots/patch-query-batch/Template.xml | 55 ++++ .../set-structure-multi-field/Template.xml | 90 +++++++ 15 files changed, 854 insertions(+), 64 deletions(-) create mode 100644 tests/skills/cases/skd-edit/add-parameter-ref-type.json create mode 100644 tests/skills/cases/skd-edit/modify-parameter-value.json create mode 100644 tests/skills/cases/skd-edit/modify-structure-preserves-selection.json create mode 100644 tests/skills/cases/skd-edit/patch-query-batch.json create mode 100644 tests/skills/cases/skd-edit/set-structure-multi-field.json create mode 100644 tests/skills/cases/skd-edit/snapshots/add-parameter-ref-type/Template.xml create mode 100644 tests/skills/cases/skd-edit/snapshots/modify-parameter-value/Template.xml create mode 100644 tests/skills/cases/skd-edit/snapshots/modify-structure-preserves-selection/Template.xml create mode 100644 tests/skills/cases/skd-edit/snapshots/patch-query-batch/Template.xml create mode 100644 tests/skills/cases/skd-edit/snapshots/set-structure-multi-field/Template.xml 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" + 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)" + 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}") + 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)}') + 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 + + + + + + + + + + + + + +