fix(form): уникальность имён во всех коллекциях форм + префикс колонок субконто ЧПС

Проверка уникальности имён элементов форм (основа — PR #21 от brake71),
портированная на актуальную ветку и расширенная на все именованные коллекции.

Корень проблемы: генератор формы счёта ПланаСчетов строил колонки таблицы
субконто с «голыми» именами (Валютный, ТолькоОбороты, ВидСубконто), из-за чего
флаг субконто сталкивался с одноимённым признаком учёта счёта → невалидный для
1С XML (форма не открывалась). Теперь имена колонок префиксуются именем таблицы
(ВидыСубконтоВалютный) — как делает generic-путь табчастей и типовая 1С.

- form-compile: fail-fast проверка уникальности в едином emit_element + по
  реквизитам, колонкам (в пределах реквизита), параметрам и командам. Хелпер
  вместо копипаста; проверка после нормализации синонимов.
- form-validate: проверка имён симметрично существующим id-пулам (элементы,
  реквизиты, колонки, команды) + новый блок параметров.
- form-edit: дедуп внутри JSON-определения и против существующих в форме —
  для элементов (рекурсивно), реквизитов (+колонки) и команд; WARN→ERROR.

Каждая коллекция — свой неймспейс (имя реквизита и имя элемента могут совпадать
легально). PS1 и PY — зеркальны. Версии: form-compile 1.74, form-validate 1.7,
form-edit 1.1. Все тест-сеты зелёные на обоих рантаймах.

Co-authored-by: brake71 <8448482+brake71@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-08 13:16:12 +03:00
co-authored by brake71 Claude Opus 4.8
parent ad89929efd
commit 41e4714773
7 changed files with 260 additions and 35 deletions
@@ -1,4 +1,4 @@
# form-compile v1.73 — Compile 1C managed form from JSON or object metadata
# form-compile v1.74 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -1230,16 +1230,19 @@ function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [
# ExtDimensionTypes table
if ($meta.MaxExtDimensionCount -gt 0) {
# Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С),
# иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта.
$edTable = "ВидыСубконто"
$edCols = @()
$edCols += [ordered]@{ input = "ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" }
$edCols += [ordered]@{ check = "ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" }
$edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" }
$edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" }
if ($meta.ExtDimensionAccountingFlags) {
foreach ($edFlag in $meta.ExtDimensionAccountingFlags) {
$edCols += [ordered]@{ check = $edFlag.Name; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" }
$edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" }
}
}
$elements += [ordered]@{
table = "ВидыСубконто"
table = $edTable
path = "Объект.ExtDimensionTypes"
columns = $edCols
}
@@ -1521,6 +1524,17 @@ function New-Id {
return $id
}
# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё
# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast.
function Assert-UniqueName {
param([string]$name, [hashtable]$seen, [string]$kind)
if ($seen.ContainsKey($name)) {
Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')"
exit 1
}
$seen[$name] = $true
}
# --- 3. XML helper ---
$script:xml = New-Object System.Text.StringBuilder 8192
@@ -2483,6 +2497,7 @@ function Emit-Element {
}
$name = Get-ElementName -el $el -typeKey $typeKey
Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element'
$id = New-Id
switch ($typeKey) {
@@ -4259,9 +4274,11 @@ function Emit-Attributes {
if (-not $attrs -or $attrs.Count -eq 0) { return }
X "$indent<Attributes>"
$seenAttrs = @{}
foreach ($attr in $attrs) {
$attrId = New-Id
$attrName = "$($attr.name)"
Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute'
X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
$inner = "$indent`t`t"
@@ -4333,12 +4350,20 @@ function Emit-Attributes {
if ($hasDirectCols -or $hasAddCols) {
X "$inner<Columns>"
if ($hasDirectCols) {
foreach ($col in $attr.columns) { Emit-AttrColumn -col $col -indent "$inner`t" }
$seenCols = @{} # колонки уникальны в пределах своего реквизита
foreach ($col in $attr.columns) {
Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'"
Emit-AttrColumn -col $col -indent "$inner`t"
}
}
if ($hasAddCols) {
foreach ($ac in @($attr.additionalColumns)) {
X "$inner`t<AdditionalColumns table=`"$($ac.table)`">"
foreach ($col in @($ac.columns)) { Emit-AttrColumn -col $col -indent "$inner`t`t" }
$seenAcCols = @{} # уникальность в пределах группы AdditionalColumns
foreach ($col in @($ac.columns)) {
Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'"
Emit-AttrColumn -col $col -indent "$inner`t`t"
}
X "$inner`t</AdditionalColumns>"
}
}
@@ -4405,7 +4430,9 @@ function Emit-Parameters {
if (-not $params -or $params.Count -eq 0) { return }
X "$indent<Parameters>"
$seenParams = @{}
foreach ($param in $params) {
Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter'
X "$indent`t<Parameter name=`"$($param.name)`">"
$inner = "$indent`t`t"
@@ -4428,8 +4455,10 @@ function Emit-Commands {
if (-not $cmds -or $cmds.Count -eq 0) { return }
X "$indent<Commands>"
$seenCmds = @{}
foreach ($cmd in $cmds) {
$cmdId = New-Id
Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command'
X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
$inner = "$indent`t`t"
@@ -4746,6 +4775,7 @@ X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8
# Reset and rebuild properly
$script:xml = New-Object System.Text.StringBuilder 8192
$script:nextId = 1
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.73 — Compile 1C managed form from JSON or object metadata
# form-compile v1.74 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -1157,14 +1157,17 @@ def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data):
# ExtDimensionTypes table
if meta.get('MaxExtDimensionCount', 0) > 0:
# Column names are prefixed with the table name (like the generic TS path and stock 1C),
# else a subconto flag column collides with a same-named account accounting-flag checkbox.
ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'
ed_cols = []
ed_cols.append(OrderedDict([('input', '\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')]))
ed_cols.append(OrderedDict([('check', '\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')]))
ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')]))
ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')]))
if meta.get('ExtDimensionAccountingFlags'):
for ed_flag in meta['ExtDimensionAccountingFlags']:
ed_cols.append(OrderedDict([('check', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")]))
ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")]))
elements.append(OrderedDict([
('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'),
('table', ed_table),
('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'),
('columns', ed_cols),
]))
@@ -1703,6 +1706,17 @@ def new_id():
return _next_id
# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё
# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast.
_seen_element_names = set() # пул имён элементов (глобально по всей форме)
def _ensure_unique(name, seen, kind):
if name in seen:
print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr)
sys.exit(1)
seen.add(name)
# --- Event handler name generator ---
EVENT_SUFFIX_MAP = {
@@ -2831,6 +2845,7 @@ def emit_element(lines, el, indent, in_cmd_bar=False):
print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr)
name = get_element_name(el, type_key)
_ensure_unique(name, _seen_element_names, 'element')
eid = new_id()
emitters = {
@@ -3975,9 +3990,11 @@ def emit_attributes(lines, attrs, indent):
return
lines.append(f'{indent}<Attributes>')
seen_attrs = set()
for attr in attrs:
attr_id = new_id()
attr_name = str(attr['name'])
_ensure_unique(attr_name, seen_attrs, 'attribute')
lines.append(f'{indent}\t<Attribute name="{attr_name}" id="{attr_id}">')
inner = f'{indent}\t\t'
@@ -4041,12 +4058,16 @@ def emit_attributes(lines, attrs, indent):
if has_direct_cols or has_add_cols:
lines.append(f'{inner}<Columns>')
if has_direct_cols:
seen_cols = set() # колонки уникальны в пределах своего реквизита
for col in attr['columns']:
_ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'")
emit_attr_column(lines, col, f'{inner}\t')
if has_add_cols:
for ac in attr['additionalColumns']:
lines.append(f'{inner}\t<AdditionalColumns table="{ac["table"]}">')
seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns
for col in (ac.get('columns') or []):
_ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'")
emit_attr_column(lines, col, f'{inner}\t\t')
lines.append(f'{inner}\t</AdditionalColumns>')
lines.append(f'{inner}</Columns>')
@@ -4105,7 +4126,9 @@ def emit_parameters(lines, params, indent):
return
lines.append(f'{indent}<Parameters>')
seen_params = set()
for param in params:
_ensure_unique(str(param['name']), seen_params, 'parameter')
lines.append(f'{indent}\t<Parameter name="{param["name"]}">')
inner = f'{indent}\t\t'
@@ -4125,8 +4148,10 @@ def emit_commands(lines, cmds, indent):
return
lines.append(f'{indent}<Commands>')
seen_cmds = set()
for cmd in cmds:
cmd_id = new_id()
_ensure_unique(str(cmd['name']), seen_cmds, 'command')
lines.append(f'{indent}\t<Command name="{cmd["name"]}" id="{cmd_id}">')
inner = f'{indent}\t\t'
@@ -4607,6 +4632,7 @@ def main():
# --- 2. Main compilation ---
_next_id = 0
_seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе)
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')