From 6e14f2502e12051b206e7d57f74bb7afdb879826 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 20 May 2026 13:38:52 +0300 Subject: [PATCH] feat(skd-edit): empty parameter values, decimal/time/fix/composite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings skd-edit to parity with the skd-compile fixes from 449f814 / 0537410 / ff2d851. Same helpers (Test-EmptyValue / Build-EmptyValueXml in ps1, is_empty_value / build_empty_value_xml in py) shared by add-parameter, modify-parameter (value=...), availableValues, add-dataParameter and modify-dataParameter. Behavior: - Sentinel empty (null / "" / "_" / "null") serializes per declared type, matching what 1C Designer writes — ref/no-type → xsi:nil, string → xsi:type="xs:string" empty, date/time/decimal/boolean → typed zero, StandardPeriod → Custom + zero dates, dataParameters → dcscor:value xsi:nil="true". @valueList omits entirely. - Build-ValueTypeXml accepts bare decimal (10,2), decimal(N) (N,0), string(N,fix) (AllowedLength=Fixed), time (DateFractions=Time), and composite array of types. - Parse-ParamShorthand / Parse-DataParamShorthand regex .+ → .* so a trailing `=` is treated as the empty-value sentinel. New @valueList flag. New test cases: empty-param-values-add / -modify / empty-dataparam-values. Three outdated skd-edit snapshots regenerated to reflect upstream skd-compile empty-value emission (rename-parameter, reorder-parameters, conditional-appearance-v2). Regression: 41/41 ps1 + 41/41 py runner; 41/41 verify-snapshots ps1 + py (live load into 1С 8.3.24). skd-compile 23/23 and skd-validate 15/15 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 164 ++++++++++++--- .claude/skills/skd-edit/scripts/skd-edit.py | 147 ++++++++++--- .../skd-edit/empty-dataparam-values.json | 37 ++++ .../skd-edit/empty-param-values-add.json | 21 ++ .../skd-edit/empty-param-values-modify.json | 39 ++++ .../conditional-appearance-v2/Template.xml | 1 + .../empty-dataparam-values/Template.xml | 84 ++++++++ .../empty-param-values-add/Template.xml | 196 ++++++++++++++++++ .../empty-param-values-modify/Template.xml | 92 ++++++++ .../snapshots/rename-parameter/Template.xml | 2 + .../snapshots/reorder-parameters/Template.xml | 4 + 11 files changed, 734 insertions(+), 53 deletions(-) create mode 100644 tests/skills/cases/skd-edit/empty-dataparam-values.json create mode 100644 tests/skills/cases/skd-edit/empty-param-values-add.json create mode 100644 tests/skills/cases/skd-edit/empty-param-values-modify.json create mode 100644 tests/skills/cases/skd-edit/snapshots/empty-dataparam-values/Template.xml create mode 100644 tests/skills/cases/skd-edit/snapshots/empty-param-values-add/Template.xml create mode 100644 tests/skills/cases/skd-edit/snapshots/empty-param-values-modify/Template.xml diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index f4b1d3da..7c15bd29 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.22 — Atomic 1C DCS editor +# skd-edit v1.23 — Atomic 1C DCS editor # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -337,7 +337,7 @@ function Parse-CalcShorthand { function Parse-ParamShorthand { param([string]$s) - $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null; hidden = $false; always = $false; availableValues = @() } + $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null; hidden = $false; always = $false; availableValues = @(); valueListAllowed = $false } # Extract availableValue=... (must be before main parse — captures to end of string) if ($s -match '\s*availableValue=(.+)$') { @@ -350,6 +350,11 @@ function Parse-ParamShorthand { $s = $s -replace '\s*@autoDates', '' } + if ($s -match '@valueList\b') { + $result.valueListAllowed = $true + $s = $s -replace '\s*@valueList\b', '' + } + if ($s -match '@hidden\b') { $result.hidden = $true $s = $s -replace '\s*@hidden\b', '' @@ -366,11 +371,14 @@ function Parse-ParamShorthand { $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } - if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') { + # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty-value sentinel + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { $result.name = $Matches[1].Trim() $result.type = Resolve-TypeStr ($Matches[2].Trim()) - if ($Matches[4]) { - $result.value = $Matches[4].Trim() + $hasEq = $null -ne $Matches[3] + $rhs = $Matches[4] + if ($hasEq) { + $result.value = if ($rhs) { $rhs.Trim() } else { "" } } } else { $result.name = $s.Trim() @@ -490,17 +498,16 @@ function Parse-DataParamShorthand { $s = $s.Trim() - if ($s -match '^([^=]+)=\s*(.+)$') { + if ($s -match '^([^=]+)=\s*(.*)$') { $result.parameter = $Matches[1].Trim() $valStr = $Matches[2].Trim() $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") - if ($periodVariants -contains $valStr) { + # Empty / sentinel — record as "" so caller emits xsi:nil + if ($valStr -eq "" -or $valStr -eq "_" -or $valStr.ToLowerInvariant() -eq "null") { + $result.value = "" + } elseif ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } - } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { - $result.value = $valStr - } elseif ($valStr -eq "true" -or $valStr -eq "false") { - $result.value = $valStr } else { $result.value = $valStr } @@ -744,10 +751,21 @@ function Parse-AvailableValueList { # --- 4. Build-* functions (XML fragment generators) --- function Build-ValueTypeXml { - param([string]$typeStr, [string]$indent) + param($typeStr, [string]$indent) if (-not $typeStr) { return "" } - $typeStr = Resolve-TypeStr $typeStr + + # Composite: array of types — concatenate per-type fragments + if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { + $parts = @() + foreach ($t in $typeStr) { + $p = Build-ValueTypeXml -typeStr "$t" -indent $indent + if ($p) { $parts += $p } + } + return $parts -join "`n" + } + + $typeStr = Resolve-TypeStr "$typeStr" $lines = @() if ($typeStr -eq "boolean") { @@ -755,20 +773,27 @@ function Build-ValueTypeXml { return $lines -join "`n" } - if ($typeStr -match '^string(\((\d+)\))?$') { + # string, string(N), string(N,fix) — fix → AllowedLength=Fixed + if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $al = if ($Matches[4]) { "Fixed" } else { "Variable" } $lines += "$indentxs:string" $lines += "$indent" $lines += "$indent`t$len" - $lines += "$indent`tVariable" + $lines += "$indent`t$al" $lines += "$indent" return $lines -join "`n" } - if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1] - $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + # decimal forms — bare decimal = money 10,2; decimal(N) = integer N,0 + if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { + if (-not $Matches[1]) { + $digits = "10"; $fraction = "2"; $sign = "Any" + } else { + $digits = $Matches[2] + $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } + $sign = if ($Matches[5]) { "Nonnegative" } else { "Any" } + } $lines += "$indentxs:decimal" $lines += "$indent" $lines += "$indent`t$digits" @@ -778,10 +803,12 @@ function Build-ValueTypeXml { return $lines -join "`n" } - if ($typeStr -match '^(date|dateTime)$') { + # date / dateTime / time — all xs:dateTime, differ only in DateFractions + if ($typeStr -match '^(date|dateTime|time)$') { $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } + "time" { "Time" } } $lines += "$indentxs:dateTime" $lines += "$indent" @@ -809,6 +836,52 @@ function Build-ValueTypeXml { return $lines -join "`n" } +# Sentinel-normalized empty check — null / "" / "_" / "null" (case-insensitive). +function Test-EmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "") { return $true } + if ($s -eq "_") { return $true } + if ($s.ToLowerInvariant() -eq "null") { return $true } + return $false +} + +# Returns XML fragment string for a type-aware empty . +# Empty + valueListAllowed → omit entirely (returns $null). +# tagPrefix used for dcscor: in data parameters. +function Build-EmptyValueXml { + param([string]$type, [string]$indent, [string]$tagPrefix = "", [string]$tagName = "value", [bool]$valueListAllowed = $false) + if ($valueListAllowed) { return $null } + $t = if ($null -eq $type) { "" } else { "$type" } + # Strip well-known XML schema prefixes so callers can pass raw text + $t = $t -replace '^xs:', '' -replace '^v8:', '' -replace '^d\d+p\d+:', '' + $pf = $tagPrefix + $tn = $tagName + $lines = @() + if ($t -eq "") { + $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" + } elseif ($t -eq "StandardPeriod") { + $lines += "$indent<${pf}${tn} xsi:type=`"v8:StandardPeriod`">" + $lines += "$indent`tCustom" + $lines += "$indent`t0001-01-01T00:00:00" + $lines += "$indent`t0001-01-01T00:00:00" + $lines += "$indent" + } elseif ($t -match '^string') { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:string`"/>" + } elseif ($t -match '^(date|time)') { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:dateTime`">0001-01-01T00:00:00" + } elseif ($t -match '^decimal') { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:decimal`">0" + } elseif ($t -eq "boolean") { + $lines += "$indent<${pf}${tn} xsi:type=`"xs:boolean`">false" + } else { + # Ref types or unknown — safe nil + $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" + } + return $lines -join "`n" +} + function Build-MLTextXml { param([string]$tag, [string]$text, [string]$indent) $lines = @() @@ -1022,8 +1095,13 @@ function Build-AvailableValueFragment { $lines = @() $lines += "$indent" - $valueLines = Build-ParamValueXml -type $declaredType -value $item.value -indent "$indent`t" - foreach ($vl in $valueLines) { $lines += $vl } + if (Test-EmptyValue $item.value) { + $emptyXml = Build-EmptyValueXml -type $declaredType -indent "$indent`t" -tagPrefix "" -tagName "value" -valueListAllowed $false + if ($emptyXml) { $lines += $emptyXml } + } else { + $valueLines = Build-ParamValueXml -type $declaredType -value $item.value -indent "$indent`t" + foreach ($vl in $valueLines) { $lines += $vl } + } if ($item.presentation) { $lines += "$indent`t" $lines += "$indent`t`t" @@ -1056,9 +1134,15 @@ function Build-ParamFragment { $lines += "$i`t" } + $vla = [bool]$parsed.valueListAllowed if ($null -ne $parsed.value) { - $valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t" - foreach ($vl in $valueLines) { $lines += $vl } + if (Test-EmptyValue $parsed.value) { + $emptyXml = Build-EmptyValueXml -type $parsed.type -indent "$i`t" -tagPrefix "" -tagName "value" -valueListAllowed $vla + if ($emptyXml) { $lines += $emptyXml } + } else { + $valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t" + foreach ($vl in $valueLines) { $lines += $vl } + } } if ($parsed.hidden) { @@ -1066,6 +1150,10 @@ function Build-ParamFragment { $lines += "$i`tfalse" } + if ($vla) { + $lines += "$i`ttrue" + } + if ($parsed.availableValues -and $parsed.availableValues.Count -gt 0) { foreach ($av in $parsed.availableValues) { $avLines = Build-AvailableValueFragment -item $av -declaredType $parsed.type -indent "$i`t" @@ -1207,6 +1295,8 @@ function Build-DataParamFragment { $lines += "$i`t`t0001-01-01T00:00:00" $lines += "$i`t`t0001-01-01T00:00:00" $lines += "$i`t" + } elseif (Test-EmptyValue $parsed.value) { + $lines += "$i`t" } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { $lines += "$i`t$(Esc-Xml "$($parsed.value)")" } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { @@ -2107,8 +2197,20 @@ switch ($Operation) { } } } - $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent - $fragXml = $valueLines -join "`n" + # Detect valueListAllowed flag on the parameter — empty value should be omitted + $vlaSet = $false + foreach ($ch in $paramEl.ChildNodes) { + if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { + if ($ch.InnerText.Trim() -eq 'true') { $vlaSet = $true } + break + } + } + if (Test-EmptyValue $value) { + $fragXml = Build-EmptyValueXml -type $declaredType -indent $childIndent -tagPrefix "" -tagName "value" -valueListAllowed $vlaSet + } else { + $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent + $fragXml = $valueLines -join "`n" + } $wasExisting = ($null -ne $existing) if ($existing) { @@ -2127,9 +2229,11 @@ switch ($Operation) { } } } - $nodes = Import-Fragment $xmlDoc $fragXml - foreach ($node in $nodes) { - Insert-BeforeElement $paramEl $node $refNode $childIndent + if ($fragXml) { + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $paramEl $node $refNode $childIndent + } } $verb = if ($wasExisting) { "updated" } else { "added" } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value $verb to $value" @@ -3072,6 +3176,8 @@ switch ($Operation) { $valLines += "$itemIndent`t0001-01-01T00:00:00" $valLines += "$itemIndent`t0001-01-01T00:00:00" $valLines += "$itemIndent" + } elseif (Test-EmptyValue $parsed.value) { + $valLines += "$itemIndent" } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { diff --git a/.claude/skills/skd-edit/scripts/skd-edit.py b/.claude/skills/skd-edit/scripts/skd-edit.py index fc75a236..e8a33c11 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.22 — Atomic 1C DCS editor (Python port) +# skd-edit v1.23 — Atomic 1C DCS editor (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -335,7 +335,7 @@ def parse_calc_shorthand(s): def parse_param_shorthand(s): - result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None, "hidden": False, "always": False, "availableValues": []} + result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None, "hidden": False, "always": False, "availableValues": [], "valueListAllowed": False} # Extract availableValue=... (must be before main parse — captures to end of string) m_av = re.search(r'\s*availableValue=(.+)$', s) @@ -347,6 +347,10 @@ def parse_param_shorthand(s): result["autoDates"] = True s = re.sub(r'\s*@autoDates', '', s) + if re.search(r'@valueList\b', s): + result["valueListAllowed"] = True + s = re.sub(r'\s*@valueList\b', '', s) + if re.search(r'@hidden\b', s): result["hidden"] = True s = re.sub(r'\s*@hidden\b', '', s) @@ -361,12 +365,13 @@ def parse_param_shorthand(s): result["title"] = m.group(1).strip() s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip() - m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.+))?$', s) + # Allow empty RHS (`= ` / `=`) as empty-value sentinel + m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.*))?$', s) if m: result["name"] = m.group(1).strip() result["type"] = resolve_type_str(m.group(2).strip()) - if m.group(4): - result["value"] = m.group(4).strip() + if m.group(3) is not None: + result["value"] = m.group(4).strip() if m.group(4) else "" else: result["name"] = s.strip() @@ -466,7 +471,7 @@ def parse_data_param_shorthand(s): s = s.strip() - m = re.match(r'^([^=]+)=\s*(.+)$', s) + m = re.match(r'^([^=]+)=\s*(.*)$', s) if m: result["parameter"] = m.group(1).strip() val_str = m.group(2).strip() @@ -480,7 +485,10 @@ def parse_data_param_shorthand(s): "TillEndOfThisWeek", "TillEndOfThisTenDays", "TillEndOfThisMonth", "TillEndOfThisQuarter", "TillEndOfThisHalfYear", "TillEndOfThisYear", ] - if val_str in period_variants: + # Empty / sentinel — record as "" so caller emits xsi:nil + if val_str == "" or val_str == "_" or val_str.lower() == "null": + result["value"] = "" + elif val_str in period_variants: result["value"] = {"variant": val_str} else: result["value"] = val_str @@ -684,8 +692,13 @@ def parse_available_value_list(s): def build_available_value_fragment(item, declared_type, indent): """Return XML lines for a single block.""" lines = [f"{indent}"] - for vl in build_param_value_xml(declared_type, item["value"], f"{indent}\t"): - lines.append(vl) + if is_empty_value(item.get("value")): + empty_xml = build_empty_value_xml(declared_type, f"{indent}\t", "", "value", False) + if empty_xml: + lines.append(empty_xml) + else: + for vl in build_param_value_xml(declared_type, item["value"], f"{indent}\t"): + lines.append(vl) if item.get("presentation"): lines.append(f'{indent}\t') lines.append(f"{indent}\t\t") @@ -702,27 +715,44 @@ def build_available_value_fragment(item, declared_type, indent): def build_value_type_xml(type_str, indent): if not type_str: return "" - type_str = resolve_type_str(type_str) + + # Composite: list/tuple → concatenate per-type fragments + if isinstance(type_str, (list, tuple)): + parts = [] + for t in type_str: + p = build_value_type_xml(str(t), indent) + if p: + parts.append(p) + return "\n".join(parts) + + type_str = resolve_type_str(str(type_str)) lines = [] if type_str == "boolean": lines.append(f"{indent}xs:boolean") return "\n".join(lines) - m = re.match(r'^string(\((\d+)\))?$', type_str) + # string, string(N), string(N,fix) — fix → AllowedLength=Fixed + m = re.match(r'^string(\((\d+)(,(fix|fixed))?\))?$', type_str) if m: length = m.group(2) if m.group(2) else "0" + al = "Fixed" if m.group(4) else "Variable" lines.append(f"{indent}xs:string") lines.append(f"{indent}") lines.append(f"{indent}\t{length}") - lines.append(f"{indent}\tVariable") + lines.append(f"{indent}\t{al}") lines.append(f"{indent}") return "\n".join(lines) - m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) + # decimal — bare = 10,2; decimal(N) = N,0 + m = re.match(r'^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$', type_str) if m: - digits, fraction = m.group(1), m.group(2) - sign = "Nonnegative" if m.group(3) else "Any" + if not m.group(1): + digits, fraction, sign = "10", "2", "Any" + else: + digits = m.group(2) + fraction = m.group(4) if m.group(4) else "0" + sign = "Nonnegative" if m.group(5) else "Any" lines.append(f"{indent}xs:decimal") lines.append(f"{indent}") lines.append(f"{indent}\t{digits}") @@ -731,9 +761,10 @@ def build_value_type_xml(type_str, indent): lines.append(f"{indent}") return "\n".join(lines) - m = re.match(r'^(date|dateTime)$', type_str) + # date / dateTime / time — all xs:dateTime + m = re.match(r'^(date|dateTime|time)$', type_str) if m: - fractions = "Date" if type_str == "date" else "DateTime" + fractions = {"date": "Date", "dateTime": "DateTime", "time": "Time"}[type_str] lines.append(f"{indent}xs:dateTime") lines.append(f"{indent}") lines.append(f"{indent}\t{fractions}") @@ -756,6 +787,52 @@ def build_value_type_xml(type_str, indent): return "\n".join(lines) +def is_empty_value(v): + """Empty sentinel — None / '' / '_' / 'null' (case-insensitive).""" + if v is None: + return True + s = str(v).strip() + if s == "": + return True + if s == "_": + return True + if s.lower() == "null": + return True + return False + + +def build_empty_value_xml(type_str, indent, tag_prefix="", tag_name="value", value_list_allowed=False): + """Type-aware empty fragment. Returns None when valueListAllowed (omit).""" + if value_list_allowed: + return None + t = "" if type_str is None else str(type_str) + # Strip well-known XML schema prefixes so callers can pass raw text + t = re.sub(r'^xs:', '', t) + t = re.sub(r'^v8:', '', t) + t = re.sub(r'^d\d+p\d+:', '', t) + pf, tn = tag_prefix, tag_name + lines = [] + if t == "": + lines.append(f'{indent}<{pf}{tn} xsi:nil="true"/>') + elif t == "StandardPeriod": + lines.append(f'{indent}<{pf}{tn} xsi:type="v8:StandardPeriod">') + lines.append(f'{indent}\tCustom') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}') + elif re.match(r'^string', t): + lines.append(f'{indent}<{pf}{tn} xsi:type="xs:string"/>') + elif re.match(r'^(date|time)', t): + lines.append(f'{indent}<{pf}{tn} xsi:type="xs:dateTime">0001-01-01T00:00:00') + elif re.match(r'^decimal', t): + lines.append(f'{indent}<{pf}{tn} xsi:type="xs:decimal">0') + elif t == "boolean": + lines.append(f'{indent}<{pf}{tn} xsi:type="xs:boolean">false') + else: + lines.append(f'{indent}<{pf}{tn} xsi:nil="true"/>') + return "\n".join(lines) + + def build_mltext_xml(tag, text, indent): lines = [ f'{indent}<{tag} xsi:type="v8:LocalStringType">', @@ -934,14 +1011,23 @@ def build_param_fragment(parsed, indent): lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t")) lines.append(f"{i}\t") + vla = bool(parsed.get("valueListAllowed")) if parsed["value"] is not None: - for vl in build_param_value_xml(parsed.get("type", ""), parsed["value"], f"{i}\t"): - lines.append(vl) + if is_empty_value(parsed["value"]): + empty_xml = build_empty_value_xml(parsed.get("type", ""), f"{i}\t", "", "value", vla) + if empty_xml: + lines.append(empty_xml) + else: + for vl in build_param_value_xml(parsed.get("type", ""), parsed["value"], f"{i}\t"): + lines.append(vl) if parsed.get("hidden"): lines.append(f"{i}\ttrue") lines.append(f"{i}\tfalse") + if vla: + lines.append(f"{i}\ttrue") + for av in parsed.get("availableValues", []) or []: for l in build_available_value_fragment(av, parsed.get("type", ""), f"{i}\t"): lines.append(l) @@ -1065,6 +1151,8 @@ def build_data_param_fragment(parsed, indent): 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 is_empty_value(val): + lines.append(f'{i}\t') elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): lines.append(f'{i}\t{esc_xml(str(val))}') elif str(val) in ("true", "false"): @@ -1802,8 +1890,16 @@ elif operation == "modify-parameter": 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 = "\n".join(value_lines) + # Detect valueListAllowed — empty value should be omitted when set + vla_set = False + vla_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueListAllowed" and etree.QName(ch.tag).namespace == SCH_NS), None) + if vla_el is not None and (vla_el.text or "").strip() == "true": + vla_set = True + if is_empty_value(value): + frag_xml = build_empty_value_xml(declared_type, child_indent, "", "value", vla_set) + else: + value_lines = build_param_value_xml(declared_type, value, child_indent) + frag_xml = "\n".join(value_lines) was_existing = existing is not None if existing is not None: # Find next-element sibling as ref before removing @@ -1812,9 +1908,10 @@ elif operation == "modify-parameter": 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) + if frag_xml: + 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" dirty = True; print(f'[OK] Parameter "{param_name}": value {verb} to {value}') elif existing is not None: @@ -2543,6 +2640,8 @@ elif operation == "modify-dataParameter": val_lines.append(f"{item_indent}\t0001-01-01T00:00:00") val_lines.append(f"{item_indent}\t0001-01-01T00:00:00") val_lines.append(f"{item_indent}") + elif is_empty_value(pv): + val_lines.append(f'{item_indent}') elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(pv)): val_lines.append(f'{item_indent}{esc_xml(str(pv))}') elif str(pv) in ("true", "false"): diff --git a/tests/skills/cases/skd-edit/empty-dataparam-values.json b/tests/skills/cases/skd-edit/empty-dataparam-values.json new file mode 100644 index 00000000..64f4e9ed --- /dev/null +++ b/tests/skills/cases/skd-edit/empty-dataparam-values.json @@ -0,0 +1,37 @@ +{ + "name": "modify-dataParameter: пустые значения (sentinel-формы) → dcscor:value xsi:nil", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [{ + "name": "Основной", + "query": "ВЫБРАТЬ 1 КАК Поле1", + "fields": ["Поле1: decimal(1,0)"] + }], + "parameters": [ + "Период: StandardPeriod = LastMonth", + "Организация: string = Альфа" + ], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Auto"], + "dataParameters": ["Период = LastMonth", "Организация = Альфа"], + "structure": "details" + } + }] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" } + }, + { + "script": "skd-edit/scripts/skd-edit", + "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "modify-dataParameter", "-Value": "Период = _" } + } + ], + "params": { + "templatePath": "Template.xml", + "operation": "modify-dataParameter", + "value": "Организация = null" + } +} diff --git a/tests/skills/cases/skd-edit/empty-param-values-add.json b/tests/skills/cases/skd-edit/empty-param-values-add.json new file mode 100644 index 00000000..f96abd26 --- /dev/null +++ b/tests/skills/cases/skd-edit/empty-param-values-add.json @@ -0,0 +1,21 @@ +{ + "name": "add-parameter: пустые значения и расширенные типы (decimal bare, time, string fix, sentinels)", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [{ + "name": "Основной", + "query": "ВЫБРАТЬ 1 КАК Поле1", + "fields": ["Поле1: decimal(1,0)"] + }] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" } + } + ], + "params": { + "templatePath": "Template.xml", + "operation": "add-parameter", + "value": "ПарСтрока: string =;;ПарСтрокаНулл: string = null;;ПарСтрокаПодч: string = _;;ПарДата: date =;;ПарВремя: time =;;ПарВремяНепусто: time = 0001-01-01T12:30:00;;ПарДатаВремя: dateTime = _;;ПарЧислоБар: decimal =;;ПарЧислоН: decimal(8) =;;ПарБул: boolean =;;ПарПериод: StandardPeriod = _;;ПарСсылка: CatalogRef.Номенклатура = null;;ПарСтрокаФикс: string(10,fix) =;;ПарСписок: string @valueList = _" + } +} diff --git a/tests/skills/cases/skd-edit/empty-param-values-modify.json b/tests/skills/cases/skd-edit/empty-param-values-modify.json new file mode 100644 index 00000000..e02e7153 --- /dev/null +++ b/tests/skills/cases/skd-edit/empty-param-values-modify.json @@ -0,0 +1,39 @@ +{ + "name": "modify-parameter: установка пустого значения через sentinel-формы (value=_, value=null)", + "preRun": [ + { + "script": "skd-compile/scripts/skd-compile", + "input": { + "dataSets": [{ + "name": "Основной", + "query": "ВЫБРАТЬ 1 КАК Поле1", + "fields": ["Поле1: decimal(1,0)"] + }], + "parameters": [ + "ПарСтрока: string = ABC", + "ПарДата: date = 2025-01-15T00:00:00", + "ПарЧисло: decimal = 42", + "ПарСсылка: CatalogRef.Номенклатура = Справочник.Номенклатура.НашаОрганизация" + ] + }, + "args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" } + }, + { + "script": "skd-edit/scripts/skd-edit", + "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "modify-parameter", "-Value": "ПарСтрока value=_" } + }, + { + "script": "skd-edit/scripts/skd-edit", + "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "modify-parameter", "-Value": "ПарДата value=null" } + }, + { + "script": "skd-edit/scripts/skd-edit", + "args": { "-TemplatePath": "{workDir}/Template.xml", "-Operation": "modify-parameter", "-Value": "ПарЧисло value=_" } + } + ], + "params": { + "templatePath": "Template.xml", + "operation": "modify-parameter", + "value": "ПарСсылка value=null" + } +} diff --git a/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml b/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml index e0a4364b..cb7e34a8 100644 --- a/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml +++ b/tests/skills/cases/skd-edit/snapshots/conditional-appearance-v2/Template.xml @@ -37,6 +37,7 @@ Variable + Основной diff --git a/tests/skills/cases/skd-edit/snapshots/empty-dataparam-values/Template.xml b/tests/skills/cases/skd-edit/snapshots/empty-dataparam-values/Template.xml new file mode 100644 index 00000000..39eddc0b --- /dev/null +++ b/tests/skills/cases/skd-edit/snapshots/empty-dataparam-values/Template.xml @@ -0,0 +1,84 @@ + + + + ИсточникДанных1 + Local + + + Основной + + Поле1 + Поле1 + + xs:decimal + + 1 + 0 + Any + + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Поле1 + + + Период + + v8:StandardPeriod + + + LastMonth + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + Организация + + xs:string + + 0 + Variable + + + Альфа + + + Основной + + + ru + Основной + + + + + + + + Период + + + + Организация + + + + + + + + + + + + + + diff --git a/tests/skills/cases/skd-edit/snapshots/empty-param-values-add/Template.xml b/tests/skills/cases/skd-edit/snapshots/empty-param-values-add/Template.xml new file mode 100644 index 00000000..abb50558 --- /dev/null +++ b/tests/skills/cases/skd-edit/snapshots/empty-param-values-add/Template.xml @@ -0,0 +1,196 @@ + + + + ИсточникДанных1 + Local + + + Основной + + Поле1 + Поле1 + + xs:decimal + + 1 + 0 + Any + + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Поле1 + + + ПарСтрока + + xs:string + + 0 + Variable + + + + + + ПарСтрокаНулл + + xs:string + + 0 + Variable + + + + + + ПарСтрокаПодч + + xs:string + + 0 + Variable + + + + + + ПарДата + + xs:dateTime + + Date + + + 0001-01-01T00:00:00 + + + ПарВремя + + xs:dateTime + + Time + + + 0001-01-01T00:00:00 + + + ПарВремяНепусто + + xs:dateTime + + Time + + + 0001-01-01T12:30:00 + + + ПарДатаВремя + + xs:dateTime + + DateTime + + + 0001-01-01T00:00:00 + + + ПарЧислоБар + + xs:decimal + + 10 + 2 + Any + + + 0 + + + ПарЧислоН + + xs:decimal + + 8 + 0 + Any + + + 0 + + + ПарБул + + xs:boolean + + false + + + ПарПериод + + v8:StandardPeriod + + + Custom + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + ПарСсылка + + d5p1:CatalogRef.Номенклатура + + + + + ПарСтрокаФикс + + xs:string + + 10 + Fixed + + + + + + ПарСписок + + xs:string + + 0 + Variable + + + true + + + Основной + + + ru + Основной + + + + + + + + + + + + + + + + diff --git a/tests/skills/cases/skd-edit/snapshots/empty-param-values-modify/Template.xml b/tests/skills/cases/skd-edit/snapshots/empty-param-values-modify/Template.xml new file mode 100644 index 00000000..35513567 --- /dev/null +++ b/tests/skills/cases/skd-edit/snapshots/empty-param-values-modify/Template.xml @@ -0,0 +1,92 @@ + + + + ИсточникДанных1 + Local + + + Основной + + Поле1 + Поле1 + + xs:decimal + + 1 + 0 + Any + + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Поле1 + + + ПарСтрока + + xs:string + + 0 + Variable + + + + + + ПарДата + + xs:dateTime + + Date + + + 0001-01-01T00:00:00 + + + ПарЧисло + + xs:decimal + + 10 + 2 + Any + + + 0 + + + ПарСсылка + + d5p1:CatalogRef.Номенклатура + + + + + Основной + + + ru + Основной + + + + + + + + + + + + + + + + diff --git a/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml b/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml index 0e1bb01a..451c6fd1 100644 --- a/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml +++ b/tests/skills/cases/skd-edit/snapshots/rename-parameter/Template.xml @@ -86,6 +86,7 @@ Variable + Основной @@ -111,6 +112,7 @@ false Организация + diff --git a/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml b/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml index 8fe4556a..b0aa80c5 100644 --- a/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml +++ b/tests/skills/cases/skd-edit/snapshots/reorder-parameters/Template.xml @@ -47,6 +47,7 @@ Variable + ПорядокОкругленияСумм @@ -57,6 +58,7 @@ Variable + Организация @@ -67,12 +69,14 @@ Variable + ПрекращаемаяДеятельность xs:boolean + false