feat: tax categories and the tax summary report
wanderbooks is a general MCP-first accounting application: every business needs its accounts mapped to where they land on its income tax return, and its CPA needs book-to-tax differences surfaced, not hidden. - ledger_core::tax: a catalog of stable categories (gross_receipts, commissions_fees, meals, guaranteed_payments, officer_compensation, nondeductible, ...) with kind (income, cost of goods, deduction), the returns each appears on (Schedule C, 1065, 1120-S, 1120), a default deductible percent (meals 50, nondeductible 0), and guidance. Keys, not line numbers: form lines move from year to year. - Accounts carry tax_category and an optional deductible_percent override, validated against the account type (and the return, at the MCP layer), journaled as account.tax_set, replayed, projected (schema v6). - tax_summary(start, end, form): the P&L grouped by category, book and deductible amounts (half away from zero to the cent), contra income (refunds) reducing receipts, and every account still uncategorized. - MCP: books.tax_categories (filtered to the configured return), books.set_tax_category, a tax_summary report in every format and in the CPA package, chart output with categories, and a fifth tax_category column in chart CSV imports, validated before anything is created. [company] tax_form, or derived from tax_classification. Scope stays book basis: no tax depreciation, basis, or filing. Tests: a hand-computed quarter (50% meals 333.33 -> 166.67, penalties at 0%, refunds as negative receipts, guaranteed payments, one uncategorized account; book net 5,276.67 vs tax 5,483.33); overrides; category/account /return validation; form parsing and derivation; CSV parsing; stdio: filtered catalog, mapping, refusal, summary, all-or-nothing import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
18 files changed
+1352
-14
No files matched your search
@@ -228,10 +228,55 @@ impl Ledger {
|
||||
parent,
|
||||
placeholder,
|
||||
currency: self.base_currency,
|
||||
tax_category: None,
|
||||
deductible_percent: None,
|
||||
})?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Set (or clear, with `None`) an account's tax category, and optionally
|
||||
/// override the category's deductible percent (0 to 100). The category
|
||||
/// must fit the account's type (see [`crate::tax::check_category`]).
|
||||
pub fn set_account_tax(
|
||||
&mut self,
|
||||
account: AccountId,
|
||||
category: Option<&str>,
|
||||
deductible_percent: Option<Decimal>,
|
||||
) -> Result<(), LedgerError> {
|
||||
self.check_account_tax(account, category, deductible_percent)?;
|
||||
self.chart
|
||||
.set_tax(account, category.map(str::to_string), deductible_percent)
|
||||
}
|
||||
|
||||
/// The validation behind [`Self::set_account_tax`], for stores that
|
||||
/// journal before mutating.
|
||||
pub fn check_account_tax(
|
||||
&self,
|
||||
account: AccountId,
|
||||
category: Option<&str>,
|
||||
deductible_percent: Option<Decimal>,
|
||||
) -> Result<(), LedgerError> {
|
||||
let a = self
|
||||
.chart
|
||||
.get(account)
|
||||
.ok_or(LedgerError::UnknownAccount(account))?;
|
||||
if let Some(key) = category {
|
||||
crate::tax::check_category(key, a.account_type, None)?;
|
||||
} else if deductible_percent.is_some() {
|
||||
return Err(LedgerError::InvalidTaxCategory(
|
||||
"a deductible percent needs a tax category".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(p) = deductible_percent
|
||||
&& (p < Decimal::ZERO || p > Decimal::from(100))
|
||||
{
|
||||
return Err(LedgerError::InvalidTaxCategory(format!(
|
||||
"deductible percent {p} must be between 0 and 100"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a draft journal entry. Returns its id.
|
||||
///
|
||||
/// This is the unchecked constructor: replay and internal callers
|
||||
|
||||
Reference in new issue
Block a user