Import G/L: use buildium_gl source, name-first matching, keep parent rows

- Post imported entries as external_source "buildium_gl" (matching the nightly
  buildium-gl-sync) so the sync's dedupe recognizes them and won't re-pull the
  same transactions as duplicates. A separate "buildium_gl_csv" source is
  invisible to that dedupe and double-counts on the next sync.
- Match accounts by name first, then code: account numbers can differ between
  Buildium and the platform chart (and a number can mean different things), so
  name is the more reliable key; code is the fallback.
- Keep IsParent rows. In some exports a parent account carries its own real
  postings rather than a rollup duplicate; the per-transaction balance check
  flags genuine double-emits instead of silently dropping lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 16:45:06 -04:00
parent 1296b9449d
commit c8cb583ec9
@@ -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;