October 8, 2026

The DETAIL Table Side-Decode in Yardi Voyager: Same Column, Different Meaning

Most Voyager tables have a discriminator column that tells you what kind of row you're looking at, and once you know that, every other column means the same thing on every row. DETAIL doesn't work that way. Two of its columns — HCHKORCHG and HINVORREC — don't have one meaning each. What they decode to depends entirely on which document class the row belongs to, and that document class isn't written down anywhere in DETAIL itself. You have to already know it, or derive it, before the row makes sense.

This is what "document class SQL" actually means on Voyager: the same physical column pair carries different semantics for an AR charge line than it does for an AP invoice line or a journal entry, and there's no flag on the row that spells that out for you.

What DETAIL actually is

DETAIL is transaction line-item and GL-distribution detail — the children of TRANS, linked back up through HINVORREC and HCHKORCHG. It's not a recurring-charge table (that's CAMRULE, the configuration table that generates billing lines before they ever post to TRANS) and its own ITYPE column is functionally unused — observed live, DETAIL.ITYPE only ever carries 0 or NULL. It isn't the discriminator. HCHKORCHG and HINVORREC, read together, are.

The side-decode

Both columns are, in a sense, foreign keys back to TRANS.HMY — but reading them correctly requires knowing that TRANS.HMY values are themselves typed by TRANS.iType (we've decoded that map separately). Cross-referencing the two columns' iType pairing against live data surfaces a consistent, document-class-specific pattern:

The pattern underneath all of it: HCHKORCHG holds the "active" side of the transaction — the payment, the charge, or the receipt acting on something — and HINVORREC holds the "target" — the invoice, the receipt, or the journal entry being acted on or line-itemized. Neither column name tells you that on its own. You learn it by cross-referencing both columns' iType against the row.

The trap that follows from it

hChargeCode on DETAIL is not populated on application lines — the (7,6) charge-to-receipt rows, the (2,3) payment-to-invoice rows, none of it. If you're trying to classify what a posted charge actually was, joining DETAIL.hChargeCode to CHARGTYP returns nothing useful for the vast majority of rows. Charge classification lives one layer up, in CAMCHARG — the generated-billing-line table that sits between CAMRULE (the recurring-charge configuration) and the posted TRANS row. If you need to know what type of charge a transaction represents, go through CAMCHARG, not DETAIL.

This is the general shape of a document-class-side-decode: a column pair whose names suggest a single fixed join target, but whose actual target and meaning depend on a discriminator (here, the iType of the row each half points to) that lives on a different table entirely. It's the same underlying trap as the polymorphic hRecord/hCode pattern elsewhere in the schema, just expressed through a pair of columns instead of one.

Why this matters for the query you're about to write

Get the side-decode wrong and the query still runs. A join through DETAIL.hChargeCode returns an empty or near-empty result set instead of an error — it looks like the charge simply has no code, when really you're reading the wrong column for the document class you're in. A join that assumes HCHKORCHG always means "the charge" will misfire the moment it hits an AP-side row, where the same column position holds a payment instead.

The fix is the same discipline that shows up everywhere in Voyager's schema: identify the document class first — by the iType of what each half of the pair resolves to — and only then decide what the columns mean for that row. There's no shortcut that skips knowing the class up front.

Don't keep this in your head

This is exactly the kind of trap that doesn't show up in a data dictionary, because Voyager's own schema doesn't document it — it's only visible once you've cross-referenced live data and traced how the columns actually get populated per document type. PropETL's SQL Query Assistant carries this as verified knowledge: the DETAIL side-decode, the charge-billing architecture it connects to, and the TRANS.iType map it depends on, all queryable inside Claude instead of re-derived from scratch every time someone hits it.

It ships in every tier. Start the free 7-day trial — no credit card — and ask it to explain what a DETAIL row actually represents before you write the next join.

Featured on FoundrListFeatured & Verified on TrustMRRPropETL on PeerPushLaunchpadly Startup DirectoryPropETL — Featured on LaunchBuck