From c6f499a099d6d2e9f39123b5bec2053a073b81e1 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 7 Mar 2026 19:10:10 +0300 Subject: [PATCH] feat(stub-db-create): scan Form.xml for register column names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse form attributes of RegisterRecordSet types and extract column names from DataPath references. Generate matching Dimension/Attribute stubs in register metadata so form field bindings survive the build. Limitation: column categories (Dimension vs Resource vs Attribute) cannot be determined from EPF sources alone — only names are preserved. Also add /Out log for UpdateDBCfg errors in both PS1 and PY. Co-Authored-By: Claude Opus 4.6 --- .../epf-build/scripts/stub-db-create.ps1 | 196 ++++++++++++++-- .../epf-build/scripts/stub-db-create.py | 214 +++++++++++------- 2 files changed, 317 insertions(+), 93 deletions(-) diff --git a/.claude/skills/epf-build/scripts/stub-db-create.ps1 b/.claude/skills/epf-build/scripts/stub-db-create.ps1 index c8723f13..63666c7b 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.ps1 +++ b/.claude/skills/epf-build/scripts/stub-db-create.ps1 @@ -92,6 +92,73 @@ foreach ($f in $xmlFiles) { } } +# --- 1b. Scan Form.xml for register record set columns --- +# When a form attribute has type like InformationRegisterRecordSet.XXX, +# the form references columns via DataPath "AttrName.ColumnName". +# We need to create matching dimensions/resources/attributes in stub registers. + +$registerColumns = @{} # "RegisterType.RegisterName" -> @{ col1=$true; col2=$true } + +# Standard attributes that don't need explicit declaration +$stdRegCols = @("LineNumber","Period","Recorder","Active","RecordType") + +foreach ($f in $xmlFiles) { + $content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8) + + # Find form attributes with register record set types using XmlDocument for reliability + $regAttrMap = @{} # formAttrName -> "RegisterType.RegisterName" + + # Only process Form.xml files (they contain with children) + if ($f.Name -eq "Form.xml" -and $content -match '') { + try { + $xml = New-Object System.Xml.XmlDocument + $xml.LoadXml($content) + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + $nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + $attrNodes = $xml.SelectNodes("//f:Attributes/f:Attribute", $nsMgr) + foreach ($attrNode in $attrNodes) { + $attrName = $attrNode.GetAttribute("name") + $typeNodes = $attrNode.SelectNodes("f:Type/v8:Type", $nsMgr) + foreach ($tn in $typeNodes) { + $typeText = $tn.InnerText + $rsMatch = [regex]::Match($typeText, '^(?:cfg:|d\dp1:)(InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet)\.(.+)$') + if ($rsMatch.Success) { + $rsPrefix = $rsMatch.Groups[1].Value + $regName = $rsMatch.Groups[2].Value + $regType = switch ($rsPrefix) { + "InformationRegisterRecordSet" { "InformationRegister" } + "AccumulationRegisterRecordSet" { "AccumulationRegister" } + "AccountingRegisterRecordSet" { "AccountingRegister" } + "CalculationRegisterRecordSet" { "CalculationRegister" } + } + $regKey = "$regType.$regName" + $regAttrMap[$attrName] = $regKey + if (-not $registerColumns.ContainsKey($regKey)) { + $registerColumns[$regKey] = @{} + } + } + } + } + } catch { + # XML parse failed, skip + } + } + + # Now find DataPath references like "AttrName.ColumnName" + if ($regAttrMap.Count -gt 0) { + $dpPattern = '([A-Za-z\u0400-\u04FF\d_]+)\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $dpPattern)) { + $attrName = $m.Groups[1].Value + $colName = $m.Groups[2].Value + if ($regAttrMap.ContainsKey($attrName) -and $colName -notin $stdRegCols) { + $regKey = $regAttrMap[$attrName] + $registerColumns[$regKey][$colName] = $true + } + } + } +} + $hasRefTypes = $typeMap.Count -gt 0 # --- 2. Determine TempBasePath --- @@ -977,13 +1044,23 @@ $stdAttrs if ($metaType -eq "DefinedType") { $childObjLine = "" } elseif ($metaType -eq "InformationRegister") { - $dimUuid = [guid]::NewGuid().ToString() - $childObjLine = @" - - - + # Check if we have actual column names from form scanning + $regKey = "InformationRegister.$objName" + $cols = if ($registerColumns.ContainsKey($regKey) -and $registerColumns[$regKey].Count -gt 0) { + $registerColumns[$regKey].Keys + } else { + @("Заглушка") + } + # First column as Dimension (for MainFilter), rest as Attributes (no index pressure) + $dimXmlParts = @() + $isFirst = $true + foreach ($colName in $cols) { + $elemUuid = [guid]::NewGuid().ToString() + if ($isFirst) { + $dimXmlParts += @" + - Заглушка + $colName @@ -1022,14 +1099,63 @@ $stdAttrs Use - "@ + $isFirst = $false + } else { + $dimXmlParts += @" + + + $colName + + + + xs:string + + 10 + Variable + + + false + + + + false + + false + false + + + false + + DontCheck + Items + + + Auto + Auto + + + Auto + Use + + +"@ + } + } + $childObjLine = "`r`n`t`t`r`n$($dimXmlParts -join "`r`n")`r`n`t`t" } elseif ($metaType -in @("AccumulationRegister","AccountingRegister","CalculationRegister")) { - $resUuid = [guid]::NewGuid().ToString() - $childObjLine = @" - - - + # Check if we have actual column names from form scanning + $regKey = "$metaType.$objName" + $cols = if ($registerColumns.ContainsKey($regKey) -and $registerColumns[$regKey].Count -gt 0) { + $registerColumns[$regKey].Keys + } else { + @() + } + $childParts = @() + # AccumulationRegister requires at least one Resource + $stubResUuid = [guid]::NewGuid().ToString() + $childParts += @" + Заглушка @@ -1064,8 +1190,48 @@ $stdAttrs Use - "@ + # Add all form-referenced columns as Dimensions (short strings to avoid index overflow) + foreach ($colName in $cols) { + $dimUuid = [guid]::NewGuid().ToString() + $childParts += @" + + + $colName + + + + xs:string + + 10 + Variable + + + false + + + + false + + false + false + + + DontCheck + Items + + + Auto + Auto + + + Auto + Use + + +"@ + } + $childObjLine = "`r`n`t`t`r`n$($childParts -join "`r`n")`r`n`t`t" } $objXml = @" @@ -1108,9 +1274,11 @@ if ($hasRefTypes) { # UpdateDBCfg Write-Host "Updating database configuration..." - $updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /DisableStartupDialogs" + $updateLog = Join-Path $env:TEMP "stub_update_log.txt" + $updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" $proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru if ($proc.ExitCode -ne 0) { + if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host } Write-Error "Failed to update DB config (code: $($proc.ExitCode))" exit 1 } diff --git a/.claude/skills/epf-build/scripts/stub-db-create.py b/.claude/skills/epf-build/scripts/stub-db-create.py index d7ada5dd..7d06e686 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.py +++ b/.claude/skills/epf-build/scripts/stub-db-create.py @@ -89,6 +89,68 @@ def scan_ref_types(source_dir): return type_map +def scan_register_columns(source_dir): + """Scan Form.xml for register record set columns referenced via DataPath. + Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}.""" + import xml.etree.ElementTree as ET + + register_columns = {} + std_cols = {'LineNumber', 'Period', 'Recorder', 'Active', 'RecordType'} + rs_type_map = { + 'InformationRegisterRecordSet': 'InformationRegister', + 'AccumulationRegisterRecordSet': 'AccumulationRegister', + 'AccountingRegisterRecordSet': 'AccountingRegister', + 'CalculationRegisterRecordSet': 'CalculationRegister', + } + rs_pattern = re.compile( + r'^(?:cfg:|d\dp1:)(InformationRegisterRecordSet|AccumulationRegisterRecordSet' + r'|AccountingRegisterRecordSet|CalculationRegisterRecordSet)\.(.+)$' + ) + dp_pattern = re.compile(r'([A-Za-z\u0400-\u04FF\d_]+)\.([A-Za-z\u0400-\u04FF\d_]+)') + + ns = { + 'v8': 'http://v8.1c.ru/8.1/data/core', + 'f': 'http://v8.1c.ru/8.3/xcf/logform', + } + + for dirpath, _, filenames in os.walk(source_dir): + for fn in filenames: + if fn != 'Form.xml': + continue + fp = os.path.join(dirpath, fn) + try: + with open(fp, 'r', encoding='utf-8-sig') as fh: + content = fh.read() + except Exception: + continue + if '' not in content: + continue + + # Parse form attributes to find register recordset types + reg_attr_map = {} # formAttrName -> "RegisterType.RegisterName" + try: + root = ET.fromstring(content) + for attr_node in root.iter('{http://v8.1c.ru/8.3/xcf/logform}Attribute'): + attr_name = attr_node.get('name', '') + for type_node in attr_node.iter('{http://v8.1c.ru/8.1/data/core}Type'): + m = rs_pattern.match(type_node.text or '') + if m: + reg_type = rs_type_map[m.group(1)] + reg_key = f"{reg_type}.{m.group(2)}" + reg_attr_map[attr_name] = reg_key + register_columns.setdefault(reg_key, {}) + except Exception: + continue + + # Find DataPath references like "AttrName.ColumnName" + for m in dp_pattern.finditer(content): + attr_name, col_name = m.group(1), m.group(2) + if attr_name in reg_attr_map and col_name not in std_cols: + register_columns[reg_attr_map[attr_name]][col_name] = True + + return register_columns + + NS = ( 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" ' 'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" ' @@ -722,6 +784,7 @@ def main(): args = parser.parse_args() type_map = scan_ref_types(args.SourceDir) + register_columns = scan_register_columns(args.SourceDir) has_ref_types = len(type_map) > 0 temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}') @@ -882,91 +945,77 @@ def main(): if meta_type == 'DefinedType': child_obj_xml = '' elif meta_type == 'InformationRegister': - dim_uuid = new_uuid() - child_obj_xml = f""" -\t\t -\t\t\t + reg_key = f'InformationRegister.{obj_name}' + cols = list(register_columns.get(reg_key, {}).keys()) or ['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430'] + parts = [] + for i, col in enumerate(cols): + u = new_uuid() + if i == 0: + parts.append(f"""\t\t\t \t\t\t\t -\t\t\t\t\t\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430 -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\t\txs:string -\t\t\t\t\t\t -\t\t\t\t\t\t\t10 -\t\t\t\t\t\t\tVariable -\t\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\tDontCheck -\t\t\t\t\tItems -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tAuto -\t\t\t\t\tAuto -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tAuto -\t\t\t\t\tfalse -\t\t\t\t\ttrue -\t\t\t\t\tfalse -\t\t\t\t\tDontIndex -\t\t\t\t\tUse -\t\t\t\t\tUse +\t\t\t\t\t{col} +\t\t\t\t\t +\t\t\t\t\txs:string10Variable +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\t +\t\t\t\t\tfalseDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tfalsetruefalse +\t\t\t\t\tDontIndexUseUse \t\t\t\t -\t\t\t -\t\t""" +\t\t\t""") + else: + parts.append(f"""\t\t\t +\t\t\t\t +\t\t\t\t\t{col} +\t\t\t\t\t +\t\t\t\t\txs:string10Variable +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\t +\t\t\t\t\tfalseDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tDontIndexUseUse +\t\t\t\t +\t\t\t""") + child_obj_xml = '\n\t\t\n' + '\n'.join(parts) + '\n\t\t' elif meta_type in ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister'): - res_uuid = new_uuid() - child_obj_xml = f""" -\t\t -\t\t\t + reg_key = f'{meta_type}.{obj_name}' + cols = list(register_columns.get(reg_key, {}).keys()) + parts = [] + # Required stub Resource + parts.append(f"""\t\t\t \t\t\t\t \t\t\t\t\t\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430 -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\t\txs:decimal -\t\t\t\t\t\t -\t\t\t\t\t\t\t15 -\t\t\t\t\t\t\t2 -\t\t\t\t\t\t\tAny -\t\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\tfalse -\t\t\t\t\tfalse -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tDontCheck -\t\t\t\t\tItems -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tAuto -\t\t\t\t\tAuto -\t\t\t\t\t -\t\t\t\t\t -\t\t\t\t\tAuto +\t\t\t\t\t +\t\t\t\t\txs:decimal152Any +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\tDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto \t\t\t\t\tUse \t\t\t\t -\t\t\t -\t\t""" +\t\t\t""") + # Form-referenced columns as Dimensions + for col in cols: + parts.append(f"""\t\t\t +\t\t\t\t +\t\t\t\t\t{col} +\t\t\t\t\t +\t\t\t\t\txs:string10Variable +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\tDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tUse +\t\t\t\t +\t\t\t""") + child_obj_xml = '\n\t\t\n' + '\n'.join(parts) + '\n\t\t' else: child_obj_xml = '\n\t\t' @@ -1007,11 +1056,18 @@ def main(): # UpdateDBCfg print('Updating database configuration...') + update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt') result = subprocess.run( - [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/DisableStartupDialogs'], + [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'], capture_output=True, text=True, ) if result.returncode != 0: + if os.path.isfile(update_log): + try: + with open(update_log, 'r', encoding='utf-8-sig') as f: + print(f.read()) + except Exception: + pass print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr) sys.exit(1)