feat: 1099 tracking — vendor forms, payment tags, year-end worksheet

Vendors carry form_1099 (NEC/MISC) and tin_on_file (never the TIN).
Entries carry the vendor they paid; bills, bill payments, and reversals
are tagged automatically. books.vendor_1099 counts cash paid per vendor
per calendar year (card payments excluded) against $600 through 2025 and
$2,000 from 2026, or an override, and flags missing W-9s. Four new tools
(64), two journal events, schema v7. Docs: finished R-FUTURE work is no
longer listed as future.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-11 21:18:32 -07:00
1 parent d65f9aa840
commit b48addd0e0
15 files changed
+967 -8

No files matched your search

+61 -1
View File
@@ -104,6 +104,8 @@ pub struct Ledger {
/// Recurring entries.
recurring: std::collections::BTreeMap<RecurringId, RecurringEntry>,
next_recurring_id: RecurringId,
/// Who each entry paid or was billed by, for 1099 reporting.
entry_vendors: HashMap<EntryId, VendorId>,
}
impl Ledger {
@@ -138,6 +140,7 @@ impl Ledger {
next_schedule_id: 1,
recurring: std::collections::BTreeMap::new(),
next_recurring_id: 1,
entry_vendors: HashMap::new(),
}
}
@@ -635,6 +638,10 @@ impl Ledger {
.ok_or(LedgerError::UnknownEntry(id))?
.mark_reversal_of(entry);
self.post_entry(id)?;
// A reversed payment un-pays the vendor: carry the tag.
if let Some(v) = self.entry_vendor(entry) {
self.entry_vendors.insert(id, v);
}
Ok(id)
}
@@ -1127,6 +1134,8 @@ impl Ledger {
Vendor {
id,
name: name.to_string(),
form_1099: None,
tin_on_file: false,
},
);
Ok(id)
@@ -1142,6 +1151,53 @@ impl Ledger {
self.vendors.get(&id)
}
/// Set which information return a vendor gets (or none), and whether
/// their W-9 is on file.
pub fn set_vendor_1099(
&mut self,
id: VendorId,
form: Option<crate::form1099::Form1099>,
tin_on_file: bool,
) -> Result<(), LedgerError> {
let v = self
.vendors
.get_mut(&id)
.ok_or(LedgerError::UnknownVendor(id))?;
v.form_1099 = form;
v.tin_on_file = tin_on_file;
Ok(())
}
/// Tag an entry (draft or posted) with the vendor it paid or was billed
/// by, or clear the tag. The tag is reporting metadata, like a receipt
/// link: it changes no amounts, so posted entries may be tagged.
pub fn set_entry_vendor(
&mut self,
entry: EntryId,
vendor: Option<VendorId>,
) -> Result<(), LedgerError> {
if !self.entries.contains_key(&entry) {
return Err(LedgerError::UnknownEntry(entry));
}
match vendor {
Some(v) => {
if !self.vendors.contains_key(&v) {
return Err(LedgerError::UnknownVendor(v));
}
self.entry_vendors.insert(entry, v);
}
None => {
self.entry_vendors.remove(&entry);
}
}
Ok(())
}
/// The vendor an entry is tagged with.
pub fn entry_vendor(&self, entry: EntryId) -> Option<VendorId> {
self.entry_vendors.get(&entry).copied()
}
/// All customers, in id order.
pub fn list_customers(&self) -> Vec<&Customer> {
let mut ids: Vec<CustomerId> = self.customers.keys().copied().collect();
@@ -1254,6 +1310,7 @@ impl Ledger {
entry_id: entry,
},
);
self.entry_vendors.insert(entry, vendor_id);
Ok(id)
}
@@ -1336,6 +1393,7 @@ impl Ledger {
let b = self.bills.get_mut(&bill_id).expect("bill exists");
b.amount_paid += amount;
b.status = arap::derive_status(b.amount_paid, b.total());
self.entry_vendors.insert(entry, bill.vendor_id);
Ok(entry)
}
@@ -1403,6 +1461,7 @@ impl Ledger {
entry_id,
},
);
self.entry_vendors.insert(entry_id, vendor_id);
Ok(())
}
@@ -1437,13 +1496,14 @@ impl Ledger {
amount: Decimal,
entry_id: EntryId,
) -> Result<(), LedgerError> {
let _ = entry_id;
let bill = self
.bills
.get_mut(&bill_id)
.ok_or(LedgerError::UnknownBill(bill_id))?;
bill.amount_paid += amount;
bill.status = arap::derive_status(bill.amount_paid, bill.total());
let vendor = bill.vendor_id;
self.entry_vendors.insert(entry_id, vendor);
Ok(())
}