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${pf}${tn}>"
+ } 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${pf}${tn}>"
+ } elseif ($t -match '^decimal') {
+ $lines += "$indent<${pf}${tn} xsi:type=`"xs:decimal`">0${pf}${tn}>"
+ } elseif ($t -eq "boolean") {
+ $lines += "$indent<${pf}${tn} xsi:type=`"xs:boolean`">false${pf}${tn}>"
+ } 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}{pf}{tn}>')
+ 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{pf}{tn}>')
+ elif re.match(r'^decimal', t):
+ lines.append(f'{indent}<{pf}{tn} xsi:type="xs:decimal">0{pf}{tn}>')
+ elif t == "boolean":
+ lines.append(f'{indent}<{pf}{tn} xsi:type="xs:boolean">false{pf}{tn}>')
+ 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