Re-link invoice lines whose target asset a seed replaced

`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.
This commit is contained in:
David 2026-10-09 00:52:52 +02:00
parent b5f6c0e16b
commit f7ce429c04

View file

@ -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));