fix(meta-compile,meta-validate): канон стандартных реквизитов регистров

Справочник порядка StandardAttributes был неполон у трёх видов регистров, а
неизвестные имена эмитятся перед фикс-списком — платформа их вкрапляет, и
роундтрип переставал быть побайтовым.

Порядок снят с выгрузки платформы, присутствие условных реквизитов теперь
выводится из свойств объекта (periodAdjustmentLength, correspondence,
registerType), а не из наличия ключа в DSL: иначе регистр, создаваемый с нуля,
терял реквизит, который платформа обязана материализовать. Наличие ключа
осталось дополняющим условием, чтобы роундтрип не зависел от точности вывода.

- регистр расчёта: 11 реквизитов, состав безусловен (проверено матрицей
  actionPeriod × basePeriod × периодичность на 8.3.27)
- регистр бухгалтерии: PeriodAdjustment при длине периода корректировки > 0,
  RecordType при correspondence=false
- регистр накопления: RecordType только у регистра остатков

meta-validate: снято ложное предупреждение об «unexpected» RecordType у
бухрегистра, условные реквизиты исключены из проверки missing.

Проверка: 739 регистров по шести конфигурациям — 0 расхождений; снэпшоты
пересняты и верифицированы загрузкой в 1С.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-09 18:06:34 +03:00
co-authored by Claude Opus 5
parent 707a5c293f
commit 16199deb92
21 changed files with 1730 additions and 160 deletions
@@ -1,4 +1,4 @@
# meta-compile v1.92 — Compile 1C metadata object from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# meta-compile v1.93 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1311,20 +1311,20 @@ $script:standardAttributesByType = @{
"Document" = @("Posted","Ref","DeletionMark","Date","Number")
"Enum" = @("Order","Ref")
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period")
"AccountingRegister" = @("PeriodAdjustment","Account","Active","LineNumber","Recorder","Period")
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry")
"AccumulationRegister" = @("RecordType","Active","LineNumber","Recorder","Period")
"AccountingRegister" = @("PeriodAdjustment","Account","RecordType","Active","LineNumber","Recorder","Period")
"CalculationRegister" = @("RegistrationPeriod","ReversingEntry","Active","EndOfBasePeriod","BegOfBasePeriod","EndOfActionPeriod","BegOfActionPeriod","ActionPeriod","CalculationType","LineNumber","Recorder")
"ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref")
"ChartOfCharacteristicTypes" = @("PredefinedDataName","ValueType","Description","Code","IsFolder","Parent","Predefined","DeletionMark","Ref")
"ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code")
"BusinessProcess" = @("Started","HeadTask","Completed","Ref","DeletionMark","Date","Number")
"Task" = @("Executed","Description","RoutePoint","BusinessProcess","Ref","DeletionMark","Date","Number")
# Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
# Условные члены перечислены в $script:stdAttrOptionalони эмитятся только при наличии
# ключа в DSL, но позицию берут отсюда.
# Условные члены перечислены в $script:stdAttrConditionsпозицию они берут отсюда, а
# присутствие определяется свойствами объекта.
# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
# У бухрегистра PeriodAdjustment, наоборот, условен (1 из 3) — он приходит как «лишний»
# ключ и эмитится ПЕРЕД фикс-списком, что совпадает с платформой.
# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
"ExchangePlan" = @("ThisNode","ReceivedNo","SentNo","Ref","DeletionMark","Description","Code")
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
}
@@ -1445,10 +1445,17 @@ function Emit-StandardAttribute {
# Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ.
$script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')
# Условные члены списка типа: позиция берётся из $script:standardAttributesByType, а сам
# реквизит эмитится только при наличии ключа в DSL (у бухрегистра PeriodAdjustment — 2 из 5).
$script:stdAttrOptional = @{
"AccountingRegister" = @("PeriodAdjustment")
# Условные члены списка типа: позиция берётся из $script:standardAttributesByType, а присутствие —
# из свойств самого объекта, как у платформы. Предикат ОБЯЗАН принимать определение параметром:
# scriptblock не видит $def вызывающей функции, и отказ был бы молчаливым.
$script:stdAttrConditions = @{
"AccountingRegister" = @{
"PeriodAdjustment" = { param($d) $v = 0; if ($null -ne $d.periodAdjustmentLength) { $v = [int]"$($d.periodAdjustmentLength)" }; $v -gt 0 }
"RecordType" = { param($d) -not ($d.correspondence -eq $true) }
}
"AccumulationRegister" = @{
"RecordType" = { param($d) $raw = if ($d.registerType) { "$($d.registerType)" } else { "Balance" }; (Normalize-EnumValue "RegisterType" $raw) -eq "Balance" }
}
}
# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
@@ -1469,9 +1476,9 @@ function Emit-StandardAttributes {
$profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} }
# Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
# их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
# перед Account, а ExtDimension1..3/ExtDimensionType1..3 — после Period), поэтому
# «условные скопом вперёд» не выражает канон. Условный эмитим по факту наличия в DSL.
$optional = $script:stdAttrOptional[$objectType]; if (-not $optional) { $optional = @() }
# перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
# поэтому «условные скопом вперёд» не выражает канон.
$cond = $script:stdAttrConditions[$objectType]
# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
@@ -1487,7 +1494,12 @@ function Emit-StandardAttributes {
$tail = @($tail | Sort-Object @{e={[int]([regex]::Match($_, '\d+').Value)}}, @{e={ if ($_ -match 'Type\d+$') { 1 } else { 0 } }})
X "$indent<StandardAttributes>"
foreach ($a in ($extra + $attrs + $tail)) {
if (($optional -contains $a) -and (-not ($sa -and $sa.PSObject.Properties[$a]))) { continue }
# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
if ($cond -and $cond.ContainsKey($a)) {
$present = ($sa -and $sa.PSObject.Properties[$a]) -or (& $cond[$a] $def)
if (-not $present) { continue }
}
$ov = @{}
if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } }
if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.92 — Compile 1C metadata object from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# meta-compile v1.93 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -1350,16 +1350,18 @@ standard_attributes_by_type = {
'Document': ['Posted', 'Ref', 'DeletionMark', 'Date', 'Number'],
'Enum': ['Order', 'Ref'],
'InformationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
'AccumulationRegister': ['Active', 'LineNumber', 'Recorder', 'Period'],
'AccountingRegister': ['PeriodAdjustment', 'Account', 'Active', 'LineNumber', 'Recorder', 'Period'],
'CalculationRegister': ['Active', 'Recorder', 'LineNumber', 'RegistrationPeriod', 'CalculationType', 'ReversingEntry'],
'AccumulationRegister': ['RecordType', 'Active', 'LineNumber', 'Recorder', 'Period'],
'AccountingRegister': ['PeriodAdjustment', 'Account', 'RecordType', 'Active', 'LineNumber', 'Recorder', 'Period'],
'CalculationRegister': ['RegistrationPeriod', 'ReversingEntry', 'Active', 'EndOfBasePeriod', 'BegOfBasePeriod', 'EndOfActionPeriod', 'BegOfActionPeriod', 'ActionPeriod', 'CalculationType', 'LineNumber', 'Recorder'],
'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
'ChartOfCharacteristicTypes': ['PredefinedDataName', 'ValueType', 'Description', 'Code', 'IsFolder', 'Parent', 'Predefined', 'DeletionMark', 'Ref'],
'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'],
# Порядок в каждом списке — канон выгрузки, снят с корпуса acc+erp (внутри типа разброса нет).
# Условные члены перечислены в std_attr_conditions — позицию они берут отсюда, а
# присутствие определяется свойствами объекта.
# У ПВХ IsFolder входит в фикс-список: он есть у всех 23 объектов корпуса с этим блоком.
# У бухрегистра PeriodAdjustment, наоборот, условен (1 из 3) — он приходит как «лишний»
# ключ и эмитится ПЕРЕД фикс-списком, что совпадает с платформой.
# У регистра расчёта список безусловен: реквизиты периода действия и базового периода
# платформа пишет при любых ActionPeriod/BasePeriod/Periodicity (синтетика, все 4 комбинации).
'BusinessProcess': ['Started', 'HeadTask', 'Completed', 'Ref', 'DeletionMark', 'Date', 'Number'],
'Task': ['Executed', 'Description', 'RoutePoint', 'BusinessProcess', 'Ref', 'DeletionMark', 'Date', 'Number'],
# Порядок снят с выгрузки: у плана обмена блок начинается с ThisNode, а не с Ref
@@ -1486,10 +1488,21 @@ def emit_standard_attribute(indent, attr_name, ov=None):
# Прочие типы → блок всегда (текущее поведение). Миграция типа = +строчка в оба справочника + снэпшоты.
std_attr_conditional_types = {'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'}
# Условные члены списка типа: позиция берётся из standard_attributes_by_type, а сам
# реквизит эмитится только при наличии ключа в DSL (у бухрегистра PeriodAdjustment — 2 из 5).
std_attr_optional = {
'AccountingRegister': ('PeriodAdjustment',),
# Условные члены списка типа: позиция берётся из standard_attributes_by_type, а присутствие —
# из свойств самого объекта, как у платформы. Предикат принимает определение параметром
# (зеркало .ps1, где scriptblock не видит $def вызывающей функции).
def _period_adjustment_used(d):
v = d.get('periodAdjustmentLength')
return v is not None and int(str(v)) > 0
std_attr_conditions = {
'AccountingRegister': {
'PeriodAdjustment': _period_adjustment_used,
'RecordType': lambda d: d.get('correspondence') is not True,
},
'AccumulationRegister': {
'RecordType': lambda d: normalize_enum_value('RegisterType', str(d.get('registerType') or 'Balance')) == 'Balance',
},
}
# Хвостовая группа: реквизиты, которых нет в списке типа и которые идут ПОСЛЕ него.
@@ -1512,9 +1525,9 @@ def emit_standard_attributes(indent, object_type):
profile = std_attr_profile.get(object_type, {})
# Список типа задаёт ПОРЯДОК всех известных стандартных реквизитов, включая условные:
# их позиция бывает и до, и после обязательных (у бухрегистра PeriodAdjustment идёт
# перед Account, а ExtDimension1..3/ExtDimensionType1..3 — после Period), поэтому
# «условные скопом вперёд» не выражает канон. Условный эмитим по факту наличия в DSL.
optional = std_attr_optional.get(object_type, ())
# перед Account, RecordType — после, а ExtDimension1..3/ExtDimensionType1..3 — после Period),
# поэтому «условные скопом вперёд» не выражает канон.
cond = std_attr_conditions.get(object_type)
# Ключи, которых нет в списке типа ВООБЩЕ. По умолчанию их позиция — ПЕРЕД списком
# (легаси вроде ExchangeDate у части планов обмена). Подходящие под хвостовой шаблон
# типа идут ПОСЛЕ, в порядке номера, а внутри номера — сначала ExtDimensionN, затем
@@ -1532,8 +1545,12 @@ def emit_standard_attributes(indent, object_type):
tail.sort(key=lambda k: (int(re.search(r'\d+', k).group()), 1 if re.search(r'Type\d+$', k) else 0))
X(f'{indent}<StandardAttributes>')
for a in extra + list(attrs) + tail:
if a in optional and not (isinstance(sa, dict) and a in sa):
continue
# Условный реквизит: эмитим, если так велят свойства объекта ЛИБО если ключ есть в DSL.
# Дизъюнкция страхует роундтрип — декомпилятор перечисляет все имена блока.
if cond and a in cond:
present = (isinstance(sa, dict) and a in sa) or cond[a](defn)
if not present:
continue
ov = dict(profile.get(a, {}))
if isinstance(sa, dict):
d = sa.get(a)
@@ -1,4 +1,4 @@
# meta-validate v1.16 — Validate 1C metadata object structure (+format_rank: свести к общему эталону)
# meta-validate v1.17 — Validate 1C metadata object structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -209,7 +209,7 @@ $standardAttributesByType = @{
"Enum" = @("Order","Ref")
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period","RecordType")
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account")
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account","PeriodAdjustment","RecordType")
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry","ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod")
"ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance")
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","IsFolder","ValueType")
@@ -220,6 +220,14 @@ $standardAttributesByType = @{
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
}
# Стандартные реквизиты, присутствие которых зависит от свойств объекта: у бухрегистра
# PeriodAdjustment — от длины периода корректировки, RecordType — от корреспонденции; у регистра
# накопления RecordType — от вида регистра. Их отсутствие законно, в «Missing» не попадают.
$stdAttrConditionalNames = @{
"AccountingRegister" = @("PeriodAdjustment","RecordType")
"AccumulationRegister" = @("RecordType")
}
# Types that have StandardAttributes block
$typesWithStdAttrs = @(
"Catalog","Document","Enum",
@@ -607,11 +615,9 @@ if ($typesWithStdAttrs -contains $mdType) {
if ($saName) {
$foundNames += $saName
if ($expectedStdAttrs -notcontains $saName) {
# AccountingRegister has dynamic ExtDimension{N}/ExtDimensionType{N} and optional PeriodAdjustment
$isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$' -or $saName -eq "PeriodAdjustment"))
# CalculationRegister has conditional period attrs
$isCalcDynamic = ($mdType -eq "CalculationRegister" -and $saName -in @("ActionPeriod","BegOfActionPeriod","EndOfActionPeriod","BegOfBasePeriod","EndOfBasePeriod"))
if (-not $isDynamic -and -not $isCalcDynamic) {
# AccountingRegister: пары субконто, число которых задаётся планом счетов
$isDynamic = ($mdType -eq "AccountingRegister" -and ($saName -match '^ExtDimension\d+$' -or $saName -match '^ExtDimensionType\d+$'))
if (-not $isDynamic) {
Report-Warn "5. Unexpected StandardAttribute '$saName' for $mdType"
}
}
@@ -622,7 +628,8 @@ if ($typesWithStdAttrs -contains $mdType) {
}
if ($expectedStdAttrs) {
$missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ })
$condNames = $stdAttrConditionalNames[$mdType]; if (-not $condNames) { $condNames = @() }
$missingAttrs = @($expectedStdAttrs | Where-Object { $foundNames -notcontains $_ -and $condNames -notcontains $_ })
if ($missingAttrs.Count -gt 0) {
Report-Warn "5. Missing StandardAttributes: $($missingAttrs -join ', ')"
}
@@ -1,4 +1,4 @@
# meta-validate v1.16 — Validate 1C metadata object structure (Python port) (+format_rank: свести к общему эталону)
# meta-validate v1.17 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -209,7 +209,7 @@ standard_attributes_by_type = {
"Enum": ["Order", "Ref"],
"InformationRegister": ["Active", "LineNumber", "Recorder", "Period"],
"AccumulationRegister": ["Active", "LineNumber", "Recorder", "Period", "RecordType"],
"AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account"],
"AccountingRegister": ["Active", "Period", "Recorder", "LineNumber", "Account", "PeriodAdjustment", "RecordType"],
"CalculationRegister": ["Active", "Recorder", "LineNumber", "RegistrationPeriod", "CalculationType", "ReversingEntry", "ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod", "BegOfBasePeriod", "EndOfBasePeriod"],
"ChartOfAccounts": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "Order", "Type", "OffBalance"],
"ChartOfCharacteristicTypes": ["PredefinedDataName", "Predefined", "Ref", "DeletionMark", "Description", "Code", "Parent", "IsFolder", "ValueType"],
@@ -220,6 +220,14 @@ standard_attributes_by_type = {
"DocumentJournal": ["Type", "Ref", "Date", "Posted", "DeletionMark", "Number"],
}
# Стандартные реквизиты, присутствие которых зависит от свойств объекта: у бухрегистра
# PeriodAdjustment — от длины периода корректировки, RecordType — от корреспонденции; у регистра
# накопления RecordType — от вида регистра. Их отсутствие законно, в «Missing» не попадают.
std_attr_conditional_names = {
"AccountingRegister": ("PeriodAdjustment", "RecordType"),
"AccumulationRegister": ("RecordType",),
}
# Types that have StandardAttributes block
types_with_std_attrs = (
"Catalog", "Document", "Enum",
@@ -595,23 +603,19 @@ if md_type in types_with_std_attrs:
if sa_name:
found_names.append(sa_name)
if sa_name not in expected_std_attrs:
# AccountingRegister has dynamic attrs
# AccountingRegister: пары субконто, число которых задаётся планом счетов
is_dynamic = (md_type == "AccountingRegister" and
(re.match(r'^ExtDimension\d+$', sa_name) or
re.match(r'^ExtDimensionType\d+$', sa_name) or
sa_name == "PeriodAdjustment"))
# CalculationRegister has conditional period attrs
is_calc_dynamic = (md_type == "CalculationRegister" and
sa_name in ("ActionPeriod", "BegOfActionPeriod", "EndOfActionPeriod",
"BegOfBasePeriod", "EndOfBasePeriod"))
if not is_dynamic and not is_calc_dynamic:
re.match(r'^ExtDimensionType\d+$', sa_name)))
if not is_dynamic:
report_warn(f"5. Unexpected StandardAttribute '{sa_name}' for {md_type}")
else:
report_error("5. StandardAttribute without 'name' attribute")
check5_ok = False
if expected_std_attrs:
missing_attrs = [a for a in expected_std_attrs if a not in found_names]
cond_names = std_attr_conditional_names.get(md_type, ())
missing_attrs = [a for a in expected_std_attrs if a not in found_names and a not in cond_names]
if missing_attrs:
report_warn(f"5. Missing StandardAttributes: {', '.join(missing_attrs)}")