fix(cfe-borrow): свойства и виды, от которых зависят стандартные поля

Сплошной прогон по типам объектов (11 минимальных объектных форм УТ, каждая
заимствована и загружена в UT_DEMO) дал три падения из одиннадцати. Все три —
один класс: в оболочке не хватает того, от чего зависит существование
стандартного поля, и платформа отвергает загрузку «Неверный путь к данным».

1. Справочник с владельцем: «Объект.Owner» не разрешается без <Owners>.
   Свойство — список <xr:Item>, а не скаляр, поэтому переносится фрагментом,
   как __TypeXml у DefinedType. Одного переноса мало: ссылка должна вести на
   объект, который в расширении есть, иначе платформа падает с access violation
   вместо сообщения. Добавлен общий проход, заимствующий владельцев (и владельцев
   владельцев) — Конфигуратор поступает так же, эталон Issue66Example7_1.

2. Регистр сведений: «Запись.Period» не разрешается без
   InformationRegisterPeriodicity. Вместе с ним переносится WriteMode, от
   которого зависит «Запись.Recorder» — Конфигуратор несёт оба, 6 эталонов из 6.

3. Задача: «Объект.Исполнитель» — это реквизит адресации, отдельный вид
   дочернего объекта. AddressingAttribute добавлен к видам, которые
   заимствуются поимённо, рядом с Dimension и Resource.

Правка сделана в Read-SourceObject и Build-BorrowedObjectXml — там, где оболочка
рождается: механизм $extraProps работает только в потоке -BorrowMainAttribute и
обычное заимствование оболочки не покрывает.

