tax summary: Schedule K items stated separately; XLSX never shows a wrong amount
From the IRS form instructions (now quoted in docs/spec/sources/ irs-tax-lines.md): - On Forms 1065 and 1120-S, Schedule K takes "only taxable portfolio interest", and charitable contributions are among the items the entity "must state separately". The tax summary counted both in ordinary income and deductions; they now go in TaxSummary::separately_stated, outside book_net and tax_net, and the report, MCP export, and viewer list them under "Separately stated to owners (Schedule K)". - Schedule C: "Do not include charitable contributions". The category no longer applies to Schedule C (a sole proprietor's gift is personal, an owner draw). - interest_income's note: interest earned in the business itself is ordinary income (other_income). XLSX: a cell number is a binary double and Excel honors 15 significant digits (Microsoft, "Excel specifications and limits"), so an amount over $9,999,999,999,999.99 or finer than a cent is written as its exact text, right-aligned, instead of a number Excel would show wrong. The old conversion also fell back to 0.0 silently; gone. Property test: every whole-cent amount up to 15 digits round-trips exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
d99589e6c1
commit
96c70f727a
9 files changed
+195
-8
No files matched your search
@@ -99,6 +99,8 @@ pub struct TaxCategory {
|
||||
|
||||
const ALL: &[TaxForm] = &TaxForm::ALL;
|
||||
const CORPS: &[TaxForm] = &[TaxForm::Form1120S, TaxForm::Form1120];
|
||||
/// Every return but Schedule C.
|
||||
const ENTITIES: &[TaxForm] = &[TaxForm::Form1065, TaxForm::Form1120S, TaxForm::Form1120];
|
||||
|
||||
const fn cat(
|
||||
key: &'static str,
|
||||
@@ -144,7 +146,7 @@ pub const CATEGORIES: &[TaxCategory] = &[
|
||||
Income,
|
||||
ALL,
|
||||
100,
|
||||
"Pass-throughs report it separately to owners (portfolio income).",
|
||||
"Portfolio interest: partnerships and S corps state it separately (Schedule K). Interest earned in the business itself (on customer accounts, say) is ordinary income: use other_income.",
|
||||
),
|
||||
cat("other_income", "Other income", Income, ALL, 100, ""),
|
||||
cat(
|
||||
@@ -176,9 +178,9 @@ pub const CATEGORIES: &[TaxCategory] = &[
|
||||
"charitable_contributions",
|
||||
"Charitable contributions",
|
||||
Deduction,
|
||||
ALL,
|
||||
ENTITIES,
|
||||
100,
|
||||
"Separately stated to owners for partnerships and S corps; limited for C corps; not a Schedule C deduction.",
|
||||
"Separately stated to owners for partnerships and S corps (Schedule K); limited for C corps. Not on Schedule C (\"Do not include charitable contributions\"): a sole proprietor's gift is personal, an owner draw.",
|
||||
),
|
||||
cat(
|
||||
"commissions_fees",
|
||||
@@ -419,6 +421,12 @@ pub struct TaxSummary {
|
||||
pub cost_of_goods: Vec<TaxSummaryLine>,
|
||||
/// Deductions.
|
||||
pub deductions: Vec<TaxSummaryLine>,
|
||||
/// Items a partnership or S corporation states separately to its owners
|
||||
/// (Schedule K) instead of netting them in ordinary income: portfolio
|
||||
/// interest and charitable contributions (docs/spec/sources/
|
||||
/// irs-tax-lines.md). Not in [`TaxSummary::book_net`] or `tax_net`.
|
||||
#[serde(default)]
|
||||
pub separately_stated: Vec<TaxSummaryLine>,
|
||||
/// P&L accounts with activity and no category.
|
||||
pub uncategorized: Vec<Uncategorized>,
|
||||
}
|
||||
@@ -497,11 +505,16 @@ pub fn tax_summary(
|
||||
income: vec![],
|
||||
cost_of_goods: vec![],
|
||||
deductions: vec![],
|
||||
separately_stated: vec![],
|
||||
uncategorized,
|
||||
};
|
||||
// Catalog order, so the summary reads like the return.
|
||||
for c in CATEGORIES {
|
||||
if let Some(line) = by_cat.remove(c.key) {
|
||||
if separately_stated(c.key, form) {
|
||||
out.separately_stated.push(line);
|
||||
continue;
|
||||
}
|
||||
match c.kind {
|
||||
TaxKind::Income => out.income.push(line),
|
||||
TaxKind::CostOfGoods => out.cost_of_goods.push(line),
|
||||
@@ -511,3 +524,13 @@ pub fn tax_summary(
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Whether a partnership or S corporation states this category separately
|
||||
/// to its owners (Schedule K) rather than in ordinary income. Form 1065
|
||||
/// and 1120-S instructions: Schedule K takes "only taxable portfolio
|
||||
/// interest", and "charitable contributions" are among the "items ... must
|
||||
/// state separately".
|
||||
pub fn separately_stated(key: &str, form: Option<TaxForm>) -> bool {
|
||||
matches!(form, Some(TaxForm::Form1065 | TaxForm::Form1120S))
|
||||
&& matches!(key, "interest_income" | "charitable_contributions")
|
||||
}
|
||||
@@ -264,3 +264,81 @@ fn forms_have_their_irs_names() {
|
||||
assert_eq!(TaxForm::parse(f.label()), Some(f));
|
||||
}
|
||||
}
|
||||
|
||||
/// A small book with portfolio interest (125) and a donation (300) besides
|
||||
/// sales (1,000), for where each lands by return.
|
||||
fn interest_and_gift() -> Ledger {
|
||||
let mut l = Ledger::new(Currency::USD);
|
||||
let cash = l
|
||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let mut acct = |code: &str, t, cat: &str| {
|
||||
let id = l.add_account(code, code, t, None, false).unwrap();
|
||||
l.set_account_tax(id, Some(cat), None).unwrap();
|
||||
id
|
||||
};
|
||||
let sales = acct("4000", AccountType::Revenue, "gross_receipts");
|
||||
let interest = acct("4200", AccountType::Revenue, "interest_income");
|
||||
let gift = acct("6900", AccountType::Expense, "charitable_contributions");
|
||||
let usd = |s: &str| Money::new(dec(s), Currency::USD);
|
||||
for (dr, cr, amt) in [
|
||||
(cash, sales, "1000"),
|
||||
(cash, interest, "125"),
|
||||
(gift, cash, "300"),
|
||||
] {
|
||||
let e = l.create_entry(d(2, 1), "x");
|
||||
l.add_line(e, dr, Side::Debit, usd(amt)).unwrap();
|
||||
l.add_line(e, cr, Side::Credit, usd(amt)).unwrap();
|
||||
l.post_entry(e).unwrap();
|
||||
}
|
||||
l
|
||||
}
|
||||
|
||||
/// Oracle: docs/spec/sources/irs-tax-lines.md. On Forms 1065 and 1120-S,
|
||||
/// Schedule K takes "only taxable portfolio interest", and charitable
|
||||
/// contributions are among the "items ... must state separately": both
|
||||
/// leave the ordinary-income net (1,000 here, not 825) and are listed
|
||||
/// apart. On Form 1120 they stay in it (lines 5 and 19).
|
||||
#[test]
|
||||
fn pass_throughs_state_interest_and_gifts_separately() {
|
||||
let l = interest_and_gift();
|
||||
for form in [TaxForm::Form1065, TaxForm::Form1120S] {
|
||||
let ts = tax_summary(&l, d(1, 1), d(12, 31), Some(form)).unwrap();
|
||||
let keys: Vec<&str> = ts
|
||||
.separately_stated
|
||||
.iter()
|
||||
.map(|x| x.category.as_str())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
keys,
|
||||
["interest_income", "charitable_contributions"],
|
||||
"{form:?}"
|
||||
);
|
||||
assert_eq!(ts.book_net(), dec("1000"), "{form:?}");
|
||||
}
|
||||
let ts = tax_summary(&l, d(1, 1), d(12, 31), Some(TaxForm::Form1120)).unwrap();
|
||||
assert!(ts.separately_stated.is_empty());
|
||||
assert_eq!(ts.book_net(), dec("825"));
|
||||
}
|
||||
|
||||
/// Oracle: Schedule C instructions, Part V ("Do not include charitable
|
||||
/// contributions"): the category is refused for a Schedule C business.
|
||||
#[test]
|
||||
fn a_sole_proprietor_cannot_deduct_a_gift_on_schedule_c() {
|
||||
assert!(matches!(
|
||||
check_category(
|
||||
"charitable_contributions",
|
||||
AccountType::Expense,
|
||||
Some(TaxForm::ScheduleC)
|
||||
),
|
||||
Err(LedgerError::InvalidTaxCategory(_))
|
||||
));
|
||||
assert!(
|
||||
check_category(
|
||||
"charitable_contributions",
|
||||
AccountType::Expense,
|
||||
Some(TaxForm::Form1120)
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user