diff --git a/src/pages/accounting/components/GLImportDialog.tsx b/src/pages/accounting/components/GLImportDialog.tsx index 8cc8093..26d40c7 100644 --- a/src/pages/accounting/components/GLImportDialog.tsx +++ b/src/pages/accounting/components/GLImportDialog.tsx @@ -20,12 +20,16 @@ import { money } from "../lib/format"; // entry. Parent rollup accounts (IsParent) and the cash book (IsCashPosting) // are skipped so the accrual book isn't double-counted. // -// Accounts are matched to the association's chart of accounts by code -// (AccountNumber) first, then by name (GLAccountName, leading code stripped). -// Transactions are keyed on the Buildium `Id` (external_id) so re-importing the -// same file is a no-op — already-imported transactions are skipped. - -const EXTERNAL_SOURCE = "buildium_gl_csv"; +// Accounts are matched to the association's chart of accounts by name first +// (GLAccountName, leading code stripped) then code (AccountNumber) — name-first +// avoids code collisions where the same number means different things in the two +// charts. Transactions are keyed on the Buildium `Id` (external_id) so +// re-importing the same file is a no-op. +// +// The source is "buildium_gl" — the same source the nightly buildium-gl-sync +// uses — so its watermark/dedupe (which only recognizes "buildium_gl") treats +// these as already-imported and won't re-pull them as duplicates. +const EXTERNAL_SOURCE = "buildium_gl"; type CoaAccount = { id: string; code: string | null; name: string; type: string }; @@ -118,8 +122,12 @@ export default function GLImportDialog({ return m; }, [accounts]); + // Name-first, then code: the chart's account *number* can differ between + // Buildium and the platform (and the same number can mean different things), + // so a name match is more reliable; fall back to code for "-- "-prefixed + // reserve components and the like whose names won't match verbatim. const resolveAccount = (code: string, name: string): CoaAccount | null => - (code && byCode.get(code.trim())) || byName.get(norm(name)) || null; + byName.get(norm(stripLeadingCode(name))) || (code && byCode.get(code.trim())) || byName.get(norm(name)) || null; function reset() { setLines([]); setError(null); setFileName(""); @@ -152,7 +160,10 @@ export default function GLImportDialog({ const isTrue = (v: string) => ["true", "1", "yes", "y"].includes(String(v).trim().toLowerCase()); const out: ParsedLine[] = []; for (const r of rows) { - if (col.isParent && isTrue(r[col.isParent])) continue; // skip rollup parents + // NOTE: IsParent rows are kept — in some exports a parent account carries + // its own postings (not a rollup duplicate). The per-transaction balance + // check below is the safeguard: a file that double-emits parent+child + // won't balance and is flagged rather than silently dropped. if (col.isCash && isTrue(r[col.isCash])) continue; // skip cash book (accrual only) const amount = Number(String(r[col.amount]).replace(/[$,]/g, "")) || 0; if (amount === 0) continue;