P3: dated 1099 decisions, payment methods and the card exception (defects 23, 12, 18, D-40)
check / check (push) Canceled after 0s

A vendor's 1099 decision is its own dated fact (files NEC or MISC, not
reportable with its reason, or undetermined), applied by each payment's
date. Payment methods are recorded; card and network payments are left
out and listed; a member's check through financing counts; a bank payment
with no method counts (D-40); $2,000 for payments after 2025. One
worksheet for the tool, the viewer and the CPA package.

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 12:12:26 -06:00
1 parent c2ceabbae9
commit 1304b9c0ad
77 files changed
+11455 -970

No files matched your search

+3 -2
View File
@@ -41,6 +41,7 @@ pub use reports::{
pub use text::{csv_escape, money, parse_date, share_text};
pub use tins::{TinSource, tin_kind, tin_type_key, vault_tin};
pub use vendor_1099::{
Vendor1099Worksheet, Vendor1099WorksheetLine, form_1099_label, payment_accounts,
vendor_1099_csv, vendor_1099_csv_name, vendor_1099_report, vendor_1099_worksheet,
VENDOR_1099_CSV_HEADER, Vendor1099Worksheet, Vendor1099WorksheetLine, form_1099_label,
payment_accounts, vendor_1099_csv, vendor_1099_csv_name, vendor_1099_report,
vendor_1099_worksheet,
};
+31 -76
View File
@@ -15,18 +15,20 @@
//! XLSX, for the fiscal year containing as_of, through as_of; the
//! fixed asset register when an asset was bought by as_of; the
//! general ledger and journal (XLSX); the journal through as_of as
//! IIF; the 1099 worksheet for as_of's calendar year through as_of
//! (or why it can't be made); with members, the K-1 worksheet; a
//! README first.
//! IIF; the 1099 worksheet for as_of's calendar year through as_of,
//! books.vendor_1099's for the package's cash and financing accounts
//! (INVARIANT 7), or why it can't be made; with members, the K-1
//! worksheet; a README first.
//! 2. With members, k1-members.csv has one row per member: name, party,
//! share, capital account, TIN and type (or last four without a TIN
//! source), and the K-1 worksheet's capital figures (ledger_core::k1)
//! for the fiscal year to date; the members' total row with more than
//! one. The 1099 worksheet adds each payee's party, last four, TIN,
//! and type with a TIN source.
//! one. The 1099 worksheet is books.vendor_1099's CSV
//! (crate::vendor_1099_csv), each payee's TIN and type added with a
//! TIN source.
//! 3. A TIN the source can't give never fails the package: the row says
//! why, and so do the README and tin_gaps (members, and 1099 payees
//! that are reportable).
//! a 1099 is or may be due for).
//! 4. With include_receipts: every current file behind the year's
//! posted entries, filed on them or linked to them, once per entry,
//! each with a row in receipts/index.csv; and the company's
@@ -62,7 +64,7 @@ use crate::reports::{
};
use crate::text::{csv_escape, money, share_text};
use crate::tins::{TinSource, tin_type_key};
use crate::vendor_1099::vendor_1099_report;
use crate::vendor_1099::{vendor_1099_csv, vendor_1099_csv_name, vendor_1099_worksheet};
/// What the package is built for.
#[derive(Clone, Copy)]
@@ -95,8 +97,8 @@ pub struct Package {
pub files: Vec<(String, Vec<u8>)>,
/// The zip.
pub zip: Vec<u8>,
/// With a TIN source: each member or reportable 1099 payee whose TIN
/// the package lacks, and why (INVARIANT 3).
/// With a TIN source: each member, or 1099 payee a 1099 is or may be
/// due for, whose TIN the package lacks, and why (INVARIANT 3).
pub tin_gaps: Vec<String>,
/// The receipts in receipts/ (one per entry they are behind).
pub receipt_count: usize,
@@ -138,16 +140,8 @@ pub fn build_package<B: PackageBooks + ?Sized>(
let pdf = |st: &ledger_import::Statement| statement_pdf(st, &meta).map_err(|e| e.to_string());
let members = report_members(company);
// One list of each per package (INVARIANT 7).
let cash_codes = if req.cash_accounts.is_empty() {
settings.cash_accounts
} else {
req.cash_accounts
};
let financing_codes = if req.financing_accounts.is_empty() {
settings.financing_accounts
} else {
req.financing_accounts
};
let payments = settings.with_accounts(req.cash_accounts, req.financing_accounts);
let (cash_codes, financing_codes) = (payments.cash_accounts, payments.financing_accounts);
// The financial statements, each as PDF and XLSX.
let mut statements: Vec<(&str, ledger_import::Statement)> = vec![
@@ -373,68 +367,29 @@ pub fn build_package<B: PackageBooks + ?Sized>(
));
// 1099 worksheet for the calendar year of as_of, through as_of
// (INVARIANT 6): the payments from January 1, out of the package's
// cash and financing accounts (INVARIANT 7).
// cash and financing accounts (INVARIANT 7), the same worksheet and
// CSV books.vendor_1099 and the viewer hand over for those accounts
// and that day (one builder, one figure).
let year = chrono::Datelike::year(&as_of);
let mut worksheet_1099 = false;
let payments = Settings {
cash_accounts: cash_codes,
financing_accounts: financing_codes,
..*settings
};
match vendor_1099_report(ledger, &payments, year, Some(as_of), None) {
Ok(r1099) => {
let mut w = String::from("vendor,form,w9_on_file,paid,reportable");
if tins.is_some() {
w.push_str(",party_id,tin_last4,tin,tin_type,tin_note");
}
w.push('\n');
for v in &r1099.vendors {
w.push_str(&format!(
"\"{}\",{},{},{},{}",
v.name.replace('"', "\"\""),
v.form.map(|f| f.label()).unwrap_or(""),
v.tin_on_file,
money(v.paid),
v.reportable
));
// The payee's full TIN from the source (INVARIANT 2).
if let Some(tins) = tins {
let party = ledger.party_of_vendor(v.vendor_id);
let last4 = party
.and_then(|id| ledger.get_party(id))
.and_then(|p| p.tin_last4())
.map(str::to_string);
let got = match party {
None => Err("TIN not in the vault: no party for this vendor".into()),
Some(id) => tins(id),
};
let (tin, kind, note) = match got {
Ok(t) => (
t.formatted(),
tin_type_key(t.kind).to_string(),
String::new(),
),
Err(why) => {
if v.reportable {
tin_gaps.push(format!("1099 payee {}: {why}", v.name));
}
(String::new(), String::new(), why)
}
};
w.push_str(&format!(
",{},{},{tin},{kind},{}",
party.map(|id| id.to_string()).unwrap_or_default(),
last4.unwrap_or_default(),
csv_escape(&note)
));
match vendor_1099_worksheet(ledger, &payments, year, None, Some(as_of), tins) {
Ok(w) => {
// A payee a 1099 is or may be due for, whose TIN the package
// lacks (INVARIANT 3).
for l in w.lines.iter().filter(|l| l.may_file()) {
if let Some(why) = &l.tin_note {
tin_gaps.push(format!("1099 payee {}: {why}", l.name));
}
w.push('\n');
}
files.push((format!("1099-worksheet-{year}.csv"), w.into_bytes()));
files.push((
vendor_1099_csv_name(year, false),
vendor_1099_csv(&w, tins.is_some()).into_bytes(),
));
worksheet_1099 = true;
}
// The threshold isn't known for this year: never guess it
// (rules.md B-19); say so in the package instead.
// The worksheet can't be made (the year's threshold isn't known,
// or the package names no cash accounts): never guess (rules.md
// B-19); say so in the package instead.
Err(e) => files.push((
format!("1099-worksheet-{year}-NOT-INCLUDED.txt"),
format!("{e}\n").into_bytes(),
@@ -611,7 +566,7 @@ pub fn build_package<B: PackageBooks + ?Sized>(
},
worksheet = if worksheet_1099 {
format!(
"1099-worksheet-{year}.csv: cash paid per vendor marked for a 1099{}, against the year's threshold{}.\n",
"1099-worksheet-{year}.csv: each vendor paid in {year}{} or decided for a 1099: what counts toward its 1099 against the year's threshold, what was paid by card or payment network (reported by the processor on Form 1099-K, Treas. Reg. §1.6041-1(a)(1)(iv)), what can't be classified until its payment method is recorded, and its 1099 decision with the reason, who made it, and when it took effect{}.\n",
if year_end {
String::new()
} else {
+23
View File
@@ -48,6 +48,29 @@ pub struct Settings<'a> {
pub investing_accounts: &'a [String],
}
impl<'a> Settings<'a> {
/// These settings with the cash and financing accounts a request
/// names: each list the request's when it gives one, else these
/// settings'. The one rule the CPA package (one list of each per
/// package, for every file in it: crate::package INVARIANT 7) and
/// `books.vendor_1099` read, so the package's 1099 worksheet is the
/// tool's for the same accounts.
pub fn with_accounts(
&self,
cash_accounts: &'a [String],
financing_accounts: &'a [String],
) -> Settings<'a> {
let or = |given: &'a [String], kept: &'a [String]| {
if given.is_empty() { kept } else { given }
};
Settings {
cash_accounts: or(cash_accounts, self.cash_accounts),
financing_accounts: or(financing_accounts, self.financing_accounts),
..*self
}
}
}
/// Resolve a statement's date range (INVARIANT 1): the dates as given,
/// each omitted one the books' default for `company` on `today`
/// (rules.md B-30).
+417 -62
View File
@@ -1,26 +1,36 @@
//! The 1099 worksheet: what `books.vendor_1099` answers and hands over as
//! CSV, and what the viewer saves, with each payee's TIN when asked.
//! CSV, what the CPA package carries, and what the viewer saves and shows,
//! with each payee's TIN when asked.
//!
//! ```text
//! GOAL: the CPA gets one 1099 worksheet, the same from the server and
//! the viewer: cash paid per vendor, who is reportable, who lacks a
//! W-9, and each payee's TIN.
//! SOURCE: ledger_core::form1099 (payments out of the cash and financing
//! accounts; an entry's shares resolved with the members); IRS
//! GOAL: the CPA gets one 1099 worksheet, the same from the server, the
//! package and the viewer: per vendor what counts toward its 1099,
//! what a card or payment network paid, what is unknown, its 1099
//! decision with the reason and who made it, who lacks a W-9, and
//! each payee's TIN.
//! SOURCE: ledger_core::form1099 (the one report: payments out of the cash
//! and financing accounts, classified by payment method; an entry's
//! shares resolved with the members; dated decisions); IRS
//! thresholds (rules.md B-19: never guessed); RFC 4180 (CSV).
//! INVARIANTS:
//! 1. Payments come from the settings' cash and financing accounts
//! (codes the chart lacks are skipped), an entry's shares resolved
//! with the company's members.
//! 1. Payments come from the cash and financing accounts of the
//! `Settings` given (codes the chart lacks are skipped): the books'
//! settings, or the CPA package's and `books.vendor_1099`'s request
//! accounts (`Settings::with_accounts`), an entry's shares resolved
//! with the company's members; with no cash accounts named,
//! refused.
//! 2. A year whose threshold isn't known is refused, never guessed.
//! 3. Full TINs only from the TIN source given (crate INVARIANT 2): with
//! one, every line carries its TIN or a note why not; without one,
//! none does.
//! 4. The CSV has a header and one row per line, in the worksheet's
//! order; the TIN columns only when asked.
//! 5. The notes are quiet: the untagged entries are one count and a
//! tool hint, each list of payments one line.
//! ```
use ledger_core::{EntryId, Ledger, PartyId, VendorId};
use chrono::NaiveDate;
use ledger_core::{Decision1099, EntryId, Ledger, PartyId, Payment1099, Status1099, VendorId};
use rust_decimal::Decimal;
use crate::reports::Settings;
@@ -43,29 +53,29 @@ pub fn payment_accounts(ledger: &Ledger, settings: &Settings<'_>) -> ledger_core
}
}
/// The 1099 report for `year`, the one every tool, the worksheet, and the
/// CPA package use: payments out of [`payment_accounts`], an entry's
/// shares resolved with the settings' members. Given `through`, only the
/// payments from January 1 through it (the CPA package as of a day,
/// rules.md B-33).
/// The 1099 report for `year`, the one every tool, the worksheet, the
/// CPA package and the viewer use: payments out of [`payment_accounts`],
/// an entry's shares resolved with the settings' members. Given
/// `through`, only the payments from January 1 through it (the CPA
/// package as of a day, rules.md B-33).
pub fn vendor_1099_report(
ledger: &Ledger,
settings: &Settings<'_>,
year: i32,
through: Option<chrono::NaiveDate>,
threshold: Option<Decimal>,
through: Option<NaiveDate>,
) -> Result<ledger_core::Vendor1099Report, ledger_core::LedgerError> {
ledger_core::vendor_1099_report_with_members(
ledger_core::vendor_1099_report(
ledger,
year,
through,
threshold,
&payment_accounts(ledger, settings),
&settings.company.party_members(ledger),
through,
)
}
/// A 1099 form's label ("1099-NEC", "1099-MISC"), if the vendor is marked.
/// A 1099 form's label ("1099-NEC", "1099-MISC"), if the vendor files one.
pub fn form_1099_label(form: Option<ledger_core::Form1099>) -> Option<String> {
form.map(|f| f.label().to_string())
}
@@ -77,18 +87,41 @@ pub struct Vendor1099WorksheetLine {
pub vendor_id: VendorId,
/// Its name.
pub name: String,
/// "1099-NEC", "1099-MISC", or none.
/// "1099-NEC", "1099-MISC", or none: the form its year-end decision
/// files.
pub form_1099: Option<String>,
/// Its decision at the end of the year: "nec", "misc",
/// "not_reportable" or "undetermined".
pub decision_1099: String,
/// The decision itself, with its reason when not reportable.
pub decision: Decision1099,
/// Who decided it and when (none: never decided, or decided before
/// decisions were kept).
pub decided: Option<ledger_core::settings::Source>,
/// The date it took effect (none: from the beginning).
pub effective: Option<NaiveDate>,
/// Every decision in force during the year, in date order.
pub decisions: Vec<ledger_core::Dated1099>,
/// W-9 on file.
pub tin_on_file: bool,
/// Cash paid in the year.
/// What counts toward its 1099.
pub paid: Decimal,
/// Marked for a 1099 and paid at least the threshold.
/// Paid by card or payment network: reported on Form 1099-K.
pub excluded_card: Decimal,
/// Paid with no payment method recorded where it matters.
pub undetermined: Decimal,
/// Paid while it was decided not reportable.
pub not_reportable: Decimal,
/// A decision is needed for some of the year's payments.
pub decision_needed: bool,
/// Where it stands.
pub status: Status1099,
/// File a 1099 (status reportable).
pub reportable: bool,
/// The vendor's party.
pub party_id: Option<PartyId>,
/// The TIN's last four, from the party's W-9 facts, else its TIN
/// facts.
/// The TIN's last four ([`ledger_core::Party::tin_last4`]: its TIN
/// facts, else its W-9 facts).
pub tin_last4: Option<String>,
/// The full TIN, formatted (123-45-6789, 12-3456789): only with a TIN
/// source, and only when it has one.
@@ -99,6 +132,17 @@ pub struct Vendor1099WorksheetLine {
pub tin_note: Option<String>,
}
impl Vendor1099WorksheetLine {
/// Whether a 1099 may be due: reportable, or undetermined while decided
/// for one some time in the year (P3 review R16: its W-9 and TIN
/// matter as much).
pub fn may_file(&self) -> bool {
self.reportable
|| (self.status == Status1099::Undetermined
&& self.decisions.iter().any(|d| d.decision.form().is_some()))
}
}
/// The 1099 worksheet for a calendar year.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Vendor1099Worksheet {
@@ -106,58 +150,145 @@ pub struct Vendor1099Worksheet {
pub year: i32,
/// The threshold applied.
pub threshold: Decimal,
/// One line per vendor marked for a 1099 or paid in the year, by name.
/// The last day counted, when it stops before December 31.
pub through: Option<NaiveDate>,
/// One line per vendor decided for a 1099 or paid in the year, by name.
pub lines: Vec<Vendor1099WorksheetLine>,
/// Reportable vendors with no W-9 on file.
/// Vendors a 1099 is or may be due for with no W-9 on file (" (may be
/// reportable)" after those undetermined).
pub missing_w9: Vec<String>,
/// Entries naming several parties whose shares don't resolve (each
/// counted in full toward its vendor).
pub unresolved: Vec<EntryId>,
/// Payments by card or payment network, excluded.
pub excluded_card_payments: Vec<Payment1099>,
/// Payments whose method must be recorded before they can be
/// classified.
pub undetermined_payments: Vec<Payment1099>,
/// Money received from a vendor for something else, not netted.
pub other_receipts: Vec<Payment1099>,
/// Posted entries that paid money out with no vendor tag.
pub untagged_payments: Vec<EntryId>,
/// What to check: what counts, exemptions, the threshold's indexing,
/// untagged entries, unresolved shares.
/// untagged entries, card payments, unknown methods, decisions needed,
/// unresolved shares.
pub notes: Vec<String>,
/// Whether the lines carry TINs from a TIN source.
pub tins_included: bool,
}
/// The 1099 worksheet for `year` (threshold: the IRS figure to use when
/// the year's isn't known, else none), each line with its full TIN from
/// `tins` when given (INVARIANT 3).
pub fn vendor_1099_worksheet(
ledger: &Ledger,
settings: &Settings<'_>,
year: i32,
threshold: Option<Decimal>,
tins: Option<TinSource<'_>>,
) -> Result<Vendor1099Worksheet, String> {
// A year past what wanderbooks knows is refused with guidance
// (research or ask, pass `threshold`, then update the rule).
let r =
vendor_1099_report(ledger, settings, year, None, threshold).map_err(|e| e.to_string())?;
let missing_w9 = r
.vendors
/// "entry 4" or "entries 4, 9".
fn entries_text(ids: &[EntryId]) -> String {
let list = ids
.iter()
.filter(|v| v.reportable && !v.tin_on_file)
.map(|v| v.name.clone())
.collect();
let untagged = ledger
.posted_entries()
.filter(|e| {
chrono::Datelike::year(&e.date()) == year && ledger.entry_vendor(e.id()).is_none()
})
.count();
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(", ");
if ids.len() == 1 {
format!("entry {list}")
} else {
format!("entries {list}")
}
}
/// "1 payment" or "3 payments", with their total.
fn payments_text(list: &[Payment1099]) -> String {
let total: Decimal = list.iter().map(|p| p.amount).sum();
let n = list.len();
format!(
"{n} payment{} totaling {}",
if n == 1 { "" } else { "s" },
money(total)
)
}
/// The worksheet's notes (INVARIANT 5).
fn notes_for(ledger: &Ledger, r: &ledger_core::Vendor1099Report) -> Vec<String> {
let year = r.year;
let name = |id: VendorId| {
ledger
.get_vendor(id)
.map_or_else(|| format!("vendor {id}"), |v| v.name.clone())
};
let mut notes = vec![
"Only entries tagged with a vendor count (books.set_entry_vendor; books.pay_bill tags automatically).".to_string(),
"Corporations are generally exempt, except attorneys' fees and medical payments; royalties report at $10. Confirm with the CPA.".to_string(),
"Each vendor's 1099 decision says why when none is due (a corporation, the United States or a state: Treas. Reg. §1.6041-3); payments to a corporation for legal or medical services stay reportable, and royalties report at $10. Confirm with the CPA.".to_string(),
];
if let Some(t) = r.through {
notes.push(format!(
"Payments from {year}-01-01 through {t} only: later payments in {year} aren't counted."
));
}
if year > 2026 {
notes.push(format!(
"The $2,000 threshold is indexed for inflation after 2026; confirm the {year} amount and pass threshold to override."
));
}
if untagged > 0 {
if !r.excluded_card_payments.is_empty() {
notes.push(format!(
"{untagged} posted entries in {year} have no vendor tag; review any that paid contractors or rent."
"{} by card or payment network are excluded: the processor reports them on Form 1099-K, whoever's card it was (Treas. Reg. §1.6041-1(a)(1)(iv)).",
payments_text(&r.excluded_card_payments)
));
}
if !r.undetermined_payments.is_empty() {
let ids: Vec<EntryId> = r.undetermined_payments.iter().map(|p| p.entry_id).collect();
notes.push(format!(
"{} through a financing account (or a member's or a payment platform's) have no payment method recorded ({}), so whether they count is undetermined: a card or the platform's goods-and-services payment is excluded, cash, a check or a friends-and-family transfer counts. Record how each was paid with books.set_entry_instrument (books.instruments lists the methods).",
payments_text(&r.undetermined_payments),
entries_text(&ids)
));
}
let undecided: Vec<&str> = r
.vendors
.iter()
.filter(|v| v.decision_needed)
.map(|v| v.name.as_str())
.collect();
if !undecided.is_empty() {
notes.push(format!(
"No 1099 decision for payments that could count: {}. Record one with books.set_vendor_1099.",
undecided.join(", ")
));
}
for v in &r.vendors {
let mut forms: Vec<&'static str> = v
.decisions
.iter()
.filter_map(|d| d.decision.form().map(|f| f.label()))
.collect();
forms.dedup();
if forms.len() > 1 {
notes.push(format!(
"{}'s 1099 form changed during {year} ({}): each payment goes on the form for what it paid for. Confirm with the CPA.",
v.name,
forms.join(", then ")
));
}
}
for p in &r.other_receipts {
// Money in is negative; its reversal gives it back.
let what = if p.amount < Decimal::ZERO {
format!("received {} from", money(-p.amount))
} else {
format!("reversed {} received from", money(p.amount))
};
notes.push(format!(
"Entry {} {what} {} for something else (a sale, its payment of an invoice, a disposal, a loan): not netted against its payments.",
p.entry_id,
name(p.vendor_id)
));
}
let untagged = &r.untagged_payments;
if !untagged.is_empty() {
let n = untagged.len();
let which = if n <= 10 {
format!(" ({})", entries_text(untagged))
} else {
String::new()
};
notes.push(format!(
"{n} posted entr{} in {year} paid money out with no vendor tag{which}; tag any that paid a contractor or landlord with books.set_entry_vendor.",
if n == 1 { "y" } else { "ies" }
));
}
if !r.unresolved.is_empty() {
@@ -170,8 +301,28 @@ pub fn vendor_1099_worksheet(
.join(", ")
));
}
notes
}
/// The 1099 worksheet for `year` (threshold: the IRS figure to use when
/// the year's isn't known, else none), entries dated after `through`
/// left out, each line with its full TIN from `tins` when given
/// (INVARIANT 3).
pub fn vendor_1099_worksheet(
ledger: &Ledger,
settings: &Settings<'_>,
year: i32,
threshold: Option<Decimal>,
through: Option<NaiveDate>,
tins: Option<TinSource<'_>>,
) -> Result<Vendor1099Worksheet, String> {
// A year past what wanderbooks knows is refused with guidance
// (research or ask, pass `threshold`, then update the rule).
let r = vendor_1099_report(ledger, settings, year, threshold, through)
.map_err(|e| e.to_string())?;
let notes = notes_for(ledger, &r);
let mut lines = Vec::new();
for v in r.vendors {
for v in &r.vendors {
let party_id = ledger.party_of_vendor(v.vendor_id);
let tin_last4 = party_id
.and_then(|id| ledger.get_party(id))
@@ -179,10 +330,20 @@ pub fn vendor_1099_worksheet(
.map(str::to_string);
let mut line = Vendor1099WorksheetLine {
vendor_id: v.vendor_id,
name: v.name,
form_1099: form_1099_label(v.form),
name: v.name.clone(),
form_1099: form_1099_label(v.form()),
decision_1099: v.decision.key().to_string(),
decision: v.decision.clone(),
decided: v.decided.clone(),
effective: v.effective,
decisions: v.decisions.clone(),
tin_on_file: v.tin_on_file,
paid: v.paid,
excluded_card: v.excluded_card,
undetermined: v.undetermined,
not_reportable: v.not_reportable,
decision_needed: v.decision_needed,
status: v.status,
reportable: v.reportable,
party_id,
tin_last4,
@@ -202,32 +363,94 @@ pub fn vendor_1099_worksheet(
}
lines.push(line);
}
let missing_w9 = lines
.iter()
.filter(|l| l.may_file() && !l.tin_on_file)
.map(|l| {
if l.reportable {
l.name.clone()
} else {
format!("{} (may be reportable)", l.name)
}
})
.collect();
Ok(Vendor1099Worksheet {
year: r.year,
threshold: r.threshold,
through: r.through,
lines,
missing_w9,
unresolved: r.unresolved,
excluded_card_payments: r.excluded_card_payments,
undetermined_payments: r.undetermined_payments,
other_receipts: r.other_receipts,
untagged_payments: r.untagged_payments,
notes,
tins_included: tins.is_some(),
})
}
/// The worksheet as CSV (INVARIANT 4): vendor id, name, form, paid,
/// reportable, W-9 on file, and last four; `with_tins` adds the TIN, its
/// type, and the note why it's missing.
/// A dated decision for the CSV: its key, with its kind when not
/// reportable, and its date when it has one ("nec",
/// "not_reportable:corporation from 2026-12-30").
fn decision_cell(d: &ledger_core::Dated1099) -> String {
let mut s = d.decision.key().to_string();
if let Decision1099::NotReportable { kind, .. } = &d.decision {
s.push(':');
s.push_str(kind.key());
}
if let Some(e) = d.effective {
s.push_str(&format!(" from {e}"));
}
s
}
/// The worksheet's CSV header (INVARIANT 4).
pub const VENDOR_1099_CSV_HEADER: &str = "vendor_id,name,decision_1099,form_1099,reason_kind,reason,decided_by,decided_on,effective,decisions_in_year,paid,excluded_card,undetermined,not_reportable,status,reportable,w9_on_file,tin_last4";
/// The worksheet as CSV (INVARIANT 4): per vendor its id and name, its
/// year-end 1099 decision (key, form, reason kind and words, who decided,
/// when, the date it took effect) and every decision in force during the
/// year, what counts, what was paid by card or network, what is
/// undetermined, what was paid while not reportable, its status, whether
/// it is reportable, its W-9 flag and last four; `with_tins` adds the TIN,
/// its type, and the note why it's missing.
pub fn vendor_1099_csv(worksheet: &Vendor1099Worksheet, with_tins: bool) -> String {
let mut csv = String::from("vendor_id,name,form_1099,paid,reportable,w9_on_file,tin_last4");
let mut csv = String::from(VENDOR_1099_CSV_HEADER);
if with_tins {
csv.push_str(",tin,tin_type,tin_note");
}
csv.push('\n');
for l in &worksheet.lines {
let (kind, reason) = match &l.decision {
Decision1099::NotReportable { kind, reason } => (kind.key(), reason.as_str()),
_ => ("", ""),
};
let mut cells = vec![
l.vendor_id.to_string(),
csv_escape(&l.name),
l.decision_1099.clone(),
l.form_1099.clone().unwrap_or_default(),
kind.to_string(),
csv_escape(reason),
csv_escape(l.decided.as_ref().map_or("", |s| s.decided_by.as_str())),
l.decided
.as_ref()
.map(|s| s.decided_on.to_string())
.unwrap_or_default(),
l.effective.map(|e| e.to_string()).unwrap_or_default(),
csv_escape(
&l.decisions
.iter()
.map(decision_cell)
.collect::<Vec<_>>()
.join("; "),
),
money(l.paid),
money(l.excluded_card),
money(l.undetermined),
money(l.not_reportable),
l.status.key().to_string(),
l.reportable.to_string(),
l.tin_on_file.to_string(),
l.tin_last4.clone().unwrap_or_default(),
@@ -252,3 +475,135 @@ pub fn vendor_1099_csv_name(year: i32, with_tins: bool) -> String {
if with_tins { "-with-tins" } else { "" }
)
}
#[cfg(test)]
mod tests {
use super::*;
use ledger_core::{AccountType, Currency, Money, Side};
fn dec(s: &str) -> Decimal {
s.parse().unwrap()
}
fn date(s: &str) -> NaiveDate {
s.parse().unwrap()
}
/// The untagged note (P3 review R7), by hand. Bank 1000 (cash), Due to
/// Members 2100 (financing), card 2050 (a business card's), capital
/// 3000, labor 6000, prepaid 1300, equipment 1500. Untagged, in 2026:
/// 1. a contribution, Dr 1000 / Cr 3000 500: money in, not counted;
/// 2. a member reimbursed, Dr 2100 / Cr 1000 80: nets to zero;
/// 3. a property contribution, Dr 1500 / Cr 3000: no payment line;
/// 4. the card bill, Dr 2050 / Cr 1000 300: nets to zero;
/// 5. a distribution, Dr 3000 / Cr 1000 200: paid out, but for no
/// expense or asset: not counted;
/// 6. a fee a member paid, Dr 6000 / Cr 2100 25: counted;
/// 7. a prepayment, Dr 1300 / Cr 1000 120: counted;
/// 8. a payment, Dr 6000 / Cr 1000 40, and 9 its reversal: both in
/// the year, so neither counts.
///
/// Two entries: 6 and 7.
#[test]
fn the_untagged_note_counts_only_money_out() {
let mut l = Ledger::new(Currency::USD);
let mut acct = |code: &str, t| l.add_account(code, code, t, None, false).unwrap();
let bank = acct("1000", AccountType::Asset);
let prepaid = acct("1300", AccountType::Asset);
let equipment = acct("1500", AccountType::Asset);
let card = acct("2050", AccountType::Liability);
let due = acct("2100", AccountType::Liability);
let capital = acct("3000", AccountType::Equity);
let labor = acct("6000", AccountType::Expense);
let post = |l: &mut Ledger, dr, cr, amt: &str| {
let e = l.create_entry(date("2026-05-01"), "x");
let m = Money::new(dec(amt), Currency::USD);
l.add_line(e, dr, Side::Debit, m).unwrap();
l.add_line(e, cr, Side::Credit, m).unwrap();
l.post_entry(e).unwrap();
e
};
post(&mut l, bank, capital, "500");
post(&mut l, due, bank, "80");
post(&mut l, equipment, capital, "900");
post(&mut l, card, bank, "300");
post(&mut l, capital, bank, "200");
let fee = post(&mut l, labor, due, "25");
let pre = post(&mut l, prepaid, bank, "120");
let wrong = post(&mut l, labor, bank, "40");
l.reverse_entry(wrong, date("2026-05-02"), "oops").unwrap();
l.add_instrument(
ledger_core::InstrumentKind::BusinessCard,
"Card",
None,
None,
card,
)
.unwrap();
let r = ledger_core::vendor_1099_report(
&l,
2026,
None,
&ledger_core::PaymentAccounts {
cash: vec![bank],
financing: vec![due],
},
&[],
None,
)
.unwrap();
assert_eq!(r.untagged_payments, vec![fee, pre]);
let notes = notes_for(&l, &r);
assert!(
notes.contains(&format!(
"2 posted entries in 2026 paid money out with no vendor tag (entries {fee}, {pre}); tag any that paid a contractor or landlord with books.set_entry_vendor."
)),
"{notes:?}"
);
}
/// The note on money received from a vendor for something else says
/// what each entry did: the sale received the money, its reversal
/// gave it back (never "received -500.00").
#[test]
fn the_other_receipts_note_says_what_each_entry_did() {
let mut l = Ledger::new(Currency::USD);
let bank = l
.add_account("1000", "Bank", AccountType::Asset, None, false)
.unwrap();
let sales = l
.add_account("4000", "Sales", AccountType::Revenue, None, false)
.unwrap();
let dana = l.add_vendor("Dana").unwrap();
let e = l.create_entry(date("2026-04-01"), "laptop sold to Dana");
let m = Money::new(dec("500.00"), Currency::USD);
l.add_line(e, bank, Side::Debit, m).unwrap();
l.add_line(e, sales, Side::Credit, m).unwrap();
l.post_entry(e).unwrap();
l.set_entry_vendor(e, Some(dana)).unwrap();
let undo = l.reverse_entry(e, date("2026-04-02"), "not Dana").unwrap();
let r = ledger_core::vendor_1099_report(
&l,
2026,
None,
&ledger_core::PaymentAccounts {
cash: vec![bank],
financing: vec![],
},
&[],
None,
)
.unwrap();
let notes = notes_for(&l, &r);
for want in [
format!(
"Entry {e} received 500.00 from Dana for something else (a sale, its payment of an invoice, a disposal, a loan): not netted against its payments."
),
format!(
"Entry {undo} reversed 500.00 received from Dana for something else (a sale, its payment of an invoice, a disposal, a loan): not netted against its payments."
),
] {
assert!(notes.contains(&want), "{want} not in {notes:?}");
}
}
}
+181 -44
View File
@@ -83,8 +83,7 @@ fn books(dir: &Path) -> Books {
.unwrap();
let vendor = s.add_vendor("Acme, \"Design\"").unwrap();
let acme = s.ledger().party_of_vendor(vendor).unwrap();
s.set_vendor_1099(vendor, Some(Form1099::Nec), true)
.unwrap();
s.set_vendor_1099(vendor, &nec(), Some(true)).unwrap();
s.set_party_tin(
acme,
Some(TinFacts {
@@ -205,6 +204,26 @@ fn company(members: bool) -> Company {
}
}
/// A 1099-NEC decision from the beginning, decided by a member.
fn nec() -> ledger_core::Dated1099 {
ledger_core::Dated1099::from_beginning(
ledger_core::Decision1099::Files(Form1099::Nec),
Some(ledger_core::settings::Source {
decided_by: "Tobin Example".into(),
decided_on: d("2026-01-02"),
citation: None,
note: None,
}),
)
}
/// The worksheet CSV's row for Acme in `books`, without TINs.
fn acme_row(vendor: u64) -> String {
format!(
"{vendor},\"Acme, \"\"Design\"\"\",nec,1099-NEC,,,Tobin Example,2026-01-02,,nec,2500.00,0.00,0.00,0.00,reportable,true,true,9105"
)
}
fn cash() -> Vec<String> {
vec!["1000".to_string()]
}
@@ -353,9 +372,28 @@ fn a_single_member_package_holds_the_documented_files_in_order() {
sale = b.sale,
)
);
// The package's 1099 worksheet is books.vendor_1099's CSV, byte for
// byte (one builder: P3 brief 5).
let w = vendor_1099_worksheet(
b.store.ledger(),
&settings(&company, &cash),
2026,
None,
None,
None,
)
.unwrap();
assert_eq!(
text(&p, "1099-worksheet-2026.csv"),
"vendor,form,w9_on_file,paid,reportable\n\"Acme, \"\"Design\"\"\",1099-NEC,true,2500.00,true\n"
vendor_1099_csv(&w, false)
);
assert_eq!(
text(&p, "1099-worksheet-2026.csv"),
format!(
"{}\n{}\n",
ledger_package::VENDOR_1099_CSV_HEADER,
acme_row(b.vendor)
)
);
let readme = text(&p, "README.txt");
assert!(readme.starts_with("Example Labs LLC — books package for the CPA\n\nFiscal year to date: 2026-01-01 through 2026-12-31. Cash basis.\n"), "{readme}");
@@ -364,6 +402,12 @@ fn a_single_member_package_holds_the_documented_files_in_order() {
"{readme}"
);
assert!(readme.contains("receipts/: the 2 receipts behind the period's entries"));
// Defect 12's wording: what the worksheet holds, every vendor paid in
// the year (decided for a 1099 or not), not "marked for a 1099".
assert!(
readme.contains("1099-worksheet-2026.csv: each vendor paid in 2026 or decided for a 1099: what counts toward its 1099 against the year's threshold, what was paid by card or payment network (reported by the processor on Form 1099-K, Treas. Reg. §1.6041-1(a)(1)(iv)), what can't be classified until its payment method is recorded, and its 1099 decision with the reason, who made it, and when it took effect.\n"),
"{readme}"
);
assert!(readme.contains("cash flow, tax summary, trial balance."));
assert!(!readme.contains("k1-members.csv") && !readme.contains("full taxpayer"));
assert!(!names(&p).contains(&"k1-members.csv"));
@@ -487,9 +531,9 @@ fn members_get_the_k1_worksheet_with_full_tins_only_from_a_source() {
assert_eq!(
text(&with, "1099-worksheet-2026.csv"),
format!(
"vendor,form,w9_on_file,paid,reportable,party_id,tin_last4,tin,tin_type,tin_note\n\
\"Acme, \"\"Design\"\"\",1099-NEC,true,2500.00,true,{},9105,{EIN},ein,\n",
b.acme
"{},tin,tin_type,tin_note\n{},{EIN},ein,\n",
ledger_package::VENDOR_1099_CSV_HEADER,
acme_row(b.vendor)
)
);
assert!(!text(&without, "1099-worksheet-2026.csv").contains(EIN));
@@ -585,7 +629,7 @@ fn the_1099_worksheet_carries_tins_only_from_a_source() {
let cash = cash();
let s = settings(&company, &cash);
let ledger = b.store.ledger();
let plain = vendor_1099_worksheet(ledger, &s, 2026, None, None).unwrap();
let plain = vendor_1099_worksheet(ledger, &s, 2026, None, None, None).unwrap();
assert_eq!(plain.year, 2026);
assert_eq!(plain.threshold, Decimal::from(2000));
assert!(!plain.tins_included);
@@ -598,44 +642,49 @@ fn the_1099_worksheet_carries_tins_only_from_a_source() {
assert_eq!(l.tin_last4.as_deref(), Some("9105"));
assert_eq!((&l.tin, &l.tin_type, &l.tin_note), (&None, &None, &None));
assert!(plain.missing_w9.is_empty());
// Five posted entries in 2026, one tagged.
// Of 2026's untagged entries only the rent paid money out for
// something: the contributions and the sale brought money in (P3
// review R7, the quieter note).
assert_eq!(plain.untagged_payments, vec![b.rent]);
assert_eq!(
plain.notes,
[
"Only entries tagged with a vendor count (books.set_entry_vendor; books.pay_bill tags automatically).",
"Corporations are generally exempt, except attorneys' fees and medical payments; royalties report at $10. Confirm with the CPA.",
"3 posted entries in 2026 have no vendor tag; review any that paid contractors or rent.",
"Only entries tagged with a vendor count (books.set_entry_vendor; books.pay_bill tags automatically).".to_string(),
"Each vendor's 1099 decision says why when none is due (a corporation, the United States or a state: Treas. Reg. §1.6041-3); payments to a corporation for legal or medical services stay reportable, and royalties report at $10. Confirm with the CPA.".to_string(),
format!("1 posted entry in 2026 paid money out with no vendor tag (entry {}); tag any that paid a contractor or landlord with books.set_entry_vendor.", b.rent),
]
);
assert_eq!(
vendor_1099_csv(&plain, false),
format!(
"vendor_id,name,form_1099,paid,reportable,w9_on_file,tin_last4\n{},\"Acme, \"\"Design\"\"\",1099-NEC,2500.00,true,true,9105\n",
b.vendor
"{}\n{}\n",
ledger_package::VENDOR_1099_CSV_HEADER,
acme_row(b.vendor)
)
);
let source = tin_source(&b);
let full = vendor_1099_worksheet(ledger, &s, 2026, None, Some(&source)).unwrap();
let full = vendor_1099_worksheet(ledger, &s, 2026, None, None, Some(&source)).unwrap();
assert!(full.tins_included);
assert_eq!(full.lines[0].tin.as_deref(), Some(EIN));
assert_eq!(full.lines[0].tin_type.as_deref(), Some("ein"));
assert_eq!(
vendor_1099_csv(&full, true),
format!(
"vendor_id,name,form_1099,paid,reportable,w9_on_file,tin_last4,tin,tin_type,tin_note\n{},\"Acme, \"\"Design\"\"\",1099-NEC,2500.00,true,true,9105,{EIN},ein,\n",
b.vendor
"{},tin,tin_type,tin_note\n{},{EIN},ein,\n",
ledger_package::VENDOR_1099_CSV_HEADER,
acme_row(b.vendor)
)
);
// A source without the payee's TIN: the note says why.
let none = |_: PartyId| -> Result<ledger_vault::Tin, String> {
Err("the TIN vault is locked, \"for now\"".into())
};
let locked = vendor_1099_worksheet(ledger, &s, 2026, None, Some(&none)).unwrap();
let locked = vendor_1099_worksheet(ledger, &s, 2026, None, None, Some(&none)).unwrap();
assert_eq!(
vendor_1099_csv(&locked, true).lines().nth(1).unwrap(),
format!(
"{},\"Acme, \"\"Design\"\"\",1099-NEC,2500.00,true,true,9105,,,\"the TIN vault is locked, \"\"for now\"\"\"",
b.vendor
"{},,,\"the TIN vault is locked, \"\"for now\"\"\"",
acme_row(b.vendor)
)
);
assert_eq!(vendor_1099_csv_name(2026, false), "1099-worksheet-2026.csv");
@@ -645,9 +694,10 @@ fn the_1099_worksheet_carries_tins_only_from_a_source() {
);
// A year whose threshold isn't known is refused, never guessed; given
// one, it is used.
let far = vendor_1099_worksheet(ledger, &s, 2031, None, None);
let far = vendor_1099_worksheet(ledger, &s, 2031, None, None, None);
assert!(far.is_err(), "{far:?}");
let given = vendor_1099_worksheet(ledger, &s, 2031, Some(Decimal::from(2100)), None).unwrap();
let given =
vendor_1099_worksheet(ledger, &s, 2031, Some(Decimal::from(2100)), None, None).unwrap();
assert_eq!(given.threshold, Decimal::from(2100));
assert!(
given
@@ -918,9 +968,13 @@ fn the_years_first_and_last_days_receipts_are_in_and_the_days_beyond_are_not() {
}
/// Contract labor paid three ways: from checking (cash), from a prepaid
/// asset that isn't cash, and by a member's advance (financing).
/// asset that isn't cash, and by a member's advance (financing). The
/// worksheet counts payments out of the accounts it is given; the CPA
/// package gives it its own (the request's, else the settings': one list
/// of each per package, P3 review R18), so the package's 1099 worksheet
/// is books.vendor_1099's for those accounts, byte for byte.
#[test]
fn payments_to_a_vendor_come_from_the_settings_accounts_alone() {
fn payments_to_a_vendor_come_from_the_packages_accounts() {
let dir = tempfile::tempdir().unwrap();
let (mut s, _) = plain_books(dir.path());
for (code, name, ty) in [
@@ -931,8 +985,7 @@ fn payments_to_a_vendor_come_from_the_settings_accounts_alone() {
s.add_account(code, name, ty, None, false).unwrap();
}
let vendor = s.add_vendor("Acme Design").unwrap();
s.set_vendor_1099(vendor, Some(Form1099::Nec), true)
.unwrap();
s.set_vendor_1099(vendor, &nec(), Some(true)).unwrap();
for (from, amount) in [("1000", "1000.00"), ("1300", "400.00"), ("2100", "700.00")] {
let e = s.create_entry(d("2026-05-01"), "Acme design work").unwrap();
s.add_line(e, id_of(&s, "6100"), Side::Debit, usd(amount))
@@ -960,11 +1013,49 @@ fn payments_to_a_vendor_come_from_the_settings_accounts_alone() {
financing: vec![id_of(&s, "2100")],
}
);
// Checking and the advance count; the prepaid asset doesn't.
let w = vendor_1099_worksheet(ledger, &st, 2026, None, None).unwrap();
// Checking counts; the member's advance is undetermined until how
// they paid is recorded (their card wouldn't count, their check
// would); the prepaid asset is no payment source.
let w = vendor_1099_worksheet(ledger, &st, 2026, None, None, None).unwrap();
assert_eq!(w.lines.len(), 1);
assert_eq!(w.lines[0].vendor_id, vendor);
assert_eq!(w.lines[0].paid, Decimal::from(1700));
assert_eq!(w.lines[0].paid, Decimal::from(1000));
assert_eq!(w.lines[0].undetermined, Decimal::from(700));
assert_eq!(w.lines[0].status, ledger_core::Status1099::UnderThreshold);
assert_eq!(w.undetermined_payments.len(), 1);
// A package naming no accounts carries this worksheet, byte for byte
// (its settings name checking alone: the cash flow refuses a code the
// chart lacks).
let checking = vec!["1000".to_string()];
let st = Settings {
cash_accounts: &checking,
..st
};
let plain = build_package(&s, &st, &request(false, None)).unwrap();
assert_eq!(
text(&plain, "1099-worksheet-2026.csv"),
vendor_1099_csv(&w, false)
);
// One naming the prepaid asset as cash carries the worksheet for its
// accounts (the 400.00 out of the prepaid asset counts, and the
// settings' financing stays), the one books.vendor_1099 gives for
// them through the package's as_of; the cash flow, the K-1 and the
// IIF read the same list.
let wider = vec!["1000".to_string(), "1300".to_string()];
let req = PackageRequest {
cash_accounts: &wider,
..request(false, None)
};
let p = build_package(&s, &st, &req).unwrap();
let theirs = st.with_accounts(&wider, &[]);
assert_eq!(theirs.financing_accounts, financing.as_slice());
let w = vendor_1099_worksheet(ledger, &theirs, 2026, None, Some(req.as_of), None).unwrap();
assert_eq!(w.lines[0].paid, Decimal::from(1400));
assert_eq!(w.lines[0].undetermined, Decimal::from(700));
assert_eq!(
text(&p, "1099-worksheet-2026.csv"),
vendor_1099_csv(&w, false)
);
}
#[test]
@@ -1380,8 +1471,7 @@ fn the_1099_worksheet_stops_at_as_of() {
s.add_account("6100", "Contract labor", AccountType::Expense, None, false)
.unwrap();
let vendor = s.add_vendor("Contractor Example").unwrap();
s.set_vendor_1099(vendor, Some(Form1099::Nec), false)
.unwrap();
s.set_vendor_1099(vendor, &nec(), Some(false)).unwrap();
for (date, amount) in [("2026-06-30", "1500.00"), ("2026-07-01", "1000.00")] {
let e = s.create_entry(d(date), "Contract work").unwrap();
s.add_line(e, id_of(&s, "6100"), Side::Debit, usd(amount))
@@ -1400,25 +1490,52 @@ fn the_1099_worksheet_stops_at_as_of() {
};
build_package(&s, &settings(&company, &cash), &req).unwrap()
};
// The worksheet the tools make through a day (books.vendor_1099's
// `through`), as CSV.
let through = |day: Option<&str>| {
let w = vendor_1099_worksheet(
s.ledger(),
&settings(&company, &cash),
2026,
None,
day.map(d),
None,
)
.unwrap();
vendor_1099_csv(&w, false)
};
let row = |csv: &str| csv.lines().nth(1).unwrap().to_string();
let mid = at("2026-06-30");
assert_eq!(
text(&mid, "1099-worksheet-2026.csv"),
"vendor,form,w9_on_file,paid,reportable\n\"Contractor Example\",1099-NEC,false,1500.00,false\n"
let mid_csv = text(&mid, "1099-worksheet-2026.csv");
assert_eq!(mid_csv, through(Some("2026-06-30")));
// paid 1,500.00, under the $2,000 threshold: not yet reportable.
assert!(
row(mid_csv).ends_with(",1500.00,0.00,0.00,0.00,under_threshold,false,false,"),
"{mid_csv}"
);
assert!(text(&mid, "README.txt").contains(
"\n1099-worksheet-2026.csv: cash paid per vendor marked for a 1099 from January 1 through 2026-06-30, against the year's threshold.\n"
"\n1099-worksheet-2026.csv: each vendor paid in 2026 from January 1 through 2026-06-30 or decided for a 1099: what counts toward its 1099 against the year's threshold, "
));
let year_end = at("2026-12-31");
assert_eq!(
text(&year_end, "1099-worksheet-2026.csv"),
"vendor,form,w9_on_file,paid,reportable\n\"Contractor Example\",1099-NEC,false,2500.00,true\n"
let year_csv = text(&year_end, "1099-worksheet-2026.csv");
assert_eq!(year_csv, through(None));
assert!(
row(year_csv).ends_with(",2500.00,0.00,0.00,0.00,reportable,true,false,"),
"{year_csv}"
);
assert!(text(&year_end, "README.txt").contains(
"\n1099-worksheet-2026.csv: cash paid per vendor marked for a 1099, against the year's threshold.\n"
"\n1099-worksheet-2026.csv: each vendor paid in 2026 or decided for a 1099: what counts toward its 1099 against the year's threshold, "
));
// The worksheet the tools make for the whole year is unchanged.
let w =
vendor_1099_worksheet(s.ledger(), &settings(&company, &cash), 2026, None, None).unwrap();
let w = vendor_1099_worksheet(
s.ledger(),
&settings(&company, &cash),
2026,
None,
None,
None,
)
.unwrap();
assert_eq!(w.lines[0].paid, Decimal::from(2500));
}
@@ -1526,8 +1643,7 @@ fn one_list_of_cash_accounts_per_package() {
],
);
let vendor = s.add_vendor("Contractor Example").unwrap();
s.set_vendor_1099(vendor, Some(Form1099::Nec), false)
.unwrap();
s.set_vendor_1099(vendor, &nec(), Some(false)).unwrap();
let e = s.create_entry(d("2026-05-01"), "Contract work").unwrap();
s.add_line(e, id_of(&s, "6100"), Side::Debit, usd("2100.00"))
.unwrap();
@@ -1551,8 +1667,29 @@ fn one_list_of_cash_accounts_per_package() {
};
assert_eq!(savings(&given), "ACCNT\tSavings\tBANK\t1010");
assert_eq!(savings(&plain), "ACCNT\tSavings\tOCASSET\t1010");
assert!(text(&given, "1099-worksheet-2026.csv").contains(",2100.00,true\n"));
assert!(text(&plain, "1099-worksheet-2026.csv").contains(",0.00,false\n"));
assert!(
text(&given, "1099-worksheet-2026.csv")
.contains(",2100.00,0.00,0.00,0.00,reportable,true,false,\n")
);
assert!(
text(&plain, "1099-worksheet-2026.csv")
.contains(",0.00,0.00,0.00,0.00,under_threshold,false,false,\n")
);
// Each is books.vendor_1099's CSV for the same accounts.
let tool = |codes: &[String]| {
let w = vendor_1099_worksheet(
s.ledger(),
&settings(&company, codes),
2026,
None,
Some(d("2026-12-31")),
None,
)
.unwrap();
vendor_1099_csv(&w, false)
};
assert_eq!(text(&given, "1099-worksheet-2026.csv"), tool(&both));
assert_eq!(text(&plain, "1099-worksheet-2026.csv"), tool(&cash));
}
/// Plan D-36 (P3, defect 7): general-journal.iif is plain ASCII, and