fix(form-add): идемпотентная регистрация <Form>/<Template> в ChildObjects

form-add и template-add вставляли запись в ChildObjects безусловно.
Если форма/макет уже зарегистрированы (например, form-compile
регистрирует <Form>, не создавая файл метаданных, а затем вызывается
form-add) — возникал дубль <Form>/<Template>, ломавший валидацию.

Приведено к идемпотентной модели, уже применённой в form-compile и
meta-compile: перед вставкой ищем существующую запись по имени; при
наличии — пропускаем и печатаем "Already registered ... (skipped
duplicate)". Зеркально в ps1 и py, регрессионный тест-кейс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-23 13:28:16 +03:00
co-authored by Claude Opus 4.8
parent b892379202
commit e01688e764
10 changed files with 213 additions and 65 deletions
+11 -3
View File
@@ -1,4 +1,4 @@
# form-add v1.10 — Add managed form to 1C config object # form-add v1.11 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -484,7 +484,10 @@ if (-not $childObjects) {
exit 1 exit 1
} }
# Добавить <Form>$FormName</Form> # Добавить <Form>$FormName</Form> — идемпотентно (не дублировать уже зарегистрированную)
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Form[text()='$FormName']", $nsMgr)
if (-not $alreadyRegistered) {
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") $formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName $formElem.InnerText = $FormName
@@ -538,6 +541,7 @@ if ($insertBefore) {
} }
} }
} }
}
# --- SetDefault --- # --- SetDefault ---
@@ -603,7 +607,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml" Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl" Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host "" Write-Host ""
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects" if ($alreadyRegistered) {
Write-Host "Already registered: <Form>$FormName</Form> in ChildObjects (skipped duplicate)"
} else {
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
}
if ($defaultUpdated) { if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue" Write-Host "${defaultPropName}: $defaultValue"
} }
+43 -37
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.10 — Add managed form to 1C config object # form-add v1.11 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -592,47 +592,50 @@ def main():
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr) print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Add <Form>$FormName</Form> # Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
form_elem = etree.Element(f"{{{ns}}}Form") already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
form_elem.text = form_name
# Find first <Template> to insert before it if not already_registered:
first_template = child_objects.find("md:Template", NSMAP) form_elem = etree.Element(f"{{{ns}}}Form")
# Find first <TabularSection> to insert before it (if no Template) form_elem.text = form_name
first_tabular = child_objects.find("md:TabularSection", NSMAP)
# Determine insertion point: before Template, before TabularSection, or at end # Find first <Template> to insert before it
insert_before = None first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None: # Find first <TabularSection> to insert before it (if no Template)
insert_before = first_template first_tabular = child_objects.find("md:TabularSection", NSMAP)
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None: # Determine insertion point: before Template, before TabularSection, or at end
# Insert before the found element insert_before = None
idx = list(child_objects).index(insert_before) if first_template is not None:
child_objects.insert(idx, form_elem) insert_before = first_template
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before) elif first_tabular is not None:
form_elem.tail = "\n\t\t\t" insert_before = first_tabular
else:
# Add to end of ChildObjects if insert_before is not None:
children = list(child_objects) # Insert before the found element
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""): idx = list(child_objects).index(insert_before)
# Empty ChildObjects (self-closing) child_objects.insert(idx, form_elem)
child_objects.text = "\n\t\t\t" # Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
child_objects.append(form_elem) form_elem.tail = "\n\t\t\t"
form_elem.tail = "\n\t\t"
else: else:
if len(children) > 0: # Add to end of ChildObjects
last_child = children[-1] children = list(child_objects)
old_tail = last_child.tail if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
last_child.tail = "\n\t\t\t" # Empty ChildObjects (self-closing)
child_objects.append(form_elem) child_objects.text = "\n\t\t\t"
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem) child_objects.append(form_elem)
form_elem.tail = "\n\t\t" form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault --- # --- SetDefault ---
@@ -677,7 +680,10 @@ def main():
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml") print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl") print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print() print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects") if already_registered:
print(f"Already registered: <Form>{form_name}</Form> in ChildObjects (skipped duplicate)")
else:
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated: if default_updated:
print(f"{default_prop_name}: {default_value}") print(f"{default_prop_name}: {default_value}")
print() print()
@@ -1,4 +1,4 @@
# template-add v1.9 — Add template to 1C object # template-add v1.10 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -329,7 +329,10 @@ if (-not $childObjects) {
exit 1 exit 1
} }
# Добавить <Template> в конец ChildObjects # Добавить <Template> в конец ChildObjects — идемпотентно (не дублировать уже зарегистрированный)
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Template[text()='$TemplateName']", $nsMgr)
if (-not $alreadyRegistered) {
$templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses") $templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses")
$templateElem.InnerText = $TemplateName $templateElem.InnerText = $TemplateName
@@ -349,6 +352,7 @@ if ($childObjects.ChildNodes.Count -eq 0) {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null $childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
} }
} }
}
# --- 4. MainDataCompositionSchema (для ExternalReport / Report) --- # --- 4. MainDataCompositionSchema (для ExternalReport / Report) ---
@@ -392,6 +396,9 @@ $writer.Close()
$stream.Close() $stream.Close()
Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)" Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)"
if ($alreadyRegistered) {
Write-Host " Already registered: <Template>$TemplateName</Template> in ChildObjects (skipped duplicate)"
}
Write-Host " Метаданные: $templateMetaPath" Write-Host " Метаданные: $templateMetaPath"
Write-Host " Содержимое: $templateFilePath" Write-Host " Содержимое: $templateFilePath"
if ($mainDCSUpdated) { if ($mainDCSUpdated) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# add-template v1.9 — Add template to 1C object # add-template v1.10 — Add template to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -446,31 +446,34 @@ def main():
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr) print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Add <Template> to end of ChildObjects # Add <Template> to end of ChildObjects — idempotent (do not duplicate already-registered template)
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template") already_registered = child_objects.find(f"md:Template[.='{template_name}']", NSMAP) is not None
template_elem.text = template_name
# Remove auto-appended element to reinsert with proper whitespace
child_objects.remove(template_elem)
children = list(child_objects) if not already_registered:
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""): template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
# Empty ChildObjects (self-closing) template_elem.text = template_name
child_objects.text = "\n\t\t\t" # Remove auto-appended element to reinsert with proper whitespace
child_objects.append(template_elem) child_objects.remove(template_elem)
template_elem.tail = "\n\t\t"
else: children = list(child_objects)
if len(children) > 0: if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
last_child = children[-1] # Empty ChildObjects (self-closing)
# last_child.tail is the trailing whitespace before </ChildObjects> child_objects.text = "\n\t\t\t"
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = old_tail if old_tail else "\n\t\t"
else:
# Has text content but no element children
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(template_elem) child_objects.append(template_elem)
template_elem.tail = "\n\t\t" template_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
# last_child.tail is the trailing whitespace before </ChildObjects>
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = old_tail if old_tail else "\n\t\t"
else:
# Has text content but no element children
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(template_elem)
template_elem.tail = "\n\t\t"
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) --- # --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
@@ -500,6 +503,8 @@ def main():
save_xml_with_bom(tree, root_xml_full) save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создан макет: {template_name} ({template_type})") print(f"[OK] Создан макет: {template_name} ({template_type})")
if already_registered:
print(f" Already registered: <Template>{template_name}</Template> in ChildObjects (skipped duplicate)")
print(f" Метаданные: {template_meta_path}") print(f" Метаданные: {template_meta_path}")
print(f" Содержимое: {template_file_path}") print(f" Содержимое: {template_file_path}")
if main_dcs_updated: if main_dcs_updated:
@@ -0,0 +1,21 @@
{
"name": "form-compile затем form-add не дублирует регистрацию формы в ChildObjects",
"setup": "none",
"preRun": [
{
"script": "epf-init/scripts/init",
"args": { "-Name": "КонсольКода", "-SrcDir": "{workDir}" }
},
{
"script": "form-compile/scripts/form-compile",
"input": {
"title": "Форма",
"attributes": [{ "name": "Объект", "type": "ExternalDataProcessorObject.КонсольКода", "main": true }]
},
"args": { "-JsonPath": "{inputFile}", "-OutputPath": "{workDir}/КонсольКода/Forms/Форма/Ext/Form.xml" }
}
],
"params": { "objectPath": "КонсольКода.xml", "formName": "Форма" },
"expect": { "stdoutContains": "Already registered" },
"validatePath": "КонсольКода/Forms/Форма"
}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" 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:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" 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="2.17">
<ExternalDataProcessor uuid="UUID-001">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>UUID-002</xr:ClassId>
<xr:ObjectId>UUID-003</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalDataProcessorObject.КонсольКода" category="Object">
<xr:TypeId>UUID-004</xr:TypeId>
<xr:ValueId>UUID-005</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>КонсольКода</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>КонсольКода</v8:content>
</v8:item>
</Synonym>
<Comment />
<DefaultForm>ExternalDataProcessor.КонсольКода.Form.Форма</DefaultForm>
<AuxiliaryForm />
</Properties>
<ChildObjects>
<Form>Форма</Form>
</ChildObjects>
</ExternalDataProcessor>
</MetaDataObject>
@@ -0,0 +1,11 @@
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" 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:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" 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="2.17">
<Form uuid="UUID-001">
<Properties>
<Name>Форма</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Форма</v8:content>
</v8:item>
</Synonym>
<Comment/>
<FormType>Managed</FormType>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>
<ExtendedPresentation/>
</Properties>
</Form>
</MetaDataObject>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<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="2.17">
<Title>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Форма</v8:content>
</v8:item>
</Title>
<AutoTitle>false</AutoTitle>
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"/>
<Attributes>
<Attribute name="Объект" id="1">
<Type>
<v8:Type>cfg:ExternalDataProcessorObject.КонсольКода</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
</Attribute>
</Attributes>
</Form>
@@ -0,0 +1,19 @@
#Область ОбработчикиСобытийФормы
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти