P3: the IIF in plain ASCII with every change listed, and the CPA package as of one day (defects 7, 21)

The CPA package's general-journal.iif now holds every posted entry through
as_of (closing entries left out, D-36), written as plain ASCII with CR LF,
DOCNUM the entry id, QuickBooks Desktop's field limits and subaccount types
honoured, and every character written differently listed in the export's
warnings and the README (grouped by substitution, TINs masked). The 1099
worksheet stops at as_of too. PDFs print CJK, Arabic, Hebrew, Thai and
Devanagari (Noto fallbacks, OFL), and so does the viewer. has_full_tin reads
Unicode separators.

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-21 09:35:32 -06:00
1 parent 1e8f9275b9
commit 2c29d48ab9
48 files changed
+5661 -317

No files matched your search

@@ -3,3 +3,4 @@
- books.export_cpa_package carries every member's and 1099 payee's full TIN from the vault (the K-1 worksheet and the 1099 worksheet). It hands the zip over: run its download command with the folder the human wants it in (books served by `wanderbooks mcp`), then tell them where it is. The viewer's "Export for CPA" makes the same package.
- Never file the package, or a file from it, into the books.
- tin_gaps lists each member or payee whose TIN it lacks and why: collect them (books.set_tin, books.set_w9) and export again.
- Everything in it is as of its as_of: the 1099 worksheet counts the year's payments through that day, and general-journal.iif (the QuickBooks Desktop file) every posted entry from the first through it, closing entries left out. That file is plain ASCII within QuickBooks Desktop's limits: warnings (and the README) say what it writes differently from the books ("Café" as "Cafe", a name cut to 31 characters, a subaccount of another type at the top level, the memos grouped); tell the human. It names no customer or vendor: the README lists the entries on accounts receivable or payable, where QuickBooks may ask the CPA for one.
+56 -23
View File
@@ -557,7 +557,7 @@ fn vendor_1099_report(
year: i32,
threshold: Option<Decimal>,
) -> Result<ledger_core::Vendor1099Report, ledger_core::LedgerError> {
ledger_package::vendor_1099_report(ledger, &package_settings(settings), year, threshold)
ledger_package::vendor_1099_report(ledger, &package_settings(settings), year, None, threshold)
}
impl BooksServer {
@@ -5330,7 +5330,7 @@ impl BooksServer {
#[tool(
name = "books.export",
title = "Export a report",
description = "Export a report. Reports: profit_and_loss, balance_sheet, cash_flow (needs cash_accounts), trial_balance (as_of), members_equity (multi-member LLC; members from the settings), general_ledger (by account: running balances, splits), general_journal (every entry in date order; alias journal), fixed_asset_register (as_of: every asset with tag, serial, cost, depreciation, book value, reconciled to the books). Formats: pdf and xlsx (base64) for the five statements and fixed_asset_register; xlsx, csv, json for every report; iif for general_journal (QuickBooks Desktop). Dates: start/end for period reports (start defaults to the first entry, end to today), as_of for balance_sheet, trial_balance, and fixed_asset_register.",
description = "Export a report. Reports: profit_and_loss, balance_sheet, cash_flow (needs cash_accounts), trial_balance (as_of), members_equity (multi-member LLC; members from the settings), general_ledger (by account: running balances, splits), general_journal (every entry in date order; alias journal), fixed_asset_register (as_of: every asset with tag, serial, cost, depreciation, book value, reconciled to the books). Formats: pdf and xlsx (base64) for the five statements and fixed_asset_register; xlsx, csv, json for every report; iif for general_journal (QuickBooks Desktop: plain ASCII text, CR LF; every posted entry from the first through as_of, period closes left out, DOCNUM the entry id; names, numbers and memos fit QuickBooks' limits; warnings says what is written differently from the books, memos grouped, to tell the human). Dates: start/end for period reports (start defaults to the first entry, end to today), as_of for balance_sheet, trial_balance, fixed_asset_register, and iif (default today).",
annotations(read_only_hint = true, open_world_hint = false)
)]
async fn export(&self, params: Parameters<ExportParams>) -> Result<Json<ExportOut>, String> {
@@ -5343,8 +5343,30 @@ impl BooksServer {
));
}
let report = p.report.to_lowercase();
if format == "iif" && report != "journal" {
return Err("iif format is only valid for the journal report".to_string());
if format == "iif" {
if !matches!(report.as_str(), "journal" | "general_journal") {
return Err(
"iif format is only valid for the general_journal report (alias journal)"
.to_string(),
);
}
if p.start.is_some() || p.end.is_some() {
return Err("iif takes as_of, not start or end: the file holds every posted entry from the first through as_of, so QuickBooks' balances match the books'".to_string());
}
// As of today by default, like every other as-of export.
let as_of = match p.as_of.as_deref() {
Some(s) => parse_date(s)?,
None => today(),
};
let iif = self.export_iif(as_of, &p.cash_accounts)?;
let warnings = iif.warnings();
return Ok(Json(ExportOut {
format,
report,
content: iif.text,
content_encoding: None,
warnings,
}));
}
if format == "xlsx" || format == "pdf" {
let bytes = if format == "xlsx" {
@@ -5357,15 +5379,7 @@ impl BooksServer {
report,
content: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes),
content_encoding: Some("base64".to_string()),
}));
}
if format == "iif" {
let content = self.export_iif()?;
return Ok(Json(ExportOut {
format,
report,
content,
content_encoding: None,
warnings: Vec::new(),
}));
}
let content = match report.as_str() {
@@ -5392,6 +5406,7 @@ impl BooksServer {
report,
content,
content_encoding: None,
warnings: Vec::new(),
}))
}
@@ -5508,15 +5523,25 @@ impl BooksServer {
ledger_import::statement_pdf(&st, &meta).map_err(|e| e.to_string())
}
/// Render the journal as IIF text via ledger-import, declaring every
/// account with its QuickBooks type: the `[books] cash_accounts` are
/// BANK, the receivable and payable control accounts AR and AP, a
/// registered asset's cost and accumulated depreciation FIXASSET, and
/// the rest follow their wanderbooks type (other current asset, other
/// current liability, equity, income, expense).
fn export_iif(&self) -> Result<String, String> {
/// The journal through `as_of` as IIF via ledger-package (the CPA
/// package's own general-journal.iif): the cash accounts (`cash`, else
/// the settings') BANK, the receivable and payable control accounts AR
/// and AP, the accounts of an asset bought by `as_of` FIXASSET, the
/// rest by their wanderbooks type; every posted entry from the first,
/// closes left out.
fn export_iif(
&self,
as_of: chrono::NaiveDate,
cash: &[String],
) -> Result<ledger_import::Iif, String> {
let store = self.lock()?;
ledger_package::export_iif(store.ledger(), &self.settings().roles.cash)
let settings = self.settings();
let cash = if cash.is_empty() {
settings.roles.cash.as_slice()
} else {
cash
};
ledger_package::export_iif(store.ledger(), cash, as_of)
}
// ------------------------------------------------------------------
@@ -8073,7 +8098,7 @@ impl BooksServer {
#[tool(
name = "books.export_cpa_package",
title = "CPA handoff package",
description = "Build the CPA handoff package as a zip: the profit and loss, balance sheet, cash flow, statement of changes in members' equity (multi-member LLCs), tax summary, and trial balance, each as PDF and XLSX; the fixed asset register as PDF and XLSX when there are fixed assets; the general ledger (by account) and general journal (entry by entry) as XLSX; the journal as IIF (imports into QuickBooks Desktop); the 1099 worksheet as CSV; with members, the K-1 worksheet (k1-members.csv: per member their party, share, capital account, TIN, and beginning capital, contributions in cash and in property, share of income, distributions, and ending capital for the fiscal year to date); every receipt behind the year's entries, filed on them or linked to them (receipts/, with an index.csv tying each file to its entry) and the company's documents (documents/), unless include_receipts is false; and a README with the journal's verified hash. By default (include_tins) the K-1 and 1099 worksheets carry every member's and payee's full TIN from the TIN vault; include_tins: false, last fours only. The zip is handed over: run its download command to save it in a folder (books served by `wanderbooks mcp`; over stdio it comes back inline). Never file it into the books. A locked vault or a missing TIN doesn't stop the package: tin_gaps lists each one (collect them with books.set_tin or books.set_w9 and export again). cash_accounts (e.g. [\"1000\"]) is required: the cash flow statement reconciles to them. as_of defaults to today; period reports cover the fiscal year to date.",
description = "Build the CPA handoff package as a zip: the profit and loss, balance sheet, cash flow, statement of changes in members' equity (multi-member LLCs), tax summary, and trial balance, each as PDF and XLSX; the fixed asset register as PDF and XLSX when there are fixed assets; the general ledger (by account) and general journal (entry by entry) as XLSX; the journal through as_of as IIF (imports into QuickBooks Desktop; plain ASCII within its limits: warnings says what is written differently from the books, and so does the README); the 1099 worksheet as CSV (payments from January 1 through as_of); with members, the K-1 worksheet (k1-members.csv: per member their party, share, capital account, TIN, and beginning capital, contributions in cash and in property, share of income, distributions, and ending capital for the fiscal year to date); every receipt behind the year's entries, filed on them or linked to them (receipts/, with an index.csv tying each file to its entry) and the company's documents (documents/), unless include_receipts is false; and a README with the journal's verified hash. By default (include_tins) the K-1 and 1099 worksheets carry every member's and payee's full TIN from the TIN vault; include_tins: false, last fours only. The zip is handed over: run its download command to save it in a folder (books served by `wanderbooks mcp`; over stdio it comes back inline). Never file it into the books. A locked vault or a missing TIN doesn't stop the package: tin_gaps lists each one (collect them with books.set_tin or books.set_w9 and export again). cash_accounts (e.g. [\"1000\"]) is required: the cash flow statement reconciles to them. as_of defaults to today; period reports cover the fiscal year to date.",
annotations(
read_only_hint = false,
destructive_hint = false,
@@ -8133,18 +8158,26 @@ impl BooksServer {
tin_files::record(&vault_dir, &package.zip)?;
}
let tin_gaps = package.tin_gaps;
let note = match (p.include_tins, tin_gaps.len()) {
let mut note = match (p.include_tins, tin_gaps.len()) {
(false, _) => "The package, with last fours only.".to_string(),
(true, 0) => "The package, with full TINs, for the CPA.".to_string(),
(true, n) => format!(
"The package, with full TINs, for the CPA. {n} TIN(s) are missing (tin_gaps): collect them with books.set_tin or books.set_w9 and export again."
),
};
let warnings = package.warnings;
if package.iif_changes > 0 {
note.push_str(&format!(
" {} 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.",
package.iif_changes
));
}
let file = self.hand_over(&book, &package.file_name, "application/zip", package.zip)?;
Ok(Json(ExportCpaPackageOut {
file,
tins_included: p.include_tins,
tin_gaps,
warnings,
note,
}))
}
+23 -4
View File
@@ -1682,7 +1682,8 @@ pub struct StaleRule {
pub struct ExportParams {
/// "csv", "json", "xlsx", "pdf", or "iif". XLSX and PDF are
/// base64-encoded; PDF covers profit_and_loss, balance_sheet, and
/// cash_flow; IIF is only valid with report "journal".
/// cash_flow; IIF (plain ASCII text) is only valid with report
/// "general_journal" (alias "journal").
pub format: String,
/// "journal", "trial_balance", "profit_and_loss", "balance_sheet", or
/// "cash_flow".
@@ -1691,10 +1692,13 @@ pub struct ExportParams {
pub start: Option<String>,
/// Period end for profit_and_loss and cash_flow, YYYY-MM-DD.
pub end: Option<String>,
/// As-of date for balance_sheet, YYYY-MM-DD.
/// As-of date for balance_sheet, trial_balance, fixed_asset_register,
/// and iif (every posted entry dated on or before it), YYYY-MM-DD.
/// Defaults to today.
pub as_of: Option<String>,
/// Cash account codes for cash_flow. Defaults to `[books]
/// cash_accounts`; one or the other is required for that report.
/// Cash account codes for cash_flow, and for iif its BANK accounts.
/// Defaults to `[books] cash_accounts`; one or the other is required
/// for cash_flow.
#[serde(default)]
pub cash_accounts: Vec<String>,
/// Liability account codes treated as financing for cash_flow.
@@ -1721,6 +1725,16 @@ pub struct ExportOut {
/// "base64" for XLSX and PDF, otherwise absent.
#[serde(skip_serializing_if = "Option::is_none")]
pub content_encoding: Option<String>,
/// IIF: what the file writes differently from the books to fit
/// QuickBooks Desktop (plain ASCII; account names at most 31
/// characters a level, numbers 7, memos 4,095; no two names or numbers
/// alike; a subaccount of another type than its parent's at the top
/// level): each account's name, number and place a line, and each thing written
/// differently in the memos a line naming the entries it is in
/// (`"—" is written "--" in the memos of entries 3, 5, 9`). No full
/// TIN. Tell the human. Absent when there are none.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
// ---------------------------------------------------------------------------
@@ -2668,6 +2682,11 @@ pub struct ExportCpaPackageOut {
/// with books.set_tin or books.set_w9.
#[serde(default)]
pub tin_gaps: Vec<String>,
/// What general-journal.iif writes differently from the books, as
/// `books.export` iif's `warnings` says it (the memos grouped); the
/// README lists them too. Absent when there are none.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
/// What to tell the human.
pub note: String,
}
+15 -3
View File
@@ -1185,15 +1185,27 @@ fn mcp_import_export_over_stdio() {
let chart = mcp.call(
"books.import_chart_csv",
json!({"csv_text": "code,name,type,parent_code\n1000,Cash,asset,\n4000,Revenue,revenue,\n"}),
json!({"csv_text": "code,name,type,parent_code\n1000,Cash,asset,\n4000,Revenue,revenue,\n6010,Café 北京,expense,\n"}),
);
assert_eq!(chart["count"], 2);
assert_eq!(chart["count"], 3);
// The IIF is plain ASCII text with CR LF line endings, inline; what it
// writes differently from the books is in warnings (rules.md B-34).
let iif = mcp.call(
"books.export",
json!({"format": "iif", "report": "journal"}),
);
assert!(iif["content"].as_str().unwrap().contains("!TRNS"));
let text = iif["content"].as_str().unwrap();
assert!(text.contains("!TRNS") && text.is_ascii() && text.ends_with("\r\n"));
assert!(
text.contains("\r\nACCNT\tCafe Bei Jing\tEXP\t6010\r\n"),
"{text}"
);
assert!(iif.get("content_encoding").is_none(), "{iif}");
assert_eq!(
iif["warnings"],
json!(["account 6010's name \"Café 北京\" is written \"Cafe Bei Jing\""])
);
let xlsx = mcp.call(
"books.export",
+157 -9
View File
@@ -1223,23 +1223,57 @@ fn xlsx_and_pdf_exports_are_real_files_with_the_books_in_them() {
/// IIF: accounts declared with QuickBooks types per the documented mapping
/// (cash accounts BANK, 1200 AR, 2000 AP, a registered asset's accounts
/// FIXASSET, the rest by type), then one balanced TRNS/SPL/ENDTRNS block
/// per posted entry. Kills: export_iif's AR and AP comparisons.
/// per posted entry dated on or before as_of, numbered (DOCNUM) with its
/// entry id (rules.md B-33, B-34); plain ASCII text with CR LF, what it
/// writes differently (a line break and double quotes, the engine's own em
/// dashes) in warnings, grouped. Kills: export_iif's AR and AP comparisons.
#[test]
fn iif_declares_accounts_by_role_and_balances_every_transaction() {
let dir = test_dir("iif");
let mut s = new_store(&dir);
rich_books(&mut s);
let posted = s.ledger().posted_entries().count();
let ledger = s.ledger().clone();
drop(s);
let mut m = Mcp::start(&dir, "1000000.00");
let iif = String::from_utf8(m.export(json!({"format": "iif", "report": "journal"}))).unwrap();
let out = m.ok(
"books.export",
json!({"format": "iif", "report": "journal", "as_of": END}),
);
assert!(out.get("content_encoding").is_none(), "plain text: {out}");
let lunch = ledger
.posted_entries()
.find(|e| e.description().starts_with("Lunch"))
.unwrap()
.id();
// The chart's `Meals, "client"`, its double quotes written single;
// then the memos, grouped by what is written differently: the lunch's
// double quotes and line break, and the em dashes of every
// depreciation memo in one line.
let months: Vec<String> = ledger
.posted_entries()
.filter(|e| e.description().starts_with("Depreciation"))
.map(|e| e.id().to_string())
.collect();
assert_eq!(months.len(), 3);
let warnings = vec![
"account 6010's name \"Meals, \\\"client\\\"\" is written \"Meals, 'client'\"".to_string(),
format!("\"\\\"\" is written \"'\" in the memo of entry {lunch}"),
format!("\"\\\"\\n\" is written \"' \" in the memo of entry {lunch}"),
format!(
"\"—\" is written \"--\" in the memos of entries {}",
months.join(", ")
),
];
assert_eq!(out["warnings"], json!(warnings));
let iif = out["content"].as_str().unwrap();
assert!(iif.ends_with("\r\n") && !iif.replace("\r\n", "").contains(['\r', '\n']));
let mut types: HashMap<String, String> = HashMap::new();
let mut header: Vec<&str> = Vec::new();
let mut blocks = 0;
let mut docnums: Vec<u64> = Vec::new();
let mut sum = Decimal::ZERO;
let mut in_trns = false;
for line in iif.lines() {
for line in iif.split("\r\n") {
let f: Vec<&str> = line.split('\t').collect();
match f[0] {
"!ACCNT" => header = f.clone(),
@@ -1252,19 +1286,29 @@ fn iif_declares_accounts_by_role_and_balances_every_transaction() {
assert!(!in_trns, "TRNS inside a transaction");
in_trns = true;
sum = Decimal::ZERO;
docnums.push(f[7].parse().unwrap());
}
assert_eq!(
f[7],
docnums.last().unwrap().to_string(),
"DOCNUM on every line"
);
sum += dec(f[6]);
}
"ENDTRNS" => {
assert!(in_trns);
assert_eq!(sum, Decimal::ZERO, "an IIF transaction sums to zero");
in_trns = false;
blocks += 1;
}
_ => {}
}
}
assert_eq!(blocks, posted, "a transaction per posted entry");
let through: Vec<u64> = ledger
.posted_entries()
.filter(|e| e.date() <= d(END))
.map(|e| e.id())
.collect();
assert_eq!(docnums, through, "every posted entry through as_of, by id");
for (code, ty) in [
("1000", "BANK"),
("1200", "AR"),
@@ -1283,6 +1327,69 @@ fn iif_declares_accounts_by_role_and_balances_every_transaction() {
"account {code}'s IIF type"
);
}
// general_journal is the report's name; journal its alias (both work).
let alias = m.ok(
"books.export",
json!({"format": "iif", "report": "general_journal", "as_of": END}),
);
assert_eq!(alias["content"], out["content"]);
// The file holds everything from the first entry: a range is refused,
// and so is another report.
let why = m.refuse(
"books.export",
json!({"format": "iif", "report": "journal", "start": START, "end": END}),
);
assert!(why.contains("iif takes as_of, not start or end"), "{why}");
let why = m.refuse(
"books.export",
json!({"format": "iif", "report": "trial_balance"}),
);
assert!(
why.contains("only valid for the general_journal report"),
"{why}"
);
}
/// P3 review R5: books.export iif without as_of is as of today, like the
/// trial balance: an entry dated later is left out, and the two exports
/// made with no dates tie, account by account.
#[test]
fn iif_without_as_of_is_as_of_today_and_ties_to_the_trial_balance() {
let dir = test_dir("iif-today");
let mut s = new_store(&dir);
rich_books(&mut s);
post(
&mut s,
"2099-01-04",
"Far future rent",
&[("6000", Dr, "1234.00"), ("1000", Cr, "1234.00")],
);
drop(s);
let mut m = Mcp::start(&dir, "1000000.00");
let iif = m.export_text(json!({"format": "iif", "report": "journal"}));
assert!(!iif.contains("Far future rent"));
let mut number: HashMap<String, String> = HashMap::new();
let mut iif_bal: BTreeMap<String, Decimal> = BTreeMap::new();
for line in iif.split("\r\n") {
let f: Vec<&str> = line.split('\t').collect();
match f[0] {
"ACCNT" => {
number.insert(f[1].to_string(), f[3].to_string());
}
"TRNS" | "SPL" => *iif_bal.entry(number[f[4]].clone()).or_default() += dec(f[6]),
_ => {}
}
}
iif_bal.retain(|_, v| !v.is_zero());
let tb = m.export_text(json!({"format": "csv", "report": "trial_balance"}));
let mut tb_bal: BTreeMap<String, Decimal> = BTreeMap::new();
for row in csv_rows(&tb, &["code", "name", "debit", "credit"]) {
if row[0] != "TOTAL" {
tb_bal.insert(row[0].clone(), dec(&row[2]) - dec(&row[3]));
}
}
tb_bal.retain(|_, v| !v.is_zero());
assert_eq!(iif_bal, tb_bal);
}
// ------------------------------------------------- P&L by class
@@ -2767,7 +2874,9 @@ fn previews_move_balances_by_normal_side() {
/// the receipts behind the year's posted entries (and only those), indexed
/// with each entry's date, description, and debits; the company's
/// documents but not loose receipts or withdrawn files; a README counting
/// the receipts; and a 1099 worksheet equal to `vendor_1099_report`. With
/// the receipts; a 1099 worksheet equal to `vendor_1099_report` through
/// as_of (a payment after it to the same contractor left out); and the
/// journal as IIF from the first entry through as_of. With
/// include_receipts false, no evidence. Kills: export_cpa_package's
/// receipt and document guards, the receipt count, the file-name cleaner.
#[test]
@@ -2775,6 +2884,8 @@ fn cpa_package_holds_the_year_and_its_evidence() {
let dir = test_dir("cpa");
let mut s = new_store(&dir);
let r = rich_books(&mut s);
// The payment after as_of went to the contractor too.
s.set_entry_vendor(r.after, Some(r.contractor)).unwrap();
// Two receipts on the rent, same-length names.
let rent_a: &[u8] = b"rent receipt A";
let rent_b: &[u8] = b"rent receipt B";
@@ -2879,6 +2990,29 @@ fn cpa_package_holds_the_year_and_its_evidence() {
let gj = xlsx_text(&files["general-journal.xlsx"]);
assert!(gj.contains("Rent January") && !gj.contains("Prior-year sale"));
assert!(!gj.contains("After mid-year"), "fiscal year to as_of only");
// The IIF: from the first entry (the prior year's sale) through as_of.
let iif = std::str::from_utf8(&files["general-journal.iif"]).unwrap();
assert!(iif.contains("\tPrior-year sale\r\n") && iif.contains("\tRent January\r\n"));
assert!(!iif.contains("After mid-year"), "through as_of only");
assert!(iif.contains("\tLunch, 'team' quarterly\r\n"));
// The meals account's double quotes, the lunch's and its line break,
// and the em dashes of the depreciation memos: written differently, so
// said, the memos grouped (four lines for one name and four memos), in
// warnings and the README alike.
let warnings: Vec<String> = out["warnings"]
.as_array()
.unwrap()
.iter()
.map(|w| w.as_str().unwrap().to_string())
.collect();
assert_eq!(warnings.len(), 4, "{out}");
let readme = std::str::from_utf8(&files["README.txt"]).unwrap();
for w in &warnings {
assert!(readme.contains(&format!("\n- {w}\n")), "{w} in {readme}");
}
assert!(out["note"].as_str().unwrap().ends_with(
" 5 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."
));
// Receipts: exactly the two behind the rent.
let receipt_files: Vec<&str> = names
@@ -2940,8 +3074,22 @@ fn cpa_package_holds_the_year_and_its_evidence() {
std::str::from_utf8(&files["1099-worksheet-2025.csv"]).unwrap(),
&["vendor", "form", "w9_on_file", "paid", "reportable"],
);
let want = ledger_core::vendor_1099_report(&ledger, 2025, None).unwrap();
let want = ledger_core::vendor_1099_report_with_members(
&ledger,
2025,
Some(d(END)),
None,
&ledger_core::PaymentAccounts::default(),
&[],
)
.unwrap();
assert_eq!(w.len(), want.vendors.len());
let contractor = w.iter().find(|row| row[0] == "Contractor").unwrap();
assert_eq!(
amt(&contractor[3]),
dec("900.00"),
"the payment after as_of is left out"
);
for (row, v) in w.iter().zip(&want.vendors) {
assert_eq!(row[0], v.name);
assert_eq!(row[1], v.form.map(|f| f.label()).unwrap_or(""));
+6 -1
View File
@@ -1261,7 +1261,6 @@ fn happy_path(name: &str) -> Mcp {
("xlsx", "balance_sheet"),
("pdf", "profit_and_loss"),
("csv", "cash_flow"),
("iif", "journal"),
("csv", "general_ledger"),
("json", "members_equity"),
("csv", "tax_summary"),
@@ -1271,6 +1270,12 @@ fn happy_path(name: &str) -> Mcp {
json!({"format": format, "report": report, "start": "2026-01-01", "end": "2026-12-31"}),
);
}
// The IIF takes as_of (every posted entry from the first through it),
// never a range.
m.ok(
"books.export",
json!({"format": "iif", "report": "general_journal", "as_of": "2026-12-31"}),
);
m.ok(
"books.export_cpa_package",
json!({"as_of": "2026-12-31", "include_receipts": false, "include_tins": false}),