feat(iif): carry each line's project in CLASS, the classes declared first

The P3 cutover review's open point 1: entries carry projects (classes,
B-37), but the QuickBooks Desktop file (B-34) left the CLASS field empty
on every line, and neither B-34 nor the README said so, though B-34 rule
6 says nothing changes silently.

Intuit's IIF Import Kit (vendored in docs/spec/sources/intuit-iif.md,
the kit's SHA-256 unchanged): the `!CLASS` header help says "Only the
NAME column is required" and a subclass's NAME is its parents' names
first, joined by colons; the kit's "Import First - List Declarations"
example declares `!CLASS` rows before any transaction, and its "2004
Deposit (Income)" example names the class line by line in CLASS. So the
file now declares every class in a `!CLASS` list after the accounts and
before `!TRNS`, and writes each line's class in CLASS (empty where the
line has none). Books with no class write no list, byte for byte as
before. A line whose class wasn't declared is refused
(IifError::UndeclaredClass), never written classless.

A class's NAME follows B-34's rules for an account's own name: plain
ASCII (rule 3), a colon written '-' (it would start a subclass),
trimmed, '?' if nothing is left, cut to 31 characters: Intuit's qbXML
Onscreen Reference gives `ClassAdd`'s Name "maxDT" : 31 (and 31 in the
Canadian, UK and Australian desktop editions; 100 only in Online), the
same as its `AccountAdd` Name (both JSON files vendored by SHA-256);
Conductor's QuickBooks Desktop API reference agrees and says the name is
case-insensitive. Classes whose NAMEs would match ignoring case get
" (id)", round after round, as accounts get " (code)", except that
among names that match, those already written differently from the
books (made safe, or tagged) yield first, a second " (id)" if need be:
a class's own name is kept unless another class's own name is the same
ignoring case (classes 1 "Café", 2 "Cafe", 3 "Cafe (1)" are written
"Cafe (1) (1)", "Cafe", "Cafe (1)"). Each class is tagged twice at most
and ids differ, so none is left alike (a hand-built list is refused). Each class name
written differently is an IifChange (IifField::ClassName), said one
line each in the warnings and in the package README, whose
"written differently" paragraph now names class names' limits; its IIF
line says CLASS is each line's project when there are classes. The MCP
and viewer notes count "account or class name(s)".

API: `iif_classes(ledger)` builds the list; `journal_to_iif` takes it
(`&[]` for none).

Docs: rules.md B-34 rule 8 with its sources; intuit-iif.md (the
`!CLASS` help, the list declarations and deposit examples, Intuit's SDK
department page, the qbXML Onscreen Reference's ClassAdd and AccountAdd
and Conductor's class reference, each with SHA-256);
mcp-interface.md (books.export iif, the package note); the bookspec
example iif-quickbooks-journal-file.toml (two projects: one carried and
renamed, one only declared; every line derived by hand); testing.md;
STATUS (open point 1 done; point 3 waits for the close, the owner said).

Tests: ledger-import
`iif_writes_each_lines_class_and_declares_the_classes_first`,
`iif_class_names_fit_quickbooks_and_every_change_is_listed`,
`iif_class_names_written_as_the_books_have_them_are_kept`,
`iif_refuses_an_undeclared_or_unfit_class`, property
`iif_class_names_never_clash`; ledger-package
`the_package_iif_carries_each_lines_project` and the bookspec example.
Broken to confirm: CLASS written empty fails the two exact-file tests,
the package test and the bookspec example; the " (id)" tag dropped fails
the names test and the property; tagging every name that meets (no
yielding) fails the kept-names test and the property (its shrunk case,
"A-B" beside "A\uFF1AB", kept in the regressions file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-22 19:31:18 -06:00
1 parent 528c070605
commit 6c587086a0
17 files changed
+1026 -89

No files matched your search

+20 -10
View File
@@ -1718,10 +1718,19 @@ posting.
level (QuickBooks gives a subaccount its parent's type); each parent
is declared before its subaccounts; an account number past 7
characters, or written like another's, is left out of ACCNUM; a memo
past 4,095 characters is cut. No line names a customer or vendor (no
past 4,095 characters is cut. Each line's class (project) is in
CLASS, every class declared first in a `!CLASS` list (none: no list),
its name written as an account's own name level is (plain ASCII, a
colon '-', at most 31 characters; classes whose names would then match
get " (id)", a name already written differently from the books (made
safe or tagged) yielding first, so a class's own name stands wherever
it can: "Café" beside "Cafe" is written "Cafe (1)" and "Cafe" is
kept, rules.md B-34 rule 8). No line names a customer or vendor (no
NAME column; see the package's README). `warnings` (absent when
there are none) says what the file writes differently from the books,
for the human: each account's name, number and place a line (`account 6010's
for the human: each account's name, number and place and each
class's name a line (`class 2's name "Café rebrand" is written "Cafe
rebrand"`, `account 6010's
name "Café meals" is written "Cafe meals"`, `account number
"12345678" is left out: QuickBooks Desktop holds 7 characters at
most`); the memos grouped, each thing written differently once with
@@ -2176,14 +2185,15 @@ until the owner upgrades them.
**QuickBooks file** (`general-journal.iif`, B-34): as `books.export`
iif makes it, through `as_of`; `warnings` says what it writes
differently from the books, in the same lines as `books.export` iif's
(account names and numbers one each, the memos grouped; every other
file keeps the books' text), the README lists the same lines (and
names the entries with a line on accounts receivable or payable, to
which QuickBooks Desktop may ask the CPA to add a customer or vendor),
and, when the file writes anything differently, `note` ends "N account name(s), number(s), place(s) or memo(s)
are written differently in general-journal.iif to fit QuickBooks
Desktop (warnings says how; so does the README): tell the human.", N
counting each name, number, place and memo.
(account and class names and account numbers one each, the memos
grouped; every other file keeps the books' text), the README lists the
same lines (and names the entries with a line on accounts receivable or
payable, to which QuickBooks Desktop may ask the CPA to add a customer
or vendor), and, when the file writes anything differently, `note` ends
"N account or class name(s), number(s), place(s) or memo(s) are written
differently in general-journal.iif to fit QuickBooks Desktop (warnings
says how; so does the README): tell the human.", N counting each name,
number, place and memo.
**Receipts** (`include_receipts`, default true): `receipts/` holds every
current file behind each posted entry of the fiscal year to date, filed
on it or linked to it (`books.link_attachment`; a linked file counts as