P3: assets' posted and scheduled depreciation, their bases, and the K-1 on the tax basis (defect 13, D-41)
check / check (push) Canceled after 0s

Book value is cost less what the books posted; the schedule's figure is
apart. An asset's acquired date comes from its entry. Contributed
property keeps its section 723 carryover basis apart from its depreciable
basis (the lower of value and adjusted basis), with its 704(c) built-in
gain and method. The K-1 worksheet reports tax-basis capital (Form 1065
Item L) beside the book figures, leaving open what only the CPA knows.

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 14:04:12 -06:00
1 parent 3a9aa92315
commit 2f999c6050
81 files changed
+9391 -2604

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ pub fn export_iif(
use ledger_import::{QbAccountType as Qb, iif_accounts, journal_to_iif};
let fixed: std::collections::HashSet<AccountId> = ledger
.registered_assets()
.filter(|a| a.purchase_date <= as_of)
.filter(|a| ledger.asset_acquired_date(a.id).is_some_and(|d| d <= as_of))
.flat_map(|a| [a.cost_account, a.accum_depr_account])
.collect();
let receivables = ledger.role_account(&ledger_core::RECEIVABLES);
+233
View File
@@ -0,0 +1,233 @@
//! The K-1 worksheet on the tax basis: `k1-tax-basis-capital.csv`, each
//! member's Item L capital account on the tax basis beside the book
//! figures of `k1-members.csv`, with the reconciliation between them.
//!
//! ```text
//! GOAL: hand the CPA Item L on the tax basis as far as the books know it,
//! line by line beside the book figures, each figure the books can't
//! know left blank and named, never guessed.
//! SOURCE: ledger_core::tax_capital (INVARIANTS 1-5); Instructions for Form
//! 1065 (2025), Item L, "Tax-basis method" (rules.md B-36);
//! DECISION D-41 (owner, 2026-10-20).
//! INVARIANTS:
//! 1. Per member, in order: the seven Item L lines (beginning; capital
//! contributed, cash and property; current year net income (loss);
//! other increase (decrease); withdrawals and distributions; ending),
//! each with its book figure, its tax-basis figure (blank while open),
//! book less tax (blank while open), its status ("tax basis" or
//! "open"), and a note: for an open line, what the books know so far
//! and every figure the CPA supplies, with the book amount it
//! replaces.
//! 2. Then the reconciliation of book to tax capital, by cause, ending
//! with "book ending less tax ending" (tax_capital INVARIANT 5): when
//! the ending is open, it reconciles to what the books know so far.
//! 3. Every amount is exact (crate::text::money); nothing is rounded.
//! ```
use crate::text::{csv_escape, money};
use ledger_core::Ledger;
use ledger_core::tax_capital::{Open, TaxCapital, TaxLine};
use rust_decimal::Decimal;
/// The file's name in the package.
pub const FILE_NAME: &str = "k1-tax-basis-capital.csv";
/// What the CPA supplies for an open item, with the book amount it
/// replaces.
pub fn open_text(ledger: &Ledger, o: &Open) -> String {
let asset = |id: u64| {
ledger
.get_asset(id)
.and_then(|a| a.tag.clone())
.unwrap_or_else(|| format!("asset {id}"))
};
match o {
Open::PriorYears => {
"last year's ending capital on the tax basis (Item L of last year's return)".into()
}
Open::PropertyBasis { entry, book } => format!(
"the adjusted basis of property contributed in entry {entry} with no tax basis in the books ({} at book; record it on its asset with books.set_asset_tax_basis)",
money(*book)
),
Open::LinkedValueExceedsEntry {
entry,
linked,
property,
} => format!(
"the basis of the property contributed in entry {entry}: the assets linked to it are valued at {}, more than the {} of property it contributed (correct the link or the values)",
money(*linked),
money(*property)
),
Open::PropertyDistribution { entry, book } => format!(
"the company's adjusted tax basis in property distributed in entry {entry} ({} at book)",
money(*book)
),
Open::TaxDepreciation {
book,
contributed_book,
} => format!(
"tax depreciation of the fixed assets in service (MACRS, IRC §168), in place of the book depreciation posted ({} in the span, {} of it on contributed property, whose tax depreciation each property's §704(c) method allocates, Treas. Reg. §1.704-3)",
money(*book),
money(*contributed_book)
),
Open::Disposal {
asset: id,
proceeds,
} => format!(
"the tax gain or loss on {}'s disposal (proceeds {}), from its adjusted tax basis",
asset(*id),
money(*proceeds)
),
Open::OtherChange { book } => format!(
"the tax treatment of a {} change in book capital the books can't explain",
money(*book)
),
}
}
/// The worksheet (INVARIANTS 1-3). `names` are the members' names, in the
/// rows' order.
pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> String {
let mut w = String::from("member,item,book,tax_basis,book_less_tax,status,note\n");
let mut push = |name: &str, item: &str, cells: [String; 5]| {
w.push_str(&csv_escape(name));
w.push(',');
w.push_str(&csv_escape(item));
for c in cells {
w.push(',');
w.push_str(&csv_escape(&c));
}
w.push('\n');
};
for (row, name) in tc.rows.iter().zip(names) {
let line = |book: Decimal, tax: &TaxLine, note: &str| -> [String; 5] {
match tax.tax() {
Some(t) => [
money(book),
money(t),
money(book - t),
"tax basis".into(),
note.into(),
],
None => {
let needs: Vec<String> =
tax.open.iter().map(|o| open_text(ledger, o)).collect();
[
money(book),
String::new(),
String::new(),
"open: the CPA's".into(),
format!(
"known so far {}; needs {}",
money(tax.known),
needs.join("; ")
),
]
}
}
};
let b = &row.book;
let cash = TaxLine {
known: row.contributions_cash,
open: Vec::new(),
};
push(
name,
"Beginning capital",
line(b.beginning, &row.beginning, ""),
);
push(
name,
"Capital contributed: cash",
line(b.contributions_cash, &cash, ""),
);
push(
name,
"Capital contributed: property",
line(
b.contributions_property,
&row.contributions_property,
"at the property's adjusted basis (IRC §723); book: its fair market value",
),
);
push(
name,
"Current year net income (loss)",
line(
b.net_income,
&row.net_income,
"the book figure: the books know of no difference from it (any other, Schedule M-1, is the CPA's)",
),
);
push(
name,
"Other increase (decrease)",
line(b.other, &row.other, ""),
);
push(
name,
"Withdrawals and distributions",
line(
b.distributions,
&row.distributions,
"cash, plus the adjusted tax basis of property distributed",
),
);
push(name, "Ending capital", line(b.ending, &row.ending, ""));
let r = &row.reconciliation;
let recon = |amount: Option<Decimal>, note: &str| -> [String; 5] {
[
String::new(),
String::new(),
amount.map(money).unwrap_or_default(),
"reconciliation".into(),
match amount {
Some(_) => note.into(),
None => "too large to compute exactly".into(),
},
]
};
push(
name,
"Reconciliation: beginning capital, book less tax",
recon(Some(r.beginning), ""),
);
push(
name,
"Reconciliation: built-in gain (loss) of property contributed",
recon(
Some(r.built_in_gain_704c),
"fair market value less §723 basis at contribution, IRC §704(c) (Treas. Reg. §1.704-3(a)(3)(ii))",
),
);
push(
name,
"Reconciliation: property contributed with no tax basis in the books, at book",
recon(
Some(r.property_without_basis),
"its adjusted basis is the CPA's",
),
);
push(
name,
"Reconciliation: less property distributed, at book",
recon(
Some(-r.property_distributed),
"the company's adjusted tax basis in it is the CPA's",
),
);
push(
name,
"Reconciliation: book ending less tax ending",
recon(
r.total(),
if row.ending.open.is_empty() {
""
} else {
"to the tax ending the books know so far, before the CPA's figures above"
},
),
);
}
w
}
+2
View File
@@ -25,6 +25,7 @@
mod books;
mod iif;
mod k1_tax;
mod package;
mod reports;
mod text;
@@ -33,6 +34,7 @@ mod vendor_1099;
pub use books::PackageBooks;
pub use iif::export_iif;
pub use k1_tax::{open_text, tax_basis_csv};
pub use package::{Package, PackageRequest, build_package, package_file_name};
pub use reports::{
Settings, as_of_date, bs_statement, cash_flow_for, cash_flow_statement, member_capitals,
+50 -6
View File
@@ -23,9 +23,11 @@
//! share, capital account, TIN and type (or last four without a TIN
//! source), and the K-1 worksheet's capital figures (ledger_core::k1)
//! for the fiscal year to date; the members' total row with more than
//! one. The 1099 worksheet is books.vendor_1099's CSV
//! (crate::vendor_1099_csv), each payee's TIN and type added with a
//! TIN source.
//! one. Beside it, k1-tax-basis-capital.csv: each member's capital
//! on the tax basis (Item L), line by line beside the book figures,
//! with the reconciliation (crate::k1_tax). The 1099 worksheet is
//! books.vendor_1099's CSV (crate::vendor_1099_csv), each payee's
//! TIN and type added with a TIN source.
//! 3. A TIN the source can't give never fails the package: the row says
//! why, and so do the README and tin_gaps (members, and 1099 payees
//! a 1099 is or may be due for).
@@ -182,6 +184,7 @@ pub fn build_package<B: PackageBooks + ?Sized>(
),
];
let mut k1_csv: Option<(String, Vec<u8>)> = None;
let mut k1_tax: Option<(String, Vec<u8>)> = None;
if !company.members.is_empty() {
let capitals = member_capitals(ledger, &company.members)?;
let me =
@@ -325,6 +328,33 @@ pub fn build_package<B: PackageBooks + ?Sized>(
),
},
);
// The K-1 worksheet on the tax basis (Item L's tax-basis method,
// D-41), beside the book figures: a row per member or none.
let names: Vec<String> = company.members.iter().map(|m| m.name.clone()).collect();
k1_tax = Some(
match ledger_core::tax_capital::tax_basis_capital(ledger, fy, as_of, &capitals, &cash)
.map_err(|e| e.to_string())
.and_then(|tc| {
if tc.rows.len() == names.len() {
Ok(tc)
} else {
Err(format!(
"the tax-basis worksheet has {} rows for {} members",
tc.rows.len(),
names.len()
))
}
}) {
Ok(tc) => (
crate::k1_tax::FILE_NAME.into(),
crate::k1_tax::tax_basis_csv(ledger, &names, &tc).into_bytes(),
),
Err(e) => (
"k1-tax-basis-capital-NOT-INCLUDED.txt".into(),
format!("{e}\n").into_bytes(),
),
},
);
}
statements.push((
"tax-summary",
@@ -342,9 +372,12 @@ pub fn build_package<B: PackageBooks + ?Sized>(
files.push((format!("{name}.pdf"), pdf(st)?));
files.push((format!("{name}.xlsx"), xlsx(XlsxReport::Statement(st))?));
}
// The fixed asset register, when an asset was bought by as_of (one
// bought later is not in it, so neither is an empty register).
if ledger.registered_assets().any(|a| a.purchase_date <= as_of) {
// The fixed asset register, when an asset was acquired by as_of (one
// acquired later is not in it, so neither is an empty register).
if ledger
.registered_assets()
.any(|a| ledger.asset_acquired_date(a.id).is_some_and(|d| d <= as_of))
{
let r = ledger_core::fixed_asset_register(ledger, as_of).map_err(|e| e.to_string())?;
files.push((
"fixed-asset-register.pdf".into(),
@@ -513,6 +546,12 @@ pub fn build_package<B: PackageBooks + ?Sized>(
if let Some(k1) = k1_csv {
files.push(k1);
}
let k1_tax_included = k1_tax
.as_ref()
.is_some_and(|(name, _)| name == crate::k1_tax::FILE_NAME);
if let Some(k1) = k1_tax {
files.push(k1);
}
let seq = books.journal_seq();
// The journal through as_of for QuickBooks, closes left out, and what
// it writes differently (INVARIANT 8).
@@ -601,6 +640,11 @@ pub fn build_package<B: PackageBooks + ?Sized>(
""
}
);
k.push_str(if k1_tax_included {
"k1-tax-basis-capital.csv: each member's capital account on the tax basis, as Item L reports it (Form 1065 instructions: the tax-basis method), line by line beside the book figures: cash contributed, property at its adjusted basis (IRC §723), the share of income as figured for tax, distributions at tax basis, and the reconciliation from book to tax capital (the §704(c) built-in gain or loss of contributed property). A line the books can't know (tax depreciation, a basis the books don't hold) is left blank, marked open, and says what the books know so far and what is needed.\n"
} else {
"k1-tax-basis-capital-NOT-INCLUDED.txt: why the tax-basis capital worksheet isn't here.\n"
});
let members = &company.members;
for (i, m) in members.iter().enumerate() {
if let Some(j) = &m.files_jointly_with