From d3ea826286815d5532c16fb4836b95ecf107b0cc Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 14 Aug 2026 14:32:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(cfe-borrow):=20=D0=BE=D1=81=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B9=20=D1=80=D0=B5=D0=BA=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B8=D1=82=D1=81=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реквизит синтезировался с зашитыми name="Объект", типом <Тип>Object и SavedData=true, а сверху доклеивались «довески» исходной формы. Для формы списка это давало реквизит «Объект» объектного типа с настройками динамического списка внутри, и платформа отвергала файл: «Исключение XDTO произошло при чтении файла». Навык при этом отрабатывал молча, exit 0. По корпусу (8675 форм с основным реквизитом, УТ + ERP) объектных только 4074: динамический список — 3847, менеджер записи — 546, xs:string — 108, набор констант — 97. встречается ровно у динамических списков, 3847 из 3847. Эталоны Конфигуратора дают единое правило для всех видов: реквизит копируется из исходной формы дословно, меняется только id. Теперь так и делается, а вычисление типа из таблицы GeneratedType уходит вместе с тихим сбоем «нет категории Object → cfg:.Имя» для перечислений, констант и регистров. Следом расшиты ещё три места, зашитые на литерал «Объект»: - вырезание путей к данным — корень берётся из имени основного реквизита, иначе у формы списка вырезало бы все пути «Список.*»; путь ровно на сам реквизит («Список» у таблицы формы) тоже сохраняется; - сбор путей для заимствования дочерних объектов — на необъектных формах не находил ничего, и в оболочку не попадало ни одного реквизита; - вид дочернего объекта: измерения и ресурсы регистра переносятся как /, а не все как . Проверено на трёх эталонах (форма документа, форма списка, форма записи регистра): основной реквизит, корни путей к данным и виды дочерних объектов оболочки совпадают. Расширение с формой списка и формой записи грузится в UT_DEMO и применяется к БД — до правки загрузка падала. Снапшоты: у основного реквизита теперь многострочный, как у Конфигуратора; синтез писал его в одну строку. Co-Authored-By: Claude Opus 5 (1M context) --- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 147 ++++++++++-------- .../skills/cfe-borrow/scripts/cfe-borrow.py | 143 ++++++++--------- .../Товары/Forms/ФормаЭлемента/Ext/Form.xml | 8 +- .../Forms/ФормаДокумента/Ext/Form.xml | 8 +- .../Расход/Forms/ФормаДокумента/Ext/Form.xml | 8 +- 5 files changed, 171 insertions(+), 143 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index a9789188..58826b57 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -23,6 +23,12 @@ $script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','Hea # Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. $script:formBindingPictureTags = @('MultipleValuePictureDataPath') +# id основного реквизита в заимствованной форме — как у Конфигуратора +$script:mainAttrId = "1000001" + +# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно) +$script:childObjectKinds = @('Attribute','Dimension','Resource') + # Прямые дети
, которые в заимствованную форму не переносятся. # Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком. $script:formStructuralSections = @('Events','Attributes','Commands','Parameters','CommandInterface') @@ -30,13 +36,15 @@ $script:formStructuralSections = @('Events','Attributes','Commands','Parameters' $script:formAttributeRefProps = @('ReportResult','DetailsData','VariantAppearance','GroupList') # Strip data-binding tags whose root attribute isn't borrowed. -# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. -# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped. +# $mainAttrName задан (BorrowMainAttribute): оставить привязки от его имени, остальные снять. +# Пусто (скелет без основного реквизита): снять все. Картиночные пути снимаются всегда. function Strip-FormBindings { - param([string]$xml, [bool]$keepObjekt) + param([string]$xml, [string]$mainAttrName) foreach ($tag in $script:formBindingDataTags) { - if ($keepObjekt) { - $xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*", '') + if ($mainAttrName) { + # Оставить и «Список.Поле», и путь ровно на сам реквизит («Список» у таблицы формы) + $root = [regex]::Escape($mainAttrName) + $xml = [regex]::Replace($xml, "\s*<$tag>(?!$root(\.|<))[^<]*", '') } else { $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*", '') } @@ -769,6 +777,15 @@ function Borrow-Form { # Get OuterXml and strip redundant namespace redeclarations (they're on root ) $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' + # Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить + # («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра) + $mainAttrInfo = $null + if ($BorrowMainAttr) { $mainAttrInfo = Get-MainAttributeInfo $srcFormEl $nsStripPattern } + $mainAttrName = if ($mainAttrInfo) { $mainAttrInfo.Name } else { "" } + if ($BorrowMainAttr -and -not $mainAttrInfo) { + Warn " У формы нет основного реквизита — -BorrowMainAttribute проигнорирован" + } + # uuid реквизитов объекта — только для формы без заимствованного основного реквизита: # там ссылки параметров выбора переводятся на непрозрачную форму пути $srcAttrUuids = @{} @@ -786,7 +803,7 @@ function Borrow-Form { $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*.*?', '') $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*', '') # Strip data-binding tags whose root attribute isn't borrowed - $autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr) + $autoCmdXml = Strip-FormBindings $autoCmdXml $mainAttrName if (-not $BorrowMainAttr) { $autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids } } @@ -799,7 +816,7 @@ function Borrow-Form { $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') # Strip data-binding tags whose root attribute isn't borrowed # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*) - $childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr) + $childItemsXml = Strip-FormBindings $childItemsXml $mainAttrName if (-not $BorrowMainAttr) { $childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids } # Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') @@ -1019,27 +1036,10 @@ function Borrow-Form { $formXmlSb.Append("`t$childItemsXml") | Out-Null $formXmlSb.Append("`r`n") | Out-Null } - # Секции основного реквизита исходной формы (, ) — их нельзя терять - $mainAttrExtra = @() - if ($BorrowMainAttr) { - $mainAttrExtra = @(Get-MainAttributeExtraXml $srcFormEl $nsStripPattern) - } - # Attributes: empty or with MainAttribute when BorrowMainAttr - if ($BorrowMainAttr) { - $objTypePrefix = "" - $gtList = $script:generatedTypes[$typeName] - if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } } - $mainAttrType = "cfg:${objTypePrefix}.${objName}" + if ($BorrowMainAttr -and $mainAttrInfo) { $formXmlSb.Append("`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`t${mainAttrType}`r`n") | Out-Null - $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null - $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null - foreach ($extraXml in $mainAttrExtra) { - $formXmlSb.Append("`t`t`t$extraXml`r`n") | Out-Null - } - $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t$($mainAttrInfo.Xml)`r`n") | Out-Null $formXmlSb.Append("`t") | Out-Null } else { $formXmlSb.Append("`t") | Out-Null @@ -1073,22 +1073,15 @@ function Borrow-Form { } # BaseForm Attributes: same as main section - if ($BorrowMainAttr) { + if ($BorrowMainAttr -and $mainAttrInfo) { $formXmlSb.Append("`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`t${mainAttrType}`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null - $formXmlSb.Append("`t`t`t`ttrue`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 - } + # В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems + $maLines = $mainAttrInfo.Xml -split "`r?`n" + for ($li = 0; $li -lt $maLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t`t$($maLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($maLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null } - $formXmlSb.Append("`t`t`t`r`n") | Out-Null $formXmlSb.Append("`t`t") | Out-Null } else { $formXmlSb.Append("`t`t") | Out-Null @@ -1274,26 +1267,30 @@ function Build-InternalInfoXml { } # --- 11b. Collect DataPath references from source Form.xml --- -# --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData --- -# Это и (доп. колонки табличных частей, объявленные прямо в форме). Их -# переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на -# колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список OuterXml без xmlns. -function Get-MainAttributeExtraXml { +# --- 11b1. Основной реквизит исходной формы --- +# Переносится ЦЕЛИКОМ, а не собирается из констант: имя, тип и состав детей зависят от вида формы. +# У формы объекта это «Объект»/<Тип>Object + SavedData/UseAlways/Columns, у формы списка — +# «Список»/DynamicList + Settings, у формы записи регистра — «Запись»/RecordManager + SavedData. +# Синтез фиксированного набора давал для необъектных форм «Исключение XDTO» при загрузке. +# Конфигуратор меняет у скопированного реквизита только id (эталоны Issue64UtB, Issue66Example2). +function Get-MainAttributeInfo { 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 + if (-not $mainAttr) { return $null } + $xml = [regex]::Replace($mainAttr.OuterXml, $nsStripPattern, '') + # id заменяется только в открывающем теге самого реквизита — у вложенных элементов свои + $xml = [regex]::Replace($xml, '^(]*?)id="[^"]*"', "`${1}id=`"$script:mainAttrId`"") + return @{ Name = $mainAttr.GetAttribute("name"); Xml = $xml } } function Collect-FormDataPaths { - param([string]$formXmlPath) + param([string]$formXmlPath, [string]$mainAttrName) + + # Корень путей — имя основного реквизита формы: «Объект» у формы объекта, «Список» у формы + # списка, «Запись» у формы записи регистра. Зашитый «Объект» не находил ничего у необъектных + # форм, и в оболочку не заимствовалось ни одного дочернего объекта. + $root = [regex]::Escape($mainAttrName) $enc = New-Object System.Text.UTF8Encoding($true) $content = [System.IO.File]::ReadAllText($formXmlPath, $enc) @@ -1304,7 +1301,7 @@ function Collect-FormDataPaths { # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) # for Объект.* references — picture-path tags carry picture indices, not data attributes. foreach ($tag in $script:formBindingDataTags) { - $bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)") + $bms = [regex]::Matches($content, "<$tag>[^<]*\b$root\.(\w+(?:\.\w+)*)") foreach ($m in $bms) { $path = $m.Groups[1].Value $segments = $path.Split(".") @@ -1322,7 +1319,7 @@ function Collect-FormDataPaths { # Also scan Объект.X — object attributes referenced by filter/conditional-appearance # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. - $fieldMatches = [regex]::Matches($content, "[^<]*\bОбъект\.(\w+(?:\.\w+)*)") + $fieldMatches = [regex]::Matches($content, "[^<]*\b$root\.(\w+(?:\.\w+)*)") foreach ($m in $fieldMatches) { $path = $m.Groups[1].Value $segments = $path.Split(".") @@ -1339,7 +1336,7 @@ function Collect-FormDataPaths { # Also scan — доп. колонки табличной части, объявленные в # самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться, # и без её заимствования платформа отвергает форму: «Неверный путь к данным». - $acMatches = [regex]::Matches($content, ' x3 и ), поэтому вид + # запоминается и выпускается как есть — иначе измерение уехало бы в файл как . + if ($script:childObjectKinds -ccontains $child.LocalName) { $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) if (-not $nameNode) { continue } $attrName = $nameNode.InnerText @@ -1408,7 +1409,7 @@ function Resolve-SourceAttributes { # Strip namespace declarations from Type $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') - $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } + $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml; Kind = $child.LocalName } } elseif ($child.LocalName -eq 'TabularSection') { $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) @@ -1479,11 +1480,11 @@ function Resolve-SourceAttributes { # --- 11d. Build adopted attribute XML --- function Build-AdoptedAttributeXml { - param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) + param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent, [string]$kind = "Attribute") $newUuid = [guid]::NewGuid().ToString() $sb = New-Object System.Text.StringBuilder - $sb.AppendLine("${indent}") | Out-Null + $sb.AppendLine("${indent}<${kind} uuid=`"${newUuid}`">") | Out-Null $sb.AppendLine("${indent}`t") | Out-Null $sb.AppendLine("${indent}`t") | Out-Null $sb.AppendLine("${indent}`t`tAdopted") | Out-Null @@ -1492,7 +1493,7 @@ function Build-AdoptedAttributeXml { $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null $sb.AppendLine("${indent}`t") | Out-Null - $sb.Append("${indent}") | Out-Null + $sb.Append("${indent}") | Out-Null return $sb.ToString() } @@ -1623,7 +1624,8 @@ function Merge-AttributesIntoObject { $allAttrXml = "" foreach ($attr in $attrsToAdd) { if ($existingNames.ContainsKey($attr.Name)) { continue } - $allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") + $kind = if ($attr.Kind) { $attr.Kind } else { "Attribute" } + $allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $kind) } # Save via text manipulation to avoid namespace issues with InnerXml @@ -1677,7 +1679,16 @@ function Borrow-MainAttribute { Write-Error "Source Form.xml not found: $srcFormXmlPath" exit 1 } - $dp = Collect-FormDataPaths $srcFormXmlPath + # Имя основного реквизита исходной формы — корень путей, которые надо собрать + $dpDoc = New-Object System.Xml.XmlDocument + $dpDoc.PreserveWhitespace = $true + $dpDoc.Load($srcFormXmlPath) + $dpInfo = Get-MainAttributeInfo $dpDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"' + if (-not $dpInfo) { + Warn " У формы нет основного реквизита — заимствовать нечего" + return + } + $dp = Collect-FormDataPaths $srcFormXmlPath $dpInfo.Name $firstLevelNames = $dp.FirstLevel $deepPaths = $dp.DeepPaths Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" @@ -1710,7 +1721,8 @@ function Borrow-MainAttribute { # Generate full object XML with attributes and TS $contentSb = New-Object System.Text.StringBuilder foreach ($attr in $insertAttrs) { - $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + $attrKind = if ($attr.Kind) { $attr.Kind } else { "Attribute" } + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrKind $contentSb.AppendLine($attrXml) | Out-Null } foreach ($ts in $insertTS) { @@ -1759,8 +1771,9 @@ function Borrow-MainAttribute { $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 ".*?')) { $allTypeXmls += $m.Value } } } diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index 513eaa19..843b095d 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -46,6 +46,12 @@ FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "Header # Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. FORM_BINDING_PICTURE_TAGS = ["MultipleValuePictureDataPath"] +# id основного реквизита в заимствованной форме — как у Конфигуратора +MAIN_ATTR_ID = "1000001" + +# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно) +CHILD_OBJECT_KINDS = ("Attribute", "Dimension", "Resource") + # Прямые дети , которые в заимствованную форму не переносятся. # Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком. FORM_STRUCTURAL_SECTIONS = ("Events", "Attributes", "Commands", "Parameters", "CommandInterface") @@ -53,13 +59,15 @@ FORM_STRUCTURAL_SECTIONS = ("Events", "Attributes", "Commands", "Parameters", "C FORM_ATTRIBUTE_REF_PROPS = ("ReportResult", "DetailsData", "VariantAppearance", "GroupList") -def strip_form_bindings(xml, keep_objekt): +def strip_form_bindings(xml, main_attr_name): """Strip data-binding tags whose root attribute isn't borrowed. - keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. - keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped.""" + main_attr_name задан (BorrowMainAttribute): оставить привязки от его имени, остальные снять. + Пусто (скелет без основного реквизита): снять все. Картиночные пути снимаются всегда.""" for tag in FORM_BINDING_DATA_TAGS: - if keep_objekt: - xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*', '', xml) + if main_attr_name: + # Оставить и «Список.Поле», и путь ровно на сам реквизит («Список» у таблицы формы) + root = re.escape(main_attr_name) + xml = re.sub(rf'\s*<{tag}>(?!{root}(\.|<))[^<]*', '', xml) else: xml = re.sub(rf'\s*<{tag}>[^<]*', '', xml) for tag in FORM_BINDING_PICTURE_TAGS: @@ -934,12 +942,13 @@ def main(): save_xml_bom(obj_tree, obj_file) info(f" Registered form in: {obj_file}") - # --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData --- - # Это и (доп. колонки табличных частей, объявленные прямо в форме). Их - # переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на - # колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список XML без xmlns. - def get_main_attribute_extra_xml(form_el, ns_strip_pattern): - result = [] + # --- 11b1. Основной реквизит исходной формы --- + # Переносится ЦЕЛИКОМ, а не собирается из констант: имя, тип и состав детей зависят от вида формы. + # У формы объекта это «Объект»/<Тип>Object + SavedData/UseAlways/Columns, у формы списка — + # «Список»/DynamicList + Settings, у формы записи регистра — «Запись»/RecordManager + SavedData. + # Синтез фиксированного набора давал для необъектных форм «Исключение XDTO» при загрузке. + # Конфигуратор меняет у скопированного реквизита только id (эталоны Issue64UtB, Issue66Example2). + def get_main_attribute_info(form_el, ns_strip_pattern): main_attr = None for child in form_el: if not isinstance(child.tag, str) or localname(child) != "Attributes": @@ -955,20 +964,21 @@ def main(): 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 + return None + # with_tail=False: хвостовой пробельный узел — часть родителя, а не секции; иначе в + # вывод попадают пустые строки, которых нет у PS (OuterXml хвост не включает). + xml = decode_numeric_entities(etree.tostring(main_attr, encoding="unicode", with_tail=False)) + xml = ns_strip_pattern.sub("", xml) + # id заменяется только в открывающем теге самого реквизита — у вложенных элементов свои + xml = re.sub(r'^(]*?)id="[^"]*"', lambda m: m.group(1) + f'id="{MAIN_ATTR_ID}"', xml) + return {"Name": main_attr.get("name"), "Xml": xml} # --- 11b. Collect DataPath references from source Form.xml --- - def collect_form_data_paths(form_xml_path): + def collect_form_data_paths(form_xml_path, main_attr_name): + # Корень путей — имя основного реквизита формы: «Объект» у формы объекта, «Список» у формы + # списка, «Запись» у формы записи регистра. Зашитый «Объект» не находил ничего у необъектных + # форм, и в оболочку не заимствовалось ни одного дочернего объекта. + root = re.escape(main_attr_name) with open(form_xml_path, "r", encoding="utf-8-sig") as fh: content = fh.read() @@ -978,7 +988,7 @@ def main(): # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) # for Объект.* references — picture-path tags carry picture indices, not data attributes. for tag in FORM_BINDING_DATA_TAGS: - for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)', content): + for m in re.finditer(r'<' + tag + r'>[^<]*\b' + root + r'\.(\w+(?:\.\w+)*)', content): path = m.group(1) segments = path.split(".") seg0 = segments[0] @@ -994,7 +1004,7 @@ def main(): # Also scan Объект.X — object attributes referenced by filter/conditional-appearance # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. - for m in re.finditer(r'[^<]*\bОбъект\.(\w+(?:\.\w+)*)', content): + for m in re.finditer(r'[^<]*\b' + root + r'\.(\w+(?:\.\w+)*)', content): path = m.group(1) segments = path.split(".") seg0 = segments[0] @@ -1011,7 +1021,7 @@ def main(): # Also scan — доп. колонки табличной части, объявленные в # самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться, # и без её заимствования платформа отвергает форму: «Неверный путь к данным». - for m in re.finditer(r' x3 и ), поэтому вид + # запоминается и выпускается как есть — иначе измерение уехало бы в файл как . + if ln in CHILD_OBJECT_KINDS: name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") if name_node is None: continue @@ -1079,7 +1093,7 @@ def main(): type_xml = etree.tostring(type_node, encoding="unicode") type_xml = ns_strip.sub("", type_xml) - attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml}) + attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml, "Kind": ln}) elif ln == "TabularSection": name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") @@ -1160,10 +1174,10 @@ def main(): return {"Attributes": attrs, "TabularSections": tab_sections, "ExtraProps": extra_props} # --- 11d. Build adopted attribute XML --- - def build_adopted_attribute_xml(name, source_uuid, type_xml, indent): + def build_adopted_attribute_xml(name, source_uuid, type_xml, indent, kind="Attribute"): new_uuid_val = new_guid() lines = [ - f'{indent}', + f'{indent}<{kind} uuid="{new_uuid_val}">', f'{indent}\t', f'{indent}\t', f'{indent}\t\tAdopted', @@ -1172,7 +1186,7 @@ def main(): f'{indent}\t\t{source_uuid}', f'{indent}\t\t{type_xml}', f'{indent}\t', - f'{indent}', + f'{indent}', ] return "\n".join(lines) @@ -1255,7 +1269,7 @@ def main(): for attr in attrs_to_add: if attr["Name"] in existing_names: continue - all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") + all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t", attr.get("Kind", "Attribute")) added += 1 if added > 0: @@ -1276,7 +1290,13 @@ def main(): if not os.path.isfile(src_form_xml_path): print(f"Source Form.xml not found: {src_form_xml_path}", file=sys.stderr) sys.exit(1) - dp = collect_form_data_paths(src_form_xml_path) + # Имя основного реквизита исходной формы — корень путей, которые надо собрать + dp_ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + dp_info = get_main_attribute_info(etree.parse(src_form_xml_path).getroot(), dp_ns_strip) + if dp_info is None: + warn(" У формы нет основного реквизита — заимствовать нечего") + return + dp = collect_form_data_paths(src_form_xml_path, dp_info["Name"]) first_level_names = dp["FirstLevel"] deep_paths = dp["DeepPaths"] info(f" Collected {len(first_level_names)} first-level DataPath references, {len(deep_paths)} deep paths") @@ -1310,7 +1330,7 @@ def main(): # Generate full object XML with attributes and TS content_parts = [] for attr in insert_attrs: - content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")) + content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t", attr.get("Kind", "Attribute"))) for ts in insert_ts: content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")) adopted_content = "\n".join(content_parts).rstrip() @@ -1351,9 +1371,9 @@ def main(): 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(".*?', cols_info["Xml"])) ref_types = collect_reference_types(all_type_xmls) info(f" Reference types to borrow: {len(ref_types)}") @@ -1568,6 +1588,13 @@ def main(): # там ссылки параметров выбора переводятся на непрозрачную форму пути src_attr_uuids = {} if borrow_main_attr else get_source_attribute_uuids(type_name, obj_name) + # Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить + # («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра) + main_attr_info = get_main_attribute_info(src_form_el, ns_strip_pattern) if borrow_main_attr else None + main_attr_name = main_attr_info["Name"] if main_attr_info else "" + if borrow_main_attr and main_attr_info is None: + warn(" У формы нет основного реквизита — -BorrowMainAttribute проигнорирован") + # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false auto_cmd_xml = "" if src_auto_cmd is not None: @@ -1580,7 +1607,7 @@ def main(): auto_cmd_xml = re.sub(r'(?s)\s*.*?', '', auto_cmd_xml) auto_cmd_xml = re.sub(r'\s*', '', auto_cmd_xml) # Strip data-binding tags whose root attribute isn't borrowed - auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr) + auto_cmd_xml = strip_form_bindings(auto_cmd_xml, main_attr_name) if not borrow_main_attr: auto_cmd_xml = rewrite_choice_parameter_links(auto_cmd_xml, src_attr_uuids) @@ -1598,7 +1625,7 @@ def main(): # Replace all CommandName values with 0 child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) # Strip data-binding tags whose root attribute isn't borrowed - child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr) + child_items_xml = strip_form_bindings(child_items_xml, main_attr_name) if not borrow_main_attr: child_items_xml = rewrite_choice_parameter_links(child_items_xml, src_attr_uuids) # Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше @@ -1785,28 +1812,10 @@ def main(): if child_items_xml: parts.append(f"\t{child_items_xml}\r\n") - # Секции основного реквизита исходной формы (, ) — их нельзя терять - 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 = "" - gt_list = GENERATED_TYPES.get(type_name, []) - for g in gt_list: - if g["category"] == "Object": - obj_type_prefix = g["prefix"] - break - main_attr_type = f"cfg:{obj_type_prefix}.{obj_name}" + if borrow_main_attr and main_attr_info: parts.append("\t\r\n") - parts.append('\t\t\r\n') - parts.append(f"\t\t\t{main_attr_type}\r\n") - parts.append("\t\t\ttrue\r\n") - parts.append("\t\t\ttrue\r\n") - for extra_xml in main_attr_extra: - parts.append(f"\t\t\t{extra_xml}\r\n") - parts.append("\t\t\r\n") + parts.append(f"\t\t{main_attr_info['Xml']}\r\n") parts.append("\t") else: parts.append("\t") @@ -1836,18 +1845,12 @@ def main(): parts.append("\r\n") # BaseForm Attributes: same as main section - if borrow_main_attr: + if borrow_main_attr and main_attr_info: parts.append("\t\t\r\n") - parts.append('\t\t\t\r\n') - parts.append(f"\t\t\t\t{main_attr_type}\r\n") - parts.append("\t\t\t\ttrue\r\n") - parts.append("\t\t\t\ttrue\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\r\n") + # В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems + for li, line in enumerate(main_attr_info['Xml'].split('\n')): + parts.append(f"\t\t\t{line}" if li == 0 else f"\t{line}") + parts.append("\r\n") parts.append("\t\t") else: parts.append("\t\t") diff --git a/tests/skills/cases/cfe-borrow/snapshots/form-bindings/Ext/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml b/tests/skills/cases/cfe-borrow/snapshots/form-bindings/Ext/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml index 234dcda4..cc8a4391 100644 --- a/tests/skills/cases/cfe-borrow/snapshots/form-bindings/Ext/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml +++ b/tests/skills/cases/cfe-borrow/snapshots/form-bindings/Ext/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml @@ -31,7 +31,9 @@ - cfg:CatalogObject.Товары + + cfg:CatalogObject.Товары + true true @@ -68,7 +70,9 @@ - cfg:CatalogObject.Товары + + cfg:CatalogObject.Товары + true true diff --git a/tests/skills/cases/cfe-borrow/snapshots/form-main-attr-columns/Ext/Documents/ЗаказКлиента/Forms/ФормаДокумента/Ext/Form.xml b/tests/skills/cases/cfe-borrow/snapshots/form-main-attr-columns/Ext/Documents/ЗаказКлиента/Forms/ФормаДокумента/Ext/Form.xml index 67749c9e..7104c943 100644 --- a/tests/skills/cases/cfe-borrow/snapshots/form-main-attr-columns/Ext/Documents/ЗаказКлиента/Forms/ФормаДокумента/Ext/Form.xml +++ b/tests/skills/cases/cfe-borrow/snapshots/form-main-attr-columns/Ext/Documents/ЗаказКлиента/Forms/ФормаДокумента/Ext/Form.xml @@ -64,7 +64,9 @@ - cfg:DocumentObject.ЗаказКлиента + + cfg:DocumentObject.ЗаказКлиента + true true @@ -151,7 +153,9 @@ - cfg:DocumentObject.ЗаказКлиента + + cfg:DocumentObject.ЗаказКлиента + true true diff --git a/tests/skills/cases/cfe-borrow/snapshots/form-row-picture-path/Ext/Documents/Расход/Forms/ФормаДокумента/Ext/Form.xml b/tests/skills/cases/cfe-borrow/snapshots/form-row-picture-path/Ext/Documents/Расход/Forms/ФормаДокумента/Ext/Form.xml index bcbcc825..86bf2045 100644 --- a/tests/skills/cases/cfe-borrow/snapshots/form-row-picture-path/Ext/Documents/Расход/Forms/ФормаДокумента/Ext/Form.xml +++ b/tests/skills/cases/cfe-borrow/snapshots/form-row-picture-path/Ext/Documents/Расход/Forms/ФормаДокумента/Ext/Form.xml @@ -35,7 +35,9 @@ - cfg:DocumentObject.Расход + + cfg:DocumentObject.Расход + true true @@ -76,7 +78,9 @@ - cfg:DocumentObject.Расход + + cfg:DocumentObject.Расход + true true