mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-20 01:20:59 +03:00
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:
co-authored by
brake71
Claude Opus 4.8
parent
ad89929efd
commit
41e4714773
@@ -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"?>')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-edit v1.0 — Edit 1C managed form elements
|
||||
# form-edit v1.1 — Edit 1C managed form elements
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -291,6 +291,16 @@ function Get-ElementName {
|
||||
return "$($el.$typeKey)"
|
||||
}
|
||||
|
||||
# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс).
|
||||
function Assert-EditUnique {
|
||||
param([string]$name, [hashtable]$seen, [string]$ctx)
|
||||
if ($seen.ContainsKey($name)) {
|
||||
Write-Host "[ERROR] Duplicate $ctx '$name' in JSON definition — names must be unique in 1C form"
|
||||
exit 1
|
||||
}
|
||||
$seen[$name] = $true
|
||||
}
|
||||
|
||||
$script:knownEvents = @{
|
||||
"input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
|
||||
"check" = @("OnChange")
|
||||
@@ -864,17 +874,31 @@ if ($def.elements -and $def.elements.Count -gt 0) {
|
||||
# Detect indent level
|
||||
$childIndent = Get-ChildIndent $targetCI
|
||||
|
||||
# Check for duplicate element names
|
||||
# Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения
|
||||
# (рекурсивно по children/columns).
|
||||
$elemTypeKeys = @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")
|
||||
function Walk-ElemNames($el, [hashtable]$seen) {
|
||||
$tk = $null
|
||||
foreach ($k in $elemTypeKeys) { if ($el.$k -ne $null) { $tk = $k; break } }
|
||||
if ($tk) { Assert-EditUnique -name (Get-ElementName -el $el -typeKey $tk) -seen $seen -ctx 'element name' }
|
||||
if ($el.children) { foreach ($c in $el.children) { Walk-ElemNames $c $seen } }
|
||||
if ($el.columns) { foreach ($c in $el.columns) { Walk-ElemNames $c $seen } }
|
||||
}
|
||||
$dslElemNames = @{}
|
||||
foreach ($el in $def.elements) { Walk-ElemNames $el $dslElemNames }
|
||||
|
||||
# Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется)
|
||||
foreach ($el in $def.elements) {
|
||||
$typeKey = $null
|
||||
foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
|
||||
foreach ($key in $elemTypeKeys) {
|
||||
if ($el.$key -ne $null) { $typeKey = $key; break }
|
||||
}
|
||||
if ($typeKey) {
|
||||
$elName = Get-ElementName -el $el -typeKey $typeKey
|
||||
$existing = Find-Element $rootCI $elName
|
||||
if ($existing) {
|
||||
Write-Host "[WARN] Element '$elName' already exists in form (id=$($existing.GetAttribute('id')))"
|
||||
Write-Host "[ERROR] Element '$elName' already exists in form (id=$($existing.GetAttribute('id'))) — element names must be unique"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -953,6 +977,22 @@ if ($def.attributes -and $def.attributes.Count -gt 0) {
|
||||
$attrChildIndent = Get-ChildIndent $attrsSection
|
||||
if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" }
|
||||
|
||||
# Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и
|
||||
# против уже существующих реквизитов формы.
|
||||
$dslAttrNames = @{}
|
||||
foreach ($attr in $def.attributes) {
|
||||
Assert-EditUnique -name "$($attr.name)" -seen $dslAttrNames -ctx 'attribute name'
|
||||
if ($attr.columns) {
|
||||
$dslColNames = @{}
|
||||
foreach ($col in $attr.columns) { Assert-EditUnique -name "$($col.name)" -seen $dslColNames -ctx "column name of '$($attr.name)'" }
|
||||
}
|
||||
$existingAttr = $attrsSection.SelectSingleNode("f:Attribute[@name='$($attr.name)']", $nsMgr)
|
||||
if ($existingAttr) {
|
||||
Write-Host "[ERROR] Attribute '$($attr.name)' already exists in form — attribute names must be unique"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate attribute fragments
|
||||
$script:xml = New-Object System.Text.StringBuilder 2048
|
||||
X "<_F $allNsDecl>"
|
||||
@@ -1021,6 +1061,17 @@ if ($def.commands -and $def.commands.Count -gt 0) {
|
||||
$cmdChildIndent = Get-ChildIndent $cmdsSection
|
||||
if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" }
|
||||
|
||||
# Уникальность имён команд: внутри JSON-определения и против существующих команд формы.
|
||||
$dslCmdNames = @{}
|
||||
foreach ($cmd in $def.commands) {
|
||||
Assert-EditUnique -name "$($cmd.name)" -seen $dslCmdNames -ctx 'command name'
|
||||
$existingCmd = $cmdsSection.SelectSingleNode("f:Command[@name='$($cmd.name)']", $nsMgr)
|
||||
if ($existingCmd) {
|
||||
Write-Host "[ERROR] Command '$($cmd.name)' already exists in form — command names must be unique"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate command fragments
|
||||
$script:xml = New-Object System.Text.StringBuilder 1024
|
||||
X "<_F $allNsDecl>"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-edit v1.0 — Edit 1C managed form elements (Python port)
|
||||
# form-edit v1.1 — Edit 1C managed form elements (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -365,6 +365,14 @@ def get_element_name(el, type_key):
|
||||
return str(el[type_key])
|
||||
|
||||
|
||||
def _assert_edit_unique(name, seen, ctx):
|
||||
# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс).
|
||||
if name in seen:
|
||||
print(f"[ERROR] Duplicate {ctx} '{name}' in JSON definition — names must be unique in 1C form")
|
||||
sys.exit(1)
|
||||
seen.add(name)
|
||||
|
||||
|
||||
known_events = {
|
||||
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
|
||||
"check": ["OnChange"],
|
||||
@@ -988,7 +996,26 @@ if elements_list:
|
||||
# Detect indent level
|
||||
child_indent = get_child_indent(target_ci)
|
||||
|
||||
# Check for duplicate element names
|
||||
# Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения
|
||||
# (рекурсивно по children/columns).
|
||||
def _walk_elem_names(el, seen):
|
||||
tk = None
|
||||
for key in ELEMENT_KEYS:
|
||||
if key in el and el[key] is not None:
|
||||
tk = key
|
||||
break
|
||||
if tk:
|
||||
_assert_edit_unique(get_element_name(el, tk), seen, "element name")
|
||||
for c in el.get("children", []):
|
||||
_walk_elem_names(c, seen)
|
||||
for c in el.get("columns", []):
|
||||
_walk_elem_names(c, seen)
|
||||
|
||||
dsl_elem_names = set()
|
||||
for el in elements_list:
|
||||
_walk_elem_names(el, dsl_elem_names)
|
||||
|
||||
# Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется).
|
||||
for el in elements_list:
|
||||
type_key = None
|
||||
for key in ELEMENT_KEYS:
|
||||
@@ -999,7 +1026,8 @@ if elements_list:
|
||||
el_name = get_element_name(el, type_key)
|
||||
existing = find_element(root_ci, el_name) if root_ci is not None else None
|
||||
if existing is not None:
|
||||
print(f"[WARN] Element '{el_name}' already exists in form (id={existing.get('id')})")
|
||||
print(f"[ERROR] Element '{el_name}' already exists in form (id={existing.get('id')}) — element names must be unique")
|
||||
sys.exit(1)
|
||||
|
||||
# Remember starting element ID for companion counting
|
||||
start_elem_id = next_elem_id
|
||||
@@ -1055,6 +1083,19 @@ if attrs_list:
|
||||
if not attr_child_indent:
|
||||
attr_child_indent = "\t\t"
|
||||
|
||||
# Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и
|
||||
# против уже существующих реквизитов формы.
|
||||
dsl_attr_names = set()
|
||||
for attr in attrs_list:
|
||||
_assert_edit_unique(str(attr["name"]), dsl_attr_names, "attribute name")
|
||||
if attr.get("columns"):
|
||||
dsl_col_names = set()
|
||||
for col in attr["columns"]:
|
||||
_assert_edit_unique(str(col["name"]), dsl_col_names, f"column name of '{attr['name']}'")
|
||||
if attrs_section.find(f"f:Attribute[@name='{attr['name']}']", NS) is not None:
|
||||
print(f"[ERROR] Attribute '{attr['name']}' already exists in form — attribute names must be unique")
|
||||
sys.exit(1)
|
||||
|
||||
# Generate attribute fragments
|
||||
xml_lines.clear()
|
||||
X(f"<_F {ALL_NS_DECL}>")
|
||||
@@ -1116,6 +1157,14 @@ if cmds_list:
|
||||
if not cmd_child_indent:
|
||||
cmd_child_indent = "\t\t"
|
||||
|
||||
# Уникальность имён команд: внутри JSON-определения и против существующих команд формы.
|
||||
dsl_cmd_names = set()
|
||||
for cmd in cmds_list:
|
||||
_assert_edit_unique(str(cmd["name"]), dsl_cmd_names, "command name")
|
||||
if cmds_section.find(f"f:Command[@name='{cmd['name']}']", NS) is not None:
|
||||
print(f"[ERROR] Command '{cmd['name']}' already exists in form — command names must be unique")
|
||||
sys.exit(1)
|
||||
|
||||
xml_lines.clear()
|
||||
X(f"<_F {ALL_NS_DECL}>")
|
||||
for cmd in cmds_list:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-validate v1.6 — Validate 1C managed form
|
||||
# form-validate v1.7 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -155,7 +155,8 @@ if (-not $stopped) {
|
||||
|
||||
# --- Collect all elements with IDs ---
|
||||
|
||||
$elementIds = @{} # id -> name (element ID pool)
|
||||
$elementIds = @{} # id -> name (element ID pool)
|
||||
$elementNames = @{} # name -> id (имена элементов уникальны в пределах формы)
|
||||
$allElements = @() # @{Name; Tag; Id; ParentName; Node}
|
||||
|
||||
function Collect-Elements {
|
||||
@@ -185,6 +186,13 @@ function Collect-Elements {
|
||||
} else {
|
||||
$elementIds[$id] = $name
|
||||
}
|
||||
|
||||
# Имена элементов уникальны (требование 1С)
|
||||
if ($elementNames.ContainsKey($name)) {
|
||||
Report-Error "Duplicate element name '$name': id=${id} and id=$($elementNames[$name])"
|
||||
} else {
|
||||
$elementNames[$name] = $id
|
||||
}
|
||||
}
|
||||
|
||||
# Recurse into ChildItems
|
||||
@@ -229,6 +237,10 @@ foreach ($attr in $attrNodes) {
|
||||
$attrName = $attr.GetAttribute("name")
|
||||
$attrId = $attr.GetAttribute("id")
|
||||
if ($attrName) {
|
||||
# Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов)
|
||||
if ($attrMap.ContainsKey($attrName)) {
|
||||
Report-Error "Duplicate attribute name '$attrName': id=${attrId} and id=$($attrMap[$attrName].GetAttribute('id'))"
|
||||
}
|
||||
$attrMap[$attrName] = $attr
|
||||
}
|
||||
if ($attrId -and $attrId -ne "") {
|
||||
@@ -241,6 +253,7 @@ foreach ($attr in $attrNodes) {
|
||||
|
||||
# Column IDs are a separate sub-pool per attribute — check uniqueness within parent
|
||||
$colIds = @{}
|
||||
$colNames = @{} # имена колонок уникальны в пределах своего реквизита
|
||||
foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) {
|
||||
$colId = $col.GetAttribute("id")
|
||||
$colName = $col.GetAttribute("name")
|
||||
@@ -251,6 +264,13 @@ foreach ($attr in $attrNodes) {
|
||||
$colIds[$colId] = $colName
|
||||
}
|
||||
}
|
||||
if ($colName) {
|
||||
if ($colNames.ContainsKey($colName)) {
|
||||
Report-Error "Duplicate column name '$colName' in '$attrName': id=${colId} and id=$($colNames[$colName])"
|
||||
} else {
|
||||
$colNames[$colName] = $colId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +290,10 @@ foreach ($cmd in $cmdNodes) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
$cmdId = $cmd.GetAttribute("id")
|
||||
if ($cmdName) {
|
||||
# Имена команд уникальны среди команд (отдельный неймспейс)
|
||||
if ($cmdMap.ContainsKey($cmdName)) {
|
||||
Report-Error "Duplicate command name '$cmdName': id=${cmdId} and id=$($cmdMap[$cmdName].GetAttribute('id'))"
|
||||
}
|
||||
$cmdMap[$cmdName] = $cmd
|
||||
}
|
||||
if ($cmdId -and $cmdId -ne "") {
|
||||
@@ -290,6 +314,20 @@ if (-not $stopped) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Collect parameters (separate name pool, без id) ---
|
||||
|
||||
$paramNames = @{} # name -> $true (имена параметров уникальны среди параметров)
|
||||
foreach ($param in $root.SelectNodes("f:Parameters/f:Parameter", $nsMgr)) {
|
||||
$paramName = $param.GetAttribute("name")
|
||||
if ($paramName) {
|
||||
if ($paramNames.ContainsKey($paramName)) {
|
||||
Report-Error "Duplicate parameter name '$paramName'"
|
||||
} else {
|
||||
$paramNames[$paramName] = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 4: Companion elements ---
|
||||
|
||||
# Define required companions per element type
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-validate v1.6 — Validate 1C managed form
|
||||
# form-validate v1.7 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -182,7 +182,8 @@ def main():
|
||||
report_error("AutoCommandBar element missing")
|
||||
|
||||
# --- Collect all elements with IDs ---
|
||||
element_ids = {} # id -> name
|
||||
element_ids = {} # id -> name
|
||||
element_names = {} # name -> id (имена элементов уникальны в пределах формы)
|
||||
all_elements = [] # list of dicts {Name, Tag, Id, ParentName, Node}
|
||||
|
||||
def collect_elements(node, parent_name):
|
||||
@@ -211,6 +212,12 @@ def main():
|
||||
else:
|
||||
element_ids[eid] = name
|
||||
|
||||
# Имена элементов уникальны (требование 1С)
|
||||
if name in element_names:
|
||||
report_error(f"Duplicate element name '{name}': id={eid} and id={element_names[name]}")
|
||||
else:
|
||||
element_names[name] = eid
|
||||
|
||||
child_items = child.find(f"{{{F_NS}}}ChildItems")
|
||||
if child_items is not None:
|
||||
collect_elements(child_items, name)
|
||||
@@ -252,6 +259,9 @@ def main():
|
||||
attr_name = attr.get("name", "")
|
||||
attr_id = attr.get("id", "")
|
||||
if attr_name:
|
||||
# Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов)
|
||||
if attr_name in attr_map:
|
||||
report_error(f"Duplicate attribute name '{attr_name}': id={attr_id} and id={attr_map[attr_name].get('id', '')}")
|
||||
attr_map[attr_name] = attr
|
||||
if attr_id:
|
||||
if attr_id in attr_ids:
|
||||
@@ -261,6 +271,7 @@ def main():
|
||||
|
||||
# Column IDs uniqueness within parent
|
||||
col_ids = {}
|
||||
col_names = {} # имена колонок уникальны в пределах своего реквизита
|
||||
columns = attr.find(f"{{{F_NS}}}Columns")
|
||||
if columns is not None:
|
||||
for col in columns.findall(f"{{{F_NS}}}Column"):
|
||||
@@ -271,6 +282,11 @@ def main():
|
||||
report_error(f"Duplicate column id={col_id} in '{attr_name}': '{col_name}' and '{col_ids[col_id]}'")
|
||||
else:
|
||||
col_ids[col_id] = col_name
|
||||
if col_name:
|
||||
if col_name in col_names:
|
||||
report_error(f"Duplicate column name '{col_name}' in '{attr_name}': id={col_id} and id={col_names[col_name]}")
|
||||
else:
|
||||
col_names[col_name] = col_id
|
||||
|
||||
if not stopped:
|
||||
if attr_ids:
|
||||
@@ -289,6 +305,9 @@ def main():
|
||||
cmd_name = cmd.get("name", "")
|
||||
cmd_id = cmd.get("id", "")
|
||||
if cmd_name:
|
||||
# Имена команд уникальны среди команд (отдельный неймспейс)
|
||||
if cmd_name in cmd_map:
|
||||
report_error(f"Duplicate command name '{cmd_name}': id={cmd_id} and id={cmd_map[cmd_name].get('id', '')}")
|
||||
cmd_map[cmd_name] = cmd
|
||||
if cmd_id:
|
||||
if cmd_id in cmd_ids:
|
||||
@@ -300,6 +319,18 @@ def main():
|
||||
if cmd_ids:
|
||||
report_ok(f"Unique command IDs: {len(cmd_ids)} entries")
|
||||
|
||||
# --- Collect parameters (separate name pool, без id) ---
|
||||
param_names = {} # name -> True (имена параметров уникальны среди параметров)
|
||||
params_parent = root.find(f"{{{F_NS}}}Parameters")
|
||||
if params_parent is not None:
|
||||
for param in params_parent.findall(f"{{{F_NS}}}Parameter"):
|
||||
param_name = param.get("name", "")
|
||||
if param_name:
|
||||
if param_name in param_names:
|
||||
report_error(f"Duplicate parameter name '{param_name}'")
|
||||
else:
|
||||
param_names[param_name] = True
|
||||
|
||||
# --- Check 4: Companion elements ---
|
||||
companion_rules = {
|
||||
"InputField": ["ContextMenu", "ExtendedTooltip"],
|
||||
|
||||
+12
-12
@@ -115,31 +115,31 @@
|
||||
<ExtendedTooltip name="ВидыСубконтоУправлениеПоискомРасширеннаяПодсказка" id="39"/>
|
||||
</SearchControlAddition>
|
||||
<ChildItems>
|
||||
<InputField name="ВидСубконто" id="40">
|
||||
<InputField name="ВидыСубконтоВидСубконто" id="40">
|
||||
<DataPath>Объект.ExtDimensionTypes.ExtDimensionType</DataPath>
|
||||
<ContextMenu name="ВидСубконтоКонтекстноеМеню" id="41"/>
|
||||
<ExtendedTooltip name="ВидСубконтоРасширеннаяПодсказка" id="42"/>
|
||||
<ContextMenu name="ВидыСубконтоВидСубконтоКонтекстноеМеню" id="41"/>
|
||||
<ExtendedTooltip name="ВидыСубконтоВидСубконтоРасширеннаяПодсказка" id="42"/>
|
||||
</InputField>
|
||||
<CheckBoxField name="ТолькоОбороты" id="43">
|
||||
<CheckBoxField name="ВидыСубконтоТолькоОбороты" id="43">
|
||||
<DataPath>Объект.ExtDimensionTypes.TurnoversOnly</DataPath>
|
||||
<CheckBoxType>Auto</CheckBoxType>
|
||||
<TitleLocation>Right</TitleLocation>
|
||||
<ContextMenu name="ТолькоОборотыКонтекстноеМеню" id="44"/>
|
||||
<ExtendedTooltip name="ТолькоОборотыРасширеннаяПодсказка" id="45"/>
|
||||
<ContextMenu name="ВидыСубконтоТолькоОборотыКонтекстноеМеню" id="44"/>
|
||||
<ExtendedTooltip name="ВидыСубконтоТолькоОборотыРасширеннаяПодсказка" id="45"/>
|
||||
</CheckBoxField>
|
||||
<CheckBoxField name="Валютный" id="46">
|
||||
<CheckBoxField name="ВидыСубконтоВалютный" id="46">
|
||||
<DataPath>Объект.ExtDimensionTypes.Валютный</DataPath>
|
||||
<CheckBoxType>Auto</CheckBoxType>
|
||||
<TitleLocation>Right</TitleLocation>
|
||||
<ContextMenu name="ВалютныйКонтекстноеМеню" id="47"/>
|
||||
<ExtendedTooltip name="ВалютныйРасширеннаяПодсказка" id="48"/>
|
||||
<ContextMenu name="ВидыСубконтоВалютныйКонтекстноеМеню" id="47"/>
|
||||
<ExtendedTooltip name="ВидыСубконтоВалютныйРасширеннаяПодсказка" id="48"/>
|
||||
</CheckBoxField>
|
||||
<CheckBoxField name="Количественный" id="49">
|
||||
<CheckBoxField name="ВидыСубконтоКоличественный" id="49">
|
||||
<DataPath>Объект.ExtDimensionTypes.Количественный</DataPath>
|
||||
<CheckBoxType>Auto</CheckBoxType>
|
||||
<TitleLocation>Right</TitleLocation>
|
||||
<ContextMenu name="КоличественныйКонтекстноеМеню" id="50"/>
|
||||
<ExtendedTooltip name="КоличественныйРасширеннаяПодсказка" id="51"/>
|
||||
<ContextMenu name="ВидыСубконтоКоличественныйКонтекстноеМеню" id="50"/>
|
||||
<ExtendedTooltip name="ВидыСубконтоКоличественныйРасширеннаяПодсказка" id="51"/>
|
||||
</CheckBoxField>
|
||||
</ChildItems>
|
||||
</Table>
|
||||
|
||||
Reference in New Issue
Block a user