diff --git a/SKILL.md b/SKILL.md index c9c8297..d47fb2e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -109,6 +109,36 @@ Use the template `templates/Reload_wrapped.bat`. Substitute: Warn user: all `git commit` commands use `--allow-empty` so they never fail even when nothing changed. +## Step 6b: Populating tableboxes programmatically + +QlikView's COM `TableBoxProperties.Fields.Add()` is BROKEN on modern QV +(returns "null-valued expression"). The working pattern is +`$tableBox.AddField($fieldName)` — called directly on the SheetObject, NOT on +its properties. Use template `templates/populate_tablebox.ps1`: + +```powershell +powershell -File populate_tablebox.ps1 ` + -QvwPath "C:\path\to\app.qvw" ` + -SheetName "Cash Flow" ` + -Fields @("%ProdPODateKey","cf_category","cf_payment_date","cf_amount_ron") +``` + +The script creates the sheet if missing (case-insensitive substring match on +name), creates a fresh TableBox on it, and calls AddField per entry. +Fields that don't exist in the data model fail silently — pass +`-ReplaceExisting $true` to purge existing TBs on the sheet first (removes +duplicates when re-running with corrected field lists). + +Common gotchas: +- Field must exist in a RESIDENT table at save time. `Dim*` / `Fact*` tables + often get dropped in a final-schema-reduction step — their fields won't + populate. Check the `1700.ExportCSV.qvs`-style DROP list. +- `%CompoundKey`-style field names (`%ProdPODateKey`, `%SupplierPOKey`) work + fine — the `%` prefix is literal. +- For Dim-only fields that DO survive (e.g. after renaming a drop list), + run the `qvw-extract-script` skill first to audit what's actually in the + final QVW schema. + ## Step 7: Changing the script include path (if needed) If the QVW's embedded script points at an old `9.QVSvN` folder and needs diff --git a/templates/populate_tablebox.ps1 b/templates/populate_tablebox.ps1 new file mode 100644 index 0000000..bece03f --- /dev/null +++ b/templates/populate_tablebox.ps1 @@ -0,0 +1,97 @@ +# populate_tablebox.ps1 — create a new sheet with a populated TableBox on a QVW. +# +# IMPORTANT: use TableBox.AddField($fieldName) — NOT TableBoxProperties.Fields.Add() +# (which fails on modern QV Document COM with "null-valued expression"). +# +# Usage: +# powershell -File populate_tablebox.ps1 ` +# -QvwPath "C:\path\to\app.qvw" ` +# -SheetName "Cash Flow (v9)" ` +# -Fields @("%ProdPODateKey","cf_category","cf_payment_date","cf_amount_ron","ext_supplier_code") +# +# The sheet is CREATED if it doesn't match `$SheetName` (case-insensitive substring). +# A fresh TableBox is always created on the sheet. Existing TBs are NOT deleted — +# pass -ReplaceExisting $true to remove old TBs on the sheet first. + +param( + [Parameter(Mandatory=$true)][string]$QvwPath, + [Parameter(Mandatory=$true)][string]$SheetName, + [Parameter(Mandatory=$true)][string[]]$Fields, + [bool]$ReplaceExisting = $false +) + +$ErrorActionPreference = "Continue" +if (-not (Test-Path $QvwPath)) { throw "QVW not found: $QvwPath" } + +$qv = New-Object -ComObject QlikTech.QlikView +$doc = $qv.OpenDoc($QvwPath, "", "") +if (-not $doc) { throw "OpenDoc returned null for $QvwPath" } +Start-Sleep -Seconds 2 + +# Find or create the sheet +$targetSheet = $null +$targetSheetId = $null +for ($i=0; $i -lt $doc.NoOfSheets(); $i++) { + $s = $doc.GetSheet($i) + $sp = $s.GetProperties() + $nm = $null + try { $nm = $sp.Name } catch { try { $nm = $sp.Name.v } catch {} } + if ($nm -and ($nm -like "*$SheetName*" -or $nm -eq $SheetName)) { + $targetSheet = $s + $targetSheetId = $sp.SheetId + Write-Host "sheet exists: $nm (id=$targetSheetId)" + break + } +} +if (-not $targetSheet) { + $targetSheet = $doc.CreateSheet() + $sp = $targetSheet.GetProperties() + try { $sp.Name = $SheetName } catch { try { $sp.Name.v = $SheetName } catch {} } + $targetSheet.SetProperties($sp) + $targetSheetId = $targetSheet.GetProperties().SheetId + Write-Host "sheet created: $SheetName (id=$targetSheetId)" +} + +# Optionally remove existing TBs on the sheet (reads -prj XML to find them) +if ($ReplaceExisting) { + $prjPath = [System.IO.Path]::GetDirectoryName($QvwPath) + "\" + [System.IO.Path]::GetFileNameWithoutExtension($QvwPath) + "-prj" + $prjXml = Join-Path $prjPath "QlikViewProject.xml" + if (Test-Path $prjXml) { + $xml = [xml](Get-Content $prjXml) + $sheetNode = $xml.PrjQlikViewProject.SHEETS.PrjSheetProperties | Where-Object { $_.SheetId -eq "Document\$targetSheetId" } + if ($sheetNode) { + foreach ($ch in $sheetNode.ChildObjects.PrjFrameParentDef) { + if ($ch.ObjectId -like "Document\TB*") { + $tbId = $ch.ObjectId.Replace("Document\","") + try { + $tb = $doc.GetSheetObject("Document\$tbId") + if ($tb) { $tb.Close() | Out-Null; Write-Host " - removed $tbId" } + } catch {} + } + } + } + } +} + +# Create fresh tablebox +$tb = $targetSheet.CreateTableBox() +Start-Sleep -Milliseconds 400 + +# Populate via AddField (THIS is the working pattern) +$added = 0; $failed = @() +foreach ($f in $Fields) { + try { + $tb.AddField($f) | Out-Null + $added++ + } catch { + $failed += $f + } +} +Write-Host "populated $added/$($Fields.Count) fields" +if ($failed.Count) { Write-Host "missed: $($failed -join ', ')" } + +$doc.Save() +Start-Sleep -Milliseconds 500 +$doc.CloseDoc() +$qv.Quit() +Write-Host "DONE"