mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-30 14:05:53 +03:00
feat(cfe-borrow): связи формы кодируются как у Конфигуратора, а не вырезаются
В заимствованной форме без основного реквизита навык вырезал связи параметров выбора на Items.<Таблица>.CurrentData.<Поле>, а связи на стандартные поля (Объект.Owner, Объект.Date) и Объект.<ТЧ>.<Реквизит> — молча. TypeLink на Items.* вырезался всегда, даже с основным реквизитом. Конфигуратор пишет такие пути кодом. Код — внутренняя ссылка платформы: в выгрузке она пишет имя, а если имя в форме не разрешается — код. Грамматика восстановлена оракулом (код в форму конфигурации → загрузка → выгрузка → имя, 8.3.27) и эталонами Конфигуратора УТ: <id реквизита> или <id таблицы>:CurrentData, далее 0:<uuid> метаданных источника, <id колонки>, <id>:<AdditionalColumns> или -k стандартного реквизита (коды своих для каждого вида объекта — таблица в навыке и в спецификации §5.5.2). Правило как у Конфигуратора: текст, где он разрешается, иначе код. Путь от реквизита формы кодируется и с основным реквизитом — текст платформа отвергает «Неверный путь к полю». TypeLink — то же правило. Некодируемое вырезается, всегда с предупреждением «Вырезано связей формы». Измерения/ресурсы регистра и реквизиты ТЧ теперь различаются как реквизиты (с основным реквизитом «Запись.Склад» больше не подменяется ссылкой на сам реквизит). Попутно: порядок заимствования ссылочных типов в PS зависел от хеша — теперь порядок первого упоминания, как в Python-порте. Оба порта, v1.41. Проверка: четыре формы УТ (ОприходованиеИзлишковТоваров, ЗначенияСвойствОбъектов, ФормаРасчетыМеждуОрганизациями без и с основным реквизитом) совпадают с выгрузкой Конфигуратора построчно; все 11 кодов новых кейсов оракул переводит обратно в исходные пути. Кейсы form-link-codes, -main, -register; form-choice-link-items ждёт код вместо вырезания. Проблема и первые данные о кодировке — PR #105. Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Sergei Pleshanov
Claude Opus 5.5
parent
8677770eea
commit
7381b0503c
@@ -1,4 +1,4 @@
|
||||
# cfe-borrow v1.40 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.41 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -80,75 +80,173 @@ function Get-BaseFormIndented {
|
||||
return $firstIndent + ($parts -join '')
|
||||
}
|
||||
|
||||
# Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
|
||||
# в <xr:DataPath> и обычным стриппингом не снимается. Текстовое имя в расширении разрешается только
|
||||
# если его корень объявлен в <Attributes> самой заимствованной формы; иначе платформа отвергает
|
||||
# загрузку — «Неверный путь к полю - X». Реквизиты формы не заимствуются никогда, поэтому ссылка на
|
||||
# них разрешима только через id: Конфигуратор подставляет id реквизита ИСХОДНОЙ формы (эталоны
|
||||
# Issue66Example4/5/6, JR2433, JR2976, JR49904 — совпадение на шести расширениях). Именно id
|
||||
# исходной, а не заимствованной: при заимствовании реквизиты перенумеровываются в 1000000+, а
|
||||
# ссылка продолжает указывать в нумерацию базовой формы.
|
||||
# Путь на основной реквизит («Объект.X») при заимствованном основном реквизите разрешается текстом
|
||||
# и остаётся читаемым; без заимствования переводится в «<id>/0:<uuid реквизита объекта>».
|
||||
# Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
|
||||
# Пути вида «Items.<Элемент>.CurrentData.<Поле>» не трогаем — их кодировка отдельная.
|
||||
function Rewrite-ChoiceParameterLinks {
|
||||
param([string]$xml, $attrUuids, $formAttrIds, [string]$mainAttrName, [bool]$mainAttrBorrowed)
|
||||
# Связи формы — параметров выбора (<ChoiceParameterLinks>/<xr:Link>) и по типу (<TypeLink>) —
|
||||
# хранят путь в <xr:DataPath>, обычным стриппингом он не снимается. Путь — внутренняя ссылка
|
||||
# платформы: в выгрузке она пишет имя, а если имя в контексте формы не разрешается — код.
|
||||
# Текстовое имя в расширении разрешается, только если его корень объявлен в <Attributes> самой
|
||||
# заимствованной формы; иначе платформа отвергает загрузку — «Неверный путь к полю - X».
|
||||
# Конфигуратор поэтому пишет текст там, где он разрешается, и код — где нет.
|
||||
# Грамматика кода (оракул «код в форму конфигурации → загрузка → выгрузка → имя», 8.3.27; эталоны
|
||||
# Конфигуратора УТ и Issue66Example4-7, JR2433, JR2976, JR49904):
|
||||
# первый сегмент <id> — реквизит ИСХОДНОЙ формы (в расширении реквизиты
|
||||
# перенумерованы в 1000000+, ссылка остаётся в нумерации базы);
|
||||
# <id>:<CurrentData> — элемент-таблица исходной формы, её текущая строка;
|
||||
# дальше 0:<uuid> — реквизит/ТЧ/измерение/ресурс метаданных источника;
|
||||
# <id> — колонка реквизита формы;
|
||||
# <id>:<AddColumn> — колонка AdditionalColumns;
|
||||
# -<k> — стандартный реквизит, код по виду объекта ($script:stdFieldCodes).
|
||||
# В расширении платформа код обратно в имя не переводит, а висячий код грузит молча — ошибка
|
||||
# кодирования не упадёт, а тихо не сработает. Поэтому кодируется только измеренное; прочее
|
||||
# вырезается с предупреждением: связь — удобство подбора, без неё форма работает, с неверной
|
||||
# текстовой — не грузится вовсе.
|
||||
$script:linkCurrentData = '02023637-7868-4a5f-8576-835a76e0c9ba'
|
||||
$script:linkAddColumn = '5bdad865-f2c5-434b-8041-ba4aad3b6687'
|
||||
# Коды стандартных реквизитов — свой порядок у каждого вида (оракул 8.3.27; в XML объекта
|
||||
# полного списка нет). Незамеренное (-1, скрытые поля, колонки ExtDimensionTypes) — не кодируем.
|
||||
$script:stdFieldCodes = @{
|
||||
'Catalog' = @{ Code=-2; Description=-3; Parent=-4; Owner=-5; IsFolder=-6; DeletionMark=-7; Ref=-8; DataVersion=-9; Predefined=-10; PredefinedDataName=-13 }
|
||||
'Document' = @{ Number=-2; Date=-3; DeletionMark=-4; Ref=-5; DataVersion=-6; Posted=-7; RegisterRecords=-8 }
|
||||
'ChartOfCharacteristicTypes' = @{ Ref=-2; DataVersion=-3; DeletionMark=-4; Predefined=-5; Parent=-6; IsFolder=-7; Code=-8; Description=-9; ValueType=-11; PredefinedDataName=-14 }
|
||||
'ChartOfAccounts' = @{ Ref=-2; DataVersion=-3; DeletionMark=-4; Predefined=-5; Parent=-6; Code=-7; Description=-8; Type=-10; OffBalance=-11; ExtDimensionTypes=-12; Order=-17; PredefinedDataName=-28 }
|
||||
'ChartOfCalculationTypes' = @{ Code=-2; Description=-3; ActionPeriodIsBasic=-4; DeletionMark=-5; Ref=-6; DataVersion=-7; Predefined=-8; PredefinedDataName=-11; DisplacingCalculationTypes=-20; LeadingCalculationTypes=-30 }
|
||||
'ExchangePlan' = @{ Code=-2; Description=-3; DeletionMark=-4; Ref=-6; DataVersion=-7; SentNo=-9; ReceivedNo=-10; ThisNode=-13; ExchangeDate=-14 }
|
||||
'BusinessProcess' = @{ Number=-2; Date=-3; DeletionMark=-4; Ref=-5; DataVersion=-6; Completed=-7; HeadTask=-8; Started=-9 }
|
||||
'Task' = @{ Number=-2; Date=-3; DeletionMark=-4; Ref=-5; DataVersion=-6; BusinessProcess=-7; RoutePoint=-8; Description=-9; Executed=-10 }
|
||||
'InformationRegisterRecord' = @{ Period=-2; Recorder=-3; LineNumber=-4; Active=-5 }
|
||||
'AccumulationRegisterRow' = @{ Period=-2; Recorder=-3; LineNumber=-4; Active=-5; RecordType=-9; SecondPeriod=-10; MinutePeriod=-11; HourPeriod=-12; DayPeriod=-13; WeekPeriod=-14; TenDaysPeriod=-15 }
|
||||
'TabularSectionRow' = @{ LineNumber=-2 }
|
||||
}
|
||||
# Тип основного реквизита → вид для таблицы стандартных кодов (запись РС — сам реквизит,
|
||||
# набор записей РН — строка таблицы на нём)
|
||||
$script:mainAttrKinds = @{
|
||||
'CatalogObject'='Catalog'; 'DocumentObject'='Document'; 'ChartOfCharacteristicTypesObject'='ChartOfCharacteristicTypes'
|
||||
'ChartOfAccountsObject'='ChartOfAccounts'; 'ChartOfCalculationTypesObject'='ChartOfCalculationTypes'
|
||||
'ExchangePlanObject'='ExchangePlan'; 'BusinessProcessObject'='BusinessProcess'; 'TaskObject'='Task'
|
||||
'InformationRegisterRecordManager'='InformationRegisterRecord'; 'AccumulationRegisterRecordSet'='AccumulationRegisterRow'
|
||||
}
|
||||
|
||||
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
|
||||
# Путь связи → текст (разрешается в расширении), код или $null (вырезать).
|
||||
# $ctx: MainName, MainId, MainBorrowed, MainKind, AttrUuids (имя/«ТЧ.Реквизит» → uuid),
|
||||
# FormAttrIds, FormAttrCols («Реквизит.Колонка» → id), AddCols («<путь таблицы>.Колонка» → id),
|
||||
# Tables (имя элемента-таблицы → @{ Id; Path }).
|
||||
function Resolve-LinkDataPath {
|
||||
param([string]$path, $ctx)
|
||||
|
||||
$mainPat = if ($mainAttrName) { [regex]::Escape($mainAttrName) } else { $null }
|
||||
$mainId = if ($mainAttrName -and $formAttrIds.ContainsKey($mainAttrName)) { $formAttrIds[$mainAttrName] } else { "1" }
|
||||
# Уже код (форма-источник сама из расширения, или висячая ссылка в типовой) — как есть
|
||||
if ($path -match '^-?\d') { return $path }
|
||||
$main = $ctx.MainName
|
||||
|
||||
$xml = [regex]::Replace($xml, '(?s)\s*<xr:Link>.*?</xr:Link>', {
|
||||
# Текущая строка элемента-таблицы
|
||||
$cd = [regex]::Match($path, '^Items\.([^.]+)\.CurrentData\.([^.]+)$')
|
||||
if ($cd.Success) {
|
||||
$tbl = $ctx.Tables[$cd.Groups[1].Value]
|
||||
$field = $cd.Groups[2].Value
|
||||
if (-not $tbl) { return $null }
|
||||
$tp = $tbl.Path
|
||||
$prefix = "$($tbl.Id):$($script:linkCurrentData)"
|
||||
if ($main -and ($tp -ceq $main -or $tp.StartsWith("$main.", [StringComparison]::Ordinal))) {
|
||||
# Таблица на основном реквизите: с заимствованным — текст разрешается (эталоны 7_1, УТ)
|
||||
if ($ctx.MainBorrowed) { return $path }
|
||||
if ($tp -ceq $main) {
|
||||
# Строки самого основного реквизита — набор записей регистра накопления
|
||||
if ($ctx.MainKind -ne 'AccumulationRegisterRow') { return $null }
|
||||
if ($ctx.AttrUuids.ContainsKey($field)) { return "$prefix/0:$($ctx.AttrUuids[$field])" }
|
||||
$code = $script:stdFieldCodes['AccumulationRegisterRow'][$field]
|
||||
if ($code) { return "$prefix/$code" }
|
||||
return $null
|
||||
}
|
||||
$ts = $tp.Substring($main.Length + 1)
|
||||
if ($ts.Contains('.')) { return $null }
|
||||
if ($ctx.AttrUuids.ContainsKey("$ts.$field")) { return "$prefix/0:$($ctx.AttrUuids["$ts.$field"])" }
|
||||
if ($ctx.AddCols.ContainsKey("$tp.$field")) { return "$prefix/$($ctx.AddCols["$tp.$field"]):$($script:linkAddColumn)" }
|
||||
$code = $script:stdFieldCodes['TabularSectionRow'][$field]
|
||||
if ($code) { return "$prefix/$code" }
|
||||
return $null
|
||||
}
|
||||
# Таблица на реквизите формы: реквизиты формы не заимствуются никогда — только код
|
||||
# (текст платформа отвергает и с заимствованным основным реквизитом, оракул 8.3.27)
|
||||
if ($ctx.FormAttrIds.ContainsKey($tp) -and $ctx.FormAttrCols.ContainsKey("$tp.$field")) {
|
||||
return "$prefix/$($ctx.FormAttrCols["$tp.$field"])"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Путь от основного реквизита формы
|
||||
if ($main -and $path.StartsWith("$main.", [StringComparison]::Ordinal)) {
|
||||
$rest = $path.Substring($main.Length + 1)
|
||||
if ($ctx.MainBorrowed) {
|
||||
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
|
||||
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
|
||||
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
|
||||
if ($ctx.AttrUuids.ContainsKey($rest)) { return $path }
|
||||
return $ctx.MainId
|
||||
}
|
||||
$segs = $rest.Split('.')
|
||||
if ($segs.Count -eq 1) {
|
||||
if ($ctx.AttrUuids.ContainsKey($rest)) { return "$($ctx.MainId)/0:$($ctx.AttrUuids[$rest])" }
|
||||
$codes = if ($ctx.MainKind) { $script:stdFieldCodes[$ctx.MainKind] } else { $null }
|
||||
if ($codes -and $codes.ContainsKey($rest)) { return "$($ctx.MainId)/$($codes[$rest])" }
|
||||
return $null
|
||||
}
|
||||
if ($segs.Count -eq 2 -and $ctx.AttrUuids.ContainsKey($segs[0]) -and $ctx.AttrUuids.ContainsKey($rest)) {
|
||||
return "$($ctx.MainId)/0:$($ctx.AttrUuids[$segs[0]])/0:$($ctx.AttrUuids[$rest])"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Путь от реквизита формы — только по id исходной формы, в обоих режимах
|
||||
$segs = $path.Split('.')
|
||||
if ($ctx.FormAttrIds.ContainsKey($segs[0])) {
|
||||
$attrId = $ctx.FormAttrIds[$segs[0]]
|
||||
if ($segs.Count -eq 1) { return $attrId }
|
||||
if ($segs.Count -eq 2 -and $ctx.FormAttrCols.ContainsKey($path)) { return "$attrId/$($ctx.FormAttrCols[$path])" }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Прочее: с заимствованным основным реквизитом текст может разрешиться, без него — нет
|
||||
if ($ctx.MainBorrowed) { return $path }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Путь в <xr:DataPath> блока связи → разрешённый; неразрешимый блок вырезается целиком
|
||||
function Rewrite-LinkBlocks {
|
||||
param([string]$xml, [string]$blockPattern, $ctx)
|
||||
|
||||
return [regex]::Replace($xml, $blockPattern, {
|
||||
param($m)
|
||||
$link = $m.Value
|
||||
$dp = [regex]::Match($link, '<xr:DataPath[^>]*>([^<]+)</xr:DataPath>')
|
||||
if (-not $dp.Success) { return $link }
|
||||
$path = $dp.Groups[1].Value
|
||||
|
||||
# Путь на основной реквизит формы
|
||||
if ($mainPat -and $path -match "^${mainPat}\.(.+)$") {
|
||||
$attrName = $Matches[1]
|
||||
if ($mainAttrBorrowed) {
|
||||
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
|
||||
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
|
||||
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
|
||||
if ($attrUuids.ContainsKey($attrName)) { return $link }
|
||||
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}`${2}")
|
||||
}
|
||||
if ($attrUuids.ContainsKey($attrName)) {
|
||||
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}/0:$($attrUuids[$attrName])`${2}")
|
||||
}
|
||||
$resolved = Resolve-LinkDataPath $path $ctx
|
||||
if ($null -eq $resolved) {
|
||||
$script:droppedLinks += $path
|
||||
return ''
|
||||
}
|
||||
|
||||
# Односегментный путь на реквизит формы — только по id исходной формы
|
||||
if ($path -notmatch '\.' -and $formAttrIds.ContainsKey($path)) {
|
||||
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}$($formAttrIds[$path])`${2}")
|
||||
}
|
||||
|
||||
# Уже непрозрачный путь (форма-источник сама из расширения) — не трогаем
|
||||
if ($path -match '^\d') { return $link }
|
||||
|
||||
# С заимствованным основным реквизитом текстовый путь разрешается: элементы формы на месте,
|
||||
# а их данные доступны через основной реквизит. Конфигуратор такие пути и оставляет текстом
|
||||
# (эталон Issue66Example7_1: «Items.Товары.CurrentData.Характеристика» перенесён как есть).
|
||||
if ($mainAttrBorrowed) { return $link }
|
||||
|
||||
# Прочее текстом не разрешается: платформа отвергает загрузку «Неверный путь к полю».
|
||||
# Сюда попадают «Items.<Элемент>.CurrentData.<Поле>» — их кодировка непрозрачна и по
|
||||
# имеющимся эталонам не воспроизводима. Связь параметров выбора — удобство подбора, а не
|
||||
# данные: без неё форма заимствуется и работает, с ней — не грузится вовсе.
|
||||
$script:droppedLinks += $path
|
||||
return ''
|
||||
if ($resolved -ceq $path) { return $link }
|
||||
return $link.Substring(0, $dp.Groups[1].Index) + $resolved + $link.Substring($dp.Groups[1].Index + $dp.Groups[1].Length)
|
||||
})
|
||||
}
|
||||
|
||||
function Rewrite-ChoiceParameterLinks {
|
||||
param([string]$xml, $ctx)
|
||||
|
||||
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
|
||||
$xml = Rewrite-LinkBlocks $xml '(?s)\s*<xr:Link>.*?</xr:Link>' $ctx
|
||||
# Опустевший контейнер платформе не нужен
|
||||
$xml = [regex]::Replace($xml, '(?s)\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '')
|
||||
return $xml
|
||||
}
|
||||
|
||||
# Связь по типу — тот же путь и то же правило (эталоны УТ ВводОстатков.ФормаРасчетыМеждуОрганизациями:
|
||||
# без основного реквизита — код, с ним — текст)
|
||||
function Rewrite-TypeLinks {
|
||||
param([string]$xml, $ctx)
|
||||
|
||||
if ($xml -notmatch '<TypeLink>') { return $xml }
|
||||
return Rewrite-LinkBlocks $xml '(?s)\s*<TypeLink>.*?</TypeLink>' $ctx
|
||||
}
|
||||
|
||||
# Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
|
||||
# заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
|
||||
# у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
|
||||
@@ -689,8 +787,8 @@ function Resolve-ModuleKinds {
|
||||
}
|
||||
|
||||
# --- 10. Helper: read source object XML ---
|
||||
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
||||
# Имена реквизитов исходного объекта → uuid: реквизиты, ТЧ, измерения и ресурсы регистра, реквизиты
|
||||
# ТЧ — ключом «ТЧ.Реквизит». Нужны для кода пути в связях формы (см. Resolve-LinkDataPath).
|
||||
function Get-SourceAttributeUuids {
|
||||
param([string]$typeName, [string]$objName)
|
||||
|
||||
@@ -712,10 +810,18 @@ function Get-SourceAttributeUuids {
|
||||
if (-not $childObjects) { return $result }
|
||||
foreach ($child in $childObjects.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -notin @('Attribute','TabularSection')) { continue }
|
||||
if ($child.LocalName -notin @('Attribute','TabularSection','Dimension','Resource')) { continue }
|
||||
$uuid = $child.GetAttribute("uuid")
|
||||
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||
if ($uuid -and $nameNode) { $result[$nameNode.InnerText.Trim()] = $uuid }
|
||||
if (-not ($uuid -and $nameNode)) { continue }
|
||||
$name = $nameNode.InnerText.Trim()
|
||||
$result[$name] = $uuid
|
||||
if ($child.LocalName -ne 'TabularSection') { continue }
|
||||
foreach ($tsAttr in @($child.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']"))) {
|
||||
$tsUuid = $tsAttr.GetAttribute("uuid")
|
||||
$tsName = $tsAttr.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||
if ($tsUuid -and $tsName) { $result["$name.$($tsName.InnerText.Trim())"] = $tsUuid }
|
||||
}
|
||||
}
|
||||
return $result
|
||||
}
|
||||
@@ -955,12 +1061,10 @@ function Borrow-Form {
|
||||
Warn " У формы нет основного реквизита — -BorrowMainAttribute проигнорирован"
|
||||
}
|
||||
|
||||
# uuid реквизитов объекта нужны ровно там, где основной реквизит НЕ попал в форму:
|
||||
# только тогда путь «<основной>.X» переводится в непрозрачный вид
|
||||
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — чтобы построить
|
||||
# непрозрачный путь, с заимствованием — чтобы отличить реквизит (разрешается текстом) от
|
||||
# стандартного поля (не разрешается)
|
||||
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — для кода пути в связях
|
||||
# формы, с заимствованием — чтобы отличить реквизит (разрешается текстом) от стандартного поля
|
||||
$srcAttrUuids = Get-SourceAttributeUuids $typeName $objName
|
||||
$linkCtx = Get-LinkContext $srcFormEl $srcMainAttrName ([bool]$mainAttrInfo) $srcAttrUuids $formAttrIds
|
||||
|
||||
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
||||
$autoCmdXml = ""
|
||||
@@ -975,7 +1079,7 @@ function Borrow-Form {
|
||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<CommandSet/>', '')
|
||||
# Strip data-binding tags whose root attribute isn't borrowed
|
||||
$autoCmdXml = Strip-FormBindings $autoCmdXml $mainAttrName
|
||||
$autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
|
||||
$autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $linkCtx
|
||||
}
|
||||
|
||||
# Картинка декорации в заимствованную форму не переносится: Конфигуратор выбрасывает <Picture>
|
||||
@@ -1002,12 +1106,11 @@ function Borrow-Form {
|
||||
# Strip data-binding tags whose root attribute isn't borrowed
|
||||
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||
$childItemsXml = Strip-FormBindings $childItemsXml $mainAttrName
|
||||
$childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
|
||||
$childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $linkCtx
|
||||
# Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше
|
||||
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
|
||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<CommandSet/>', '')
|
||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '')
|
||||
$childItemsXml = Rewrite-TypeLinks $childItemsXml $linkCtx
|
||||
# Strip element-level Events (base form handlers not in extension)
|
||||
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<Events>.*?</Events>', '')
|
||||
|
||||
@@ -1277,7 +1380,7 @@ function Borrow-Form {
|
||||
Info " Created: $formXmlFile"
|
||||
if ($script:droppedLinks.Count -gt 0) {
|
||||
$uniq = @($script:droppedLinks | Sort-Object -Unique)
|
||||
Warn " Вырезано связей параметров выбора: $($uniq.Count) — путь не разрешается в расширении: $($uniq -join ', ')"
|
||||
Warn " Вырезано связей формы: $($uniq.Count) — путь не разрешается в расширении: $($uniq -join ', ')"
|
||||
$script:droppedLinks = @()
|
||||
}
|
||||
|
||||
@@ -1572,6 +1675,54 @@ function Get-FormAttributeIds {
|
||||
return $result
|
||||
}
|
||||
|
||||
# Контекст разрешения путей в связях формы (см. Resolve-LinkDataPath) — всё по ИСХОДНОЙ форме:
|
||||
# колонки реквизитов формы, колонки AdditionalColumns, элементы-таблицы и вид основного реквизита.
|
||||
function Get-LinkContext {
|
||||
param($formEl, [string]$mainName, [bool]$mainBorrowed, $attrUuids, $formAttrIds)
|
||||
|
||||
$cols = @{}; $addCols = @{}; $tables = @{}; $kind = $null
|
||||
$attrs = $formEl.SelectSingleNode("*[local-name()='Attributes']")
|
||||
if ($attrs) {
|
||||
foreach ($a in $attrs.ChildNodes) {
|
||||
if ($a.NodeType -ne 'Element' -or $a.LocalName -ne 'Attribute') { continue }
|
||||
$nm = $a.GetAttribute("name")
|
||||
$columns = $a.SelectSingleNode("*[local-name()='Columns']")
|
||||
if ($columns) {
|
||||
foreach ($c in $columns.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
if ($c.LocalName -eq 'Column') {
|
||||
$cols["$nm.$($c.GetAttribute('name'))"] = $c.GetAttribute('id')
|
||||
} elseif ($c.LocalName -eq 'AdditionalColumns') {
|
||||
$tblPath = $c.GetAttribute('table')
|
||||
foreach ($ac in @($c.SelectNodes("*[local-name()='Column']"))) {
|
||||
$addCols["$tblPath.$($ac.GetAttribute('name'))"] = $ac.GetAttribute('id')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($mainName -and $nm -ceq $mainName) {
|
||||
$typeNode = $a.SelectSingleNode("*[local-name()='Type']/*[local-name()='Type']")
|
||||
if ($typeNode) {
|
||||
$tm = [regex]::Match($typeNode.InnerText.Trim(), '^(?:\w+:)?(\w+)\.')
|
||||
if ($tm.Success -and $script:mainAttrKinds.ContainsKey($tm.Groups[1].Value)) { $kind = $script:mainAttrKinds[$tm.Groups[1].Value] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$items = $formEl.SelectSingleNode("*[local-name()='ChildItems']")
|
||||
if ($items) {
|
||||
foreach ($t in @($items.SelectNodes(".//*[local-name()='Table']"))) {
|
||||
$dp = $t.SelectSingleNode("*[local-name()='DataPath']")
|
||||
if ($dp) { $tables[$t.GetAttribute('name')] = @{ Id = $t.GetAttribute('id'); Path = $dp.InnerText.Trim() } }
|
||||
}
|
||||
}
|
||||
$mainId = if ($mainName -and $formAttrIds.ContainsKey($mainName)) { $formAttrIds[$mainName] } else { "1" }
|
||||
return @{
|
||||
MainName = $mainName; MainId = $mainId; MainBorrowed = $mainBorrowed; MainKind = $kind
|
||||
AttrUuids = $attrUuids; FormAttrIds = $formAttrIds; FormAttrCols = $cols; AddCols = $addCols; Tables = $tables
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MainAttributeInfo {
|
||||
param($formEl, [string]$nsStripPattern)
|
||||
|
||||
@@ -1861,7 +2012,8 @@ function Build-AdoptedTabularSectionXml {
|
||||
function Collect-ReferenceTypes {
|
||||
param([string[]]$typeXmls)
|
||||
|
||||
$result = @{}
|
||||
# Порядок первого упоминания: от него зависит порядок заимствования в ChildObjects
|
||||
$result = [ordered]@{}
|
||||
foreach ($typeXml in $typeXmls) {
|
||||
# cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc.
|
||||
$refMatches = [regex]::Matches($typeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||
@@ -1869,7 +2021,7 @@ function Collect-ReferenceTypes {
|
||||
$refPrefix = $m.Groups[1].Value # e.g. "Catalog", "Enum", "Document"
|
||||
$objName = $m.Groups[2].Value
|
||||
$key = "${refPrefix}.${objName}"
|
||||
if (-not $result.ContainsKey($key)) {
|
||||
if (-not $result.Contains($key)) {
|
||||
$result[$key] = @{ TypeName = $refPrefix; ObjName = $objName }
|
||||
}
|
||||
}
|
||||
@@ -1878,7 +2030,7 @@ function Collect-ReferenceTypes {
|
||||
foreach ($m in $dtMatches) {
|
||||
$dtName = $m.Groups[1].Value
|
||||
$key = "DefinedType.${dtName}"
|
||||
if (-not $result.ContainsKey($key)) {
|
||||
if (-not $result.Contains($key)) {
|
||||
$result[$key] = @{ TypeName = "DefinedType"; ObjName = $dtName }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-borrow v1.40 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.41 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -99,78 +99,173 @@ def indent_for_base_form(xml, first_indent):
|
||||
|
||||
DROPPED_LINKS = []
|
||||
|
||||
# Связи формы — параметров выбора (<ChoiceParameterLinks>/<xr:Link>) и по типу (<TypeLink>) —
|
||||
# хранят путь в <xr:DataPath>, обычным стриппингом он не снимается. Путь — внутренняя ссылка
|
||||
# платформы: в выгрузке она пишет имя, а если имя в контексте формы не разрешается — код.
|
||||
# Текстовое имя в расширении разрешается, только если его корень объявлен в <Attributes> самой
|
||||
# заимствованной формы; иначе платформа отвергает загрузку — «Неверный путь к полю - X».
|
||||
# Конфигуратор поэтому пишет текст там, где он разрешается, и код — где нет.
|
||||
# Грамматика кода (оракул «код в форму конфигурации → загрузка → выгрузка → имя», 8.3.27; эталоны
|
||||
# Конфигуратора УТ и Issue66Example4-7, JR2433, JR2976, JR49904):
|
||||
# первый сегмент <id> — реквизит ИСХОДНОЙ формы (в расширении реквизиты
|
||||
# перенумерованы в 1000000+, ссылка остаётся в нумерации базы);
|
||||
# <id>:<CurrentData> — элемент-таблица исходной формы, её текущая строка;
|
||||
# дальше 0:<uuid> — реквизит/ТЧ/измерение/ресурс метаданных источника;
|
||||
# <id> — колонка реквизита формы;
|
||||
# <id>:<AddColumn> — колонка AdditionalColumns;
|
||||
# -<k> — стандартный реквизит, код по виду объекта (STD_FIELD_CODES).
|
||||
# В расширении платформа код обратно в имя не переводит, а висячий код грузит молча — ошибка
|
||||
# кодирования не упадёт, а тихо не сработает. Поэтому кодируется только измеренное; прочее
|
||||
# вырезается с предупреждением: связь — удобство подбора, без неё форма работает, с неверной
|
||||
# текстовой — не грузится вовсе.
|
||||
LINK_CURRENT_DATA = '02023637-7868-4a5f-8576-835a76e0c9ba'
|
||||
LINK_ADD_COLUMN = '5bdad865-f2c5-434b-8041-ba4aad3b6687'
|
||||
# Коды стандартных реквизитов — свой порядок у каждого вида (оракул 8.3.27; в XML объекта
|
||||
# полного списка нет). Незамеренное (-1, скрытые поля, колонки ExtDimensionTypes) — не кодируем.
|
||||
STD_FIELD_CODES = {
|
||||
'Catalog': {'Code': -2, 'Description': -3, 'Parent': -4, 'Owner': -5, 'IsFolder': -6, 'DeletionMark': -7, 'Ref': -8, 'DataVersion': -9, 'Predefined': -10, 'PredefinedDataName': -13},
|
||||
'Document': {'Number': -2, 'Date': -3, 'DeletionMark': -4, 'Ref': -5, 'DataVersion': -6, 'Posted': -7, 'RegisterRecords': -8},
|
||||
'ChartOfCharacteristicTypes': {'Ref': -2, 'DataVersion': -3, 'DeletionMark': -4, 'Predefined': -5, 'Parent': -6, 'IsFolder': -7, 'Code': -8, 'Description': -9, 'ValueType': -11, 'PredefinedDataName': -14},
|
||||
'ChartOfAccounts': {'Ref': -2, 'DataVersion': -3, 'DeletionMark': -4, 'Predefined': -5, 'Parent': -6, 'Code': -7, 'Description': -8, 'Type': -10, 'OffBalance': -11, 'ExtDimensionTypes': -12, 'Order': -17, 'PredefinedDataName': -28},
|
||||
'ChartOfCalculationTypes': {'Code': -2, 'Description': -3, 'ActionPeriodIsBasic': -4, 'DeletionMark': -5, 'Ref': -6, 'DataVersion': -7, 'Predefined': -8, 'PredefinedDataName': -11, 'DisplacingCalculationTypes': -20, 'LeadingCalculationTypes': -30},
|
||||
'ExchangePlan': {'Code': -2, 'Description': -3, 'DeletionMark': -4, 'Ref': -6, 'DataVersion': -7, 'SentNo': -9, 'ReceivedNo': -10, 'ThisNode': -13, 'ExchangeDate': -14},
|
||||
'BusinessProcess': {'Number': -2, 'Date': -3, 'DeletionMark': -4, 'Ref': -5, 'DataVersion': -6, 'Completed': -7, 'HeadTask': -8, 'Started': -9},
|
||||
'Task': {'Number': -2, 'Date': -3, 'DeletionMark': -4, 'Ref': -5, 'DataVersion': -6, 'BusinessProcess': -7, 'RoutePoint': -8, 'Description': -9, 'Executed': -10},
|
||||
'InformationRegisterRecord': {'Period': -2, 'Recorder': -3, 'LineNumber': -4, 'Active': -5},
|
||||
'AccumulationRegisterRow': {'Period': -2, 'Recorder': -3, 'LineNumber': -4, 'Active': -5, 'RecordType': -9, 'SecondPeriod': -10, 'MinutePeriod': -11, 'HourPeriod': -12, 'DayPeriod': -13, 'WeekPeriod': -14, 'TenDaysPeriod': -15},
|
||||
'TabularSectionRow': {'LineNumber': -2},
|
||||
}
|
||||
# Тип основного реквизита → вид для таблицы стандартных кодов (запись РС — сам реквизит,
|
||||
# набор записей РН — строка таблицы на нём)
|
||||
MAIN_ATTR_KINDS = {
|
||||
'CatalogObject': 'Catalog', 'DocumentObject': 'Document', 'ChartOfCharacteristicTypesObject': 'ChartOfCharacteristicTypes',
|
||||
'ChartOfAccountsObject': 'ChartOfAccounts', 'ChartOfCalculationTypesObject': 'ChartOfCalculationTypes',
|
||||
'ExchangePlanObject': 'ExchangePlan', 'BusinessProcessObject': 'BusinessProcess', 'TaskObject': 'Task',
|
||||
'InformationRegisterRecordManager': 'InformationRegisterRecord', 'AccumulationRegisterRecordSet': 'AccumulationRegisterRow',
|
||||
}
|
||||
|
||||
def rewrite_choice_parameter_links(xml, attr_uuids, form_attr_ids, main_attr_name, main_attr_borrowed):
|
||||
"""Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
|
||||
в <xr:DataPath> и обычным стриппингом не снимается. Текстовое имя в расширении разрешается только
|
||||
если его корень объявлен в <Attributes> самой заимствованной формы; иначе платформа отвергает
|
||||
загрузку — «Неверный путь к полю - X». Реквизиты формы не заимствуются никогда, поэтому ссылка на
|
||||
них разрешима только через id: Конфигуратор подставляет id реквизита ИСХОДНОЙ формы (эталоны
|
||||
Issue66Example4/5/6, JR2433, JR2976, JR49904 — совпадение на шести расширениях). Именно id
|
||||
исходной, а не заимствованной: при заимствовании реквизиты перенумеровываются в 1000000+, а
|
||||
ссылка продолжает указывать в нумерацию базовой формы.
|
||||
Путь на основной реквизит («Объект.X») при заимствованном основном реквизите разрешается текстом
|
||||
и остаётся читаемым; без заимствования переводится в «<id>/0:<uuid реквизита объекта>».
|
||||
Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком."""
|
||||
if '<ChoiceParameterLinks>' not in xml:
|
||||
return xml
|
||||
|
||||
main_pat = re.escape(main_attr_name) if main_attr_name else None
|
||||
main_id = form_attr_ids.get(main_attr_name, "1") if main_attr_name else "1"
|
||||
def resolve_link_data_path(path, ctx):
|
||||
"""Путь связи → текст (разрешается в расширении), код или None (вырезать).
|
||||
ctx: MainName, MainId, MainBorrowed, MainKind, AttrUuids (имя/«ТЧ.Реквизит» → uuid),
|
||||
FormAttrIds, FormAttrCols («Реквизит.Колонка» → id), AddCols («<путь таблицы>.Колонка» → id),
|
||||
Tables (имя элемента-таблицы → {Id, Path})."""
|
||||
# Уже код (форма-источник сама из расширения, или висячая ссылка в типовой) — как есть
|
||||
if re.match(r'^-?\d', path):
|
||||
return path
|
||||
main = ctx['MainName']
|
||||
attr_uuids = ctx['AttrUuids']
|
||||
|
||||
# Текущая строка элемента-таблицы
|
||||
cd = re.match(r'^Items\.([^.]+)\.CurrentData\.([^.]+)$', path)
|
||||
if cd:
|
||||
tbl = ctx['Tables'].get(cd.group(1))
|
||||
field = cd.group(2)
|
||||
if not tbl:
|
||||
return None
|
||||
tp = tbl['Path']
|
||||
prefix = f"{tbl['Id']}:{LINK_CURRENT_DATA}"
|
||||
if main and (tp == main or tp.startswith(main + '.')):
|
||||
# Таблица на основном реквизите: с заимствованным — текст разрешается (эталоны 7_1, УТ)
|
||||
if ctx['MainBorrowed']:
|
||||
return path
|
||||
if tp == main:
|
||||
# Строки самого основного реквизита — набор записей регистра накопления
|
||||
if ctx['MainKind'] != 'AccumulationRegisterRow':
|
||||
return None
|
||||
if field in attr_uuids:
|
||||
return f"{prefix}/0:{attr_uuids[field]}"
|
||||
code = STD_FIELD_CODES['AccumulationRegisterRow'].get(field)
|
||||
return f"{prefix}/{code}" if code else None
|
||||
ts = tp[len(main) + 1:]
|
||||
if '.' in ts:
|
||||
return None
|
||||
if f"{ts}.{field}" in attr_uuids:
|
||||
return f"{prefix}/0:{attr_uuids[ts + '.' + field]}"
|
||||
if f"{tp}.{field}" in ctx['AddCols']:
|
||||
return f"{prefix}/{ctx['AddCols'][tp + '.' + field]}:{LINK_ADD_COLUMN}"
|
||||
code = STD_FIELD_CODES['TabularSectionRow'].get(field)
|
||||
return f"{prefix}/{code}" if code else None
|
||||
# Таблица на реквизите формы: реквизиты формы не заимствуются никогда — только код
|
||||
# (текст платформа отвергает и с заимствованным основным реквизитом, оракул 8.3.27)
|
||||
if tp in ctx['FormAttrIds'] and f"{tp}.{field}" in ctx['FormAttrCols']:
|
||||
return f"{prefix}/{ctx['FormAttrCols'][tp + '.' + field]}"
|
||||
return None
|
||||
|
||||
# Путь от основного реквизита формы
|
||||
if main and path.startswith(main + '.'):
|
||||
rest = path[len(main) + 1:]
|
||||
if ctx['MainBorrowed']:
|
||||
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
|
||||
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
|
||||
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
|
||||
if rest in attr_uuids:
|
||||
return path
|
||||
return ctx['MainId']
|
||||
segs = rest.split('.')
|
||||
if len(segs) == 1:
|
||||
if rest in attr_uuids:
|
||||
return f"{ctx['MainId']}/0:{attr_uuids[rest]}"
|
||||
codes = STD_FIELD_CODES.get(ctx['MainKind']) if ctx['MainKind'] else None
|
||||
if codes and rest in codes:
|
||||
return f"{ctx['MainId']}/{codes[rest]}"
|
||||
return None
|
||||
if len(segs) == 2 and segs[0] in attr_uuids and rest in attr_uuids:
|
||||
return f"{ctx['MainId']}/0:{attr_uuids[segs[0]]}/0:{attr_uuids[rest]}"
|
||||
return None
|
||||
|
||||
# Путь от реквизита формы — только по id исходной формы, в обоих режимах
|
||||
segs = path.split('.')
|
||||
if segs[0] in ctx['FormAttrIds']:
|
||||
attr_id = ctx['FormAttrIds'][segs[0]]
|
||||
if len(segs) == 1:
|
||||
return attr_id
|
||||
if len(segs) == 2 and path in ctx['FormAttrCols']:
|
||||
return f"{attr_id}/{ctx['FormAttrCols'][path]}"
|
||||
return None
|
||||
|
||||
# Прочее: с заимствованным основным реквизитом текст может разрешиться, без него — нет
|
||||
if ctx['MainBorrowed']:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def rewrite_link_blocks(xml, block_pattern, ctx):
|
||||
"""Путь в <xr:DataPath> блока связи → разрешённый; неразрешимый блок вырезается целиком."""
|
||||
def repl(m):
|
||||
link = m.group(0)
|
||||
dp = re.search(r'<xr:DataPath[^>]*>([^<]+)</xr:DataPath>', link)
|
||||
if not dp:
|
||||
return link
|
||||
path = dp.group(1)
|
||||
|
||||
# Путь на основной реквизит формы
|
||||
if main_pat:
|
||||
mm = re.match('^' + main_pat + r'\.(.+)$', path)
|
||||
if mm:
|
||||
attr_name = mm.group(1)
|
||||
if main_attr_borrowed:
|
||||
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
|
||||
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
|
||||
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
|
||||
if attr_name in attr_uuids:
|
||||
return link
|
||||
return re.sub(r'(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)',
|
||||
lambda x: f"{x.group(1)}{main_id}{x.group(2)}", link)
|
||||
if attr_name in attr_uuids:
|
||||
return re.sub(r'(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)',
|
||||
lambda x: f"{x.group(1)}{main_id}/0:{attr_uuids[attr_name]}{x.group(2)}", link)
|
||||
return ''
|
||||
|
||||
# Односегментный путь на реквизит формы — только по id исходной формы
|
||||
if '.' not in path and path in form_attr_ids:
|
||||
return re.sub(r'(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)',
|
||||
lambda x: f"{x.group(1)}{form_attr_ids[path]}{x.group(2)}", link)
|
||||
|
||||
# Уже непрозрачный путь (форма-источник сама из расширения) — не трогаем
|
||||
if re.match(r'^\d', path):
|
||||
resolved = resolve_link_data_path(path, ctx)
|
||||
if resolved is None:
|
||||
DROPPED_LINKS.append(path)
|
||||
return ''
|
||||
if resolved == path:
|
||||
return link
|
||||
return link[:dp.start(1)] + resolved + link[dp.end(1):]
|
||||
|
||||
# С заимствованным основным реквизитом текстовый путь разрешается: элементы формы на месте,
|
||||
# а их данные доступны через основной реквизит. Конфигуратор такие пути и оставляет текстом
|
||||
# (эталон Issue66Example7_1: «Items.Товары.CurrentData.Характеристика» перенесён как есть).
|
||||
if main_attr_borrowed:
|
||||
return link
|
||||
return re.sub(block_pattern, repl, xml, flags=re.DOTALL)
|
||||
|
||||
# Прочее текстом не разрешается: платформа отвергает загрузку «Неверный путь к полю».
|
||||
# Сюда попадают «Items.<Элемент>.CurrentData.<Поле>» — их кодировка непрозрачна и по
|
||||
# имеющимся эталонам не воспроизводима. Связь параметров выбора — удобство подбора, а не
|
||||
# данные: без неё форма заимствуется и работает, с ней — не грузится вовсе.
|
||||
DROPPED_LINKS.append(path)
|
||||
return ''
|
||||
|
||||
xml = re.sub(r'\s*<xr:Link>.*?</xr:Link>', repl, xml, flags=re.DOTALL)
|
||||
def rewrite_choice_parameter_links(xml, ctx):
|
||||
if '<ChoiceParameterLinks>' not in xml:
|
||||
return xml
|
||||
xml = rewrite_link_blocks(xml, r'\s*<xr:Link>.*?</xr:Link>', ctx)
|
||||
# Опустевший контейнер платформе не нужен
|
||||
xml = re.sub(r'\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '', xml, flags=re.DOTALL)
|
||||
return xml
|
||||
|
||||
|
||||
def rewrite_type_links(xml, ctx):
|
||||
"""Связь по типу — тот же путь и то же правило (эталоны УТ ВводОстатков.ФормаРасчетыМеждуОрганизациями:
|
||||
без основного реквизита — код, с ним — текст)."""
|
||||
if '<TypeLink>' not in xml:
|
||||
return xml
|
||||
return rewrite_link_blocks(xml, r'\s*<TypeLink>.*?</TypeLink>', ctx)
|
||||
|
||||
|
||||
def get_own_child_object_names(obj_file):
|
||||
"""Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
|
||||
заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
|
||||
@@ -931,8 +1026,8 @@ def main():
|
||||
|
||||
# --- Helper functions ---
|
||||
def get_source_attribute_uuids(type_name, obj_name):
|
||||
"""Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||
параметров выбора (см. rewrite_choice_parameter_links)."""
|
||||
"""Имена реквизитов исходного объекта → uuid: реквизиты, ТЧ, измерения и ресурсы регистра,
|
||||
реквизиты ТЧ — ключом «ТЧ.Реквизит». Нужны для кода пути в связях формы (см. resolve_link_data_path)."""
|
||||
result = {}
|
||||
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||
if not dir_name:
|
||||
@@ -953,22 +1048,37 @@ def main():
|
||||
if not isinstance(child.tag, str) or localname(child) != "ChildObjects":
|
||||
continue
|
||||
for sub in child:
|
||||
if not isinstance(sub.tag, str) or localname(sub) not in ("Attribute", "TabularSection"):
|
||||
if not isinstance(sub.tag, str) or localname(sub) not in ("Attribute", "TabularSection", "Dimension", "Resource"):
|
||||
continue
|
||||
uuid_val = sub.get("uuid")
|
||||
name_val = None
|
||||
for props in sub:
|
||||
if isinstance(props.tag, str) and localname(props) == "Properties":
|
||||
for prop in props:
|
||||
if isinstance(prop.tag, str) and localname(prop) == "Name":
|
||||
name_val = (prop.text or "").strip()
|
||||
break
|
||||
break
|
||||
if uuid_val and name_val:
|
||||
result[name_val] = uuid_val
|
||||
name_val = md_name(sub)
|
||||
if not (uuid_val and name_val):
|
||||
continue
|
||||
result[name_val] = uuid_val
|
||||
if localname(sub) != "TabularSection":
|
||||
continue
|
||||
for ts_children in sub:
|
||||
if not isinstance(ts_children.tag, str) or localname(ts_children) != "ChildObjects":
|
||||
continue
|
||||
for ts_attr in ts_children:
|
||||
if not isinstance(ts_attr.tag, str) or localname(ts_attr) != "Attribute":
|
||||
continue
|
||||
ts_uuid, ts_name = ts_attr.get("uuid"), md_name(ts_attr)
|
||||
if ts_uuid and ts_name:
|
||||
result[f"{name_val}.{ts_name}"] = ts_uuid
|
||||
break
|
||||
return result
|
||||
|
||||
def md_name(el):
|
||||
"""<Properties>/<Name> объекта метаданных."""
|
||||
for props in el:
|
||||
if isinstance(props.tag, str) and localname(props) == "Properties":
|
||||
for prop in props:
|
||||
if isinstance(prop.tag, str) and localname(prop) == "Name":
|
||||
return (prop.text or "").strip()
|
||||
break
|
||||
return None
|
||||
|
||||
def read_source_object(type_name, obj_name):
|
||||
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||
if not dir_name:
|
||||
@@ -1260,6 +1370,53 @@ def main():
|
||||
break
|
||||
return result
|
||||
|
||||
def get_link_context(form_el, main_name, main_borrowed, attr_uuids, form_attr_ids):
|
||||
"""Контекст разрешения путей в связях формы (см. resolve_link_data_path) — всё по ИСХОДНОЙ
|
||||
форме: колонки реквизитов формы, колонки AdditionalColumns, элементы-таблицы и вид основного
|
||||
реквизита."""
|
||||
cols, add_cols, tables, kind = {}, {}, {}, None
|
||||
for child in form_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if localname(child) == "Attributes":
|
||||
for a in child:
|
||||
if not isinstance(a.tag, str) or localname(a) != "Attribute":
|
||||
continue
|
||||
nm = a.get("name")
|
||||
for sub in a:
|
||||
if not isinstance(sub.tag, str):
|
||||
continue
|
||||
if localname(sub) == "Columns":
|
||||
for c in sub:
|
||||
if not isinstance(c.tag, str):
|
||||
continue
|
||||
if localname(c) == "Column":
|
||||
cols[f"{nm}.{c.get('name')}"] = c.get("id")
|
||||
elif localname(c) == "AdditionalColumns":
|
||||
tbl_path = c.get("table")
|
||||
for ac in c:
|
||||
if isinstance(ac.tag, str) and localname(ac) == "Column":
|
||||
add_cols[f"{tbl_path}.{ac.get('name')}"] = ac.get("id")
|
||||
elif localname(sub) == "Type" and main_name and nm == main_name and kind is None:
|
||||
for tn in sub:
|
||||
if isinstance(tn.tag, str) and localname(tn) == "Type":
|
||||
tm = re.match(r'^(?:\w+:)?(\w+)\.', (tn.text or "").strip())
|
||||
if tm and tm.group(1) in MAIN_ATTR_KINDS:
|
||||
kind = MAIN_ATTR_KINDS[tm.group(1)]
|
||||
break
|
||||
elif localname(child) == "ChildItems":
|
||||
for tbl in child.iter():
|
||||
if not isinstance(tbl.tag, str) or localname(tbl) != "Table":
|
||||
continue
|
||||
for dp in tbl:
|
||||
if isinstance(dp.tag, str) and localname(dp) == "DataPath":
|
||||
tables[tbl.get("name")] = {"Id": tbl.get("id"), "Path": (dp.text or "").strip()}
|
||||
break
|
||||
main_id = form_attr_ids.get(main_name, "1") if main_name else "1"
|
||||
return {"MainName": main_name, "MainId": main_id, "MainBorrowed": main_borrowed, "MainKind": kind,
|
||||
"AttrUuids": attr_uuids, "FormAttrIds": form_attr_ids, "FormAttrCols": cols,
|
||||
"AddCols": add_cols, "Tables": tables}
|
||||
|
||||
def get_main_attribute_info(form_el, ns_strip_pattern):
|
||||
main_attr = None
|
||||
for child in form_el:
|
||||
@@ -1920,10 +2077,11 @@ def main():
|
||||
# Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить
|
||||
# («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра)
|
||||
main_attr_info = src_main_info if borrow_main_attr else None
|
||||
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — чтобы построить
|
||||
# непрозрачный путь, с заимствованием — чтобы отличить реквизит (разрешается текстом) от
|
||||
# стандартного поля (не разрешается)
|
||||
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — для кода пути в связях
|
||||
# формы, с заимствованием — чтобы отличить реквизит (разрешается текстом) от стандартного поля
|
||||
src_attr_uuids = get_source_attribute_uuids(type_name, obj_name)
|
||||
link_ctx = get_link_context(src_form_el, src_main_attr_name, main_attr_info is not None,
|
||||
src_attr_uuids, form_attr_ids)
|
||||
main_attr_name = main_attr_info["Name"] if main_attr_info else ""
|
||||
if borrow_main_attr and main_attr_info is None:
|
||||
warn(" У формы нет основного реквизита — -BorrowMainAttribute проигнорирован")
|
||||
@@ -1941,8 +2099,7 @@ def main():
|
||||
auto_cmd_xml = re.sub(r'\s*<CommandSet/>', '', auto_cmd_xml)
|
||||
# Strip data-binding tags whose root attribute isn't borrowed
|
||||
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, main_attr_name)
|
||||
auto_cmd_xml = rewrite_choice_parameter_links(
|
||||
auto_cmd_xml, src_attr_uuids, form_attr_ids, src_main_attr_name, main_attr_info is not None)
|
||||
auto_cmd_xml = rewrite_choice_parameter_links(auto_cmd_xml, link_ctx)
|
||||
|
||||
# ChildItems: copy full tree, clean up base-config references
|
||||
child_items_xml = ""
|
||||
@@ -1975,13 +2132,11 @@ def main():
|
||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||
# Strip data-binding tags whose root attribute isn't borrowed
|
||||
child_items_xml = strip_form_bindings(child_items_xml, main_attr_name)
|
||||
child_items_xml = rewrite_choice_parameter_links(
|
||||
child_items_xml, src_attr_uuids, form_attr_ids, src_main_attr_name, main_attr_info is not None)
|
||||
child_items_xml = rewrite_choice_parameter_links(child_items_xml, link_ctx)
|
||||
# Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше
|
||||
child_items_xml = re.sub(r'(?s)\s*<CommandSet>.*?</CommandSet>', '', child_items_xml)
|
||||
child_items_xml = re.sub(r'\s*<CommandSet/>', '', child_items_xml)
|
||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||
child_items_xml = re.sub(r'\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '', child_items_xml, flags=re.DOTALL)
|
||||
child_items_xml = rewrite_type_links(child_items_xml, link_ctx)
|
||||
# Strip element-level Events
|
||||
child_items_xml = re.sub(r'\s*<Events>.*?</Events>', '', child_items_xml, flags=re.DOTALL)
|
||||
|
||||
@@ -2199,7 +2354,7 @@ def main():
|
||||
info(f" Created: {form_xml_file}")
|
||||
if DROPPED_LINKS:
|
||||
uniq = sorted(set(DROPPED_LINKS))
|
||||
warn(f" Вырезано связей параметров выбора: {len(uniq)} — путь не разрешается в расширении: {', '.join(uniq)}")
|
||||
warn(f" Вырезано связей формы: {len(uniq)} — путь не разрешается в расширении: {', '.join(uniq)}")
|
||||
DROPPED_LINKS.clear()
|
||||
|
||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||
|
||||
Reference in New Issue
Block a user