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

+108 -8
View File
@@ -1,6 +1,7 @@
//! What the books need: drafts left unposted, missing receipts, schedules
//! and recurring entries due, open past periods, uncategorized tax lines,
//! 1099 vendors without a W-9, overdue bills and invoices, money owed to
//! 1099 vendors without a W-9 or without a 1099 decision, payments whose
//! method the 1099 worksheet needs, overdue bills and invoices, money owed to
//! owners, fixed asset depreciation not posted, assets without a tag or a
//! tax basis, contractors and landlords paid without W-9 facts, bank and
//! card accounts not reconciled through last month, and legal
@@ -12,7 +13,7 @@ use crate::books::{Attention, Books, Severity};
use crate::fmt;
use crate::nav::Route;
use chrono::Datelike;
use ledger_core::{AccountType, EntryStatus, PeriodStatus, vendor_1099_report};
use ledger_core::{AccountType, EntryStatus, PeriodStatus};
use rust_decimal::Decimal;
/// A figure the books can't compute (rules.md B-28: books an earlier
@@ -38,7 +39,9 @@ fn cant_compute(what: String, why: &ledger_core::LedgerError, target: Option<Rou
/// prompt that gets the agent to fix it; never change the books.
/// SOURCE: DECISION (owner: the viewer is read-only; its "Needs
/// attention" list offers prompts, AGENTS.md); the conditions
/// below; docs/spec/sources/irs-1099-thresholds.md (W-9s).
/// below; docs/spec/sources/irs-1099-thresholds.md (W-9s);
/// DECISION D-40 (owner, 2026-10-20: ask only about vendors paid
/// in a way that could need a 1099).
/// INVARIANTS:
/// 1. Pure: reads `b`, writes nothing.
/// 2. One item per: draft entry (Todo); posted entry missing a receipt
@@ -48,8 +51,21 @@ fn cant_compute(what: String, why: &ledger_core::LedgerError, target: Option<Rou
/// category, with activity or not (ledger_core::tax::
/// uncategorized_accounts: one item; Info, Todo under
/// policy.require_tax_category = refuse); legal rule known only
/// through a past year (Todo); 1099
/// vendor without a W-9 (Problem); overdue bills, and overdue
/// through a past year (Todo); for
/// this calendar year, and last year in January, from
/// Books::vendor_1099 (the server's books.vendor_1099): a vendor a
/// 1099 is due for (reportable) without a W-9 (Problem); a vendor
/// paid that year in a way that could count toward a 1099 while its
/// 1099 decision was undetermined (Vendor1099Line::decision_needed:
/// Problem when its status is undetermined, else Todo, its prompt
/// naming books.set_vendor_1099); the year's payments whose method
/// decides whether they count (Vendor1099Report::
/// undetermined_payments, none for a vendor decided not reportable),
/// one Todo counting reversal chains, its prompt naming
/// books.instruments, books.add_instrument and
/// books.set_entry_instrument; a year whose worksheet can't be made
/// for want of named cash accounts while vendors were paid in it
/// (Todo); overdue bills, and overdue
/// invoices (Problem, one each, with the total); money owed to
/// owners (Info, per account with a balance); the database differing
/// from the journal or unreadable, and damaged or missing stored
@@ -305,9 +321,93 @@ pub fn find(b: &Books) -> Vec<Attention> {
for year in years {
// A year whose threshold isn't known is flagged by the stale-rule
// item above; never guess it here.
let Ok(r) = vendor_1099_report(&b.ledger, year, None) else {
continue;
let r = match b.vendor_1099(year) {
Ok(r) => r,
Err(ledger_core::LedgerError::Invalid1099(why))
if b.ledger.posted_entries().any(|e| {
e.date().year() == year && b.ledger.entry_vendor(e.id()).is_some()
}) =>
{
out.push(Attention {
severity: Severity::Todo,
title: format!("The {year} 1099 worksheet can't be made"),
detail: format!("{why}."),
target: Some(Route::Reports(crate::nav::ReportKind::Vendor1099)),
prompt: format!(
"In wanderbooks, the {year} 1099 worksheet can't be made: {why}. Ask me which accounts hold the company's cash, then set them with books.set_setting."
),
});
continue;
}
Err(_) => continue,
};
let in_year = if year == today.year() {
String::new()
} else {
format!(" for {year}")
};
for v in r.vendors.iter().filter(|v| v.decision_needed) {
let party = b.ledger.party_of_vendor(v.vendor_id);
let unknown = if v.undetermined > Decimal::ZERO {
format!(
" and {} more whose payment method isn't recorded",
fmt::dollars(v.undetermined)
)
} else {
String::new()
};
out.push(Attention {
severity: if v.status == ledger_core::Status1099::Undetermined {
Severity::Problem
} else {
Severity::Todo
},
title: format!("Decide whether {} gets a 1099{in_year}", v.name),
detail: format!(
"Paid {} by check, ACH, transfer or cash{unknown} in {year}, with no 1099 decision.",
fmt::dollars(v.paid)
),
target: Some(party.map_or(Route::Company, Route::Party)),
prompt: format!(
"In wanderbooks, {} was paid {}{unknown} in {year} and has no 1099 decision. Ask me whether it gets a 1099-NEC, a 1099-MISC, or none and why (a corporation, the United States or a state, a payee selling only merchandise, ...); then record it with books.set_vendor_1099, with who decided and when.",
v.name,
fmt::dollars(v.paid)
),
});
}
// Payments whose method decides whether they count, a payment
// and its reversals counted once.
let mut chains: Vec<ledger_core::EntryId> = r
.undetermined_payments
.iter()
.map(|p| b.ledger.reversal_chain(p.entry_id)[0])
.collect();
chains.sort_unstable();
chains.dedup();
if !chains.is_empty() {
let n = chains.len();
let ids = chains
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ");
let (payments, entries) = if n == 1 {
("payment has", "entry")
} else {
("payments have", "entries")
};
out.push(Attention {
severity: Severity::Todo,
title: format!("{n} {payments} no payment method{in_year}"),
detail: format!(
"Paid for the company through a financing account in {year} ({entries} {ids}): a member's card doesn't count toward a 1099, their cash or check does, so the 1099 worksheet can't tell until each is recorded."
),
target: Some(Route::Reports(crate::nav::ReportKind::Vendor1099)),
prompt: format!(
"In wanderbooks, {entries} {ids} paid a vendor in {year} through a financing account with no payment method recorded, so the 1099 worksheet can't tell whether they count. Ask me how each was paid (whose card, or cash, check or a transfer); add any method not yet in books.instruments with books.add_instrument, then tag each entry with books.set_entry_instrument."
),
});
}
for v in r.vendors.iter().filter(|v| v.reportable && !v.tin_on_file) {
if year == today.year() {
asked_w9.insert(v.vendor_id);
@@ -323,7 +423,7 @@ pub fn find(b: &Books) -> Vec<Attention> {
),
target: Some(party.map_or(Route::Company, Route::Party)),
prompt: format!(
"Remind me to request a W-9 from {}; once I have it, mark it on file in wanderbooks with books.set_vendor_1099.",
"Remind me to request a W-9 from {}; once I have it, record what it says in wanderbooks with books.set_w9 (that marks it on file and keeps the 1099 decision).",
v.name
),
});
+50 -34
View File
@@ -156,18 +156,19 @@ impl AssetTrail {
/// 4a. Each of 3 and 4 is None when it can't be summed exactly (a
/// document is restored as an earlier version journaled it, of any
/// size: rules.md B-28), never a wrong number.
/// 5. form_1099: its vendor record's line in
/// vendor_1099_report_with(ledger, year, None, cash and financing
/// accounts), when it has a vendor
/// record in that report and the report knows the year's threshold
/// (threshold_1099 is then that threshold).
/// 5. form_1099: its vendor record's line in Books::vendor_1099(year)
/// (the server's books.vendor_1099), when it has a vendor record in
/// that report and the report can be made (the year's threshold is
/// known, the cash accounts are named; threshold_1099 is then that
/// threshold).
/// 6. payroll_runs: the runs with a line whose employee is the party,
/// or (older runs, no party on the line) whose employee_name is the
/// party's name ignoring case and surrounding space.
/// 7. events: indexes into Books::journal of party.* events with its
/// id, entry.party_set or entry.parties_set naming it, and its
/// records' vendor.added,
/// customer.added, vendor.1099_set, and entry.vendor_set events.
/// customer.added, vendor.1099_set, vendor.w9_flag_set, and
/// entry.vendor_set events.
/// ```
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PartyInfo {
@@ -1020,12 +1021,45 @@ impl Books {
}
/// Whether a party files a 1099: it has a 1099 role (contractor,
/// landlord), or its vendor record is marked for a 1099.
/// landlord), or its vendor record has been decided for one (from any
/// date: its W-9 matters for those payments).
pub fn files_1099(&self, p: &Party) -> bool {
p.roles.iter().any(|r| r.gets_1099())
|| p.vendor
.and_then(|v| self.ledger.get_vendor(v))
.is_some_and(|v| v.form_1099.is_some())
.is_some_and(|v| v.decisions_1099.iter().any(|d| d.decision.form().is_some()))
}
/// The 1099 report for calendar `year`, the server's
/// (ledger_package::vendor_1099_report, books.vendor_1099 with no
/// accounts named): payments out of the settings' cash and financing
/// accounts, classified by
/// payment method, shares resolved with the members. Every 1099
/// figure the viewer shows comes from here.
pub fn vendor_1099(&self, year: i32) -> Result<ledger_core::Vendor1099Report, LedgerError> {
let settings = ledger_package::Settings {
company: &self.settings.company,
cash_accounts: &self.settings.cash_accounts,
financing_accounts: &self.settings.financing_accounts,
investing_accounts: &self.settings.investing_accounts,
};
ledger_package::vendor_1099_report(&self.ledger, &settings, year, None, None)
}
/// The 1099 worksheet for calendar `year` without TINs: the server's
/// books.vendor_1099 (its lines are Books::vendor_1099's, with its
/// notes).
pub fn vendor_1099_worksheet(
&self,
year: i32,
) -> Result<ledger_package::Vendor1099Worksheet, String> {
let settings = ledger_package::Settings {
company: &self.settings.company,
cash_accounts: &self.settings.cash_accounts,
financing_accounts: &self.settings.financing_accounts,
investing_accounts: &self.settings.investing_accounts,
};
ledger_package::vendor_1099_worksheet(&self.ledger, &settings, year, None, None, None)
}
/// Every party's [`PartyInfo`].
@@ -1089,11 +1123,7 @@ impl Books {
}
}
}
let sources = ledger_core::PaymentAccounts {
cash: self.cash_accounts.clone(),
financing: self.financing_accounts.clone(),
};
if let Ok(r) = ledger_core::vendor_1099_report_with(l, year, None, &sources) {
if let Ok(r) = self.vendor_1099(year) {
for v in &r.vendors {
if let Some(info) = l.party_of_vendor(v.vendor_id).and_then(|p| out.get_mut(&p)) {
info.form_1099 = Some(v.clone());
@@ -1143,7 +1173,7 @@ impl Books {
}
"vendor.added" => u(p, "id").and_then(|v| l.party_of_vendor(v)),
"customer.added" => u(p, "id").and_then(|c| l.party_of_customer(c)),
"vendor.1099_set" | "entry.vendor_set" => {
"vendor.1099_set" | "vendor.w9_flag_set" | "entry.vendor_set" => {
u(p, "vendor_id").and_then(|v| l.party_of_vendor(v))
}
_ => None,
@@ -1894,27 +1924,13 @@ impl Books {
}
/// A party's 1099 worksheet line for a calendar year and that year's
/// threshold: ledger-core's vendor_1099_report_with_members line for
/// its vendor record, with Books::money_cash_accounts, the financing
/// accounts and the members (the server's books.vendor_1099 and
/// books.party amount_1099); None without a vendor record in the
/// report, or when the year's threshold isn't known.
/// threshold: its vendor record's line in Books::vendor_1099 (the
/// server's books.vendor_1099); None without a vendor record in the
/// report, or when the report can't be made (the year's threshold
/// isn't known, or no cash accounts are named).
pub fn party_1099(&self, party: PartyId, year: i32) -> Option<(Vendor1099Line, Decimal)> {
let l = &self.ledger;
let vendor = l.get_party(party)?.vendor?;
let sources = ledger_core::PaymentAccounts {
cash: self.money_cash_accounts.clone(),
financing: self.financing_accounts.clone(),
};
let r = ledger_core::vendor_1099_report_with_members(
l,
year,
None,
None,
&sources,
&self.members,
)
.ok()?;
let vendor = self.ledger.get_party(party)?.vendor?;
let r = self.vendor_1099(year).ok()?;
let line = r.vendors.iter().find(|v| v.vendor_id == vendor)?.clone();
Some((line, r.threshold))
}
+26 -5
View File
@@ -9,8 +9,8 @@
use chrono::{Datelike, Days, Months, NaiveDate};
use ledger_core::{
AccountType, CloseTo, Currency, Form1099, Frequency, Money, NewAsset, NewRecurring,
NewSchedule, ScheduleKind, Side, arap::DocumentLine,
AccountType, CloseTo, Currency, Dated1099, Decision1099, Form1099, Frequency, InstrumentKind,
Money, NewAsset, NewRecurring, NewSchedule, ScheduleKind, Side, arap::DocumentLine,
};
use ledger_store::{Statement, Store, StoreError, Subject};
use rust_decimal::Decimal;
@@ -245,12 +245,33 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
let client_class = s.add_class("Client work")?;
let template_class = s.add_class("Templates")?;
// Each vendor's 1099 decision, as the owner made it when they were
// first paid; the card is a payment method, so what was charged to it
// shows on the 1099 worksheet as excluded (Form 1099-K's).
let decided = |form| {
Dated1099::from_beginning(
Decision1099::Files(form),
Some(ledger_core::settings::Source {
decided_by: "Alex Rivera".into(),
decided_on: start,
citation: None,
note: None,
}),
)
};
let sam = s.add_vendor("Sam Okafor")?;
s.set_vendor_1099(sam, Some(Form1099::Nec), true)?;
s.set_vendor_1099(sam, &decided(Form1099::Nec), Some(true))?;
let riley = s.add_vendor("Riley Chen")?;
s.set_vendor_1099(riley, Some(Form1099::Nec), false)?;
s.set_vendor_1099(riley, &decided(Form1099::Nec), Some(false))?;
let landlord = s.add_vendor("Everett Lofts LLC")?;
s.set_vendor_1099(landlord, Some(Form1099::Misc), true)?;
s.set_vendor_1099(landlord, &decided(Form1099::Misc), Some(true))?;
s.add_instrument(
InstrumentKind::BusinessCard,
"Business credit card",
Some("4242"),
None,
card,
)?;
let customers: Vec<u64> = [
"Fern & Field Coffee",
"Bramble Outdoor Co.",
+3 -2
View File
@@ -201,6 +201,7 @@ impl Job {
&settings,
year,
None,
None,
Some(&tin),
)?;
let csv = ledger_package::vendor_1099_csv(&w, true);
@@ -210,10 +211,10 @@ impl Job {
let gaps = w
.lines
.iter()
.filter(|l| l.reportable && l.tin.is_none())
.filter(|l| l.may_file() && l.tin.is_none())
.count();
let note = (gaps > 0).then(|| {
format!("{gaps} reportable payee(s) have no TIN in it: ask your agent to collect them.")
format!("{gaps} payee(s) a 1099 is or may be due for have no TIN in it: ask your agent to collect them.")
});
Ok((path, note))
}
+16 -8
View File
@@ -11,8 +11,8 @@ use crate::theme::{self, pal};
use crate::widgets;
use egui::{CornerRadius, RichText, Sense, Ui, vec2};
use egui_phosphor::regular as icon;
use ledger_core::PeriodStatus;
use ledger_core::assets::{accumulated_depreciation, book_value};
use ledger_core::{PeriodStatus, vendor_1099_report};
use rust_decimal::Decimal;
fn kv(ui: &mut Ui, k: &str, v: &str) {
@@ -597,7 +597,7 @@ fn parties(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
}
};
let year = chrono::Datelike::year(&b.today);
let r1099 = vendor_1099_report(&b.ledger, year, None).ok();
let r1099 = b.vendor_1099(year).ok();
columns(ui, 980.0, &[1.0, 1.0], |i, ui| match i {
0 => {
widgets::titled_card(
@@ -618,13 +618,21 @@ fn parties(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
.unwrap_or_default();
ui.horizontal_wrapped(|ui| {
name(ui, &v.name, b.ledger.party_of_vendor(v.id));
if let Some(f) = v.form_1099 {
widgets::pill(ui, f.label(), p.accent, p.accent_soft);
if v.tin_on_file {
widgets::pill(ui, "W-9", p.positive, p.positive_soft);
} else {
widgets::pill(ui, "no W-9", p.negative, p.negative_soft);
match v.decision_on(b.today) {
Some(ledger_core::Decision1099::Files(f)) => {
widgets::pill(ui, f.label(), p.accent, p.accent_soft);
if v.tin_on_file {
widgets::pill(ui, "W-9", p.positive, p.positive_soft);
} else {
widgets::pill(ui, "no W-9", p.negative, p.negative_soft);
}
}
Some(ledger_core::Decision1099::NotReportable { .. }) => {
widgets::pill(ui, "No 1099", p.muted, p.hover);
}
// Undecided: nothing to show (the 1099
// worksheet and Needs attention ask).
_ => {}
}
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
+71 -15
View File
@@ -631,21 +631,36 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
let vendor = p.get("vendor_id").and_then(|v| v.as_u64());
let name = vendor
.and_then(|v| b.ledger.get_vendor(v))
.map(|v| v.name.clone());
let form = p
.get("form_1099")
.and_then(|v| v.as_str())
.and_then(ledger_core::Form1099::parse)
.map(|f| f.label());
let w9 = p.get("tin_on_file").and_then(|v| v.as_bool());
let text = match (name, form, w9) {
(Some(n), Some(f), Some(w)) => format!(
"1099 details of {n}: {f}, W-9 {}",
if w { "on file" } else { "not on file" }
),
(Some(n), None, Some(_)) => format!("{n} no longer gets a 1099"),
(Some(n), _, None) => format!("Updated the 1099 details of {n}"),
(None, ..) => "Updated a vendor's 1099 details".into(),
.map_or_else(|| "a vendor".to_string(), |v| v.name.clone());
let w9 = |w: bool| if w { "on file" } else { "not on file" };
let text = match ledger_store::form1099::decision_of(line) {
// Recorded with who decided (plan defects 18, 23; D-40).
Ok((_, d, _)) if d.source.is_some() => {
let s = d.source.as_ref().expect("checked");
let what = match &d.decision {
ledger_core::Decision1099::Undetermined => "undetermined".to_string(),
other => crate::pages::reports::decision_text(other),
};
let from = d
.effective
.map(|e| format!(" from {}", fmt::date(e)))
.unwrap_or_default();
format!(
"1099 decision for {name}{from}: {what} (by {}, {})",
s.decided_by,
fmt::date(s.decided_on)
)
}
// Written before decisions were kept: a form, or none
// (which can't say whether anyone decided: plan 5.5).
Ok((_, d, flag)) => match d.decision.form() {
Some(f) => format!("1099 details of {name}: {}, W-9 {}", f.label(), w9(flag)),
None => format!(
"1099 details of {name}: no form recorded (undetermined), W-9 {}",
w9(flag)
),
},
Err(_) => format!("Updated the 1099 details of {name}"),
};
ev(
icon::IDENTIFICATION_CARD,
@@ -657,6 +672,47 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
),
)
}
"vendor.w9_flag_set" => {
let vendor = p.get("vendor_id").and_then(|v| v.as_u64());
let name = vendor
.and_then(|v| b.ledger.get_vendor(v))
.map_or_else(|| "a vendor".to_string(), |v| v.name.clone());
let text = match p.get("tin_on_file").and_then(|v| v.as_bool()) {
Some(true) => format!("W-9 of {name} marked on file"),
Some(false) => format!("W-9 of {name} marked not on file"),
None => format!("Updated the W-9 of {name}"),
};
ev(
icon::IDENTIFICATION_CARD,
text,
Some(
vendor
.and_then(|v| b.ledger.party_of_vendor(v))
.map_or(Route::Company, Route::Party),
),
)
}
"instrument.added" => {
let text = match ledger_store::form1099::instrument_of(line) {
Ok(i) => format!("Added payment method {} ({})", i.label, i.kind.label()),
Err(_) => "Added a payment method".to_string(),
};
ev(icon::CREDIT_CARD, text, None)
}
"entry.instrument_set" => {
let e = entry_id.unwrap_or(0);
let instrument = p.get("instrument_id").and_then(|v| v.as_u64());
Some(Event {
icon: icon::CREDIT_CARD,
text: match instrument.and_then(|i| b.ledger.get_instrument(i)) {
Some(i) => format!("Payment method of #{e}: {}", i.label),
None if instrument.is_none() => format!("Cleared the payment method of #{e}"),
None => format!("Tagged #{e} with a payment method"),
},
route: entry_id.map(Route::Entry),
also: None,
})
}
"entry.vendor_set" => {
let vendor = p.get("vendor_id").and_then(|v| v.as_u64());
let e = entry_id.unwrap_or(0);
+20 -7
View File
@@ -751,21 +751,27 @@ pub fn show_party(ui: &mut Ui, b: &Books, id: PartyId, shell: &mut Shell<'_>) {
b.party_1099(id, tax_year)
};
if let Some((l, threshold)) = &form {
let sub = match l.form {
Some(_) if l.reportable => (
let sub = match l.status {
ledger_core::Status1099::Reportable => (
format!("Reportable · threshold {}", fmt::dollars(*threshold)),
p.warning,
),
Some(_) => (
ledger_core::Status1099::UnderThreshold => (
format!("Under the {} threshold", fmt::dollars(*threshold)),
p.muted,
),
None => ("Not marked for a 1099".into(), p.muted),
ledger_core::Status1099::NotReportable => ("Not reportable".into(), p.muted),
ledger_core::Status1099::Undetermined if l.decision_needed => {
("Needs a 1099 decision".into(), p.warning)
}
ledger_core::Status1099::Undetermined => {
("Undetermined: how was it paid?".into(), p.warning)
}
};
tiles.push((
format!("1099 amount, {tax_year}"),
Some(l.paid),
"Cash paid in the calendar year, from the 1099 worksheet (ledger-core): posted entries tagged with its vendor record, net of refunds and reversals. Card payments aren't counted (the card issuer reports them).".into(),
"What counts toward its 1099 in the calendar year, from the 1099 worksheet (the server's books.vendor_1099): posted entries tagged with its vendor record, paid by check, ACH, transfer or cash, net of refunds and reversals. Card and payment-network payments aren't counted (the processor reports them on Form 1099-K); what a member paid with no payment method recorded is undetermined until it is.".into(),
Some(sub),
));
}
@@ -1176,8 +1182,15 @@ fn records_card(ui: &mut Ui, b: &Books, party: &Party, info: &PartyInfo, go: &mu
k(ui, "Vendor");
ui.horizontal_wrapped(|ui| {
ui.label(format!("{} · vendor {}", v.name, v.id));
if let Some(f) = v.form_1099 {
widgets::pill(ui, f.label(), p.accent, p.accent_soft);
match v.decision_on(b.today) {
Some(ledger_core::Decision1099::Files(f)) => {
widgets::pill(ui, f.label(), p.accent, p.accent_soft);
}
Some(ledger_core::Decision1099::NotReportable { .. }) => {
widgets::pill(ui, "No 1099", p.muted, p.hover);
}
// Undecided: nothing (Needs attention asks).
_ => {}
}
});
ui.end_row();
+72 -22
View File
@@ -18,7 +18,7 @@ use ledger_core::reports::{
balance_sheet_as_of, general_ledger, members_equity, profit_and_loss_for_period,
trial_balance_as_of,
};
use ledger_core::{AccountId, AccountType, MemberCapital, tax_summary, vendor_1099_report};
use ledger_core::{AccountId, AccountType, MemberCapital, tax_summary};
use rust_decimal::Decimal;
use std::collections::BTreeMap;
@@ -2193,51 +2193,94 @@ fn gl(ui: &mut Ui, b: &Books, s: NaiveDate, e: NaiveDate, shell: &mut Shell<'_>)
}
}
/// A 1099 decision in the viewer's words: "1099-NEC", "Not reportable: a
/// corporation (by name)", "Undecided".
pub fn decision_text(d: &ledger_core::Decision1099) -> String {
match d {
ledger_core::Decision1099::Undetermined => "Undecided".into(),
ledger_core::Decision1099::Files(f) => f.label().into(),
ledger_core::Decision1099::NotReportable { kind, reason } => {
format!("Not reportable: {} ({reason})", kind.label())
}
}
}
/// A worksheet line's 1099 decisions in words, each change with its date.
fn decisions_text(l: &ledger_package::Vendor1099WorksheetLine) -> String {
if l.decisions.is_empty() {
return decision_text(&ledger_core::Decision1099::Undetermined);
}
l.decisions
.iter()
.enumerate()
.map(|(i, d)| match d.effective {
Some(e) if i > 0 => format!("from {}: {}", fmt::date(e), decision_text(&d.decision)),
_ => decision_text(&d.decision),
})
.collect::<Vec<_>>()
.join(" → ")
}
fn vendor_1099(ui: &mut Ui, b: &Books, year: i32, shell: &mut Shell<'_>) {
let p = pal(ui.ctx());
let r = match vendor_1099_report(&b.ledger, year, None) {
Ok(r) => r,
let w = match b.vendor_1099_worksheet(year) {
Ok(w) => w,
Err(e) => {
// Never guess a threshold (rules.md B-19): say why there's no
// worksheet, in the engine's words.
ui.label(RichText::new(e.to_string()).color(p.muted));
// Never guess a threshold or the cash accounts (rules.md
// B-19): say why there's no worksheet, in the engine's words.
ui.label(RichText::new(e).color(p.muted));
return;
}
};
ui.label(
RichText::new(format!(
"Calendar {year}. Threshold {} ($600 through 2025; $2,000 from 2026, indexed after). Counts cash paid on entries tagged with the vendor; card payments are reported by the card processor (1099-K) and drop out.",
fmt::dollars(r.threshold)
"Calendar {year}. Threshold {} ($600 through 2025; $2,000 from 2026, indexed after). Counts what each vendor was paid by check, ACH, transfer or cash on entries tagged with it, under the 1099 decision in force on each payment's date. Card and payment-network payments are reported by the processor on Form 1099-K and don't count; what a member paid with no payment method recorded is undetermined until it is.",
fmt::dollars(w.threshold)
))
.color(p.muted),
);
ui.add_space(10.0);
let mut t = Table::new(vec!["Paid".into()]);
for v in &r.vendors {
let form = v.form.map(|f| f.label()).unwrap_or("No 1099");
let mut t = Table::new(vec![
"Counts".into(),
"Card or network".into(),
"Undetermined".into(),
]);
for v in &w.lines {
let w9 = if v.tin_on_file {
"W-9 on file"
} else {
"no W-9"
};
let status = if v.reportable {
if v.tin_on_file {
"File a 1099"
} else {
"File a 1099 · get a W-9 first"
let status = match v.status {
ledger_core::Status1099::Reportable if v.tin_on_file => "File a 1099",
ledger_core::Status1099::Reportable => "File a 1099 · get a W-9 first",
ledger_core::Status1099::UnderThreshold => "Under the threshold",
ledger_core::Status1099::NotReportable => "No 1099",
ledger_core::Status1099::Undetermined if v.decision_needed => {
"Decide whether it gets a 1099"
}
} else {
"Not reportable"
ledger_core::Status1099::Undetermined => "Record how it was paid",
};
let mut detail = format!("{} · {w9} · {status}", decisions_text(v));
if !v.not_reportable.is_zero() {
detail.push_str(&format!(
" · {} paid while not reportable",
fmt::dollars(v.not_reportable)
));
}
t.rows.push(Row {
kind: Kind::Line,
label: v.name.clone(),
detail: format!("{form} · {w9} · {status}"),
cells: vec![Cell::of(v.paid)],
detail,
cells: vec![
Cell::of(v.paid),
Cell::of(v.excluded_card),
Cell::of(v.undetermined),
],
});
}
if r.vendors.is_empty() {
t.note("No vendors marked for a 1099 and none paid this year.");
if w.lines.is_empty() {
t.note("No vendors decided for a 1099 and none paid this year.");
}
let pdf = PdfInfo::new(
b,
@@ -2245,6 +2288,13 @@ fn vendor_1099(ui: &mut Ui, b: &Books, year: i32, shell: &mut Shell<'_>) {
&format!("Calendar year {year}"),
);
render(ui, &t, &pdf, shell);
// The worksheet's notes, as books.vendor_1099 gives them, whole (a
// table note is one clipped line).
ui.add_space(10.0);
for n in &w.notes {
ui.add(egui::Label::new(RichText::new(n).font(theme::regular(12.0)).color(p.muted)).wrap());
ui.add_space(4.0);
}
}
#[cfg(test)]