Files
bookspec/examples/iif-quickbooks-journal-file.toml
swaitsandClaude Opus 5.5 bae14e0bca 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
2026-10-22 19:31:18 -06:00

203 lines
9.1 KiB
TOML

# The journal file QuickBooks Desktop imports (IIF), as of a day. A format,
# not accounting: P13's appendix of interchange formats.
#
# wanderbooks: P3 defects 7 and 21, decision D-36 and the P3 review (the
# file numbered transactions by their place in it, held entries dated after
# the package's as_of, wrote '?' for every character past ASCII, and knew
# nothing of QuickBooks' length limits, its double quote, or its rule that
# a subaccount has its parent's type, and could declare a subaccount
# before its parent; and the cutover review: it left CLASS empty though
# lines carry projects); rules.md B-34; regression tests
# `iif_docnum_is_the_entry_id`,
# `iif_writes_plain_ascii_and_lists_every_change`,
# `iif_fits_quickbooks_desktop_limits`,
# `iif_a_subaccount_of_another_type_stands_at_the_top_level`,
# `iif_declares_each_parent_before_its_subaccounts`,
# `iif_writes_each_lines_class_and_declares_the_classes_first`,
# `iif_class_names_fit_quickbooks_and_every_change_is_listed` (ledger-import
# tests/spec_import.rs) and `as_of_package_iif_stops_at_as_of`
# (ledger-package tests/package.rs).
#
# Setup beyond the accounts: `cash_accounts` are the bank accounts (BANK);
# an account's QuickBooks type otherwise follows its type (asset OCASSET,
# liability OCLIAB, equity EQUITY, revenue INC, expense EXP); an account's
# `parent` is its parent's code; project N in `setup.projects` is class N
# (the books number classes from 1). Event N is entry N (bookspec/README.md).
# Expected values: iif_lines gives the file's lines in order, each a
# string of fields joined by tabs ("\t"), every line ending CR LF in the
# file; iif_changes gives each text written differently from the books,
# in file order (the accounts as declared: by code, each after its
# parent; then the classes; then the entries), as [what, books' text,
# file's text], where what is "name <code>", "number" (left out of ACCNUM
# when the file's text is empty), "top level <code>" (written apart from
# its parent, the books' text: the parent's code), "class <id>" (a
# project's name) or "memo <entry id>".
id = "iif-quickbooks-journal-file"
appendix = "formats"
title = "A QuickBooks Desktop journal file as of a day"
rule = "An IIF journal file as of a day D MUST declare every account first (NAME, ACCNTTYPE, ACCNUM), then hold one GENERAL JOURNAL transaction per posted entry dated on or before D, period closes excepted, in the order posted: a TRNS line and SPL lines, debits positive and credits negative, dated M/D/YYYY, each numbered (DOCNUM) with the entry's id; then ENDTRNS. It MUST be ASCII text, every line ending CR LF, no field holding a tab, a line break or a double quote; an account name level MUST be at most 31 characters and no two NAMEs alike, an account number at most 7 characters and no two alike (else left out), a memo at most 4,095 characters; each parent MUST be declared before its subaccounts, and a subaccount whose QuickBooks type differs from its parent's MUST be written at the top level. Every project MUST be declared as a class (NAME, one level of at most 31 characters, no two alike) after the accounts and before the transactions, and each line's project MUST be its CLASS. Each text written differently from the books MUST be reported."
status = "CHOICE"
sources = [
"Intuit, IIF Overview: import kit, sample files, and headers: IIF files are ASCII text, tab-separated; an account not already in the company file is created as a new bank account (docs/spec/sources/intuit-iif.md)",
"Intuit, IIF Import Kit, IIF Header Help !TRNS, !SPL, !ACCNT: TRNS and SPL lines then ENDTRNS; \"Debit amounts are always positive, credit amounts are always negative\"; DOCNUM is the transaction's number; a subaccount's NAME its parents' names first, joined by colons (docs/spec/sources/intuit-iif.md)",
"Intuit, IIF Import Kit, 2007 General Journal Entry example file: GENERAL JOURNAL, M/D/YYYY, every line ending CR LF (docs/spec/sources/intuit-iif.md)",
"Intuit, Character limitations for fields in QuickBooks (US, updated 8/3/2026): Account Name 31, Account Numbers 7, Memo 4,095 (docs/spec/sources/intuit-iif.md)",
"Intuit, IIF Import Kit, IIF Header Help !CLASS: \"Only the NAME column is required\"; a subclass's NAME its parents' names first, colons between; the kit's list declarations example declares !CLASS before any transaction (docs/spec/sources/intuit-iif.md)",
"Conductor, QuickBooks Desktop API reference, Create a class: a class's name, \"Maximum length: 31 characters\", case-insensitive (docs/spec/sources/intuit-iif.md; not Intuit: Intuit states no class limit)",
"Intuit, Add, edit, or delete accounts in QuickBooks Desktop (updated 8/4/2026): \"The parent and subaccount must be the same account type.\" (docs/spec/sources/intuit-iif.md)",
"Reckon, IIF import changes a double quote to a single one by design; J. Clark (2003): QuickBooks uses double quotes as text delimiters (docs/spec/sources/intuit-iif.md; not Intuit)",
"DECISION D-36 (owner, 2026-10-20): plain ASCII, what isn't ASCII transliterated and listed; CR LF; DOCNUM the entry's id; closes left out (wanderbooks plan/served.md; rules.md B-34)",
]
tags = ["iif", "quickbooks", "as-of", "format", "transliteration", "projects"]
[setup]
currency = "USD"
fiscal_year_start = "01-01"
basis = "cash"
rounding = "half-away-from-zero"
cash_accounts = ["1000"]
[[setup.accounts]]
code = "1090"
name = "Bank accounts"
type = "asset"
# A bank account (BANK) under a header that is no bank (OCASSET): written
# at the top level.
[[setup.accounts]]
code = "1000"
name = "Checking"
type = "asset"
parent = "1090"
[[setup.accounts]]
code = "12345678"
name = "Suspense"
type = "expense"
[[setup.accounts]]
code = "3000"
name = "Owner capital"
type = "equity"
[[setup.accounts]]
code = "4000"
name = "Sales"
type = "revenue"
[[setup.accounts]]
code = "6000"
name = "Café meals"
type = "expense"
# Coded before its parent: declared after it.
[[setup.accounts]]
code = "5900"
name = "Snacks"
type = "expense"
parent = "6000"
[[setup.accounts]]
code = "6100"
name = "Professional services and consulting fees"
type = "expense"
# Class 1: a colon and an em dash, written "-" and "--".
[[setup.projects]]
name = "Café: lunch — rebrand"
# Class 2: declared though no line carries it, as every account is.
[[setup.projects]]
name = "Glaze tests"
# Entry 1.
[[events]]
date = "2026-01-02"
description = "Owner contribution"
lines = [
{ account = "1000", side = "debit", amount = "5000.00" },
{ account = "3000", side = "credit", amount = "5000.00" },
]
# Entry 2. An em dash and double quotes; the meal is project 1's.
[[events]]
date = "2026-01-15"
description = "Lunch — \"team\""
lines = [
{ account = "6000", side = "debit", amount = "42.50", project = "Café: lunch — rebrand" },
{ account = "1000", side = "credit", amount = "42.50" },
]
# Entry 3. An account named past 31 characters.
[[events]]
date = "2026-01-20"
description = "Consulting"
lines = [
{ account = "6100", side = "debit", amount = "300.00" },
{ account = "1000", side = "credit", amount = "300.00" },
]
# Entry 4, dated the day itself: in.
[[events]]
date = "2026-02-01"
description = "Sale"
lines = [
{ account = "1000", side = "debit", amount = "1000.00" },
{ account = "4000", side = "credit", amount = "1000.00" },
]
# Entry 5, the day after: out.
[[events]]
date = "2026-02-02"
description = "Second sale"
lines = [
{ account = "1000", side = "debit", amount = "250.00" },
{ account = "4000", side = "credit", amount = "250.00" },
]
[[expect]]
query = "iif_lines"
as_of = "2026-02-01"
values = [
"!ACCNT\tNAME\tACCNTTYPE\tACCNUM",
"ACCNT\tChecking\tBANK\t1000",
"ACCNT\tBank accounts\tOCASSET\t1090",
"ACCNT\tSuspense\tEXP\t",
"ACCNT\tOwner capital\tEQUITY\t3000",
"ACCNT\tSales\tINC\t4000",
"ACCNT\tCafe meals\tEXP\t6000",
"ACCNT\tCafe meals:Snacks\tEXP\t5900",
"ACCNT\tProfessional services and consu\tEXP\t6100",
"!CLASS\tNAME",
"CLASS\tCafe- lunch -- rebrand",
"CLASS\tGlaze tests",
"!TRNS\tTRNSID\tTRNSTYPE\tDATE\tACCNT\tCLASS\tAMOUNT\tDOCNUM\tMEMO",
"!SPL\tSPLID\tTRNSTYPE\tDATE\tACCNT\tCLASS\tAMOUNT\tDOCNUM\tMEMO",
"!ENDTRNS",
"TRNS\t\tGENERAL JOURNAL\t1/2/2026\tChecking\t\t5000.00\t1\tOwner contribution",
"SPL\t\tGENERAL JOURNAL\t1/2/2026\tOwner capital\t\t-5000.00\t1\tOwner contribution",
"ENDTRNS",
"TRNS\t\tGENERAL JOURNAL\t1/15/2026\tCafe meals\tCafe- lunch -- rebrand\t42.50\t2\tLunch -- 'team'",
"SPL\t\tGENERAL JOURNAL\t1/15/2026\tChecking\t\t-42.50\t2\tLunch -- 'team'",
"ENDTRNS",
"TRNS\t\tGENERAL JOURNAL\t1/20/2026\tProfessional services and consu\t\t300.00\t3\tConsulting",
"SPL\t\tGENERAL JOURNAL\t1/20/2026\tChecking\t\t-300.00\t3\tConsulting",
"ENDTRNS",
"TRNS\t\tGENERAL JOURNAL\t2/1/2026\tChecking\t\t1000.00\t4\tSale",
"SPL\t\tGENERAL JOURNAL\t2/1/2026\tSales\t\t-1000.00\t4\tSale",
"ENDTRNS",
]
[[expect]]
query = "iif_changes"
as_of = "2026-02-01"
values = [
["top level 1000", "1090", ""],
["number", "12345678", ""],
["name 6000", "Café meals", "Cafe meals"],
["name 6100", "Professional services and consulting fees", "Professional services and consu"],
["class 1", "Café: lunch — rebrand", "Cafe- lunch -- rebrand"],
["memo 2", "Lunch — \"team\"", "Lunch -- 'team'"],
]