Проверено: те же 11 форм грузятся 11 из 11; ПВХ с путём «Объект.ValueType»
грузился и раньше — Конфигуратор Type и CharacteristicExtValues тоже не
переносит (эталон Issue66Example8), правило «свойство, включающее поле»
подтверждается с обеих сторон.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-14 19:43:23 +03:00
co-authored by Claude Opus 5
parent 112166dc0c
commit 83bb5b6fd3
17 changed files with 814 additions and 4 deletions
@@ -1,4 +1,4 @@
# cfe-borrow v1.30 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -30,7 +30,7 @@ $script:droppedLinks = @()
$script:mainAttrId = "1000001"
# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно)
$script:childObjectKinds = @('Attribute','Dimension','Resource')
$script:childObjectKinds = @('Attribute','Dimension','Resource','AddressingAttribute')
# Прямые дети <Form>, которые в заимствованную форму не переносятся.
# Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком.
@@ -462,6 +462,16 @@ $typesWithChildObjects = @(
# CommonModule properties to copy from source
$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall")
# Свойства объекта, от которых зависит существование стандартного поля: без них платформа
# отвергает загрузку — «Неверный путь к данным». Конфигуратор переносит ровно их (эталоны
# Issue66Example7_1 и Issue66Example2). Проверено сплошным прогоном по типам: у регистра сведений
# без InformationRegisterPeriodicity не разрешается «Запись.Period».
$script:typeGateProps = @{
"InformationRegister" = @("InformationRegisterPeriodicity","WriteMode")
}
# Владельцы справочника — список <xr:Item>, а не скаляр: переносится фрагментом, как __TypeXml
$script:typesWithOwners = @("Catalog","ChartOfCharacteristicTypes")
# Standard system fields to skip when collecting DataPath references
$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName")
@@ -672,6 +682,19 @@ function Read-SourceObject {
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
}
}
# Владельцы: стандартное поле «Owner» появляется у справочника, только если задан Owners
if ($script:typesWithOwners -ccontains $typeName) {
$ownersNode = $propsNode.SelectSingleNode("md:Owners", $srcNs)
if ($ownersNode -and $ownersNode.HasChildNodes) {
$srcProps["__OwnersXml"] = [regex]::Replace($ownersNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
}
}
# Скалярные свойства, включающие стандартные поля своего типа
foreach ($gp in @($script:typeGateProps[$typeName])) {
if (-not $gp) { continue }
$gpNode = $propsNode.SelectSingleNode("md:${gp}", $srcNs)
if ($gpNode) { $srcProps[$gp] = $gpNode.InnerText.Trim() }
}
}
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
@@ -2036,6 +2059,16 @@ function Build-BorrowedObjectXml {
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
}
# Свойства, от которых зависят стандартные поля (см. $script:typeGateProps / $script:typesWithOwners)
foreach ($gp in @($script:typeGateProps[$typeName])) {
if ($gp -and $sourceProps.ContainsKey($gp)) {
$sb.AppendLine("`t`t`t<${gp}>$($sourceProps[$gp])</${gp}>") | Out-Null
}
}
if ($sourceProps.ContainsKey("__OwnersXml")) {
$sb.AppendLine("`t`t`t$($sourceProps['__OwnersXml'])") | Out-Null
}
$sb.AppendLine("`t`t</Properties>") | Out-Null
# ChildObjects (for types that need it)
@@ -2203,6 +2236,47 @@ foreach ($item in $items) {
}
}
# --- 14b. Владельцы заимствованных справочников ---
# Ссылка в <Owners> должна вести на объект, который в расширении есть: иначе платформа падает при
# загрузке (проверено — access violation, не сообщение об ошибке). Конфигуратор владельца
# заимствует (эталон Issue66Example7_1: вместе со справочником перенесён и его ПВХ-владелец).
# Проход общий и повторяется, пока находятся новые: у владельца может быть свой владелец.
$ownerPass = 0
while ($true) {
$ownerPass++
if ($ownerPass -gt 10) { break }
$newOwners = @()
foreach ($shell in (Get-ChildItem -Path $extDir -Filter "*.xml" -Recurse -File)) {
$shellText = [System.IO.File]::ReadAllText($shell.FullName)
if ($shellText -notmatch '<Owners>') { continue }
foreach ($om in [regex]::Matches($shellText, '<xr:Item[^>]*>(\w+)\.(\w+)</xr:Item>')) {
$oType = $om.Groups[1].Value; $oName = $om.Groups[2].Value
if (-not $childTypeDirMap.ContainsKey($oType)) { continue }
if (Test-ObjectBorrowed $oType $oName) { continue }
if ($newOwners | Where-Object { $_.T -eq $oType -and $_.N -eq $oName }) { continue }
$newOwners += @{ T = $oType; N = $oName }
}
}
if ($newOwners.Count -eq 0) { break }
foreach ($ow in $newOwners) {
$owSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$ow.T]) "$($ow.N).xml"
if (-not (Test-Path $owSrcFile)) {
Warn " Владелец $($ow.T).$($ow.N) не найден в источнике — ссылка останется висячей"
continue
}
$owSrc = Read-SourceObject $ow.T $ow.N
$owXml = Build-BorrowedObjectXml $ow.T $ow.N $owSrc.Uuid $owSrc.Properties
$owDir = Join-Path $extDir $childTypeDirMap[$ow.T]
if (-not (Test-Path $owDir)) { New-Item -ItemType Directory -Path $owDir -Force | Out-Null }
$owFile = Join-Path $owDir "$($ow.N).xml"
$owEnc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($owFile, $owXml, $owEnc)
Add-ToChildObjects $ow.T $ow.N
$script:borrowedFiles += $owFile
Info " Auto-borrowed owner: $($ow.T).$($ow.N)"
}
}
# --- 15. Save modified Configuration.xml ---
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.30 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -50,7 +50,7 @@ FORM_BINDING_PICTURE_TAGS = ["MultipleValuePictureDataPath"]
MAIN_ATTR_ID = "1000001"
# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно)
CHILD_OBJECT_KINDS = ("Attribute", "Dimension", "Resource")
CHILD_OBJECT_KINDS = ("Attribute", "Dimension", "Resource", "AddressingAttribute")
# Прямые дети <Form>, которые в заимствованную форму не переносятся.
# Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком.
@@ -455,6 +455,16 @@ TYPES_WITH_CHILD_OBJECTS = [
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
# Свойства объекта, от которых зависит существование стандартного поля: без них платформа
# отвергает загрузку — «Неверный путь к данным». Конфигуратор переносит ровно их (эталоны
# Issue66Example7_1 и Issue66Example2). Проверено сплошным прогоном по типам: у регистра
# сведений без InformationRegisterPeriodicity не разрешается «Запись.Period».
TYPE_GATE_PROPS = {
"InformationRegister": ["InformationRegisterPeriodicity", "WriteMode"],
}
# Владельцы справочника — список <xr:Item>, а не скаляр: переносится фрагментом, как __TypeXml
TYPES_WITH_OWNERS = ("Catalog", "ChartOfCharacteristicTypes")
# Standard system fields to skip when collecting DataPath references
STANDARD_FIELDS = [
"Code", "Description", "Ref", "Parent", "DeletionMark",
@@ -808,6 +818,17 @@ def main():
if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
# Владельцы: стандартное поле «Owner» появляется у справочника, только если задан Owners
if type_name in TYPES_WITH_OWNERS:
owners_node = props_node.find(f"{{{MD_NS}}}Owners")
if owners_node is not None and len(owners_node):
owners_xml = etree.tostring(owners_node, encoding="unicode")
src_props["__OwnersXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', owners_xml)
# Скалярные свойства, включающие стандартные поля своего типа
for gp in TYPE_GATE_PROPS.get(type_name, []):
gp_node = props_node.find(f"{{{MD_NS}}}{gp}")
if gp_node is not None:
src_props[gp] = (gp_node.text or "").strip()
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None
@@ -886,6 +907,13 @@ def main():
if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}")
# Свойства, от которых зависят стандартные поля (см. TYPE_GATE_PROPS / TYPES_WITH_OWNERS)
for gp in TYPE_GATE_PROPS.get(type_name, []):
if gp in source_props:
lines.append(f"\t\t\t<{gp}>{source_props[gp]}</{gp}>")
if "__OwnersXml" in source_props:
lines.append(f"\t\t\t{source_props['__OwnersXml']}")
lines.append("\t\t</Properties>")
if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS:
@@ -2068,6 +2096,47 @@ def main():
borrowed_files.append(target_file)
borrowed_count += 1
# --- Владельцы заимствованных справочников ---
# Ссылка в <Owners> должна вести на объект, который в расширении есть: иначе платформа падает
# при загрузке (проверено — access violation, не сообщение об ошибке). Конфигуратор владельца
# заимствует (эталон Issue66Example7_1: вместе со справочником перенесён и его ПВХ-владелец).
# Проход общий и повторяется, пока находятся новые: у владельца может быть свой владелец.
for _owner_pass in range(10):
new_owners = []
for root_dir, _dirs, files in os.walk(ext_dir):
for fn in files:
if not fn.endswith(".xml"):
continue
with open(os.path.join(root_dir, fn), "r", encoding="utf-8-sig") as fh:
shell_text = fh.read()
if "<Owners>" not in shell_text:
continue
for om in re.finditer(r'<xr:Item[^>]*>(\w+)\.(\w+)</xr:Item>', shell_text):
o_type, o_name = om.group(1), om.group(2)
if o_type not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(o_type, o_name):
continue
if (o_type, o_name) in new_owners:
continue
new_owners.append((o_type, o_name))
if not new_owners:
break
for o_type, o_name in new_owners:
ow_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[o_type], f"{o_name}.xml")
if not os.path.isfile(ow_src_file):
warn(f" Владелец {o_type}.{o_name} не найден в источнике — ссылка останется висячей")
continue
ow_src = read_source_object(o_type, o_name)
ow_xml = build_borrowed_object_xml(o_type, o_name, ow_src["Uuid"], ow_src["Properties"])
ow_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[o_type])
os.makedirs(ow_dir, exist_ok=True)
ow_file = os.path.join(ow_dir, f"{o_name}.xml")
write_utf8_bom(ow_file, ow_xml)
add_to_child_objects(o_type, o_name)
borrowed_files.append(ow_file)
info(f" Auto-borrowed owner: {o_type}.{o_name}")
# --- Save modified Configuration.xml ---
save_xml_bom(tree, ext_resolved)
info(f"Saved: {ext_resolved}")