mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-26 21:19:42 +03:00
feat(skd): canonical @autoDates pattern + new params operations + use preservation
Fix-pack from skills-improvements-v4 feedback addressing 6 issues found during real-world ФСД report development. skd-compile (v1.10 → v1.11): - @autoDates: emit canonical БСП pattern for ДатаНачала/ДатаОкончания — with title, useRestriction=true, value 0001-01-01T00:00:00, expression. Removed availableAsField=false so БСП creates two separate Start/End fields in the quick settings panel (was rendering as a single picker). - StandardPeriod value: always emit v8:startDate/v8:endDate to match how 1C Designer saves the schema (avoids spurious diff on first re-save). - parameter shorthand: support [Title] syntax mirroring add-field. skd-edit (v1.9 → v1.10): - modify-filter / modify-dataParameter: preserve <use> when @off/@on not explicitly set (was silently stripping <use>false</use>). Tristate parser: None=don't touch, False=@off, True=@on. - modify-parameter: support [Title] for setting/replacing <title>. - rename-parameter: new operation "OldName => NewName" — atomically renames parameter, updates &Name references in expressions of other parameters (full identifier match only), and dcscor:parameter entries in dataParameters of all variants. Query text is not touched. - reorder-parameters: new operation "Name1, Name2, ..." — partial list, named params go first in given order, rest preserve original order. - StandardPeriod value: same v8:startDate/v8:endDate fix as compile. Tests: 4 new test cases (rename-parameter, reorder-parameters, modify-parameter-title, modify-dataParameter-preserves-use). 48/48 passing on both PowerShell and Python runtimes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
384e68cab4
commit
83b289de32
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.9 — Atomic 1C DCS editor
|
||||
# skd-edit v1.10 — Atomic 1C DCS editor
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -11,6 +11,7 @@ param(
|
||||
"add-dataSet","add-variant","add-conditionalAppearance","add-drilldown",
|
||||
"set-query","patch-query","set-outputParameter","set-structure",
|
||||
"modify-field","modify-filter","modify-dataParameter","modify-parameter",
|
||||
"rename-parameter","reorder-parameters",
|
||||
"clear-selection","clear-order","clear-filter",
|
||||
"remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")]
|
||||
[string]$Operation,
|
||||
@@ -285,13 +286,19 @@ function Parse-CalcShorthand {
|
||||
function Parse-ParamShorthand {
|
||||
param([string]$s)
|
||||
|
||||
$result = @{ name = ""; type = ""; value = $null; autoDates = $false }
|
||||
$result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null }
|
||||
|
||||
if ($s -match '@autoDates') {
|
||||
$result.autoDates = $true
|
||||
$s = $s -replace '\s*@autoDates', ''
|
||||
}
|
||||
|
||||
# Extract optional [Title] (mirrors Parse-FieldShorthand)
|
||||
if ($s -match '\[([^\]]*)\]') {
|
||||
$result.title = $Matches[1].Trim()
|
||||
$s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim()
|
||||
}
|
||||
|
||||
if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
|
||||
$result.name = $Matches[1].Trim()
|
||||
$result.type = Resolve-TypeStr ($Matches[2].Trim())
|
||||
@@ -308,7 +315,9 @@ function Parse-ParamShorthand {
|
||||
function Parse-FilterShorthand {
|
||||
param([string]$s)
|
||||
|
||||
$result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null }
|
||||
# use is tristate: $null = not specified (modify-* won't touch),
|
||||
# $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false.
|
||||
$result = @{ field = ""; op = "Equal"; value = $null; use = $null; userSettingID = $null; viewMode = $null }
|
||||
|
||||
if ($s -match '@user') {
|
||||
$result.userSettingID = "auto"
|
||||
@@ -318,6 +327,10 @@ function Parse-FilterShorthand {
|
||||
$result.use = $false
|
||||
$s = $s -replace '\s*@off', ''
|
||||
}
|
||||
if ($s -match '@on\b') {
|
||||
$result.use = $true
|
||||
$s = $s -replace '\s*@on\b', ''
|
||||
}
|
||||
if ($s -match '@quickAccess') {
|
||||
$result.viewMode = "QuickAccess"
|
||||
$s = $s -replace '\s*@quickAccess', ''
|
||||
@@ -383,7 +396,9 @@ function Parse-FilterShorthand {
|
||||
function Parse-DataParamShorthand {
|
||||
param([string]$s)
|
||||
|
||||
$result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null }
|
||||
# use is tristate: $null = not specified (modify-* won't touch),
|
||||
# $false = @off (explicit), $true = @on (explicit). add-* writes <use>false</use> only when $false.
|
||||
$result = @{ parameter = ""; value = $null; use = $null; userSettingID = $null; viewMode = $null }
|
||||
|
||||
if ($s -match '@user') {
|
||||
$result.userSettingID = "auto"
|
||||
@@ -393,6 +408,10 @@ function Parse-DataParamShorthand {
|
||||
$result.use = $false
|
||||
$s = $s -replace '\s*@off', ''
|
||||
}
|
||||
if ($s -match '@on\b') {
|
||||
$result.use = $true
|
||||
$s = $s -replace '\s*@on\b', ''
|
||||
}
|
||||
if ($s -match '@quickAccess') {
|
||||
$result.viewMode = "QuickAccess"
|
||||
$s = $s -replace '\s*@quickAccess', ''
|
||||
@@ -784,6 +803,10 @@ function Build-ParamFragment {
|
||||
$lines += "$i<parameter>"
|
||||
$lines += "$i`t<name>$(Esc-Xml $parsed.name)</name>"
|
||||
|
||||
if ($parsed.title) {
|
||||
$lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t")
|
||||
}
|
||||
|
||||
if ($parsed.type) {
|
||||
$lines += "$i`t<valueType>"
|
||||
$lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
|
||||
@@ -795,6 +818,8 @@ function Build-ParamFragment {
|
||||
if ($parsed.type -eq "StandardPeriod") {
|
||||
$lines += "$i`t<value xsi:type=`"v8:StandardPeriod`">"
|
||||
$lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>"
|
||||
$lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
|
||||
$lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
|
||||
$lines += "$i`t</value>"
|
||||
} elseif ($parsed.type -match '^date') {
|
||||
$lines += "$i`t<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
|
||||
@@ -813,25 +838,30 @@ function Build-ParamFragment {
|
||||
if ($parsed.autoDates) {
|
||||
$paramName = $parsed.name
|
||||
|
||||
# Canonical БСП pattern: title + valueType + value + useRestriction + expression
|
||||
$bLines = @()
|
||||
$bLines += "$i<parameter>"
|
||||
$bLines += "$i`t<name>ДатаНачала</name>"
|
||||
$bLines += (Build-MLTextXml -tag "title" -text "Начало периода" -indent "$i`t")
|
||||
$bLines += "$i`t<valueType>"
|
||||
$bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t")
|
||||
$bLines += "$i`t</valueType>"
|
||||
$bLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>"
|
||||
$bLines += "$i`t<useRestriction>true</useRestriction>"
|
||||
$bLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаНачала")</expression>"
|
||||
$bLines += "$i`t<availableAsField>false</availableAsField>"
|
||||
$bLines += "$i</parameter>"
|
||||
$fragments += ($bLines -join "`r`n")
|
||||
|
||||
$eLines = @()
|
||||
$eLines += "$i<parameter>"
|
||||
$eLines += "$i`t<name>ДатаОкончания</name>"
|
||||
$eLines += (Build-MLTextXml -tag "title" -text "Конец периода" -indent "$i`t")
|
||||
$eLines += "$i`t<valueType>"
|
||||
$eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t")
|
||||
$eLines += "$i`t</valueType>"
|
||||
$eLines += "$i`t<value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</value>"
|
||||
$eLines += "$i`t<useRestriction>true</useRestriction>"
|
||||
$eLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаОкончания")</expression>"
|
||||
$eLines += "$i`t<availableAsField>false</availableAsField>"
|
||||
$eLines += "$i</parameter>"
|
||||
$fragments += ($eLines -join "`r`n")
|
||||
}
|
||||
@@ -929,6 +959,8 @@ function Build-DataParamFragment {
|
||||
if ($parsed.value -is [hashtable] -and $parsed.value.variant) {
|
||||
$lines += "$i`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
|
||||
$lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>"
|
||||
$lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
|
||||
$lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
|
||||
$lines += "$i`t</dcscor:value>"
|
||||
} elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') {
|
||||
$lines += "$i`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>"
|
||||
@@ -1711,7 +1743,14 @@ switch ($Operation) {
|
||||
|
||||
"modify-parameter" {
|
||||
foreach ($val in $values) {
|
||||
# Parse: "ParamName key=value key=value"
|
||||
# Parse: "ParamName [Title] key=value key=value"
|
||||
# Extract optional [Title] first (mirrors Parse-FieldShorthand)
|
||||
$titleVal = $null
|
||||
if ($val -match '\[([^\]]*)\]') {
|
||||
$titleVal = $Matches[1].Trim()
|
||||
$val = ($val -replace '\s*\[[^\]]*\]\s*', ' ').Trim()
|
||||
}
|
||||
|
||||
$parts = $val -split '\s+', 2
|
||||
$paramName = $parts[0].Trim()
|
||||
$rest = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" }
|
||||
@@ -1725,6 +1764,32 @@ switch ($Operation) {
|
||||
|
||||
$childIndent = Get-ChildIndent $paramEl
|
||||
|
||||
# Set/replace title (must come right after <name>, before <valueType>)
|
||||
if ($null -ne $titleVal) {
|
||||
$existingTitle = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'title') {
|
||||
$existingTitle = $ch; break
|
||||
}
|
||||
}
|
||||
if ($existingTitle) {
|
||||
Remove-NodeWithWhitespace $existingTitle
|
||||
}
|
||||
# Insert before first of (valueType, value, useRestriction, expression, availableAsField, ...)
|
||||
$titleRef = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -ne 'name') {
|
||||
$titleRef = $ch; break
|
||||
}
|
||||
}
|
||||
$titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent
|
||||
$titleNodes = Import-Fragment $xmlDoc $titleFrag
|
||||
foreach ($node in $titleNodes) {
|
||||
Insert-BeforeElement $paramEl $node $titleRef $childIndent
|
||||
}
|
||||
Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`""
|
||||
}
|
||||
|
||||
# Separate availableValue=... from simple kv pairs
|
||||
$simpleRest = $rest
|
||||
$avPart = $null
|
||||
@@ -1809,6 +1874,160 @@ switch ($Operation) {
|
||||
}
|
||||
}
|
||||
|
||||
"rename-parameter" {
|
||||
foreach ($val in $values) {
|
||||
# Shorthand: "OldName => NewName"
|
||||
if ($val -notmatch '^\s*(.+?)\s*=>\s*(.+?)\s*$') {
|
||||
Write-Host "[WARN] rename-parameter expects 'OldName => NewName', got: $val"
|
||||
continue
|
||||
}
|
||||
$oldName = $Matches[1].Trim()
|
||||
$newName = $Matches[2].Trim()
|
||||
|
||||
if ($oldName -eq $newName) {
|
||||
Write-Host "[WARN] rename-parameter: old and new names are equal — skipped"
|
||||
continue
|
||||
}
|
||||
|
||||
# 1. Rename <parameter><name>OldName</name>
|
||||
$root = $xmlDoc.DocumentElement
|
||||
$paramEl = Find-ElementByChildValue $root "parameter" "name" $oldName $schNs
|
||||
if (-not $paramEl) {
|
||||
Write-Host "[WARN] Parameter `"$oldName`" not found — skipped"
|
||||
continue
|
||||
}
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $schNs) {
|
||||
$ch.InnerText = $newName
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Update <expression> in other <parameter> elements.
|
||||
# Regex matches "&OldName" only when followed by a non-identifier char (or end),
|
||||
# so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета".
|
||||
$escOld = [regex]::Escape($oldName)
|
||||
$exprRegex = "&$escOld(?=[^\w\u0400-\u04FF]|$)"
|
||||
$exprUpdated = 0
|
||||
foreach ($ch in $root.ChildNodes) {
|
||||
if ($ch.NodeType -ne 'Element' -or $ch.LocalName -ne 'parameter' -or $ch.NamespaceURI -ne $schNs) { continue }
|
||||
foreach ($gc in $ch.ChildNodes) {
|
||||
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'expression' -and $gc.NamespaceURI -eq $schNs) {
|
||||
$oldExpr = $gc.InnerText
|
||||
$newExpr = [regex]::Replace($oldExpr, $exprRegex, "&$newName")
|
||||
if ($newExpr -ne $oldExpr) {
|
||||
$gc.InnerText = $newExpr
|
||||
$exprUpdated++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants.
|
||||
# Note: <settingsVariant> is in schNs, but <settings> and <dataParameters> are in setNs.
|
||||
# IMPORTANT: don't use $variant — it collides with script parameter [string]$Variant
|
||||
# (PowerShell vars are case-insensitive, and the [string] type would coerce XmlNode to "").
|
||||
$dpUpdated = 0
|
||||
foreach ($variantNode in $root.ChildNodes) {
|
||||
if ($variantNode.NodeType -ne 'Element' -or $variantNode.LocalName -ne 'settingsVariant' -or $variantNode.NamespaceURI -ne $schNs) { continue }
|
||||
$settings = Find-FirstElement $variantNode @("settings") $setNs
|
||||
if (-not $settings) { continue }
|
||||
$dpEl = Find-FirstElement $settings @("dataParameters") $setNs
|
||||
if (-not $dpEl) { continue }
|
||||
foreach ($item in $dpEl.ChildNodes) {
|
||||
if ($item.NodeType -ne 'Element' -or $item.LocalName -ne 'item') { continue }
|
||||
foreach ($gc in $item.ChildNodes) {
|
||||
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'parameter' -and $gc.NamespaceURI -eq $corNs) {
|
||||
if ($gc.InnerText.Trim() -eq $oldName) {
|
||||
$gc.InnerText = $newName
|
||||
$dpUpdated++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)"
|
||||
}
|
||||
}
|
||||
|
||||
"reorder-parameters" {
|
||||
foreach ($val in $values) {
|
||||
# Shorthand: "Name1, Name2, Name3" — partial list, listed names go first in order, rest preserve original order
|
||||
$order = @($val -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
if ($order.Count -eq 0) {
|
||||
Write-Host "[WARN] reorder-parameters: empty list — skipped"
|
||||
continue
|
||||
}
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
# Collect all <parameter> in document order with their child indent
|
||||
$allParams = @()
|
||||
foreach ($ch in $root.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'parameter' -and $ch.NamespaceURI -eq $schNs) {
|
||||
$allParams += $ch
|
||||
}
|
||||
}
|
||||
if ($allParams.Count -eq 0) {
|
||||
Write-Host "[WARN] reorder-parameters: no parameters in schema"
|
||||
continue
|
||||
}
|
||||
|
||||
$childIndent = Get-ChildIndent $root
|
||||
|
||||
# Build name -> element map
|
||||
$byName = @{}
|
||||
foreach ($pe in $allParams) {
|
||||
foreach ($gc in $pe.ChildNodes) {
|
||||
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) {
|
||||
$byName[$gc.InnerText.Trim()] = $pe
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Build new order
|
||||
$newOrder = @()
|
||||
$used = @{}
|
||||
foreach ($name in $order) {
|
||||
if ($byName.ContainsKey($name)) {
|
||||
$newOrder += $byName[$name]
|
||||
$used[$name] = $true
|
||||
} else {
|
||||
Write-Host "[WARN] reorder-parameters: parameter `"$name`" not found — skipped"
|
||||
}
|
||||
}
|
||||
foreach ($pe in $allParams) {
|
||||
$peName = $null
|
||||
foreach ($gc in $pe.ChildNodes) {
|
||||
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) {
|
||||
$peName = $gc.InnerText.Trim(); break
|
||||
}
|
||||
}
|
||||
if ($peName -and -not $used.ContainsKey($peName)) {
|
||||
$newOrder += $pe
|
||||
}
|
||||
}
|
||||
|
||||
# Find anchor: element right after the last parameter in original order
|
||||
$lastParam = $allParams[-1]
|
||||
$anchor = $lastParam.NextSibling
|
||||
|
||||
# Remove all parameters with surrounding whitespace
|
||||
foreach ($pe in $allParams) {
|
||||
Remove-NodeWithWhitespace $pe
|
||||
}
|
||||
|
||||
# Re-insert in new order before anchor
|
||||
foreach ($pe in $newOrder) {
|
||||
Insert-BeforeElement $root $pe $anchor $childIndent
|
||||
}
|
||||
|
||||
Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)"
|
||||
}
|
||||
}
|
||||
|
||||
"add-filter" {
|
||||
$settings = Resolve-VariantSettings
|
||||
$varName = Get-VariantName
|
||||
@@ -2276,11 +2495,11 @@ switch ($Operation) {
|
||||
Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent
|
||||
}
|
||||
|
||||
# Update use
|
||||
# Update use (only when explicitly set via @off / @on)
|
||||
if ($parsed.use -eq $false) {
|
||||
Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent
|
||||
} else {
|
||||
# If explicitly not @off, remove use=false if exists
|
||||
} elseif ($parsed.use -eq $true) {
|
||||
# @on: remove existing use=false if any
|
||||
$useEl = $null
|
||||
foreach ($ch in $filterItem.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) {
|
||||
@@ -2346,6 +2565,8 @@ switch ($Operation) {
|
||||
if ($parsed.value -is [hashtable] -and $parsed.value.variant) {
|
||||
$valLines += "$itemIndent<dcscor:value xsi:type=`"v8:StandardPeriod`">"
|
||||
$valLines += "$itemIndent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $parsed.value.variant)</v8:variant>"
|
||||
$valLines += "$itemIndent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
|
||||
$valLines += "$itemIndent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
|
||||
$valLines += "$itemIndent</dcscor:value>"
|
||||
} elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') {
|
||||
$valLines += "$itemIndent<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($parsed.value)")</dcscor:value>"
|
||||
@@ -2361,10 +2582,11 @@ switch ($Operation) {
|
||||
}
|
||||
}
|
||||
|
||||
# Update use
|
||||
# Update use (only when explicitly set via @off / @on)
|
||||
if ($parsed.use -eq $false) {
|
||||
Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent
|
||||
} else {
|
||||
} elseif ($parsed.use -eq $true) {
|
||||
# @on: remove existing use=false if any
|
||||
$useEl = $null
|
||||
foreach ($ch in $dpItem.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.9 — Atomic 1C DCS editor (Python port)
|
||||
# skd-edit v1.10 — Atomic 1C DCS editor (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -19,6 +19,7 @@ VALID_OPS = [
|
||||
"add-dataSet", "add-variant", "add-conditionalAppearance", "add-drilldown",
|
||||
"set-query", "patch-query", "set-outputParameter", "set-structure",
|
||||
"modify-field", "modify-filter", "modify-dataParameter", "modify-parameter",
|
||||
"rename-parameter", "reorder-parameters",
|
||||
"clear-selection", "clear-order", "clear-filter",
|
||||
"remove-field", "remove-total", "remove-calculated-field", "remove-parameter", "remove-filter",
|
||||
]
|
||||
@@ -282,12 +283,18 @@ def parse_calc_shorthand(s):
|
||||
|
||||
|
||||
def parse_param_shorthand(s):
|
||||
result = {"name": "", "type": "", "value": None, "autoDates": False}
|
||||
result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None}
|
||||
|
||||
if re.search(r'@autoDates', s):
|
||||
result["autoDates"] = True
|
||||
s = re.sub(r'\s*@autoDates', '', s)
|
||||
|
||||
# Extract optional [Title] (mirrors parse_field_shorthand)
|
||||
m = re.search(r'\[([^\]]*)\]', s)
|
||||
if m:
|
||||
result["title"] = m.group(1).strip()
|
||||
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
|
||||
|
||||
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s)
|
||||
if m:
|
||||
result["name"] = m.group(1).strip()
|
||||
@@ -301,7 +308,9 @@ def parse_param_shorthand(s):
|
||||
|
||||
|
||||
def parse_filter_shorthand(s):
|
||||
result = {"field": "", "op": "Equal", "value": None, "use": True, "userSettingID": None, "viewMode": None}
|
||||
# use is tristate: None = not specified (modify-* won't touch),
|
||||
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
|
||||
result = {"field": "", "op": "Equal", "value": None, "use": None, "userSettingID": None, "viewMode": None}
|
||||
|
||||
if re.search(r'@user', s):
|
||||
result["userSettingID"] = "auto"
|
||||
@@ -309,6 +318,9 @@ def parse_filter_shorthand(s):
|
||||
if re.search(r'@off', s):
|
||||
result["use"] = False
|
||||
s = re.sub(r'\s*@off', '', s)
|
||||
if re.search(r'@on\b', s):
|
||||
result["use"] = True
|
||||
s = re.sub(r'\s*@on\b', '', s)
|
||||
if re.search(r'@quickAccess', s):
|
||||
result["viewMode"] = "QuickAccess"
|
||||
s = re.sub(r'\s*@quickAccess', '', s)
|
||||
@@ -366,7 +378,9 @@ def parse_filter_shorthand(s):
|
||||
|
||||
|
||||
def parse_data_param_shorthand(s):
|
||||
result = {"parameter": "", "value": None, "use": True, "userSettingID": None, "viewMode": None}
|
||||
# use is tristate: None = not specified (modify-* won't touch),
|
||||
# False = @off (explicit), True = @on (explicit). add-* writes <use>false</use> only when False.
|
||||
result = {"parameter": "", "value": None, "use": None, "userSettingID": None, "viewMode": None}
|
||||
|
||||
if re.search(r'@user', s):
|
||||
result["userSettingID"] = "auto"
|
||||
@@ -374,6 +388,9 @@ def parse_data_param_shorthand(s):
|
||||
if re.search(r'@off', s):
|
||||
result["use"] = False
|
||||
s = re.sub(r'\s*@off', '', s)
|
||||
if re.search(r'@on\b', s):
|
||||
result["use"] = True
|
||||
s = re.sub(r'\s*@on\b', '', s)
|
||||
if re.search(r'@quickAccess', s):
|
||||
result["viewMode"] = "QuickAccess"
|
||||
s = re.sub(r'\s*@quickAccess', '', s)
|
||||
@@ -696,6 +713,10 @@ def build_param_fragment(parsed, indent):
|
||||
fragments = []
|
||||
|
||||
lines = [f"{i}<parameter>", f"{i}\t<name>{esc_xml(parsed['name'])}</name>"]
|
||||
|
||||
if parsed.get("title"):
|
||||
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
|
||||
|
||||
if parsed.get("type"):
|
||||
lines.append(f"{i}\t<valueType>")
|
||||
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
|
||||
@@ -706,6 +727,8 @@ def build_param_fragment(parsed, indent):
|
||||
if parsed.get("type") == "StandardPeriod":
|
||||
lines.append(f'{i}\t<value xsi:type="v8:StandardPeriod">')
|
||||
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
|
||||
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
||||
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
||||
lines.append(f"{i}\t</value>")
|
||||
elif parsed.get("type", "").startswith("date"):
|
||||
lines.append(f'{i}\t<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
|
||||
@@ -721,14 +744,17 @@ def build_param_fragment(parsed, indent):
|
||||
|
||||
if parsed.get("autoDates"):
|
||||
param_name = parsed["name"]
|
||||
# Canonical БСП pattern: title + valueType + value + useRestriction + expression
|
||||
b_lines = [
|
||||
f"{i}<parameter>",
|
||||
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430</name>",
|
||||
build_mltext_xml("title", "\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
|
||||
f"{i}\t<valueType>",
|
||||
build_value_type_xml("date", f"{i}\t\t"),
|
||||
f"{i}\t</valueType>",
|
||||
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
|
||||
f"{i}\t<useRestriction>true</useRestriction>",
|
||||
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430')}</expression>",
|
||||
f"{i}\t<availableAsField>false</availableAsField>",
|
||||
f"{i}</parameter>",
|
||||
]
|
||||
fragments.append("\r\n".join(b_lines))
|
||||
@@ -736,11 +762,13 @@ def build_param_fragment(parsed, indent):
|
||||
e_lines = [
|
||||
f"{i}<parameter>",
|
||||
f"{i}\t<name>\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f</name>",
|
||||
build_mltext_xml("title", "\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430", f"{i}\t"),
|
||||
f"{i}\t<valueType>",
|
||||
build_value_type_xml("date", f"{i}\t\t"),
|
||||
f"{i}\t</valueType>",
|
||||
f'{i}\t<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>',
|
||||
f"{i}\t<useRestriction>true</useRestriction>",
|
||||
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f')}</expression>",
|
||||
f"{i}\t<availableAsField>false</availableAsField>",
|
||||
f"{i}</parameter>",
|
||||
]
|
||||
fragments.append("\r\n".join(e_lines))
|
||||
@@ -824,6 +852,8 @@ def build_data_param_fragment(parsed, indent):
|
||||
if isinstance(val, dict) and val.get("variant"):
|
||||
lines.append(f'{i}\t<dcscor:value xsi:type="v8:StandardPeriod">')
|
||||
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val["variant"])}</v8:variant>')
|
||||
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
||||
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
||||
lines.append(f"{i}\t</dcscor:value>")
|
||||
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
|
||||
lines.append(f'{i}\t<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(val))}</dcscor:value>')
|
||||
@@ -1480,6 +1510,13 @@ elif operation == "add-parameter":
|
||||
|
||||
elif operation == "modify-parameter":
|
||||
for val in values:
|
||||
# Extract optional [Title] first (mirrors parse_field_shorthand)
|
||||
title_val = None
|
||||
m_title = re.search(r'\[([^\]]*)\]', val)
|
||||
if m_title:
|
||||
title_val = m_title.group(1).strip()
|
||||
val = re.sub(r'\s*\[[^\]]*\]\s*', ' ', val).strip()
|
||||
|
||||
parts = val.split(None, 1)
|
||||
param_name = parts[0].strip()
|
||||
rest = parts[1].strip() if len(parts) > 1 else ""
|
||||
@@ -1491,6 +1528,18 @@ elif operation == "modify-parameter":
|
||||
|
||||
child_indent = get_child_indent(param_el)
|
||||
|
||||
# Set/replace title (must come right after <name>, before <valueType>)
|
||||
if title_val is not None:
|
||||
existing_title = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "title"), None)
|
||||
if existing_title is not None:
|
||||
remove_node_with_whitespace(existing_title)
|
||||
# Insert before the first child after <name>
|
||||
title_ref = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) != "name"), None)
|
||||
title_frag = build_mltext_xml("title", title_val, child_indent)
|
||||
for node in import_fragment(xml_doc, title_frag):
|
||||
insert_before_element(param_el, node, title_ref, child_indent)
|
||||
print(f'[OK] Parameter "{param_name}": title set to "{title_val}"')
|
||||
|
||||
# Separate availableValue=... from simple kv pairs
|
||||
simple_rest = rest
|
||||
av_part = None
|
||||
@@ -1553,6 +1602,126 @@ elif operation == "modify-parameter":
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
print(f'[OK] Parameter "{param_name}": availableValue added')
|
||||
|
||||
elif operation == "rename-parameter":
|
||||
root = xml_doc
|
||||
for val in values:
|
||||
m_rn = re.match(r'^\s*(.+?)\s*=>\s*(.+?)\s*$', val)
|
||||
if not m_rn:
|
||||
print(f'[WARN] rename-parameter expects "OldName => NewName", got: {val}')
|
||||
continue
|
||||
old_name = m_rn.group(1).strip()
|
||||
new_name = m_rn.group(2).strip()
|
||||
|
||||
if old_name == new_name:
|
||||
print('[WARN] rename-parameter: old and new names are equal -- skipped')
|
||||
continue
|
||||
|
||||
# 1. Rename <parameter><name>OldName</name>
|
||||
param_el = find_element_by_child_value(root, "parameter", "name", old_name, SCH_NS)
|
||||
if param_el is None:
|
||||
print(f'[WARN] Parameter "{old_name}" not found -- skipped')
|
||||
continue
|
||||
for ch in param_el:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "name" and etree.QName(ch.tag).namespace == SCH_NS:
|
||||
ch.text = new_name
|
||||
break
|
||||
|
||||
# 2. Update <expression> in other <parameter> elements.
|
||||
# Regex matches "&OldName" only when followed by a non-identifier char (or end),
|
||||
# so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета".
|
||||
esc_old = re.escape(old_name)
|
||||
expr_regex = re.compile(rf'&{esc_old}(?=[^\w\u0400-\u04FF]|$)')
|
||||
expr_updated = 0
|
||||
for ch in root:
|
||||
if not (isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS):
|
||||
continue
|
||||
for gc in ch:
|
||||
if isinstance(gc.tag, str) and local_name(gc) == "expression" and etree.QName(gc.tag).namespace == SCH_NS:
|
||||
old_expr = gc.text or ""
|
||||
new_expr = expr_regex.sub(f'&{new_name}', old_expr)
|
||||
if new_expr != old_expr:
|
||||
gc.text = new_expr
|
||||
expr_updated += 1
|
||||
|
||||
# 3. Update <dcscor:parameter>OldName</dcscor:parameter> in dataParameters of all variants.
|
||||
dp_updated = 0
|
||||
for variant_node in root:
|
||||
if not (isinstance(variant_node.tag, str) and local_name(variant_node) == "settingsVariant" and etree.QName(variant_node.tag).namespace == SCH_NS):
|
||||
continue
|
||||
settings_node = find_first_element(variant_node, ["settings"], SET_NS)
|
||||
if settings_node is None:
|
||||
continue
|
||||
dp_el = find_first_element(settings_node, ["dataParameters"], SET_NS)
|
||||
if dp_el is None:
|
||||
continue
|
||||
for item in dp_el:
|
||||
if not (isinstance(item.tag, str) and local_name(item) == "item"):
|
||||
continue
|
||||
for gc in item:
|
||||
if isinstance(gc.tag, str) and local_name(gc) == "parameter" and etree.QName(gc.tag).namespace == COR_NS:
|
||||
if (gc.text or "").strip() == old_name:
|
||||
gc.text = new_name
|
||||
dp_updated += 1
|
||||
|
||||
print(f'[OK] Parameter renamed: "{old_name}" => "{new_name}" (expressions updated: {expr_updated}, dataParameters updated: {dp_updated})')
|
||||
|
||||
elif operation == "reorder-parameters":
|
||||
root = xml_doc
|
||||
for val in values:
|
||||
order = [s.strip() for s in val.split(",") if s.strip()]
|
||||
if not order:
|
||||
print('[WARN] reorder-parameters: empty list -- skipped')
|
||||
continue
|
||||
|
||||
all_params = []
|
||||
for ch in root:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "parameter" and etree.QName(ch.tag).namespace == SCH_NS:
|
||||
all_params.append(ch)
|
||||
if not all_params:
|
||||
print('[WARN] reorder-parameters: no parameters in schema')
|
||||
continue
|
||||
|
||||
child_indent = get_child_indent(root)
|
||||
|
||||
by_name = {}
|
||||
for pe in all_params:
|
||||
for gc in pe:
|
||||
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
|
||||
by_name[(gc.text or "").strip()] = pe
|
||||
break
|
||||
|
||||
new_order = []
|
||||
used = set()
|
||||
for name in order:
|
||||
if name in by_name:
|
||||
new_order.append(by_name[name])
|
||||
used.add(name)
|
||||
else:
|
||||
print(f'[WARN] reorder-parameters: parameter "{name}" not found -- skipped')
|
||||
|
||||
for pe in all_params:
|
||||
pe_name = None
|
||||
for gc in pe:
|
||||
if isinstance(gc.tag, str) and local_name(gc) == "name" and etree.QName(gc.tag).namespace == SCH_NS:
|
||||
pe_name = (gc.text or "").strip()
|
||||
break
|
||||
if pe_name and pe_name not in used:
|
||||
new_order.append(pe)
|
||||
|
||||
# Anchor: element right after the last parameter in original order
|
||||
last_param = all_params[-1]
|
||||
anchor = last_param.getnext()
|
||||
|
||||
# Remove all parameters with surrounding whitespace
|
||||
for pe in all_params:
|
||||
remove_node_with_whitespace(pe)
|
||||
|
||||
# Re-insert in new order before anchor
|
||||
for pe in new_order:
|
||||
insert_before_element(root, pe, anchor, child_indent)
|
||||
|
||||
print(f'[OK] Parameters reordered ({len(all_params)} total, {len(order)} explicit)')
|
||||
|
||||
elif operation == "add-filter":
|
||||
settings = resolve_variant_settings()
|
||||
var_name = get_variant_name()
|
||||
@@ -1922,9 +2091,11 @@ elif operation == "modify-filter":
|
||||
vt = parsed.get("valueType", "xs:string")
|
||||
set_or_create_child_element_with_attr(filter_item, "right", SET_NS, str(parsed["value"]), vt, item_indent)
|
||||
|
||||
# Update use (only when explicitly set via @off / @on)
|
||||
if parsed.get("use") is False:
|
||||
set_or_create_child_element(filter_item, "use", SET_NS, "false", item_indent)
|
||||
else:
|
||||
elif parsed.get("use") is True:
|
||||
# @on: remove existing use=false if any
|
||||
for ch in filter_item:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SET_NS:
|
||||
if (ch.text or "").strip() == "false":
|
||||
@@ -1971,6 +2142,8 @@ elif operation == "modify-dataParameter":
|
||||
if isinstance(pv, dict) and pv.get("variant"):
|
||||
val_lines.append(f'{item_indent}<dcscor:value xsi:type="v8:StandardPeriod">')
|
||||
val_lines.append(f'{item_indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(pv["variant"])}</v8:variant>')
|
||||
val_lines.append(f"{item_indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
||||
val_lines.append(f"{item_indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
||||
val_lines.append(f"{item_indent}</dcscor:value>")
|
||||
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(pv)):
|
||||
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(pv))}</dcscor:value>')
|
||||
@@ -1984,9 +2157,11 @@ elif operation == "modify-dataParameter":
|
||||
for node in val_nodes:
|
||||
insert_before_element(dp_item, node, None, item_indent)
|
||||
|
||||
# Update use (only when explicitly set via @off / @on)
|
||||
if parsed.get("use") is False:
|
||||
set_or_create_child_element(dp_item, "use", COR_NS, "false", item_indent)
|
||||
else:
|
||||
elif parsed.get("use") is True:
|
||||
# @on: remove existing use=false if any
|
||||
for ch in dp_item:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == COR_NS:
|
||||
if (ch.text or "").strip() == "false":
|
||||
|
||||
Reference in New Issue
Block a user