From f7ce429c041733712af7f17a6c9412e65675e80f Mon Sep 17 00:00:00 2001 From: David Date: Fri, 9 Oct 2026 00:52:52 +0200 Subject: [PATCH] Re-link invoice lines whose target asset a seed replaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `booked_asset` stores an asset record ID, and `npm run seed` deletes and re-creates every asset — so the id a line was booked against stops existing after a seed. The line then points at nothing and silently drops out of monthly_income, which is exactly the reported symptom: booked yesterday, unbooked today, with the line still carrying the correct code. The booking step now re-derives the pointer from the register code the line already carries (`asset_code`, falling back to the machine code) whenever the stored target is empty or no longer exists. It only ever points a line at the asset its own code names, so it never invents a match and never clears a site booking. A line with no matching code stays unbooked exactly as before. This makes the documented run order self-healing: `npm run seed && node scripts/book-invoice.mjs` now restores the links, so a reset cannot silently unbook the register. Verified by running a full seed and re-booking: 29 pointers were invalidated by the seed and all 29 were re-linked, giving 45 posted rows and 3 unbooked lines — the two MPLANT05 containers and one WT02 trailer, which have no matching plant item. --- scripts/book-invoice.mjs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/book-invoice.mjs b/scripts/book-invoice.mjs index ab1ad94..7fe75d4 100644 --- a/scripts/book-invoice.mjs +++ b/scripts/book-invoice.mjs @@ -74,6 +74,40 @@ if (!targets.length) { const allLines = await pb.collection("invoice_lines").getFullList({ sort: "line_no" }); const existingIncome = await pb.collection("monthly_income").getFullList(); +// ---- Repair stale booking pointers ------------------------------------------------ +// `booked_asset` stores an asset RECORD ID, and `npm run seed` deletes and re-creates +// every asset — so the id a line was booked against stops existing after a seed. The line +// then points at nothing and silently drops out of monthly_income, which is exactly the +// "it booked yesterday and is unbooked today" symptom. +// +// The invoice line already carries the register code it was matched on (`asset_code`), +// so the pointer can be re-derived rather than retyped. This ONLY ever points a line at +// the asset its own code names; it never invents a match and never clears one, so a line +// with no code stays unbooked exactly as before. +const allAssets = await pb.collection("assets").getFullList(); +const assetIds = new Set(allAssets.map((a) => a.id)); +const idByCode = new Map(allAssets.filter((a) => a.code).map((a) => [String(a.code).trim().toUpperCase(), a.id])); +const codeFromMachine = (code) => { + const m = /^([A-Za-z]+)[- ]?([0-9]{1,3})/.exec(String(code || "").trim()); + return m ? `${m[1].toUpperCase()}${m[2]}` : ""; +}; + +let repaired = 0; +if (!dryRun) { + for (const l of allLines) { + if (l.booked_site) continue; // booked to a site — a different, deliberate choice + const fresh = (String(l.asset_code || "").trim().toUpperCase() || codeFromMachine(l.machine_code)) + && idByCode.get(String(l.asset_code || "").trim().toUpperCase() || codeFromMachine(l.machine_code)); + // Only act when the pointer is genuinely gone: empty, or pointing at a deleted asset. + const needsRepair = !l.booked_asset || !assetIds.has(l.booked_asset); + if (!needsRepair || !fresh || fresh === l.booked_asset) continue; + await pb.collection("invoice_lines").update(l.id, { booked_asset: fresh }); + l.booked_asset = fresh; + repaired++; + } +} +if (repaired) console.log(` · re-linked ${repaired} line(s) whose target asset had been replaced by a seed`); + // Remove previously invoice-derived rows for the target invoices only. const targetIds = new Set(targets.map((i) => i.id)); const linesOfTargets = allLines.filter((l) => targetIds.has(l.invoice));