- /fedex: batch create, document upload/replace/delete, save shipment, imported list
- /api/orders, /api/invoices, /api/pb-file, /api/pb/refresh-token, /api/stock-codes
- /api/payments/bundle: generate + download-all remittance bundles
- shared app/components/DocCell.tsx and app/lib/{invoice-docs,pb-fetch,pb-token}.ts
- recon: invoice reconciliation reports and PowerShell fix-plan scripts
- .gitignore: exclude one-off scripts/_* debugging artefacts, recon/pdfs+txts, data backups
204 lines
No EOL
6.7 KiB
PowerShell
204 lines
No EOL
6.7 KiB
PowerShell
# JEI Line Fix Plan (dry-run)
|
|
# For each affected invoice, parse PDF + DB and output the proposed deletes + adds.
|
|
# Does NOT make any changes. Just produces a plan.
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$pbUrl = 'https://pb.agent.apphub.co.za'
|
|
$envPath = 'C:\Users\SJ\.minimax-agent\projects\JEI\.env.local'
|
|
$reconDir = 'C:\Users\SJ\.minimax-agent\projects\JEI\recon'
|
|
$txtsDir = "$reconDir\txts"
|
|
|
|
$token = (Get-Content $envPath | Select-String -Pattern '^PB_TOKEN=' | ForEach-Object { $_ -replace '^PB_TOKEN=','' }).Trim()
|
|
$headers = @{ Authorization = "Bearer $token" }
|
|
|
|
# Invoices to dedupe
|
|
$dedupInvoices = @(
|
|
'151778','151779','155655','155743','156092','156703','160107','160453',
|
|
'161454','161611','162609','162923','163374','163841','163842','151346','161453'
|
|
)
|
|
|
|
$codeCache = @{}
|
|
function Get-StockCode($sid) {
|
|
if ($codeCache.ContainsKey($sid)) { return $codeCache[$sid] }
|
|
$r = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_stock_codes/records/$sid`?fields=code" -Headers $headers -Method Get
|
|
$codeCache[$sid] = $r.code
|
|
$r.code
|
|
}
|
|
|
|
function Get-PdfLines($txtPath) {
|
|
if (-not (Test-Path $txtPath)) { return @() }
|
|
$txt = [System.IO.File]::ReadAllText($txtPath, [System.Text.Encoding]::UTF8)
|
|
$pages = $txt -split "`f"
|
|
$lines = @()
|
|
for ($i=0; $i -lt $pages.Count; $i++) {
|
|
$p = $pages[$i]
|
|
$matches = [regex]::Matches($p, '(?m)^\s*(\d+\.\d+)\s+(\S+)')
|
|
foreach ($m in $matches) {
|
|
$code = $m.Groups[2].Value.Trim() -replace '\s+', ' '
|
|
$qty = [double]$m.Groups[1].Value
|
|
$lines += [pscustomobject]@{ qty = $qty; code = $code; page = $i+1 }
|
|
}
|
|
}
|
|
$lines
|
|
}
|
|
|
|
function Normalize-Code($code) {
|
|
($code -replace '\s+', ' ').Trim().ToUpper()
|
|
}
|
|
|
|
$totalDeletes = 0
|
|
$totalAdds = 0
|
|
$reportLines = @()
|
|
$plan = @()
|
|
|
|
$reportLines += "==================================================="
|
|
$reportLines += "FIX PLAN -- DRY RUN (no DB changes)"
|
|
$reportLines += "==================================================="
|
|
|
|
foreach ($invNo in $dedupInvoices) {
|
|
$txtPath = "$txtsDir\$invNo.txt"
|
|
if (-not (Test-Path $txtPath)) {
|
|
$reportLines += ""
|
|
$reportLines += "==== $invNo ===="
|
|
$reportLines += " SKIP -- no PDF text file"
|
|
continue
|
|
}
|
|
|
|
$invRec = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoices/records?filter=invoice_no=`"$invNo`"&fields=id" -Headers $headers -Method Get
|
|
if ($invRec.items.Count -eq 0) { continue }
|
|
$invId = $invRec.items[0].id
|
|
|
|
$db = Invoke-RestMethod -Uri "$pbUrl/api/collections/JEI_invoice_lines/records?filter=invoice=`"$invId`"&perPage=500" -Headers $headers -Method Get
|
|
$dbLines = foreach ($r in $db.items) {
|
|
[pscustomobject]@{
|
|
id = $r.id
|
|
line_no = [int]$r.line_no
|
|
code = Get-StockCode $r.stock_code
|
|
code_raw = $r.stock_code
|
|
qty = [double]$r.quantity
|
|
net = [double]$r.net_amount
|
|
unit = [double]$r.unit_price
|
|
country = $r.country_of_origin
|
|
commodity = $r.commodity_code
|
|
}
|
|
}
|
|
|
|
$pdfLines = Get-PdfLines $txtPath
|
|
$pdfTuples = @{}
|
|
foreach ($pl in $pdfLines) {
|
|
$key = "$(Normalize-Code $pl.code)|$([string]$pl.qty)"
|
|
if (-not $pdfTuples.ContainsKey($key)) {
|
|
$pdfTuples[$key] = $pl
|
|
}
|
|
}
|
|
|
|
$deletes = @()
|
|
$adds = @()
|
|
$keep = @()
|
|
|
|
# Pass 1: exact (code, qty) match
|
|
$matched = @{}
|
|
foreach ($d in $dbLines) {
|
|
$key = "$(Normalize-Code $d.code)|$([string]$d.qty)"
|
|
if ($pdfTuples.ContainsKey($key)) {
|
|
if (-not $matched.ContainsKey($key)) {
|
|
$matched[$key] = @()
|
|
}
|
|
$matched[$key] += $d
|
|
}
|
|
}
|
|
|
|
# Pass 2: fuzzy match — DB record's qty matches ANY PDF line's qty (same qty, same item but slightly different code name)
|
|
# Already-matched DB records are excluded.
|
|
$matchedDbIds = @{}
|
|
foreach ($arr in $matched.Values) { foreach ($d in $arr) { $matchedDbIds[$d.id] = $true } }
|
|
|
|
$qtyToPdfLines = @{}
|
|
foreach ($pl in $pdfTuples.Values) {
|
|
$qkey = "$([string]$pl.qty)"
|
|
if (-not $qtyToPdfLines.ContainsKey($qkey)) { $qtyToPdfLines[$qkey] = @() }
|
|
$qtyToPdfLines[$qkey] += $pl
|
|
}
|
|
|
|
foreach ($d in $dbLines) {
|
|
if ($matchedDbIds.ContainsKey($d.id)) { continue } # already exact-matched
|
|
$qkey = "$([string]$d.qty)"
|
|
if ($qtyToPdfLines.ContainsKey($qkey)) {
|
|
# Match by qty alone — find any unused PDF line with this qty
|
|
foreach ($pl in $qtyToPdfLines[$qkey]) {
|
|
$plKey = "$(Normalize-Code $pl.code)|$([string]$pl.qty)"
|
|
if (-not $matched.ContainsKey($plKey) -or ($matched[$plKey].Count -eq 1 -and $matched[$plKey][0].id -eq $d.id)) {
|
|
# OK to match
|
|
if (-not $matched.ContainsKey($plKey)) { $matched[$plKey] = @() }
|
|
$matched[$plKey] += $d
|
|
$matchedDbIds[$d.id] = $true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Now unmatched DB records
|
|
foreach ($d in $dbLines) {
|
|
if (-not $matchedDbIds.ContainsKey($d.id)) {
|
|
$deletes += [pscustomobject]@{ rec=$d; reason='NO_PDF_MATCH' }
|
|
}
|
|
}
|
|
|
|
foreach ($k in $matched.Keys) {
|
|
$arr = $matched[$k]
|
|
if ($arr.Count -gt 1) {
|
|
$sorted = $arr | Sort-Object id
|
|
$keep += $sorted[0]
|
|
foreach ($d in ($sorted | Select-Object -Skip 1)) {
|
|
$deletes += [pscustomobject]@{ rec=$d; reason='DUPLICATE_OF_KEPT' }
|
|
}
|
|
} else {
|
|
$keep += $arr[0]
|
|
}
|
|
}
|
|
|
|
foreach ($k in $pdfTuples.Keys) {
|
|
if (-not $matched.ContainsKey($k)) {
|
|
$adds += $pdfTuples[$k]
|
|
}
|
|
}
|
|
|
|
$reportLines += ""
|
|
$reportLines += "==== $invNo ===="
|
|
$reportLines += (" PDF lines={0} db_records={1} will_keep={2}" -f $pdfTuples.Count, $dbLines.Count, $keep.Count)
|
|
$reportLines += (" Deletes: {0}" -f $deletes.Count)
|
|
foreach ($d in $deletes) {
|
|
$reportLines += (" DELETE rec={0,-22} line_no={1,3} code={2,-22} qty={3,5} net={4,10} [{5}]" -f $d.rec.id, $d.rec.line_no, $d.rec.code, $d.rec.qty, $d.rec.net, $d.reason)
|
|
}
|
|
$reportLines += (" Adds: {0}" -f $adds.Count)
|
|
foreach ($a in $adds) {
|
|
$reportLines += (" ADD code={0,-22} qty={1,5} pdf-page={2}" -f $a.code, $a.qty, $a.page)
|
|
}
|
|
$totalDeletes += $deletes.Count
|
|
$totalAdds += $adds.Count
|
|
|
|
$plan += [pscustomobject]@{
|
|
invoice = $invNo
|
|
pdf_lines = $pdfTuples.Count
|
|
db_lines = $dbLines.Count
|
|
keep = $keep.Count
|
|
deletes = @($deletes)
|
|
adds = @($adds)
|
|
}
|
|
}
|
|
|
|
$reportLines += ""
|
|
$reportLines += "==================================================="
|
|
$reportLines += ("TOTALS -- would delete {0} records, add {1} records" -f $totalDeletes, $totalAdds)
|
|
$reportLines += "==================================================="
|
|
|
|
$reportLines | Out-File "$reconDir\fix_plan.txt" -Encoding UTF8
|
|
|
|
# Also save the structured plan as JSON for the apply step
|
|
$jsonOut = ($plan | ConvertTo-Json -Depth 8)
|
|
$jsonOut | Out-File "$reconDir\fix_plan.json" -Encoding UTF8
|
|
|
|
# Print to console
|
|
$reportLines | ForEach-Object { Write-Host $_ } |