P3 review: asset bases and the tax-basis K-1

Schema 13 is as shipped and the asset columns are schema 14 (shipped
migrations pinned by hash); the tax-basis K-1 leaves net income open (only
the return has it) and takes a contributed prepaid plan at its book amount
as its section 723 basis; a disposal of contributed property names its
704(c) gain or loss; aliases compare by meaning; fixture amounts made up.

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 17:43:44 -06:00
1 parent 2ae93fb2d6
commit 6288ead747
24 files changed
+860 -127

No files matched your search

+7 -7
View File
@@ -724,8 +724,8 @@ mod tests {
} }
/// INVARIANT 3, by hand: a contract the members hand over (Dr 1300 /// INVARIANT 3, by hand: a contract the members hand over (Dr 1300
/// Prepaid 25 / Cr 3000 12.50, Cr 3001 12.50, the AppleCare+ with the /// Prepaid 36 / Cr 3000 18.00, Cr 3001 18.00, a service plan with a
/// MacBook) is property, 12.50 each. Members paying a 3,000 annual /// laptop) is property, 18.00 each. Members paying a 3,000 annual
/// service for the company go through 2100: Dr 1300 / Cr 2100 3,000, /// service for the company go through 2100: Dr 1300 / Cr 2100 3,000,
/// then Dr 2100 / Cr 3000 1,500, Cr 3001 1,500: 1,500 cash each. /// then Dr 2100 / Cr 3000 1,500, Cr 3001 1,500: 1,500 cash each.
#[test] #[test]
@@ -737,13 +737,13 @@ mod tests {
1, 1,
5, 5,
&[ &[
(prepaid, Side::Debit, "25"), (prepaid, Side::Debit, "36"),
(a, Side::Credit, "12.50"), (a, Side::Credit, "18.00"),
(b, Side::Credit, "12.50"), (b, Side::Credit, "18.00"),
], ],
); );
let ms = members(&bk); let ms = members(&bk);
let handed = (d("0"), d("12.50"), d("0")); let handed = (d("0"), d("18.00"), d("0"));
assert_eq!(split(&bk, &ms), vec![handed, handed]); assert_eq!(split(&bk, &ms), vec![handed, handed]);
let due = let due =
bk.l.add_account( bk.l.add_account(
@@ -770,7 +770,7 @@ mod tests {
(b, Side::Credit, "1500"), (b, Side::Credit, "1500"),
], ],
); );
let each = (d("1500"), d("12.50"), d("0")); let each = (d("1500"), d("18.00"), d("0"));
assert_eq!(split(&bk, &ms), vec![each, each]); assert_eq!(split(&bk, &ms), vec![each, each]);
} }
+14 -9
View File
@@ -2586,7 +2586,11 @@ impl Ledger {
/// Record a depreciation run (`asset.depreciation_posted`). Checks only /// Record a depreciation run (`asset.depreciation_posted`). Checks only
/// that the asset and the entry exist: every run an earlier version /// that the asset and the entry exist: every run an earlier version
/// journaled replays, whatever its period or amount (rules.md B-28; a /// journaled replays, whatever its period or amount (rules.md B-28; a
/// store checks new runs first, [`Self::check_depreciation_run`]). /// store checks new runs first, [`Self::check_depreciation_run`]). A
/// second line for the same asset, period and entry (a journal edited
/// by hand; a store never writes one) is the run already recorded: one
/// entry is carried once, as the projection keys it (P3 review,
/// 2026-10-21: replay counted it twice, the reload once).
pub fn record_depreciation_run( pub fn record_depreciation_run(
&mut self, &mut self,
asset: AssetId, asset: AssetId,
@@ -2600,14 +2604,15 @@ impl Ledger {
if !self.entries.contains_key(&entry) { if !self.entries.contains_key(&entry) {
return Err(LedgerError::UnknownEntry(entry)); return Err(LedgerError::UnknownEntry(entry));
} }
self.asset_runs let runs = self.asset_runs.entry(asset).or_default();
.entry(asset) if runs.iter().any(|r| r.entry == entry && r.period == period) {
.or_default() return Ok(());
.push(assets::DepreciationPosting { }
period: period.to_string(), runs.push(assets::DepreciationPosting {
entry, period: period.to_string(),
amount, entry,
}); amount,
});
Ok(()) Ok(())
} }
+98 -9
View File
@@ -31,20 +31,29 @@
//! `known`, the part the books have, and `open`, each missing item //! `known`, the part the books have, and `open`, each missing item
//! with the book amount it replaces. A line with no open item is the //! with the book amount it replaces. A line with no open item is the
//! tax figure. //! tax figure.
//! - Net income: tax depreciation of every fixed asset in service //! - Net income: always the return's (Item L: "as figured for tax
//! in the span (MACRS, and for contributed property the §704(c) //! purposes"); the books are kept on their own basis and can't
//! method's allocation) is the CPA's, as is the tax gain on a //! know a book-tax difference (Schedule M-1). `known` is the book
//! disposal; `known` is the book share, the open items say so. //! share; the open items name the differences the books do see:
//! tax depreciation of every fixed asset in service in the span
//! (MACRS, and for contributed property the §704(c) method's
//! allocation), and the tax gain on a disposal, with the
//! property's built-in gain or loss at contribution (§704(c)(1)(A)).
//! - Property distributed: its adjusted tax basis is the CPA's. //! - Property distributed: its adjusted tax basis is the CPA's.
//! - A change in book capital the books can't explain ("other"). //! - A change in book capital the books can't explain ("other").
//! - Beginning capital from earlier years whose tax figures were open. //! - Beginning capital from earlier years whose tax figures were open.
//! 4. Each row: ending.known = beginning.known + contributions_cash + //! 4. Each row: ending.known = beginning.known + contributions_cash +
//! contributions_property.known + net_income.known + other.known − //! contributions_property.known + net_income.known + other.known −
//! distributions.known, exactly; ending.open is every open item of //! distributions.known, exactly; ending.open is every open item of
//! the row. //! the row, and so is always open (net income is).
//! 5. Reconciliation: book ending − tax ending.known = (book beginning − //! 5. Reconciliation: book ending − tax ending.known = (book beginning −
//! tax beginning.known) + built-in gain share + property without a //! tax beginning.known) + built-in gain share + property without a
//! basis (book) − property distributed (book), exactly. //! basis (book) − property distributed (book), exactly.
//! 6. Property contributed to a prepaid account (a prepaid-expense
//! schedule's balance account) with no fixed asset for it is taken
//! at its book amount, with a note (Note::PrepaidAtBook), never open:
//! a service contract's §723 basis is what the books recorded (the
//! CPA to confirm).
//! SCENARIOS: //! SCENARIOS:
//! 1. IF two members jointly contribute a laptop worth 1,440.00 whose //! 1. IF two members jointly contribute a laptop worth 1,440.00 whose
//! adjusted basis is 1,688.40, and nothing else happens: //! adjusted basis is 1,688.40, and nothing else happens:
@@ -55,6 +64,9 @@
//! - SHOULD leave the tax net income open, with the book depreciation //! - SHOULD leave the tax net income open, with the book depreciation
//! posted in the span //! posted in the span
//! BECAUSE tax depreciation is the CPA's (rules.md B-15, B-25). //! BECAUSE tax depreciation is the CPA's (rules.md B-15, B-25).
//! 3. IF nothing but cash comes in and goes out:
//! - SHOULD still leave the tax net income open, the book share known
//! BECAUSE income "as figured for tax purposes" is the return's.
//! ``` //! ```
use crate::k1::{K1Kind, K1Row, k1_detail}; use crate::k1::{K1Kind, K1Row, k1_detail};
@@ -158,12 +170,26 @@ pub enum Open {
contributed_book: Decimal, contributed_book: Decimal,
}, },
/// A fixed asset disposed in the span: its tax gain or loss (from its /// A fixed asset disposed in the span: its tax gain or loss (from its
/// adjusted tax basis) is the CPA's. /// adjusted tax basis) is the CPA's. For contributed property, the gain
/// or loss is shared so as to take account of its built-in gain or loss
/// (IRC §704(c)(1)(A)), which goes to the contributing member(s).
Disposal { Disposal {
/// The asset. /// The asset.
asset: AssetId, asset: AssetId,
/// The proceeds. /// The proceeds.
proceeds: Decimal, proceeds: Decimal,
/// Its §704(c) built-in gain (+) or loss (−) at contribution, when
/// it is contributed property with a basis recorded; `None` for a
/// purchase or no basis.
built_in_gain_704c: Option<Decimal>,
},
/// The member's share of net income (loss) as figured for tax purposes
/// (Item L): the return's, never the books'. The books are kept on
/// their own basis; every difference between book and tax income
/// (Schedule M-1) is the CPA's. `book` is the member's book share.
TaxNetIncome {
/// The member's share of book net income (loss).
book: Decimal,
}, },
/// A change in book capital the books can't explain (the statement of /// A change in book capital the books can't explain (the statement of
/// members' equity's "other"): its tax treatment is the CPA's. /// members' equity's "other"): its tax treatment is the CPA's.
@@ -173,6 +199,24 @@ pub enum Open {
}, },
} }
/// A figure the worksheet takes on a stated rule, for the CPA to confirm
/// (INVARIANT 6).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Note {
/// Property contributed in `entry` to a prepaid account (the balance
/// account of a prepaid-expense schedule: a service contract the
/// member handed over, B-13) with no fixed asset for it: its §723
/// basis is taken as its book amount, this member's share `book`
/// (DECISION, P3 review 2026-10-21: the contract's carryover basis is
/// what the books recorded for it; the CPA to confirm).
PrepaidAtBook {
/// The contribution entry.
entry: EntryId,
/// This member's share of the prepaid, at book and at basis.
book: Decimal,
},
}
/// One Item L line on the tax basis ([`Open`]: INVARIANT 3). /// One Item L line on the tax basis ([`Open`]: INVARIANT 3).
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaxLine { pub struct TaxLine {
@@ -181,6 +225,9 @@ pub struct TaxLine {
/// What the CPA must still settle; empty when `known` is the tax /// What the CPA must still settle; empty when `known` is the tax
/// figure. /// figure.
pub open: Vec<Open>, pub open: Vec<Open>,
/// Figures taken on a stated rule, for the CPA to confirm (INVARIANT
/// 6); they never make a line open.
pub notes: Vec<Note>,
} }
impl TaxLine { impl TaxLine {
@@ -188,6 +235,7 @@ impl TaxLine {
Self { Self {
known, known,
open: Vec::new(), open: Vec::new(),
notes: Vec::new(),
} }
} }
@@ -319,6 +367,8 @@ fn share(whole: Decimal, weights: &[Decimal]) -> Result<Vec<Decimal>, LedgerErro
struct EntryProperty { struct EntryProperty {
/// Σ built-in gain of the linked assets with a contribution basis. /// Σ built-in gain of the linked assets with a contribution basis.
built_in_gain: Decimal, built_in_gain: Decimal,
/// The property debited to prepaid accounts (INVARIANT 6), at book.
prepaid: Decimal,
/// The property's book amount with no basis in the books. /// The property's book amount with no basis in the books.
without_basis: Decimal, without_basis: Decimal,
/// The linked assets' values exceed the entry's property (linked, /// The linked assets' values exceed the entry's property (linked,
@@ -339,6 +389,24 @@ fn entry_property(
}) })
.try_fold(Decimal::ZERO, |t, l| t.checked_add(l.amount.amount)) .try_fold(Decimal::ZERO, |t, l| t.checked_add(l.amount.amount))
.ok_or_else(too_large)?; .ok_or_else(too_large)?;
// Prepaid accounts: those a prepaid-expense schedule amortizes, and no
// registered asset carries its cost in (INVARIANT 6).
let prepaid_account = |id: AccountId| {
ledger.schedules().any(|s| {
s.kind == crate::schedules::ScheduleKind::PrepaidExpense && s.balance_account == id
}) && !ledger.registered_assets().any(|a| a.cost_account == id)
};
let prepaid = entry
.lines()
.iter()
.filter(|l| {
l.side == Side::Debit
&& crate::k1::is_property_account(ledger, cash, l.account)
&& prepaid_account(l.account)
})
.try_fold(Decimal::ZERO, |t, l| t.checked_add(l.amount.amount))
.ok_or_else(too_large)?;
let pool = pool - prepaid;
let (mut linked, mut big, mut unknown) = (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO); let (mut linked, mut big, mut unknown) = (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO);
for a in ledger for a in ledger
.registered_assets() .registered_assets()
@@ -359,6 +427,7 @@ fn entry_property(
if linked > pool { if linked > pool {
return Ok(EntryProperty { return Ok(EntryProperty {
built_in_gain: Decimal::ZERO, built_in_gain: Decimal::ZERO,
prepaid,
without_basis: pool, without_basis: pool,
mismatch: Some((linked, pool)), mismatch: Some((linked, pool)),
}); });
@@ -366,6 +435,7 @@ fn entry_property(
let rest = pool - linked; let rest = pool - linked;
Ok(EntryProperty { Ok(EntryProperty {
built_in_gain: big, built_in_gain: big,
prepaid,
without_basis: unknown.checked_add(rest).ok_or_else(too_large)?, without_basis: unknown.checked_add(rest).ok_or_else(too_large)?,
mismatch: None, mismatch: None,
}) })
@@ -385,6 +455,7 @@ fn span(
let mut without = vec![Decimal::ZERO; n]; let mut without = vec![Decimal::ZERO; n];
let mut distributed = vec![Decimal::ZERO; n]; let mut distributed = vec![Decimal::ZERO; n];
let mut prop_open: Vec<Vec<Open>> = vec![Vec::new(); n]; let mut prop_open: Vec<Vec<Open>> = vec![Vec::new(); n];
let mut prop_notes: Vec<Vec<Note>> = vec![Vec::new(); n];
let mut dist_open: Vec<Vec<Open>> = vec![Vec::new(); n]; let mut dist_open: Vec<Vec<Open>> = vec![Vec::new(); n];
// Entries in journal order, each once. // Entries in journal order, each once.
let mut entries: Vec<EntryId> = Vec::new(); let mut entries: Vec<EntryId> = Vec::new();
@@ -412,7 +483,14 @@ fn span(
let p = entry_property(ledger, e, cash)?; let p = entry_property(ledger, e, cash)?;
let g = share(p.built_in_gain, &takes)?; let g = share(p.built_in_gain, &takes)?;
let w = share(p.without_basis, &takes)?; let w = share(p.without_basis, &takes)?;
let pp = share(p.prepaid, &takes)?;
for i in (0..n).filter(|&i| !takes[i].is_zero()) { for i in (0..n).filter(|&i| !takes[i].is_zero()) {
if !pp[i].is_zero() {
prop_notes[i].push(Note::PrepaidAtBook {
entry: id,
book: pp[i],
});
}
big[i] = big[i].checked_add(g[i]).ok_or_else(too_large)?; big[i] = big[i].checked_add(g[i]).ok_or_else(too_large)?;
without[i] = without[i].checked_add(w[i]).ok_or_else(too_large)?; without[i] = without[i].checked_add(w[i]).ok_or_else(too_large)?;
if let Some((linked, property)) = p.mismatch { if let Some((linked, property)) = p.mismatch {
@@ -495,6 +573,7 @@ fn span(
open.push(Open::Disposal { open.push(Open::Disposal {
asset: a.id, asset: a.id,
proceeds: proceeds.amount, proceeds: proceeds.amount,
built_in_gain_704c: a.basis_figures()?.and_then(|f| f.built_in_gain_704c),
}); });
} }
} }
@@ -508,6 +587,7 @@ fn span(
} else { } else {
vec![Open::PriorYears] vec![Open::PriorYears]
}, },
notes: Vec::new(),
}, },
None => TaxLine::complete(book.beginning), None => TaxLine::complete(book.beginning),
}; };
@@ -518,10 +598,16 @@ fn span(
.and_then(|x| x.checked_sub(without[i])) .and_then(|x| x.checked_sub(without[i]))
.ok_or_else(too_large)?, .ok_or_else(too_large)?,
open: std::mem::take(&mut prop_open[i]), open: std::mem::take(&mut prop_open[i]),
notes: std::mem::take(&mut prop_notes[i]),
}; };
let net_income = TaxLine { let net_income = TaxLine {
known: book.net_income, known: book.net_income,
open: open.clone(), open: std::iter::once(Open::TaxNetIncome {
book: book.net_income,
})
.chain(open.iter().cloned())
.collect(),
notes: Vec::new(),
}; };
let other = TaxLine { let other = TaxLine {
known: book.other, known: book.other,
@@ -530,6 +616,7 @@ fn span(
} else { } else {
vec![Open::OtherChange { book: book.other }] vec![Open::OtherChange { book: book.other }]
}, },
notes: Vec::new(),
}; };
let distributions = TaxLine { let distributions = TaxLine {
known: book known: book
@@ -537,6 +624,7 @@ fn span(
.checked_sub(distributed[i]) .checked_sub(distributed[i])
.ok_or_else(too_large)?, .ok_or_else(too_large)?,
open: std::mem::take(&mut dist_open[i]), open: std::mem::take(&mut dist_open[i]),
notes: Vec::new(),
}; };
let known = beginning let known = beginning
.known .known
@@ -558,6 +646,7 @@ fn span(
.iter() .iter()
.flat_map(|l| l.open.iter().cloned()) .flat_map(|l| l.open.iter().cloned())
.collect(), .collect(),
notes: Vec::new(),
}; };
let reconciliation = Reconciliation { let reconciliation = Reconciliation {
beginning: book beginning: book
@@ -601,8 +690,8 @@ mod tests {
vec![d("-510.69"), d("-510.68")] vec![d("-510.69"), d("-510.68")]
); );
assert_eq!( assert_eq!(
share(d("25"), &[d("525"), d("525")]).unwrap(), share(d("36"), &[d("525"), d("525")]).unwrap(),
vec![d("12.50"), d("12.50")] vec![d("18.00"), d("18.00")]
); );
assert_eq!( assert_eq!(
share(d("10"), &[d("0"), d("0")]).unwrap(), share(d("10"), &[d("0"), d("0")]).unwrap(),
+3 -3
View File
@@ -463,11 +463,11 @@ fn acquisition_links_only_to_a_live_entry_debiting_the_cost_account() {
let capital = ledger let capital = ledger
.add_account("3000", "Capital", AccountType::Equity, None, false) .add_account("3000", "Capital", AccountType::Equity, None, false)
.unwrap(); .unwrap();
let id = van_asset(&mut ledger, 1650, cost, accum, expense); let id = van_asset(&mut ledger, 1480, cost, accum, expense);
let post = |l: &mut Ledger, dr: u64, cr: u64| { let post = |l: &mut Ledger, dr: u64, cr: u64| {
let e = l.create_entry(date(2026, 1, 15), "contribution"); let e = l.create_entry(date(2026, 1, 15), "contribution");
l.add_line(e, dr, Side::Debit, usd("1650")).unwrap(); l.add_line(e, dr, Side::Debit, usd("1480")).unwrap();
l.add_line(e, cr, Side::Credit, usd("1650")).unwrap(); l.add_line(e, cr, Side::Credit, usd("1480")).unwrap();
l.post_entry(e).unwrap(); l.post_entry(e).unwrap();
e e
}; };
@@ -1021,6 +1021,22 @@ fn the_registers_book_value_is_what_the_books_carry() {
let _ = id; let _ = id;
} }
/// A second line for the same run (asset, period and entry: a journal
/// edited by hand) is the run already recorded, as the projection keys it:
/// its entry is carried once (P3 review, 2026-10-21: replay counted it
/// twice, 200.00, and a reload once).
#[test]
fn a_duplicated_run_line_is_carried_once() {
let (mut l, _, id, e) = one_run();
l.record_depreciation_run(id, "2026-01", e, d("100.00"))
.unwrap();
assert_eq!(l.depreciation_runs(id).len(), 1);
assert_eq!(
l.accumulated_posted(id, date(2026, 1, 31)).unwrap(),
d("100.00")
);
}
/// check_depreciation_run: a month, a positive amount in cents, a posted /// check_depreciation_run: a month, a positive amount in cents, a posted
/// entry crediting the asset's accumulated depreciation by exactly that; /// entry crediting the asset's accumulated depreciation by exactly that;
/// record_depreciation_run (replay) needs only the asset and entry. /// record_depreciation_run (replay) needs only the asset and entry.
+258 -16
View File
@@ -14,10 +14,10 @@ use std::str::FromStr;
use chrono::NaiveDate; use chrono::NaiveDate;
use ledger_core::reports::MemberCapital; use ledger_core::reports::MemberCapital;
use ledger_core::tax_capital::{Open, Reconciliation, tax_basis_capital}; use ledger_core::tax_capital::{Note, Open, Reconciliation, tax_basis_capital};
use ledger_core::{ use ledger_core::{
AccountId, AccountType, AssetBasis, AssetId, BasisRule, Currency, EntryId, Ledger, Money, AccountId, AccountType, AssetBasis, AssetId, BasisRule, Currency, EntryId, Ledger, Money,
NewAsset, Sec704cMethod, Side, NewAsset, NewSchedule, ScheduleConvention, ScheduleKind, Sec704cMethod, Side,
}; };
use rust_decimal::Decimal; use rust_decimal::Decimal;
@@ -243,12 +243,16 @@ fn tax_basis_capital_per_member() {
assert_eq!(row.contributions_property.tax(), Some(d("1384.20"))); assert_eq!(row.contributions_property.tax(), Some(d("1384.20")));
assert_eq!(row.net_income.tax(), None, "tax depreciation is the CPA's"); assert_eq!(row.net_income.tax(), None, "tax depreciation is the CPA's");
assert_eq!(row.net_income.known, d("-385.00")); assert_eq!(row.net_income.known, d("-385.00"));
assert_eq!(row.net_income.open, vec![depreciation.clone()]); let income = Open::TaxNetIncome { book: d("-385.00") };
assert_eq!(
row.net_income.open,
vec![income.clone(), depreciation.clone()]
);
assert_eq!(row.other.tax(), Some(d("0"))); assert_eq!(row.other.tax(), Some(d("0")));
assert_eq!(row.distributions.tax(), Some(d("300.00"))); assert_eq!(row.distributions.tax(), Some(d("300.00")));
assert_eq!(row.ending.tax(), None); assert_eq!(row.ending.tax(), None);
assert_eq!(row.ending.known, d("1699.20")); assert_eq!(row.ending.known, d("1699.20"));
assert_eq!(row.ending.open, vec![depreciation.clone()]); assert_eq!(row.ending.open, vec![income, depreciation.clone()]);
// Book less tax, by cause. // Book less tax, by cause.
assert_eq!( assert_eq!(
row.reconciliation, row.reconciliation,
@@ -292,9 +296,10 @@ fn next_years_beginning_carries_what_is_known_and_says_what_isnt() {
} }
/// Cash only, no fixed assets: every line is the tax figure, equal to the /// Cash only, no fixed assets: every line is the tax figure, equal to the
/// book figure. /// book figure, but net income (and so the ending), which is the return's
/// whatever the books hold (Item L: "as figured for tax purposes").
#[test] #[test]
fn cash_only_books_are_complete() { fn cash_only_books_leave_only_the_tax_income_open() {
let mut b = books(); let mut b = books();
let (cash, tobin, morgan, revenue) = (b.cash, b.tobin, b.morgan, b.revenue); let (cash, tobin, morgan, revenue) = (b.cash, b.tobin, b.morgan, b.revenue);
post( post(
@@ -323,9 +328,17 @@ fn cash_only_books_are_complete() {
) )
.unwrap(); .unwrap();
assert!(tc.open.is_empty()); assert!(tc.open.is_empty());
for row in &tc.rows { for (row, income) in tc.rows.iter().zip(["50.01", "50.00"]) {
assert_eq!(row.ending.tax(), Some(row.book.ending)); let income = vec![Open::TaxNetIncome { book: d(income) }];
assert!(row.ending.open.is_empty()); assert_eq!(row.beginning.tax(), Some(d("0")));
assert_eq!(row.contributions_cash, d("250.00"));
assert_eq!(row.contributions_property.tax(), Some(d("0")));
assert_eq!(row.distributions.tax(), Some(d("0")));
assert_eq!(row.net_income.tax(), None, "the return's, never the books'");
assert_eq!(row.net_income.open, income);
assert_eq!(row.ending.tax(), None);
assert_eq!(row.ending.known, row.book.ending);
assert_eq!(row.ending.open, income);
assert_eq!(row.reconciliation, Reconciliation::default()); assert_eq!(row.reconciliation, Reconciliation::default());
} }
} }
@@ -343,9 +356,9 @@ fn property_without_a_basis_is_open_never_guessed() {
&mut b.l, &mut b.l,
date(2026, 1, 5), date(2026, 1, 5),
&[ &[
(prepaid, Side::Debit, "25.00"), (prepaid, Side::Debit, "36.00"),
(tobin, Side::Credit, "12.50"), (tobin, Side::Credit, "18.00"),
(morgan, Side::Credit, "12.50"), (morgan, Side::Credit, "18.00"),
], ],
); );
let tool = post( let tool = post(
@@ -379,14 +392,14 @@ fn property_without_a_basis_is_open_never_guessed() {
) )
.unwrap(); .unwrap();
let (t, m) = (&tc.rows[0], &tc.rows[1]); let (t, m) = (&tc.rows[0], &tc.rows[1]);
assert_eq!(t.book.contributions_property, d("412.50")); assert_eq!(t.book.contributions_property, d("418.00"));
assert_eq!(t.contributions_property.known, d("0")); assert_eq!(t.contributions_property.known, d("0"));
assert_eq!( assert_eq!(
t.contributions_property.open, t.contributions_property.open,
vec![ vec![
Open::PropertyBasis { Open::PropertyBasis {
entry: contract, entry: contract,
book: d("12.50") book: d("18.00")
}, },
Open::PropertyBasis { Open::PropertyBasis {
entry: tool, entry: tool,
@@ -394,9 +407,9 @@ fn property_without_a_basis_is_open_never_guessed() {
}, },
] ]
); );
assert_eq!(t.reconciliation.property_without_basis, d("412.50")); assert_eq!(t.reconciliation.property_without_basis, d("418.00"));
assert_eq!(m.contributions_property.known, d("0")); assert_eq!(m.contributions_property.known, d("0"));
assert_eq!(m.reconciliation.property_without_basis, d("12.50")); assert_eq!(m.reconciliation.property_without_basis, d("18.00"));
for row in &tc.rows { for row in &tc.rows {
assert_eq!( assert_eq!(
row.reconciliation.total(), row.reconciliation.total(),
@@ -492,3 +505,232 @@ fn every_span_rolls_forward_and_reconciles() {
} }
} }
} }
/// A contributed asset with `rule`, in service 2026-01-01 over 24 months.
fn contributed(
b: &mut Books,
rule: BasisRule,
fmv: &str,
adjusted: &str,
entry: EntryId,
) -> AssetId {
let id =
b.l.register_asset(NewAsset {
description: format!("Device {fmv}"),
in_service_date: date(2026, 1, 1),
cost: usd(fmv),
salvage_value: usd("0.00"),
useful_life_months: 24,
cost_account: b.equipment,
accum_depr_account: b.accum,
expense_account: b.depreciation,
})
.unwrap();
b.l.set_asset_acquisition(id, entry).unwrap();
b.l.set_asset_basis(id, AssetBasis::contribution(rule, d(adjusted), d(fmv)))
.unwrap();
id
}
/// Four devices and a prepaid service plan two members hand over jointly
/// from personal use, in one entry (P3 review, by hand; invented figures):
/// - D1 worth 1,200.00, basis 1,500.00; D2 worth 800.00, basis 1,100.00
/// (both worth less than basis); D3 worth 1,000.00, basis 700.00 (worth
/// more); D4 worth 600.00, basis 600.00 (equal); the plan 40.00, a
/// prepaid-expense schedule's account. Dr 1500 3,600.00, Dr 1300 40.00
/// / Cr 3000 1,820.00, Cr 3001 1,820.00.
/// - Depreciable basis, the lower of value and basis: 1,200.00 + 800.00 +
/// 700.00 + 600.00 = 3,300.00. §723: 1,500.00 + 1,100.00 + 700.00 +
/// 600.00 = 3,900.00. Built-in: −300.00, −300.00, +300.00, 0.00 =
/// −300.00, never netted per property (D1 and D2 §704(c)(1)(C)).
/// - Item L property, each: book 1,820.00; tax (3,900.00 + 40.00) / 2 =
/// 1,970.00 = 1,820.00 − (−300.00 / 2), the plan at its book 20.00 each
/// with a note, never open.
/// - D3 sold 2026-06-30 for 1,100.00: its tax gain is the CPA's, flagged
/// with its built-in gain 300.00 (§704(c)(1)(A)).
#[test]
fn four_devices_and_a_plan_by_hand() {
let mut b = books();
let (equipment, prepaid, tobin, morgan, cash) =
(b.equipment, b.prepaid, b.tobin, b.morgan, b.cash);
let plan_expense =
b.l.add_account("6300", "Service plans", AccountType::Expense, None, false)
.unwrap();
b.l.register_schedule(NewSchedule {
description: "Service plan".into(),
kind: ScheduleKind::PrepaidExpense,
total: usd("40.00"),
start: date(2026, 1, 1),
months: 12,
balance_account: prepaid,
pl_account: plan_expense,
convention: ScheduleConvention::Daily,
})
.unwrap();
let e = post(
&mut b.l,
date(2026, 1, 1),
&[
(equipment, Side::Debit, "1200.00"),
(equipment, Side::Debit, "800.00"),
(equipment, Side::Debit, "1000.00"),
(equipment, Side::Debit, "600.00"),
(prepaid, Side::Debit, "40.00"),
(tobin, Side::Credit, "1820.00"),
(morgan, Side::Credit, "1820.00"),
],
);
let p = BasisRule::PersonalUseConversion;
let ids = [
contributed(&mut b, p, "1200.00", "1500.00", e),
contributed(&mut b, p, "800.00", "1100.00", e),
contributed(&mut b, p, "1000.00", "700.00", e),
contributed(&mut b, p, "600.00", "600.00", e),
];
let a = |i: usize| b.l.get_asset(ids[i]).unwrap();
let want = [
("1200.00", "1500.00", "-300.00", true),
("800.00", "1100.00", "-300.00", true),
("700.00", "700.00", "300.00", false),
("600.00", "600.00", "0.00", false),
];
for (i, (dep, t723, big, loss)) in want.into_iter().enumerate() {
assert_eq!(a(i).depreciable_basis(), Some(d(dep)), "D{}", i + 1);
assert_eq!(a(i).tax_basis_723(), Some(d(t723)), "D{}", i + 1);
assert_eq!(a(i).built_in_gain_704c(), Some(d(big)), "D{}", i + 1);
assert_eq!(a(i).sec704c_1c(), loss, "D{}", i + 1);
}
b.l.dispose_asset(ids[2], date(2026, 6, 30), usd("1100.00"))
.unwrap();
let tc = tax_basis_capital(
&b.l,
date(2026, 1, 1),
date(2026, 12, 31),
&members(&b),
&[cash],
)
.unwrap();
assert!(tc.open.contains(&Open::Disposal {
asset: ids[2],
proceeds: d("1100.00"),
built_in_gain_704c: Some(d("300.00")),
}));
for row in &tc.rows {
assert_eq!(row.book.contributions_property, d("1820.00"));
assert_eq!(row.contributions_property.tax(), Some(d("1970.00")));
assert_eq!(
row.contributions_property.notes,
vec![Note::PrepaidAtBook {
entry: e,
book: d("20.00")
}]
);
assert_eq!(row.reconciliation.built_in_gain_704c, d("-150.00"));
assert_eq!(row.reconciliation.property_without_basis, d("0"));
assert_eq!(
row.reconciliation.total(),
Some(row.book.ending - row.ending.known)
);
}
}
/// Property contributed subject to a liability (Item L: "reduced by any
/// liabilities ... to which the property is subject"), by hand: worth
/// 500.00, basis 200.00, a 300.00 loan on it; Dr 1500 500.00 / Cr 2200
/// 300.00, Cr 3000 200.00. Book property 200.00 (value net of the loan);
/// tax 200.00 − 300.00 = −100.00 ("This amount might be negative"); the
/// other member nothing.
#[test]
fn property_subject_to_a_liability_enters_net_of_it() {
let mut b = books();
let (equipment, tobin, cash) = (b.equipment, b.tobin, b.cash);
let loan =
b.l.add_account("2200", "Loan", AccountType::Liability, None, false)
.unwrap();
let e = post(
&mut b.l,
date(2026, 1, 1),
&[
(equipment, Side::Debit, "500.00"),
(loan, Side::Credit, "300.00"),
(tobin, Side::Credit, "200.00"),
],
);
contributed(&mut b, BasisRule::Carryover723, "500.00", "200.00", e);
let tc = tax_basis_capital(
&b.l,
date(2026, 1, 1),
date(2026, 12, 31),
&members(&b),
&[cash],
)
.unwrap();
let (t, m) = (&tc.rows[0], &tc.rows[1]);
assert_eq!(t.book.contributions_property, d("200.00"));
assert_eq!(t.contributions_property.tax(), Some(d("-100.00")));
assert_eq!(t.reconciliation.built_in_gain_704c, d("300.00"));
assert_eq!(m.contributions_property.tax(), Some(d("0")));
assert_eq!(m.reconciliation, Reconciliation::default());
}
/// Three members with unequal shares, one contributing nothing, by hand:
/// a 1,000.00 machine (basis 400.00, business use) handed over 600.00 by
/// the first and 400.00 by the second; its 600.00 built-in gain is shared
/// as its value is, 360.00 and 240.00, so tax property 240.00 and 160.00
/// (the basis, 0.6 and 0.4); the third member has every line 0.00.
#[test]
fn unequal_contributors_and_a_member_with_none() {
let mut b = books();
let (equipment, tobin, morgan, cash) = (b.equipment, b.tobin, b.morgan, b.cash);
let third =
b.l.add_account(
"3002",
"Capital, Avery Quinn",
AccountType::Equity,
None,
false,
)
.unwrap();
let e = post(
&mut b.l,
date(2026, 1, 1),
&[
(equipment, Side::Debit, "1000.00"),
(tobin, Side::Credit, "600.00"),
(morgan, Side::Credit, "400.00"),
],
);
contributed(&mut b, BasisRule::Carryover723, "1000.00", "400.00", e);
let [t, m] = members(&b);
let ms = [
MemberCapital {
share: d("0.5"),
..t
},
MemberCapital {
share: d("0.3"),
..m
},
MemberCapital {
name: "Avery Quinn".into(),
capital_account: third,
distribution_account: None,
share: d("0.2"),
},
];
let tc = tax_basis_capital(&b.l, date(2026, 1, 1), date(2026, 12, 31), &ms, &[cash]).unwrap();
let want = [
("600.00", "240.00", "360.00"),
("400.00", "160.00", "240.00"),
];
for (row, (book, tax, big)) in tc.rows.iter().zip(want) {
assert_eq!(row.book.contributions_property, d(book));
assert_eq!(row.contributions_property.tax(), Some(d(tax)));
assert_eq!(row.reconciliation.built_in_gain_704c, d(big));
}
let none = &tc.rows[2];
assert_eq!(none.book.ending, d("0"));
assert_eq!(none.contributions_property.tax(), Some(d("0")));
assert_eq!(none.ending.known, d("0"));
assert_eq!(none.reconciliation, Reconciliation::default());
}
+6 -3
View File
@@ -203,8 +203,11 @@ pub fn book_vs_tax(rule: Option<BasisRule>) -> &'static str {
Some(BasisRule::Purchase) => { Some(BasisRule::Purchase) => {
"Book cost and tax basis are both what the company paid; tax depreciation starts from the tax basis — the CPA's." "Book cost and tax basis are both what the company paid; tax depreciation starts from the tax basis — the CPA's."
} }
_ => { Some(BasisRule::Carryover723 | BasisRule::PersonalUseConversion) => {
"Book value is the fair market value at contribution; the tax figures are at contribution, and tax depreciation starts from the depreciable basis — the CPA's." "Book cost is the fair market value at contribution, and book value that cost less the depreciation posted; the tax figures are at contribution, and tax depreciation starts from the depreciable basis — the CPA's."
}
None => {
"No tax basis is recorded: book value is cost less the depreciation posted; the tax figures follow once the basis is recorded."
} }
} }
} }
@@ -1433,7 +1436,7 @@ fn tax_card(ui: &mut Ui, a: &Asset) {
ui.add( ui.add(
egui::Label::new( egui::Label::new(
RichText::new( RichText::new(
"Ask the bookkeeper to record it (books.set_asset_tax_basis): was it bought by the company, contributed by a member from business use, or converted from personal use? Book value is the fair market value at contribution; tax depreciation starts from the tax basis — the CPA's.", "Ask the bookkeeper to record it (books.set_asset_tax_basis): was it bought by the company, contributed by a member from business use, or converted from personal use? For a contribution, book cost is the fair market value at contribution; tax depreciation starts from the depreciable basis — the CPA's.",
) )
.font(theme::regular(12.5)) .font(theme::regular(12.5))
.color(p.muted), .color(p.muted),
+7
View File
@@ -363,6 +363,9 @@ fn assets(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
AssetStatus::Disposed { date, .. } if *date <= b.today => Some(*date), AssetStatus::Disposed { date, .. } if *date <= b.today => Some(*date),
_ => None, _ => None,
}; };
// Not on the books until its acquired date: no book value
// yet (the register leaves it out the same way).
let later = b.ledger.asset_acquired_date(a.id).filter(|d| *d > b.today);
let acc = b.ledger.accumulated_posted(a.id, b.today); let acc = b.ledger.accumulated_posted(a.id, b.today);
let scheduled = accumulated_scheduled_through(a, b.today); let scheduled = accumulated_scheduled_through(a, b.today);
let dep = a.cost.amount - a.salvage_value.amount; let dep = a.cost.amount - a.salvage_value.amount;
@@ -391,6 +394,10 @@ fn assets(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
); );
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let text = match (&acc, disposed) { let text = match (&acc, disposed) {
_ if later.is_some() => format!(
"not on the books until {}",
fmt::date(later.expect("checked"))
),
(_, Some(_)) => format!("book value {}", fmt::dollars(Decimal::ZERO)), (_, Some(_)) => format!("book value {}", fmt::dollars(Decimal::ZERO)),
(Ok(acc), None) => { (Ok(acc), None) => {
format!("book value {}", fmt::dollars(a.cost.amount - *acc)) format!("book value {}", fmt::dollars(a.cost.amount - *acc))
+4 -1
View File
@@ -65,7 +65,10 @@ fn attachment_name(b: &Books, id: u64) -> String {
/// depreciable basis $1,440.00, built-in loss $248.40". The figures are /// depreciable basis $1,440.00, built-in loss $248.40". The figures are
/// ledger-core's (`Asset::basis_figures`) for the event's facts on the /// ledger-core's (`Asset::basis_figures`) for the event's facts on the
/// asset; a pre-P3 `asset.tax_basis_set` (`legacy`: its rule by its old /// asset; a pre-P3 `asset.tax_basis_set` (`legacy`: its rule by its old
/// name, no value) reads the value as the book cost, as it was recorded. /// name, no value) reads the value as the asset's book cost today, which
/// is the cost it was recorded against unless the cost was corrected since
/// (a correction moves a pre-P3 record's built-in gain, and the line shows
/// today's figure).
fn tax_basis_text(b: &Books, asset_id: Option<u64>, p: &Value, legacy: bool) -> String { fn tax_basis_text(b: &Books, asset_id: Option<u64>, p: &Value, legacy: bool) -> String {
let asset = asset_id.and_then(|id| b.ledger.get_asset(id)); let asset = asset_id.and_then(|id| b.ledger.get_asset(id));
let name = match (asset, asset_id) { let name = match (asset, asset_id) {
+23 -12
View File
@@ -1148,17 +1148,20 @@ fn aging(
/// The fixed asset register on `as_of`: ledger-core's /// The fixed asset register on `as_of`: ledger-core's
/// `fixed_asset_register`, laid out, with its reconciliation to the /// `fixed_asset_register`, laid out, with its reconciliation to the
/// general ledger and what that says (the Assets page's notices). Each /// general ledger and what that says (the Assets page's notices). The
/// asset's federal tax basis sits beside its book figures: contributor's /// book figures in the Assets page's order (cost, posted, book value =
/// basis, tax basis, and built-in gain (loss), the register row's own, /// cost less posted, then the schedule's figure); beside them each
/// with its rule (and citation) in the detail; the tax basis total is the /// asset's tax figures at contribution, the register row's own: §723
/// basis, depreciable basis and §704(c) built-in gain (loss), with its
/// rule (and citation), value at contribution, method and, for a built-in
/// loss, the IRC §704(c)(1)(C) note in the detail; the totals are the
/// register's. /// register's.
fn asset_register(b: &Books, as_of: NaiveDate) -> Table { fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
let mut t = Table::new(vec![ let mut t = Table::new(vec![
"Cost".into(), "Cost".into(),
"Scheduled".into(),
"Posted".into(), "Posted".into(),
"Book value".into(), "Book value".into(),
"Scheduled".into(),
// Short enough for the columns: the note says what each is. // Short enough for the columns: the note says what each is.
"§723 basis".into(), "§723 basis".into(),
"Depr. basis".into(), "Depr. basis".into(),
@@ -1217,6 +1220,12 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
ledger_import::asset_register::method_cited(m) ledger_import::asset_register::method_cited(m)
)); ));
} }
if tax.built_in_gain_704c.is_some_and(|g| g < Decimal::ZERO) {
detail.push(
"a built-in loss counts only for the contributing member (IRC §704(c)(1)(C))"
.into(),
);
}
let opt = |v: Option<Decimal>| Cell { let opt = |v: Option<Decimal>| Cell {
value: v, value: v,
drill: None, drill: None,
@@ -1232,9 +1241,9 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
} }
None => vec![ None => vec![
Cell::of(x.cost), Cell::of(x.cost),
Cell::of(x.accumulated_scheduled),
Cell::of(x.accumulated_posted), Cell::of(x.accumulated_posted),
Cell::of(x.book_value), Cell::of(x.book_value),
Cell::of(x.accumulated_scheduled),
opt(tax.tax_basis_723), opt(tax.tax_basis_723),
opt(tax.depreciable_basis), opt(tax.depreciable_basis),
opt(tax.built_in_gain_704c), opt(tax.built_in_gain_704c),
@@ -1252,9 +1261,9 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
"Total held", "Total held",
vec![ vec![
Cell::of(r.total_cost), Cell::of(r.total_cost),
Cell::of(r.total_accumulated_scheduled),
Cell::of(r.total_accumulated_posted), Cell::of(r.total_accumulated_posted),
Cell::of(r.total_book_value), Cell::of(r.total_book_value),
Cell::of(r.total_accumulated_scheduled),
Cell::of(r.total_tax_basis_723), Cell::of(r.total_tax_basis_723),
Cell::of(r.total_depreciable_basis), Cell::of(r.total_depreciable_basis),
Cell::none(), Cell::none(),
@@ -1292,11 +1301,11 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
value: Some(r.ledger_accumulated), value: Some(r.ledger_accumulated),
drill: accum_drill, drill: accum_drill,
}, },
Cell::of(r.ledger_cost - r.ledger_accumulated),
Cell { Cell {
value: Some(r.ledger_accumulated), value: Some(r.ledger_accumulated),
drill: accum_drill, drill: accum_drill,
}, },
Cell::of(r.ledger_cost - r.ledger_accumulated),
Cell::none(), Cell::none(),
Cell::none(), Cell::none(),
Cell::none(), Cell::none(),
@@ -1307,9 +1316,9 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
"Register less books", "Register less books",
vec![ vec![
Cell::of(-r.unregistered_cost()), Cell::of(-r.unregistered_cost()),
Cell::of(r.unposted_depreciation()),
Cell::of(-r.unattributed_accumulated()), Cell::of(-r.unattributed_accumulated()),
Cell::of(r.total_book_value - (r.ledger_cost - r.ledger_accumulated)), Cell::of(r.total_book_value - (r.ledger_cost - r.ledger_accumulated)),
Cell::of(r.unposted_depreciation()),
Cell::none(), Cell::none(),
Cell::none(), Cell::none(),
Cell::none(), Cell::none(),
@@ -2441,9 +2450,9 @@ mod tests {
t.columns, t.columns,
[ [
"Cost", "Cost",
"Scheduled",
"Posted", "Posted",
"Book value", "Book value",
"Scheduled",
"§723 basis", "§723 basis",
"Depr. basis", "Depr. basis",
"Built-in gain" "Built-in gain"
@@ -2462,9 +2471,9 @@ mod tests {
v, v,
[ [
Some(x.cost), Some(x.cost),
Some(x.accumulated_scheduled),
Some(x.accumulated_posted), Some(x.accumulated_posted),
Some(x.book_value), Some(x.book_value),
Some(x.accumulated_scheduled),
f.tax_basis_723, f.tax_basis_723,
Some(f.depreciable_basis), Some(f.depreciable_basis),
f.built_in_gain_704c, f.built_in_gain_704c,
@@ -2476,7 +2485,9 @@ mod tests {
let total = t.rows.iter().find(|row| row.label == "Total held").unwrap(); let total = t.rows.iter().find(|row| row.label == "Total held").unwrap();
assert_eq!(total.cells[5].value, Some(r.total_depreciable_basis)); assert_eq!(total.cells[5].value, Some(r.total_depreciable_basis));
assert_eq!(total.cells[5].value, r.rows[0].depreciable_basis); assert_eq!(total.cells[5].value, r.rows[0].depreciable_basis);
assert_eq!(total.cells[2].value, Some(r.total_accumulated_posted)); assert_eq!(total.cells[1].value, Some(r.total_accumulated_posted));
assert_eq!(total.cells[2].value, Some(r.total_book_value));
assert_eq!(total.cells[3].value, Some(r.total_accumulated_scheduled));
assert!( assert!(
t.rows t.rows
.iter() .iter()
+1 -1
View File
@@ -4,4 +4,4 @@
- Never file the package, or a file from it, into the books. - Never file the package, or a file from it, into the books.
- tin_gaps lists each member or payee whose TIN it lacks and why: collect them (books.set_tin, books.set_w9) and export again. - tin_gaps lists each member or payee whose TIN it lacks and why: collect them (books.set_tin, books.set_w9) and export again.
- Everything in it is as of its as_of: the 1099 worksheet counts the year's payments through that day, and general-journal.iif (the QuickBooks Desktop file) every posted entry from the first through it, closing entries left out. That file is plain ASCII within QuickBooks Desktop's limits: warnings (and the README) say what it writes differently from the books ("Café" as "Cafe", a name cut to 31 characters, a subaccount of another type at the top level, the memos grouped); tell the human. It names no customer or vendor: the README lists the entries on accounts receivable or payable, where QuickBooks may ask the CPA for one. - Everything in it is as of its as_of: the 1099 worksheet counts the year's payments through that day, and general-journal.iif (the QuickBooks Desktop file) every posted entry from the first through it, closing entries left out. That file is plain ASCII within QuickBooks Desktop's limits: warnings (and the README) say what it writes differently from the books ("Café" as "Cafe", a name cut to 31 characters, a subaccount of another type at the top level, the memos grouped); tell the human. It names no customer or vendor: the README lists the entries on accounts receivable or payable, where QuickBooks may ask the CPA for one.
- With members, the K-1 worksheet is two files: k1-members.csv (each member's capital on the books, with their TIN) and k1-tax-basis-capital.csv (the same capital on the tax basis, as Schedule K-1 Item L reports it: property contributed at the member's adjusted basis, IRC §723, and the reconciliation from book to tax capital). A line the books can't know (tax depreciation, the basis of property no asset records) is left open with what the books have: tell the human the CPA supplies it; never fill it in. - With members, the K-1 worksheet is two files: k1-members.csv (each member's capital on the books, with their TIN) and k1-tax-basis-capital.csv (the same capital on the tax basis, as Schedule K-1 Item L reports it: property contributed at the member's adjusted basis, IRC §723, a prepaid contract handed over at its book amount for the CPA to confirm, and the reconciliation from book to tax capital). A line the books can't know (the share of income as figured for tax, always; tax depreciation; a disposal's tax gain; the basis of property no asset records) is left open with what the books have: tell the human the CPA supplies it; never fill it in.
+1 -1
View File
@@ -6,5 +6,5 @@
- Two dates: the in-service date (in_service_date: the day it is ready and available for its use) starts depreciation; the acquired date is the date of the entry that put it on the books (its purchase or a member's contribution: give it as acquisition_entry, dated on or before the in-service date), and puts it on the register. Something bought before it is used is on the register from the purchase and depreciates from the day it serves. - Two dates: the in-service date (in_service_date: the day it is ready and available for its use) starts depreciation; the acquired date is the date of the entry that put it on the books (its purchase or a member's contribution: give it as acquisition_entry, dated on or before the in-service date), and puts it on the register. Something bought before it is used is on the register from the purchase and depreciates from the day it serves.
- Two accumulated figures, never confused: what the books carry (accumulated_posted: its depreciation runs, less any reversed) and what the schedule says is due by a date (accumulated_scheduled_through). Book value is cost less what the books carry; the difference is depreciation not yet run. - Two accumulated figures, never confused: what the books carry (accumulated_posted: its depreciation runs, less any reversed) and what the schedule says is due by a date (accumulated_scheduled_through). Book value is cost less what the books carry; the difference is depreciation not yet run.
- Every asset gets a tag for the label on the thing itself (books.register_asset generates the next one, like FA-0001, unless the human has their own); record the manufacturer's serial number too, change either with books.label_asset, and print label sheets with books.asset_labels (give the human the PDF; print at 100% on Avery 5160/8160). Asset tools take the tag in place of the id. - Every asset gets a tag for the label on the thing itself (books.register_asset generates the next one, like FA-0001, unless the human has their own); record the manufacturer's serial number too, change either with books.label_asset, and print label sheets with books.asset_labels (give the human the PDF; print at 100% on Avery 5160/8160). Asset tools take the tag in place of the id.
- Record every asset's tax basis beside its book value (books.set_asset_tax_basis, or basis_rule on books.register_asset): purchase for something bought (basis is its cost); for a member's contribution, carryover_723 (their business property) or personal_use_conversion (their personal property), with the member's adjusted basis (cost plus improvements, less depreciation allowed or allowable) and its fair market value when contributed: ask the human for both and who says so (basis_source), never assume the value is the cost. Three figures follow, each named for what it is: the §723 basis (the member's adjusted basis), the depreciable basis (that basis; for personal use, the lesser of it and the value), and the §704(c) built-in gain or loss (value less §723 basis; a loss counts only for the contributing member). They are at contribution; tax depreciation, and what remains of the gain or loss later, are the CPA's. Record the §704(c) method (traditional, traditional_with_curative, remedial) once chosen with the CPA. Book value stays fair market value. - Record every asset's tax basis beside its book value (books.set_asset_tax_basis, or basis_rule on books.register_asset): purchase for something bought (basis is its cost); for a member's contribution, carryover_723 (their business property) or personal_use_conversion (their personal property), with the member's adjusted basis (cost plus improvements, less depreciation allowed or allowable) and its fair market value when contributed: ask the human for both and who says so (basis_source), never assume the value is the cost. Three figures follow, each named for what it is: the §723 basis (the member's adjusted basis), the depreciable basis (that basis; for personal use, the lesser of it and the value), and the §704(c) built-in gain or loss (value less §723 basis; a loss counts only for the contributing member). They are at contribution; tax depreciation, and what remains of the gain or loss later, are the CPA's. Record the §704(c) method (traditional, traditional_with_curative, remedial) once chosen with the CPA. The book cost of a contribution is its fair market value at contribution; book value is that cost less the depreciation posted.
- books.export fixed_asset_register is the register for the CPA (posted and scheduled, every tax figure, reconciled to the books); the CPA package's k1-tax-basis-capital.csv carries each member's capital on the tax basis beside the book figures, with what only the CPA can supply left open. books.show asset opens one in the viewer. - books.export fixed_asset_register is the register for the CPA (posted and scheduled, every tax figure, reconciled to the books); the CPA package's k1-tax-basis-capital.csv carries each member's capital on the tax basis beside the book figures, with what only the CPA can supply left open. books.show asset opens one in the viewer.
+55 -5
View File
@@ -1438,16 +1438,19 @@ fn settings_problems(ledger: &Ledger, cfg: &Settings) -> Vec<String> {
} }
/// A parameter and its deprecated alias (one release): the value given, /// A parameter and its deprecated alias (one release): the value given,
/// noting when it came by the alias; both given and different is refused. /// noting when it came by the alias; both given and meaning different
/// values (by `same`: an amount, a day or a rule however written) is
/// refused.
fn aliased<'a>( fn aliased<'a>(
name: &str, name: &str,
value: Option<&'a str>, value: Option<&'a str>,
alias: &str, alias: &str,
old: Option<&'a str>, old: Option<&'a str>,
notes: &mut Vec<String>, notes: &mut Vec<String>,
same: fn(&str, &str) -> bool,
) -> Result<Option<&'a str>, String> { ) -> Result<Option<&'a str>, String> {
match (value, old) { match (value, old) {
(Some(v), Some(o)) if v != o => Err(format!( (Some(v), Some(o)) if !same(v, o) => Err(format!(
"{name} {v:?} and its old name {alias} {o:?} disagree: give {name} only" "{name} {v:?} and its old name {alias} {o:?} disagree: give {name} only"
)), )),
(Some(v), _) => Ok(Some(v)), (Some(v), _) => Ok(Some(v)),
@@ -1459,6 +1462,35 @@ fn aliased<'a>(
} }
} }
/// Two amounts given for one figure mean the same (`"100"` and
/// `"100.00"`); text that isn't an amount compares as text.
fn same_amount(a: &str, b: &str) -> bool {
match (a.trim().parse::<Decimal>(), b.trim().parse::<Decimal>()) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
/// Two days given for one date mean the same.
fn same_day(a: &str, b: &str) -> bool {
match (parse_date(a), parse_date(b)) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
/// Two basis rules mean the same, by wire or pre-P3 name
/// (`"carryover"` and `"carryover_723"`).
fn same_rule(a: &str, b: &str) -> bool {
let rule = |r: &str| {
ledger_core::BasisRule::parse(r).or_else(|| ledger_core::BasisRule::parse_legacy(r))
};
match (rule(a), rule(b)) {
(Some(x), Some(y)) => x == y,
_ => a == b,
}
}
/// A basis rule from a tool: its wire name, or its pre-P3 name (an alias /// A basis rule from a tool: its wire name, or its pre-P3 name (an alias
/// for one release, noted). /// for one release, noted).
fn parse_basis_rule(rule: &str, notes: &mut Vec<String>) -> Result<ledger_core::BasisRule, String> { fn parse_basis_rule(rule: &str, notes: &mut Vec<String>) -> Result<ledger_core::BasisRule, String> {
@@ -5609,7 +5641,7 @@ impl BooksServer {
#[tool( #[tool(
name = "books.correct_asset", name = "books.correct_asset",
title = "Correct a fixed asset", title = "Correct a fixed asset",
description = "Correct a registered fixed asset's cost, salvage value, useful life, or in-service date (never before its acquisition entry's date) when it was registered wrong, with the reason; omitted fields keep their values. Refused while the books carry depreciation for it (a run posted and not reversed: correct with entries then). Its tag, serial, links, and basis record stay (a contribution's value and built-in gain are as contributed); correct the entry that put it on the books too, so the books and the register agree (books.export fixed_asset_register shows any difference). Journaled with the old figures.", description = "Correct a registered fixed asset's cost, salvage value, useful life, or in-service date (never before its acquisition entry's date) when it was registered wrong, with the reason; omitted fields keep their values. Refused while the books carry depreciation for it (a run posted and not reversed: correct with entries then). Its tag, serial, links and basis record stay (a recorded value at contribution never moves; a pre-P3 basis reads it as the book cost, so its built-in gain moves, noted); correct the entry that put it on the books too (books.export fixed_asset_register shows any difference). Journaled with the old figures.",
annotations( annotations(
read_only_hint = false, read_only_hint = false,
destructive_hint = true, destructive_hint = true,
@@ -5628,6 +5660,7 @@ impl BooksServer {
"in_service", "in_service",
p.in_service.as_deref(), p.in_service.as_deref(),
&mut notes, &mut notes,
same_day,
)?; )?;
let mut store = self.lock()?; let mut store = self.lock()?;
let id = resolve_asset_ref(&store, &p.asset)?; let id = resolve_asset_ref(&store, &p.asset)?;
@@ -5648,11 +5681,19 @@ impl BooksServer {
None => a.in_service_date, None => a.in_service_date,
}, },
}; };
let before = a.built_in_gain_704c();
store store
.correct_asset(id, fix, &p.reason) .correct_asset(id, fix, &p.reason)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let ledger = store.ledger(); let ledger = store.ledger();
let a = ledger.get_asset(id).expect("resolved"); let a = ledger.get_asset(id).expect("resolved");
if a.built_in_gain_704c() != before {
notes.push(format!(
"its basis was recorded before P3 with no value at contribution, which reads as the book cost, so its §704(c) built-in gain moved with the cost ({} to {}): record the value at contribution with books.set_asset_tax_basis",
before.map_or("none".into(), money),
a.built_in_gain_704c().map_or("none".into(), money)
));
}
Ok(Json(AssetSummaryOne { Ok(Json(AssetSummaryOne {
asset_id: id, asset_id: id,
tag: a.tag.clone(), tag: a.tag.clone(),
@@ -5691,6 +5732,7 @@ impl BooksServer {
"contributor_basis", "contributor_basis",
p.contributor_basis.as_deref(), p.contributor_basis.as_deref(),
&mut notes, &mut notes,
same_amount,
)?; )?;
let basis = parse_asset_basis( let basis = parse_asset_basis(
&p.rule, &p.rule,
@@ -6256,6 +6298,7 @@ impl BooksServer {
"purchase_date", "purchase_date",
p.purchase_date.as_deref(), p.purchase_date.as_deref(),
&mut notes, &mut notes,
same_day,
)? )?
.ok_or("in_service_date is required: the day it was placed in service")?, .ok_or("in_service_date is required: the day it was placed in service")?,
)?; )?;
@@ -6316,6 +6359,7 @@ impl BooksServer {
"tax_basis_rule", "tax_basis_rule",
p.tax_basis_rule.as_deref(), p.tax_basis_rule.as_deref(),
&mut notes, &mut notes,
same_rule,
)?; )?;
let adjusted = aliased( let adjusted = aliased(
"contributor_adjusted_basis", "contributor_adjusted_basis",
@@ -6323,6 +6367,7 @@ impl BooksServer {
"contributor_basis", "contributor_basis",
p.contributor_basis.as_deref(), p.contributor_basis.as_deref(),
&mut notes, &mut notes,
same_amount,
)?; )?;
let basis = match rule { let basis = match rule {
Some(rule) => Some(parse_asset_basis( Some(rule) => Some(parse_asset_basis(
@@ -9235,7 +9280,7 @@ impl BooksServer {
.map_err(|e| e.to_string()); .map_err(|e| e.to_string());
} }
let mut csv = String::from( let mut csv = String::from(
"tag,description,serial,acquired,in_service,cost,salvage,useful_life_months,accumulated_scheduled,accumulated_posted,book_value,status,proceeds,basis_rule,contributor_adjusted_basis,fmv_at_contribution,fmv_recorded,tax_basis_723,depreciable_basis,built_in_gain_704c,sec704c_method,basis_source\n", "tag,description,serial,acquired,in_service,cost,salvage,useful_life_months,accumulated_scheduled,accumulated_posted,book_value,status,proceeds,basis_rule,contributor_adjusted_basis,fmv_at_contribution,fmv_recorded,tax_basis_723,depreciable_basis,built_in_gain_704c,sec704c_1c,sec704c_method,basis_source\n",
); );
for a in &r.rows { for a in &r.rows {
let cells = [ let cells = [
@@ -9266,6 +9311,11 @@ impl BooksServer {
a.tax_basis_723.map(money).unwrap_or_default(), a.tax_basis_723.map(money).unwrap_or_default(),
a.depreciable_basis.map(money).unwrap_or_default(), a.depreciable_basis.map(money).unwrap_or_default(),
a.built_in_gain_704c.map(money).unwrap_or_default(), a.built_in_gain_704c.map(money).unwrap_or_default(),
// IRC §704(c)(1)(C): a built-in loss counts only for the
// contributor; blank for a purchase or no basis.
a.built_in_gain_704c
.map(|g| (g < Decimal::ZERO).to_string())
.unwrap_or_default(),
a.sec704c_method a.sec704c_method
.map(|m| m.key()) .map(|m| m.key())
.unwrap_or_default() .unwrap_or_default()
@@ -9276,7 +9326,7 @@ impl BooksServer {
csv.push('\n'); csv.push('\n');
} }
csv.push_str(&format!( csv.push_str(&format!(
"TOTAL,,,,,{},,,{},{},{},,,,,,,{},{},,,", "TOTAL,,,,,{},,,{},{},{},,,,,,,{},{},,,,",
money(r.total_cost), money(r.total_cost),
money(r.total_accumulated_scheduled), money(r.total_accumulated_scheduled),
money(r.total_accumulated_posted), money(r.total_accumulated_posted),
+87
View File
@@ -1079,6 +1079,34 @@ fn basis_rule_legacy_names_are_aliases() {
let _ = t2; let _ = t2;
} }
/// A name and its alias that mean the same, however written, agree (P3
/// review, 2026-10-21: they were compared as text, so "100" and "100.00",
/// or carryover and carryover_723, were refused as disagreeing).
#[test]
fn a_name_and_its_alias_that_mean_the_same_agree() {
let mut m = books("alias-same");
let (_, t) = m.register_ok(json!({}));
let r = m.ok(
"books.set_asset_tax_basis",
json!({"asset": t, "rule": "carryover_723", "contributor_adjusted_basis": "900",
"contributor_basis": "900.00", "fmv_at_contribution": "1200.00"}),
);
assert_eq!(r["basis"]["contributor_adjusted_basis"], "900.00", "{r}");
m.register_ok(
json!({"basis_rule": "carryover_723", "tax_basis_rule": "carryover",
"contributor_adjusted_basis": "400.00", "contributor_basis": "400",
"fmv_at_contribution": "1200.00"}),
);
m.register_ok(json!({"basis_rule": "purchase", "tax_basis_rule": "cost"}));
assert_eq!(m.assets().len(), 3);
let why = m.refuse(
"books.set_asset_tax_basis",
json!({"asset": t, "rule": "carryover_723", "contributor_adjusted_basis": "900.00",
"contributor_basis": "900.01", "fmv_at_contribution": "1200.00"}),
);
assert!(why.contains("disagree"), "{why}");
}
/// A contribution's fair market value is a fact the human gives, never /// A contribution's fair market value is a fact the human gives, never
/// assumed from the cost (review R11): without it the call is refused. /// assumed from the cost (review R11): without it the call is refused.
#[test] #[test]
@@ -1507,6 +1535,7 @@ fn export_fixed_asset_register_carries_posted_scheduled_and_704c() {
"tax_basis_723", "tax_basis_723",
"depreciable_basis", "depreciable_basis",
"built_in_gain_704c", "built_in_gain_704c",
"sec704c_1c",
"sec704c_method", "sec704c_method",
"basis_source" "basis_source"
] ]
@@ -1526,7 +1555,14 @@ fn export_fixed_asset_register_carries_posted_scheduled_and_704c() {
assert_eq!(canon(&r[col("tax_basis_723")]), "650.25"); assert_eq!(canon(&r[col("tax_basis_723")]), "650.25");
assert_eq!(canon(&r[col("built_in_gain_704c")]), "549.75"); assert_eq!(canon(&r[col("built_in_gain_704c")]), "549.75");
assert_eq!(r[col("fmv_recorded")], "true"); assert_eq!(r[col("fmv_recorded")], "true");
assert_eq!(r[col("sec704c_1c")], "false", "a built-in gain");
assert_eq!(
csv_row("CONV-1")[col("sec704c_1c")],
"true",
"a built-in loss"
);
let r = csv_row("BOUGHT-1"); let r = csv_row("BOUGHT-1");
assert_eq!(r[col("sec704c_1c")], "", "a purchase");
assert_eq!(canon(&r[col("accumulated_posted")]), "100"); assert_eq!(canon(&r[col("accumulated_posted")]), "100");
assert_eq!(canon(&r[col("book_value")]), "1100"); assert_eq!(canon(&r[col("book_value")]), "1100");
assert_eq!(r[col("contributor_adjusted_basis")], ""); assert_eq!(r[col("contributor_adjusted_basis")], "");
@@ -1731,3 +1767,54 @@ fn a_skipped_month_is_caught_up_and_said() {
); );
assert!(err.contains("can't be reversed"), "{err}"); assert!(err.contains("can't be reversed"), "{err}");
} }
/// A basis recorded before P3 has no value at contribution and reads it as
/// the book cost, so correcting the cost moves its built-in gain: the
/// result says so (P3 review, 2026-10-21: the tool text said the built-in
/// gain was "as contributed" whatever the record). By hand: cost 1,200.00,
/// basis 400.00, gain 800.00; cost corrected to 1,100.00, gain 700.00.
#[test]
fn correcting_a_legacy_contributions_cost_says_its_built_in_gain_moved() {
let mut m = books("legacy-correct");
let (id, t) = m.register_ok(json!({}));
let dir = m.books_dir.take().unwrap();
let data = m.data.clone();
drop(m);
let mut w = ledger_store::JournalWriter::open(&data.join("journal.jsonl")).unwrap();
w.append(
"asset.tax_basis_set",
json!({"asset_id": id, "rule": "carryover", "contributor_basis": "400.00"}),
)
.unwrap();
drop(w);
let toml = dir.join("wanderbooks.toml");
let mut m = Mcp::start(&dir, &toml);
m.books_dir = Some(dir);
assert_eq!(m.asset(id)["basis"]["built_in_gain_704c"], "800.00");
let r = m.ok(
"books.correct_asset",
json!({"asset": t, "cost": "1100.00", "reason": "invoice misread"}),
);
assert_eq!(r["basis"]["built_in_gain_704c"], "700.00", "{r}");
let notes = r["notes"].to_string();
assert!(
notes.contains("moved with the cost (800.00 to 700.00)"),
"{notes}"
);
// A recorded value never moves, and says nothing.
m.ok(
"books.set_asset_tax_basis",
json!({"asset": t, "rule": "carryover_723", "contributor_adjusted_basis": "400.00",
"fmv_at_contribution": "1100.00"}),
);
let r = m.ok(
"books.correct_asset",
json!({"asset": t, "cost": "1000.00", "reason": "invoice misread again"}),
);
assert_eq!(r["basis"]["built_in_gain_704c"], "700.00", "{r}");
assert!(
r.get("notes")
.is_none_or(|n| n.as_array().is_some_and(|a| a.is_empty())),
"{r}"
);
}
+92 -13
View File
@@ -26,7 +26,7 @@
use crate::text::{csv_escape, money}; use crate::text::{csv_escape, money};
use ledger_core::Ledger; use ledger_core::Ledger;
use ledger_core::tax_capital::{Open, TaxCapital, TaxLine}; use ledger_core::tax_capital::{Note, Open, TaxCapital, TaxLine};
use rust_decimal::Decimal; use rust_decimal::Decimal;
/// The file's name in the package. /// The file's name in the package.
@@ -73,10 +73,24 @@ pub fn open_text(ledger: &Ledger, o: &Open) -> String {
Open::Disposal { Open::Disposal {
asset: id, asset: id,
proceeds, proceeds,
} => format!( built_in_gain_704c,
"the tax gain or loss on {}'s disposal (proceeds {}), from its adjusted tax basis", } => match built_in_gain_704c {
asset(*id), Some(g) if !g.is_zero() => format!(
money(*proceeds) "the tax gain or loss on {}'s disposal (proceeds {}), from its adjusted tax basis; it is contributed property with a built-in {} of {} at contribution, which the gain or loss must take account of, allocated to the contributing member(s) (IRC §704(c)(1)(A); Treas. Reg. §1.704-3)",
asset(*id),
money(*proceeds),
if *g < Decimal::ZERO { "loss" } else { "gain" },
money(g.abs())
),
_ => format!(
"the tax gain or loss on {}'s disposal (proceeds {}), from its adjusted tax basis",
asset(*id),
money(*proceeds)
),
},
Open::TaxNetIncome { book } => format!(
"the member's share of net income (loss) as figured for tax purposes, from the return ({} on the books; every book-tax difference, Schedule M-1, is the CPA's)",
money(*book)
), ),
Open::OtherChange { book } => format!( Open::OtherChange { book } => format!(
"the tax treatment of a {} change in book capital the books can't explain", "the tax treatment of a {} change in book capital the books can't explain",
@@ -85,6 +99,17 @@ pub fn open_text(ledger: &Ledger, o: &Open) -> String {
} }
} }
/// What a note says: a figure taken on a stated rule, for the CPA to
/// confirm.
pub fn note_text(n: &Note) -> String {
match n {
Note::PrepaidAtBook { entry, book } => format!(
"{} of it is a prepaid contract handed over in entry {entry}, taken at its book amount as its basis (IRC §723 carryover; the CPA to confirm)",
money(*book)
),
}
}
/// The worksheet (INVARIANTS 1-3). `names` are the members' names, in the /// The worksheet (INVARIANTS 1-3). `names` are the members' names, in the
/// rows' order. /// rows' order.
pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> String { pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> String {
@@ -101,13 +126,18 @@ pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> Stri
}; };
for (row, name) in tc.rows.iter().zip(names) { for (row, name) in tc.rows.iter().zip(names) {
let line = |book: Decimal, tax: &TaxLine, note: &str| -> [String; 5] { let line = |book: Decimal, tax: &TaxLine, note: &str| -> [String; 5] {
let note: String = std::iter::once(note.to_string())
.filter(|n| !n.is_empty())
.chain(tax.notes.iter().map(note_text))
.collect::<Vec<_>>()
.join("; ");
match tax.tax() { match tax.tax() {
Some(t) => [ Some(t) => [
money(book), money(book),
money(t), money(t),
money(book - t), money(book - t),
"tax basis".into(), "tax basis".into(),
note.into(), note,
], ],
None => { None => {
let needs: Vec<String> = let needs: Vec<String> =
@@ -118,9 +148,14 @@ pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> Stri
String::new(), String::new(),
"open: the CPA's".into(), "open: the CPA's".into(),
format!( format!(
"known so far {}; needs {}", "known so far {}; needs {}{}",
money(tax.known), money(tax.known),
needs.join("; ") needs.join("; "),
if note.is_empty() {
String::new()
} else {
format!("; {note}")
}
), ),
] ]
} }
@@ -130,6 +165,7 @@ pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> Stri
let cash = TaxLine { let cash = TaxLine {
known: row.contributions_cash, known: row.contributions_cash,
open: Vec::new(), open: Vec::new(),
notes: Vec::new(),
}; };
push( push(
name, name,
@@ -153,11 +189,7 @@ pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> Stri
push( push(
name, name,
"Current year net income (loss)", "Current year net income (loss)",
line( line(b.net_income, &row.net_income, ""),
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( push(
name, name,
@@ -231,3 +263,50 @@ pub fn tax_basis_csv(ledger: &Ledger, names: &[String], tc: &TaxCapital) -> Stri
} }
w w
} }
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn d(s: &str) -> Decimal {
Decimal::from_str(s).unwrap()
}
/// A disposal of contributed property names its built-in gain or loss
/// at contribution and §704(c)(1)(A); a purchase's doesn't. A prepaid
/// taken at book says so, for the CPA to confirm.
#[test]
fn disposal_and_prepaid_texts_say_what_the_cpa_needs() {
let l = Ledger::new(ledger_core::Currency::USD);
let sold = |g: Option<&str>| {
open_text(
&l,
&Open::Disposal {
asset: 7,
proceeds: d("1100.00"),
built_in_gain_704c: g.map(d),
},
)
};
let gain = sold(Some("300.00"));
assert!(gain.contains("built-in gain of 300.00"), "{gain}");
assert!(gain.contains("IRC §704(c)(1)(A)"), "{gain}");
let loss = sold(Some("-300.00"));
assert!(loss.contains("built-in loss of 300.00"), "{loss}");
for none in [sold(None), sold(Some("0"))] {
assert!(!none.contains("§704(c)"), "{none}");
assert!(
none.contains("asset 7's disposal (proceeds 1100.00)"),
"{none}"
);
}
let n = note_text(&Note::PrepaidAtBook {
entry: 3,
book: d("20.00"),
});
assert!(n.contains("20.00") && n.contains("entry 3") && n.contains("the CPA to confirm"));
let i = open_text(&l, &Open::TaxNetIncome { book: d("-5.00") });
assert!(i.contains("from the return (-5.00 on the books"), "{i}");
}
}
+1 -1
View File
@@ -641,7 +641,7 @@ pub fn build_package<B: PackageBooks + ?Sized>(
} }
); );
k.push_str(if k1_tax_included { 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" "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; a prepaid contract handed over at its book amount, the CPA to confirm), 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 is left blank, marked open, and says what the books know so far and what is needed: the share of income as figured for tax (always the return's), so the ending too; tax depreciation; the tax gain on a disposal (with a contributed property's built-in gain or loss); a basis the books don't hold.\n"
} else { } else {
"k1-tax-basis-capital-NOT-INCLUDED.txt: why the tax-basis capital worksheet isn't here.\n" "k1-tax-basis-capital-NOT-INCLUDED.txt: why the tax-basis capital worksheet isn't here.\n"
}); });
+3 -2
View File
@@ -2241,6 +2241,7 @@ fn the_k1_worksheet_reports_tax_basis_capital() {
"member,item,book,tax_basis,book_less_tax,status,note" "member,item,book,tax_basis,book_less_tax,status,note"
); );
assert_eq!(lines.len(), 1 + 2 * 12, "{csv}"); assert_eq!(lines.len(), 1 + 2 * 12, "{csv}");
let income_note = "the member's share of net income (loss) as figured for tax purposes, from the return (-385.00 on the books; every book-tax difference, Schedule M-1, is the CPA's)";
let depreciation_note = "tax depreciation of the fixed assets in service (MACRS, IRC §168), in place of the book depreciation posted (1370.00 in the span, 1370.00 of it on contributed property, whose tax depreciation each property's §704(c) method allocates, Treas. Reg. §1.704-3)"; let depreciation_note = "tax depreciation of the fixed assets in service (MACRS, IRC §168), in place of the book depreciation posted (1370.00 in the span, 1370.00 of it on contributed property, whose tax depreciation each property's §704(c) method allocates, Treas. Reg. §1.704-3)";
for member in ["Tobin Example", "Mo Example"] { for member in ["Tobin Example", "Mo Example"] {
for want in [ for want in [
@@ -2250,14 +2251,14 @@ fn the_k1_worksheet_reports_tax_basis_capital() {
"{member},Capital contributed: property,1370.00,1384.20,-14.20,tax basis,at the property's adjusted basis (IRC §723); book: its fair market value" "{member},Capital contributed: property,1370.00,1384.20,-14.20,tax basis,at the property's adjusted basis (IRC §723); book: its fair market value"
), ),
format!( format!(
"{member},Current year net income (loss),-385.00,,,open: the CPA's,\"known so far -385.00; needs {depreciation_note}\"" "{member},Current year net income (loss),-385.00,,,open: the CPA's,\"known so far -385.00; needs {income_note}; {depreciation_note}\""
), ),
format!("{member},Other increase (decrease),0.00,0.00,0.00,tax basis,"), format!("{member},Other increase (decrease),0.00,0.00,0.00,tax basis,"),
format!( format!(
"{member},Withdrawals and distributions,300.00,300.00,0.00,tax basis,\"cash, plus the adjusted tax basis of property distributed\"" "{member},Withdrawals and distributions,300.00,300.00,0.00,tax basis,\"cash, plus the adjusted tax basis of property distributed\""
), ),
format!( format!(
"{member},Ending capital,1685.00,,,open: the CPA's,\"known so far 1699.20; needs {depreciation_note}\"" "{member},Ending capital,1685.00,,,open: the CPA's,\"known so far 1699.20; needs {income_note}; {depreciation_note}\""
), ),
format!( format!(
"{member},\"Reconciliation: beginning capital, book less tax\",,,0.00,reconciliation," "{member},\"Reconciliation: beginning capital, book less tax\",,,0.00,reconciliation,"
+17 -13
View File
@@ -5,7 +5,7 @@
//! is the current version. //! is the current version.
/// Current schema version. /// Current schema version.
pub const SCHEMA_VERSION: u32 = 13; pub const SCHEMA_VERSION: u32 = 14;
/// Migrations, in order. `MIGRATIONS[i]` migrates from version `i` to `i+1`. /// Migrations, in order. `MIGRATIONS[i]` migrates from version `i` to `i+1`.
pub const MIGRATIONS: &[&str] = &[ pub const MIGRATIONS: &[&str] = &[
@@ -326,18 +326,15 @@ CREATE TABLE IF NOT EXISTS entry_approvals(
"#, "#,
r#" r#"
-- v13: 1099 decisions and payment methods (plan P3, defects 12, 18 and -- v13: 1099 decisions and payment methods (plan P3, defects 12, 18 and
-- 23; D-40), and assets' basis facts with their depreciation runs keyed -- 23; D-40). Each 1099 decision recorded with who decided (journal
-- by entry (plan P3, defect 13; D-41). One migration: none of P3 had -- feature vendor_1099_decisions), line by line: the date it takes effect
-- shipped when they met. -- (NULL: from the beginning), nec, misc, not_reportable or NULL
-- -- (undetermined), and for not_reportable its kind and reason. A decision
-- Each 1099 decision recorded with who decided (journal feature -- from the beginning also sets vendors.form_1099, as every decision
-- vendor_1099_decisions), line by line: the date it takes effect (NULL: -- written before these did. Payment methods (feature payment_instruments)
-- from the beginning), nec, misc, not_reportable or NULL (undetermined), -- and the method each entry was paid with. Rows only come from lines
-- and for not_reportable its kind and reason. A decision from the -- written under the features, so books written before them project the
-- beginning also sets vendors.form_1099, as every decision written before -- same as before.
-- these did. Payment methods (feature payment_instruments) and the method
-- each entry was paid with. Rows only come from lines written under the
-- features, so books written before them project the same as before.
CREATE TABLE IF NOT EXISTS vendor_1099_decisions( CREATE TABLE IF NOT EXISTS vendor_1099_decisions(
seq INTEGER PRIMARY KEY, seq INTEGER PRIMARY KEY,
vendor_id INTEGER NOT NULL, vendor_id INTEGER NOT NULL,
@@ -359,6 +356,13 @@ CREATE TABLE IF NOT EXISTS entry_instruments(
entry_id INTEGER PRIMARY KEY, entry_id INTEGER PRIMARY KEY,
instrument_id INTEGER NOT NULL instrument_id INTEGER NOT NULL
); );
"#,
r#"
-- v14: assets' basis facts and their depreciation runs keyed by entry
-- (plan P3, defect 13; D-41). Its own migration: v13 shipped on main
-- before these columns, so a v13 file already built never gains them by
-- an edit to v13 (each schema's file is rebuilt from the journal, and a
-- migration's text never changes once shipped).
-- --
-- An asset's basis facts beyond the rule and contributor basis -- An asset's basis facts beyond the rule and contributor basis
-- (asset.basis_set): the fair market value at contribution (TEXT, exact -- (asset.basis_set): the fair market value at contribution (TEXT, exact
+78 -3
View File
@@ -18,7 +18,7 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use ledger_core::{AccountType, AssetId, Currency, LedgerError, Money, NewAsset}; use ledger_core::{AccountType, AssetId, Currency, LedgerError, Money, NewAsset};
use ledger_store::journal::read_and_verify_journal; use ledger_store::journal::read_and_verify_journal;
use ledger_store::{JOURNAL_FILE, JournalWriter, Store, StoreError, db_file}; use ledger_store::{JOURNAL_FILE, JournalWriter, Store, StoreError, db_file, db_file_for};
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -887,6 +887,49 @@ fn downgrade_db_to_v12(db: &Path) {
.unwrap(); .unwrap();
} }
/// A database at v13 exactly as it shipped on main before the assets'
/// basis columns (P3 review, 2026-10-21: those columns were first written
/// into v13 itself, so a v13 file built by the earlier commits opened as
/// current and failed to load, "no such column"): its file is another
/// schema's, left byte for byte, and this version builds its own from the
/// journal, with every run, and verifies.
#[test]
fn a_v13_file_built_before_the_basis_columns_is_left_and_rebuilt() {
use ledger_core::BasisRule::*;
let (dir, mut s, _, x, y) = fixture("v13-shipped");
s.run_depreciation(x, "2026-01").unwrap();
s.set_asset_basis(y, contribution(Carryover723, "640.00", "1200.00"))
.unwrap();
drop(s);
let v13 = dir.join(db_file_for(13));
std::fs::rename(dir.join(db_file()), &v13).unwrap();
let conn = rusqlite::Connection::open(&v13).unwrap();
conn.execute_batch(
"ALTER TABLE assets DROP COLUMN fmv_at_contribution;
ALTER TABLE assets DROP COLUMN sec704c_method;
ALTER TABLE assets DROP COLUMN basis_source_json;
CREATE TABLE asset_depreciation_v12(asset_id INTEGER NOT NULL, period TEXT NOT NULL,
entry_id INTEGER NOT NULL, amount TEXT NOT NULL, PRIMARY KEY (asset_id, period));
INSERT INTO asset_depreciation_v12 SELECT asset_id, period, entry_id, amount FROM asset_depreciation;
DROP TABLE asset_depreciation;
ALTER TABLE asset_depreciation_v12 RENAME TO asset_depreciation;
UPDATE meta SET value = '13' WHERE key = 'schema_version';",
)
.unwrap();
drop(conn);
let shipped = std::fs::read(&v13).unwrap();
let s = Store::open(&dir).expect("books with a shipped v13 file open");
assert_eq!(s.database_file(), db_file());
s.verify().unwrap();
assert_eq!(s.ledger().depreciation_runs(x).len(), 1);
assert_eq!(
basis_of(&s, y),
Some(contribution(Carryover723, "640.00", "1200.00"))
);
drop(s);
assert_eq!(std::fs::read(&v13).unwrap(), shipped, "left byte for byte");
}
/// Downgrade the database to schema v10 (none of v11's attachment links, /// Downgrade the database to schema v10 (none of v11's attachment links,
/// statements, reconciliations, or payroll_lines.employee_id). /// statements, reconciliations, or payroll_lines.employee_id).
fn downgrade_db_to_v10(dir: &Path) { fn downgrade_db_to_v10(dir: &Path) {
@@ -909,12 +952,12 @@ fn downgrade_db_to_v10(dir: &Path) {
} }
/// P2's database (`wanderbooks.db`, schema v12) is left as it is, byte /// P2's database (`wanderbooks.db`, schema v12) is left as it is, byte
/// for byte: this version builds its own v13 file from the journal, its /// for byte: this version builds its own file from the journal, its
/// runs table keyed by entry with every run kept and the new columns /// runs table keyed by entry with every run kept and the new columns
/// empty, and it verifies; a basis set after then reloads from it (each /// empty, and it verifies; a basis set after then reloads from it (each
/// schema in a file of its own: rules.md B-27). /// schema in a file of its own: rules.md B-27).
#[test] #[test]
fn a_p2_database_is_left_and_v13_built_from_the_journal() { fn a_p2_database_is_left_and_this_versions_built_from_the_journal() {
use ledger_core::BasisRule::*; use ledger_core::BasisRule::*;
let (dir, mut s, _, x, y) = fixture("v12"); let (dir, mut s, _, x, y) = fixture("v12");
s.run_depreciation(x, "2026-01").unwrap(); s.run_depreciation(x, "2026-01").unwrap();
@@ -1213,3 +1256,35 @@ fn the_viewer_and_writer_agree_on_a_bad_depreciation_posted_payload() {
} }
} }
} }
/// B-27: books an older version wrote (format 1, declaring nothing) refuse
/// a basis while they wait for the owner's format upgrade, a purchase's
/// too, journaling nothing; the feature is never declared on the way (P3
/// review, 2026-10-21).
#[test]
fn a_basis_in_books_pending_their_upgrade_is_refused_and_journals_nothing() {
use ledger_core::BasisRule::*;
let dir = test_dir("basis-pending");
let mut s = Store::create_in_format(
&dir,
Currency::USD,
&ledger_store::format::JournalFormat::default(),
)
.unwrap();
let a = accounts(&mut s);
let x = laptop(&mut s, &a, "Laptop A");
let was = std::fs::read(dir.join(JOURNAL_FILE)).unwrap();
for b in [
ledger_core::AssetBasis::purchase(),
contribution(Carryover723, "640.00", "1200.00"),
] {
match s.set_asset_basis(x, b) {
Err(StoreError::FormatUpgradePending { need }) => {
assert!(need.contains(ledger_store::format::ASSET_BASIS), "{need:?}")
}
other => panic!("expected FormatUpgradePending, got {other:?}"),
}
}
assert_eq!(std::fs::read(dir.join(JOURNAL_FILE)).unwrap(), was);
assert_eq!(basis_of(&s, x), None);
}
+33
View File
@@ -369,3 +369,36 @@ fn new_books_get_their_schemas_own_file() {
assert_eq!(ledger_store::is_database_file(name), is, "{name}"); assert_eq!(ledger_store::is_database_file(name), is, "{name}");
} }
} }
/// A shipped migration's text never changes (P3 review, 2026-10-21: the
/// assets' v13 columns were first added to the v13 already on main, so a
/// v13 file built before them would open without them). Each schema's
/// file is built once by these scripts; a change is a new migration. The
/// SHA-256 of each shipped one, v1 to v13 (append the next as it ships).
#[test]
fn a_shipped_migration_never_changes() {
use sha2::{Digest, Sha256};
const SHIPPED: [&str; 13] = [
"8b253dded247d8518a7454b1c894a147ac211ee32ea2671dca2c586c5184c046",
"9fc19f844ed33b8f08eee8437ee3eb038e854737d5febe79ce3ac80abeb18614",
"215af9a7a54cda382f721488fedb34f68eb36399730de1b23371d44eb16fd79d",
"7923eb31ab8a08f20b8d7df017814095861478cb796af14994dbb85dbe558c20",
"95caa91ee3327b5590ad1ddefa76bad189950091cfcd140121f978e0af258e8a",
"74181a819beb4349808b9a095fbcda0073890fa6f54bc3fa5535c5a6644e35c6",
"df008d2a7cf832173908d1e33f795b88d272eae9cd23a5ebebe7207871d7076f",
"d0cd600ef4aa7bc2301e682bace23f3ce311b5f991e5d7bff447a9520ca8e89f",
"b2e54e8cf47b7f55f42f3fd4876fce5183e229aea08059518a50cbbffdca2db2",
"8881527992c586f69644276db921e640fb6548ee1d2f7c313a46c53e1e63430e",
"cc18b31e929b5889c0d09190b3211ccefd9d73df8e1e05c93f9e460097e1553a",
"dd2a93d760350f2edd9462f4a52888c0645040582739d3a93b17a3195238d501",
"9aa8a0ea7ea15315a4e38281e3a6d4e14beb26333991ebb2824d9c8cc5a51941",
];
for (i, want) in SHIPPED.iter().enumerate() {
let got: String = Sha256::digest(MIGRATIONS[i].as_bytes())
.iter()
.map(|b| format!("{b:02x}"))
.collect();
assert_eq!(&got, want, "migration v{} changed after it shipped", i + 1);
}
assert_eq!(MIGRATIONS.len() as u32, SCHEMA_VERSION);
}
+17 -7
View File
@@ -605,7 +605,7 @@ life's last day is 9999-12-31 at the latest.
name's initials without LLC/Inc; "FA" when none). `in_service_date` (the name's initials without LLC/Inc; "FA" when none). `in_service_date` (the
day it is ready and available for its use) starts depreciation (P3, day it is ready and available for its use) starts depreciation (P3,
defect 13; B-15); `purchase_date` is its alias for one release (both defect 13; B-15); `purchase_date` is its alias for one release (both
given and different: refused; the alias noted in `notes`). The given and meaning different days: refused; the alias noted in `notes`). The
`acquisition_entry` (posted, debiting the cost account, dated on or `acquisition_entry` (posted, debiting the cost account, dated on or
before the in-service date) gives `acquired_date`, which puts the asset before the in-service date) gives `acquired_date`, which puts the asset
on the register; with none, the acquired date is the in-service date. on the register; with none, the acquired date is the in-service date.
@@ -632,11 +632,16 @@ life's last day is 9999-12-31 at the latest.
`accumulated_posted` (what the books carry: its depreciation runs, less `accumulated_posted` (what the books carry: its depreciation runs, less
reversals, a reversal of a reversal reinstating), reversals, a reversal of a reversal reinstating),
`accumulated_scheduled_through` (what the schedule says is due by `accumulated_scheduled_through` (what the schedule says is due by
`as_of`), `unposted_depreciation` (the difference: run `as_of`), `unposted_depreciation` (the difference for this asset, its
`books.run_depreciation`), `book_value` (cost less posted: what the schedule less its runs: run `books.run_depreciation`; the register's
books carry), and `basis` (as `books.set_asset_tax_basis` reports it). total `unposted_depreciation` compares the schedules with the general
Disposed by `as_of`: every figure 0.00; acquired after `as_of`: the ledger instead, so a hand entry in accumulated depreciation shows there,
figures null. The rows are the register's (`fixed_asset_register`). An as `unattributed_accumulated`, not here), `book_value` (cost less
posted: what the books carry), and `basis` (as
`books.set_asset_tax_basis` reports it). `cost` is always the
capitalized cost; `held` says whether the books carry it. Disposed by
`as_of`: every other figure 0.00; acquired after `as_of`: those figures
null. The rows are the register's (`fixed_asset_register`). An
asset whose figures can't be computed (one an earlier version took) asset whose figures can't be computed (one an earlier version took)
lists with them null and `schedule_error` saying why: one bad asset lists with them null and `schedule_error` saying why: one bad asset
never fails the list. never fails the list.
@@ -675,6 +680,9 @@ life's last day is 9999-12-31 at the latest.
Refused while the books carry depreciation for it (a run posted and not Refused while the books carry depreciation for it (a run posted and not
reversed), and for an in-service date before its acquisition entry's reversed), and for an in-service date before its acquisition entry's
date; `in_service` is the alias of `in_service_date` for one release. date; `in_service` is the alias of `in_service_date` for one release.
A recorded value at contribution, and so the built-in gain, never
moves; a basis recorded before P3 has none and reads it as the book
cost, so a cost correction moves its built-in gain, and `notes` says so.
- `books.run_depreciation { period: "YYYY-MM", asset_id? }` - `books.run_depreciation { period: "YYYY-MM", asset_id? }`
→ `runs: [{ asset_id, description, period, amount, caught_up, entry_id, → `runs: [{ asset_id, description, period, amount, caught_up, entry_id,
already_posted }]` and `skipped: [{ asset_id, description, reason }]`. already_posted }]` and `skipped: [{ asset_id, description, reason }]`.
@@ -731,7 +739,9 @@ life's last day is 9999-12-31 at the latest.
salvage, useful_life_months, accumulated_scheduled, accumulated_posted, salvage, useful_life_months, accumulated_scheduled, accumulated_posted,
book_value, status, proceeds, basis_rule, contributor_adjusted_basis, book_value, status, proceeds, basis_rule, contributor_adjusted_basis,
fmv_at_contribution, fmv_recorded, tax_basis_723, depreciable_basis, fmv_at_contribution, fmv_recorded, tax_basis_723, depreciable_basis,
built_in_gain_704c, sec704c_method, basis_source`, then a TOTAL row. built_in_gain_704c, sec704c_1c, sec704c_method, basis_source`, then a
TOTAL row; `sec704c_1c` is `true` for a built-in loss (IRC
§704(c)(1)(C)), blank without a contribution basis.
- `books.show { asset }` opens an asset in the viewer; `page: "assets"` - `books.show { asset }` opens an asset in the viewer; `page: "assets"`
and `report: "fixed_asset_register"` open the register. and `report: "fixed_asset_register"` open the register.
+25 -16
View File
@@ -234,8 +234,8 @@ earnings the same running way, D-38, 2026-10-20)
analysis "is based on the partnership's books and records" (the Partner's analysis "is based on the partnership's books and records" (the Partner's
Instructions' Basis Limitations section, `sources/irs-partnerships.md`; Instructions' Basis Limitations section, `sources/irs-partnerships.md`;
not Item L, which an earlier version of this rule spliced in). The K-1 not Item L, which an earlier version of this rule spliced in). The K-1
worksheet is the book capital analysis, which the CPA converts for worksheet carries the book capital analysis, and beside it the tax-basis
Item L. capital as far as the books know it (B-36).
- **No authority** says how to round a share to the cent. Largest - **No authority** says how to round a share to the cent. Largest
remainder, ties to the member listed first: DECISION (the owner, remainder, ties to the member listed first: DECISION (the owner,
2026-10-13). Closes made before then replay with the last member 2026-10-13). Closes made before then replay with the last member
@@ -2467,20 +2467,29 @@ book capital reconciles to tax capital:
gain or loss at contribution, shared within the entry as its book value gain or loss at contribution, shared within the entry as its book value
is (B-13). Property contributed with no basis in the books (no is (B-13). Property contributed with no basis in the books (no
registered asset linked to the entry holds a contribution basis for it) registered asset linked to the entry holds a contribution basis for it)
is open. is open. Property contributed to a prepaid account (the balance
- What the books can't know is never guessed: the tax depreciation of account of a prepaid-expense schedule: a service contract the member
every fixed asset in service (MACRS, and for contributed property the handed over, B-13) with no fixed asset for it enters at its book
§704(c) method's allocation: the CPA's, B-15), the tax gain on a amount, with a note: a contract's carryover basis is what the books
disposal, the adjusted tax basis of property distributed, the tax recorded for it (DECISION, P3 review 2026-10-21; the CPA to confirm).
treatment of a change in book capital the books can't explain, and - What the books can't know is never guessed. The share of net income
last year's tax ending when an earlier year left a figure open. Each "as figured for tax purposes" is the return's, always: the books are
such line is left blank and marked open, with what the books know so kept on their own basis, and every book-tax difference (Schedule M-1)
far and the book amount the CPA's figure replaces. The book is the CPA's; so is the ending, and the beginning of any year after
depreciation the tax figure replaces is the period's depreciation the first (Item L: "the partner's ending capital account as
expense, the P&L's: each asset's runs and what its disposal entry posts determined for last year"). The line names the differences the books
to its expense account (B-15 rule 8; before the seam review the runs do see: the tax depreciation of every fixed asset in service (MACRS,
alone, so a disposal that credited back part of a month's run stated and for contributed property the §704(c) method's allocation: the
more than the P&L). CPA's, B-15), and the tax gain on a disposal, with a contributed
property's built-in gain or loss at contribution, which that gain must
take account of (IRC §704(c)(1)(A)). Also open: the adjusted tax basis
of property distributed, and the tax treatment of a change in book
capital the books can't explain. Each such line is left blank and
marked open, with what the books know so far and the book amount the
CPA's figure replaces.
The book depreciation the tax figure replaces is the period's
depreciation expense, the P&L's: each asset's runs and what its
disposal entry posts to its expense account (B-15 rule 8).
- The reconciliation: book ending less tax ending (as far as the books - The reconciliation: book ending less tax ending (as far as the books
know it) = beginning (book less tax) + the built-in gain or loss share know it) = beginning (book less tax) + the built-in gain or loss share
+ property without a basis (at book) − property distributed (at book), + property without a basis (at book) − property distributed (at book),
+1 -1
View File
@@ -61,7 +61,7 @@ Source: <https://www.irs.gov/instructions/i1065sk1>
Retrieved: 2026-10-20 Retrieved: 2026-10-20
SHA-256 of the retrieved file (`i1065sk1.html`): `d7d6298286601fd95ab8e445c10fd62499ea1e37f8e5c3531f8e1783862d8d03` SHA-256 of the retrieved file (`i1065sk1.html`): `d7d6298286601fd95ab8e445c10fd62499ea1e37f8e5c3531f8e1783862d8d03`
**Item L, its first paragraph** (the partner's capital account, on the tax basis; the K-1 worksheet carries the book capital account, which the CPA converts) **Item L, its first paragraph** (the partner's capital account, on the tax basis; the K-1 worksheet carries the book capital account and, beside it, the tax-basis one as far as the books know it, rules.md B-36)
> The partnership must report your beginning capital account and ending capital account for the year using the tax-basis method, including the amount of capital you contributed to the partnership during the year, your share of the partnership’s current-year net income or loss as computed for tax purposes, any withdrawals and distributions made to you by the partnership, and any other increases or decreases to your capital account determined in a manner generally consistent with figuring the partner’s adjusted tax basis in its partnership interest (without regard to partnership liabilities), taking into account the rules and principles of sections 705, 722, 733, and 742. See the Instructions for Form 1065 for more details. > The partnership must report your beginning capital account and ending capital account for the year using the tax-basis method, including the amount of capital you contributed to the partnership during the year, your share of the partnership’s current-year net income or loss as computed for tax purposes, any withdrawals and distributions made to you by the partnership, and any other increases or decreases to your capital account determined in a manner generally consistent with figuring the partner’s adjusted tax basis in its partnership interest (without regard to partnership liabilities), taking into account the rules and principles of sections 705, 722, 733, and 742. See the Instructions for Form 1065 for more details.
+13 -4
View File
@@ -1579,8 +1579,14 @@ basis. Mutation runs wait for P12 (D-33); none was run.
`record_depreciation_run`); `tax_capital.rs` (tax-basis capital per `record_depreciation_run`); `tax_capital.rs` (tax-basis capital per
member with a cash contribution, a contribution of property, a member with a cash contribution, a contribution of property, a
distribution and a year of depreciation; next year's beginning; cash-only distribution and a year of depreciation; next year's beginning; cash-only
books; property without a basis; property distributed; the roll-forward books, whose tax income is still the return's; property without a basis;
and reconciliation over spans); `properties.rs` property distributed; the roll-forward and reconciliation over spans;
and, from the P3 review, four devices and a prepaid plan by hand (two
worth less than basis, one more, one equal; the plan at book with its
note; the gain property's disposal flagged with its built-in gain),
property subject to a liability (negative tax property), and three
members with unequal shares, one contributing nothing);
`spec_asset_basis.rs` `a_duplicated_run_line_is_carried_once`; `properties.rs`
`prop_basis_figures_laws` (5,000 cases); `kernel_bridge.rs` `prop_basis_figures_laws` (5,000 cases); `kernel_bridge.rs`
`basis_figures_agree`; Kani `basis_figures_laws` (`src/proofs.rs`, 83 `basis_figures_agree`; Kani `basis_figures_laws` (`src/proofs.rs`, 83
checks, its cover satisfied); `spec_asset_tags.rs` (the register's checks, its cover satisfied); `spec_asset_tags.rs` (the register's
@@ -1591,8 +1597,11 @@ basis. Mutation runs wait for P12 (D-33); none was run.
line whose figures disagree with today's formula still replays; an line whose figures disagree with today's formula still replays; an
unknown rule or method is newer, a malformed figure corrupt; pre-P3 unknown rule or method is newer, a malformed figure corrupt; pre-P3
`asset.tax_basis_set` lines give the figures they gave; P2's v12 `asset.tax_basis_set` lines give the figures they gave; P2's v12
database left as it is while this version builds its v13 file, runs database left as it is while this version builds its v14 file, runs
keyed by entry (`a_p2_database_is_left_and_v13_built_from_the_journal`); keyed by entry (`a_p2_database_is_left_and_this_versions_built_from_the_journal`);
a v13 file as it shipped before the basis columns left too and v14
built (`a_v13_file_built_before_the_basis_columns_is_left_and_rebuilt`;
`schema_files.rs` `a_shipped_migration_never_changes`);
v9 and v10 databases under this version's name rebuilt). The old books v9 and v10 databases under this version's name rebuilt). The old books
(`books_an_earlier_version_wrote_open_to_its_figures`) open to exactly (`books_an_earlier_version_wrote_open_to_its_figures`) open to exactly
their figures. their figures.