P3: dated 1099 decisions, payment methods and the card exception (defects 23, 12, 18, D-40)
check / check (push) Canceled after 0s
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:
77 files changed
+11455
-970
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user