feat(cfe-borrow): перенос UseAlways/Columns основного реквизита формы

Заимствование формы с -BorrowMainAttribute синтезировало <Attribute
name="Объект"> с нуля — Type/MainAttribute/SavedData и всё. Секции
<UseAlways> и <Columns> исходной формы терялись, а вместе с ними и
дополнительные колонки табличных частей, объявленные прямо в форме:
платформа отвергала загрузку («Неверный путь к данным: Объект.Товары.Артикул»).

Три связанных следствия:
- секции переносятся из исходной формы (helper Get-MainAttributeExtraXml
  на оба порта, отступ в BaseForm — тем же приёмом, что у ChildItems);
- табличная часть, упомянутая только в <AdditionalColumns table="…">,
  теперь попадает в сбор путей и заимствуется — иначе «Колонки не могут
  быть добавлены к реквизиту»;
- типы колонок дозаимствуются через collect_reference_types, как это
  делает Конфигуратор (DefinedTypes/Артикул в эталоне).

Попутно снят PS↔PY-дрейф: lxml включал хвостовой пробельный узел в
tostring, из-за чего .py вставлял пустые строки, которых нет у .ps1
(with_tail=False в четырёх местах).

E2E на UT_DEMO (8.3.27, формат 2.20, форма заказа поставщику): режим с
основным реквизитом грузится «Load completed successfully» без ручных
правок. Регресс 11/11 на обоих рантаймах.
This commit is contained in:
Nick Shirokov
2026-08-12 15:49:34 +03:00
parent 902b643a3e
commit 3fafa099b2
21 changed files with 1576 additions and 5 deletions
@@ -1,4 +1,4 @@
# cfe-borrow v1.21 — Borrow objects from configuration into extension (CFE) (полный набор GeneratedType у заимствованных оболочек)
# cfe-borrow v1.22 — Borrow objects from configuration into extension (CFE) (перенос UseAlways/Columns основного реквизита формы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -892,6 +892,12 @@ function Borrow-Form {
$formXmlSb.Append("`t$childItemsXml") | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
}
# Секции основного реквизита исходной формы (<UseAlways>, <Columns>) — их нельзя терять
$mainAttrExtra = @()
if ($BorrowMainAttr) {
$mainAttrExtra = @(Get-MainAttributeExtraXml $srcFormEl $nsStripPattern)
}
# Attributes: empty or with MainAttribute when BorrowMainAttr
if ($BorrowMainAttr) {
$objTypePrefix = ""
@@ -903,6 +909,9 @@ function Borrow-Form {
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
foreach ($extraXml in $mainAttrExtra) {
$formXmlSb.Append("`t`t`t$extraXml`r`n") | Out-Null
}
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
$formXmlSb.Append("`t</Attributes>") | Out-Null
} else {
@@ -943,6 +952,15 @@ function Borrow-Form {
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
foreach ($extraXml in $mainAttrExtra) {
# В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
$exLines = $extraXml -split "`r?`n"
for ($li = 0; $li -lt $exLines.Count; $li++) {
if ($li -eq 0) { $formXmlSb.Append("`t`t`t`t$($exLines[$li])") | Out-Null }
else { $formXmlSb.Append("`t$($exLines[$li])") | Out-Null }
$formXmlSb.Append("`r`n") | Out-Null
}
}
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
} else {
@@ -1129,6 +1147,24 @@ function Build-InternalInfoXml {
}
# --- 11b. Collect DataPath references from source Form.xml ---
# --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData ---
# Это <UseAlways> и <Columns> (доп. колонки табличных частей, объявленные прямо в форме). Их
# переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на
# колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список OuterXml без xmlns.
function Get-MainAttributeExtraXml {
param($formEl, [string]$nsStripPattern)
$result = @()
$mainAttr = $formEl.SelectSingleNode("*[local-name()='Attributes']/*[local-name()='Attribute'][*[local-name()='MainAttribute']='true']")
if (-not $mainAttr) { return $result }
foreach ($child in $mainAttr.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -in @('Type','MainAttribute','SavedData')) { continue }
$result += [regex]::Replace($child.OuterXml, $nsStripPattern, '')
}
return $result
}
function Collect-FormDataPaths {
param([string]$formXmlPath)
@@ -1173,6 +1209,16 @@ function Collect-FormDataPaths {
}
}
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
$acMatches = [regex]::Matches($content, '<AdditionalColumns table="Объект\.(\w+)"')
foreach ($m in $acMatches) {
$seg0 = $m.Groups[1].Value
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
}
# Deduplicate deep paths
$seen = @{}
$uniqueDeep = @()
@@ -1589,6 +1635,20 @@ function Borrow-MainAttribute {
foreach ($ts in $srcTS) {
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
}
# Типы из <Columns> основного реквизита формы: колонку мы переносим (Borrow-Form), значит и её
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
# формы заказа поставщику).
$srcFormForCols = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (Test-Path $srcFormForCols) {
$colsDoc = New-Object System.Xml.XmlDocument
$colsDoc.PreserveWhitespace = $true
$colsDoc.Load($srcFormForCols)
foreach ($extraXml in (Get-MainAttributeExtraXml $colsDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"')) {
if ($extraXml -like "<Columns*") { $allTypeXmls += $extraXml }
}
}
$refTypes = Collect-ReferenceTypes $allTypeXmls
Info " Reference types to borrow: $($refTypes.Count)"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.21 — Borrow objects from configuration into extension (CFE) (полный набор GeneratedType у заимствованных оболочек)
# cfe-borrow v1.22 — Borrow objects from configuration into extension (CFE) (перенос UseAlways/Columns основного реквизита формы)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -815,6 +815,39 @@ def main():
save_xml_bom(obj_tree, obj_file)
info(f" Registered form in: {obj_file}")
# --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData ---
# Это <UseAlways> и <Columns> (доп. колонки табличных частей, объявленные прямо в форме). Их
# переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на
# колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список XML без xmlns.
def get_main_attribute_extra_xml(form_el, ns_strip_pattern):
result = []
main_attr = None
for child in form_el:
if not isinstance(child.tag, str) or localname(child) != "Attributes":
continue
for attr in child:
if not isinstance(attr.tag, str) or localname(attr) != "Attribute":
continue
for sub in attr:
if isinstance(sub.tag, str) and localname(sub) == "MainAttribute" and (sub.text or "").strip() == "true":
main_attr = attr
break
if main_attr is not None:
break
break
if main_attr is None:
return result
for sub in main_attr:
if not isinstance(sub.tag, str):
continue
if localname(sub) in ("Type", "MainAttribute", "SavedData"):
continue
# with_tail=False: хвостовой пробельный узел — часть родителя, а не секции; иначе в
# вывод попадают пустые строки, которых нет у PS (OuterXml хвост не включает).
xml = decode_numeric_entities(etree.tostring(sub, encoding="unicode", with_tail=False))
result.append(ns_strip_pattern.sub("", xml))
return result
# --- 11b. Collect DataPath references from source Form.xml ---
def collect_form_data_paths(form_xml_path):
with open(form_xml_path, "r", encoding="utf-8-sig") as fh:
@@ -856,6 +889,15 @@ def main():
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
for m in re.finditer(r'<AdditionalColumns table="Объект\.(\w+)"', content):
seg0 = m.group(1)
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
# Deduplicate deep paths
seen = set()
unique_deep = []
@@ -1194,6 +1236,18 @@ def main():
for ts in src_ts:
for tsa in ts["Attributes"]:
all_type_xmls.append(tsa["TypeXml"])
# Типы из <Columns> основного реквизита формы: колонку мы переносим (borrow_form), значит и её
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
# формы заказа поставщику).
src_form_for_cols = os.path.join(cfg_dir, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml")
if os.path.isfile(src_form_for_cols):
cols_tree = etree.parse(src_form_for_cols)
cols_ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
for extra_xml in get_main_attribute_extra_xml(cols_tree.getroot(), cols_ns_strip):
if extra_xml.startswith("<Columns"):
all_type_xmls.append(extra_xml)
ref_types = collect_reference_types(all_type_xmls)
info(f" Reference types to borrow: {len(ref_types)}")
@@ -1388,14 +1442,16 @@ def main():
continue
if not reached_visual:
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
# with_tail=False — хвостовой пробел принадлежит родителю; с ним в вывод попадали
# пустые строки, которых нет у PS-порта (OuterXml хвост не включает).
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode", with_tail=False)))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = ""
if src_auto_cmd is not None:
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode", with_tail=False))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
@@ -1413,7 +1469,7 @@ def main():
break
if src_child_items is not None:
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode", with_tail=False))
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
@@ -1602,6 +1658,11 @@ def main():
if child_items_xml:
parts.append(f"\t{child_items_xml}\r\n")
# Секции основного реквизита исходной формы (<UseAlways>, <Columns>) — их нельзя терять
main_attr_extra = []
if borrow_main_attr:
main_attr_extra = get_main_attribute_extra_xml(src_form_el, ns_strip_pattern)
# Attributes: empty or with MainAttribute when borrow_main_attr
if borrow_main_attr:
obj_type_prefix = ""
@@ -1616,6 +1677,8 @@ def main():
parts.append(f"\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
parts.append("\t\t\t<MainAttribute>true</MainAttribute>\r\n")
parts.append("\t\t\t<SavedData>true</SavedData>\r\n")
for extra_xml in main_attr_extra:
parts.append(f"\t\t\t{extra_xml}\r\n")
parts.append("\t\t</Attribute>\r\n")
parts.append("\t</Attributes>")
else:
@@ -1652,6 +1715,11 @@ def main():
parts.append(f"\t\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
parts.append("\t\t\t\t<MainAttribute>true</MainAttribute>\r\n")
parts.append("\t\t\t\t<SavedData>true</SavedData>\r\n")
for extra_xml in main_attr_extra:
# В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
for li, line in enumerate(extra_xml.split("\n")):
parts.append(f"\t\t\t\t{line}" if li == 0 else f"\t{line}")
parts.append("\r\n")
parts.append("\t\t\t</Attribute>\r\n")
parts.append("\t\t</Attributes>")
else: