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:
24 files changed
+860
-127
No files matched your search
@@ -724,8 +724,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// MacBook) is property, 12.50 each. Members paying a 3,000 annual
|
||||
/// Prepaid 36 / Cr 3000 18.00, Cr 3001 18.00, a service plan with a
|
||||
/// 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,
|
||||
/// then Dr 2100 / Cr 3000 1,500, Cr 3001 1,500: 1,500 cash each.
|
||||
#[test]
|
||||
@@ -737,13 +737,13 @@ mod tests {
|
||||
1,
|
||||
5,
|
||||
&[
|
||||
(prepaid, Side::Debit, "25"),
|
||||
(a, Side::Credit, "12.50"),
|
||||
(b, Side::Credit, "12.50"),
|
||||
(prepaid, Side::Debit, "36"),
|
||||
(a, Side::Credit, "18.00"),
|
||||
(b, Side::Credit, "18.00"),
|
||||
],
|
||||
);
|
||||
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]);
|
||||
let due =
|
||||
bk.l.add_account(
|
||||
@@ -770,7 +770,7 @@ mod tests {
|
||||
(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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2586,7 +2586,11 @@ impl Ledger {
|
||||
/// Record a depreciation run (`asset.depreciation_posted`). Checks only
|
||||
/// that the asset and the entry exist: every run an earlier version
|
||||
/// 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(
|
||||
&mut self,
|
||||
asset: AssetId,
|
||||
@@ -2600,14 +2604,15 @@ impl Ledger {
|
||||
if !self.entries.contains_key(&entry) {
|
||||
return Err(LedgerError::UnknownEntry(entry));
|
||||
}
|
||||
self.asset_runs
|
||||
.entry(asset)
|
||||
.or_default()
|
||||
.push(assets::DepreciationPosting {
|
||||
period: period.to_string(),
|
||||
entry,
|
||||
amount,
|
||||
});
|
||||
let runs = self.asset_runs.entry(asset).or_default();
|
||||
if runs.iter().any(|r| r.entry == entry && r.period == period) {
|
||||
return Ok(());
|
||||
}
|
||||
runs.push(assets::DepreciationPosting {
|
||||
period: period.to_string(),
|
||||
entry,
|
||||
amount,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -31,20 +31,29 @@
|
||||
//! `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
|
||||
//! tax figure.
|
||||
//! - Net income: tax depreciation of every fixed asset in service
|
||||
//! in the span (MACRS, and for contributed property the §704(c)
|
||||
//! method's allocation) is the CPA's, as is the tax gain on a
|
||||
//! disposal; `known` is the book share, the open items say so.
|
||||
//! - Net income: always the return's (Item L: "as figured for tax
|
||||
//! purposes"); the books are kept on their own basis and can't
|
||||
//! know a book-tax difference (Schedule M-1). `known` is the book
|
||||
//! 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.
|
||||
//! - A change in book capital the books can't explain ("other").
|
||||
//! - Beginning capital from earlier years whose tax figures were open.
|
||||
//! 4. Each row: ending.known = beginning.known + contributions_cash +
|
||||
//! contributions_property.known + net_income.known + other.known −
|
||||
//! 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 −
|
||||
//! tax beginning.known) + built-in gain share + property without a
|
||||
//! 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:
|
||||
//! 1. IF two members jointly contribute a laptop worth 1,440.00 whose
|
||||
//! 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
|
||||
//! posted in the span
|
||||
//! 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};
|
||||
@@ -158,12 +170,26 @@ pub enum Open {
|
||||
contributed_book: Decimal,
|
||||
},
|
||||
/// 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 {
|
||||
/// The asset.
|
||||
asset: AssetId,
|
||||
/// The proceeds.
|
||||
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
|
||||
/// 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).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TaxLine {
|
||||
@@ -181,6 +225,9 @@ pub struct TaxLine {
|
||||
/// What the CPA must still settle; empty when `known` is the tax
|
||||
/// figure.
|
||||
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 {
|
||||
@@ -188,6 +235,7 @@ impl TaxLine {
|
||||
Self {
|
||||
known,
|
||||
open: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +367,8 @@ fn share(whole: Decimal, weights: &[Decimal]) -> Result<Vec<Decimal>, LedgerErro
|
||||
struct EntryProperty {
|
||||
/// Σ built-in gain of the linked assets with a contribution basis.
|
||||
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.
|
||||
without_basis: Decimal,
|
||||
/// 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))
|
||||
.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);
|
||||
for a in ledger
|
||||
.registered_assets()
|
||||
@@ -359,6 +427,7 @@ fn entry_property(
|
||||
if linked > pool {
|
||||
return Ok(EntryProperty {
|
||||
built_in_gain: Decimal::ZERO,
|
||||
prepaid,
|
||||
without_basis: pool,
|
||||
mismatch: Some((linked, pool)),
|
||||
});
|
||||
@@ -366,6 +435,7 @@ fn entry_property(
|
||||
let rest = pool - linked;
|
||||
Ok(EntryProperty {
|
||||
built_in_gain: big,
|
||||
prepaid,
|
||||
without_basis: unknown.checked_add(rest).ok_or_else(too_large)?,
|
||||
mismatch: None,
|
||||
})
|
||||
@@ -385,6 +455,7 @@ fn span(
|
||||
let mut without = 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_notes: Vec<Vec<Note>> = vec![Vec::new(); n];
|
||||
let mut dist_open: Vec<Vec<Open>> = vec![Vec::new(); n];
|
||||
// Entries in journal order, each once.
|
||||
let mut entries: Vec<EntryId> = Vec::new();
|
||||
@@ -412,7 +483,14 @@ fn span(
|
||||
let p = entry_property(ledger, e, cash)?;
|
||||
let g = share(p.built_in_gain, &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()) {
|
||||
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)?;
|
||||
without[i] = without[i].checked_add(w[i]).ok_or_else(too_large)?;
|
||||
if let Some((linked, property)) = p.mismatch {
|
||||
@@ -495,6 +573,7 @@ fn span(
|
||||
open.push(Open::Disposal {
|
||||
asset: a.id,
|
||||
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 {
|
||||
vec![Open::PriorYears]
|
||||
},
|
||||
notes: Vec::new(),
|
||||
},
|
||||
None => TaxLine::complete(book.beginning),
|
||||
};
|
||||
@@ -518,10 +598,16 @@ fn span(
|
||||
.and_then(|x| x.checked_sub(without[i]))
|
||||
.ok_or_else(too_large)?,
|
||||
open: std::mem::take(&mut prop_open[i]),
|
||||
notes: std::mem::take(&mut prop_notes[i]),
|
||||
};
|
||||
let net_income = TaxLine {
|
||||
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 {
|
||||
known: book.other,
|
||||
@@ -530,6 +616,7 @@ fn span(
|
||||
} else {
|
||||
vec![Open::OtherChange { book: book.other }]
|
||||
},
|
||||
notes: Vec::new(),
|
||||
};
|
||||
let distributions = TaxLine {
|
||||
known: book
|
||||
@@ -537,6 +624,7 @@ fn span(
|
||||
.checked_sub(distributed[i])
|
||||
.ok_or_else(too_large)?,
|
||||
open: std::mem::take(&mut dist_open[i]),
|
||||
notes: Vec::new(),
|
||||
};
|
||||
let known = beginning
|
||||
.known
|
||||
@@ -558,6 +646,7 @@ fn span(
|
||||
.iter()
|
||||
.flat_map(|l| l.open.iter().cloned())
|
||||
.collect(),
|
||||
notes: Vec::new(),
|
||||
};
|
||||
let reconciliation = Reconciliation {
|
||||
beginning: book
|
||||
@@ -601,8 +690,8 @@ mod tests {
|
||||
vec![d("-510.69"), d("-510.68")]
|
||||
);
|
||||
assert_eq!(
|
||||
share(d("25"), &[d("525"), d("525")]).unwrap(),
|
||||
vec![d("12.50"), d("12.50")]
|
||||
share(d("36"), &[d("525"), d("525")]).unwrap(),
|
||||
vec![d("18.00"), d("18.00")]
|
||||
);
|
||||
assert_eq!(
|
||||
share(d("10"), &[d("0"), d("0")]).unwrap(),
|
||||
|
||||
@@ -463,11 +463,11 @@ fn acquisition_links_only_to_a_live_entry_debiting_the_cost_account() {
|
||||
let capital = ledger
|
||||
.add_account("3000", "Capital", AccountType::Equity, None, false)
|
||||
.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 e = l.create_entry(date(2026, 1, 15), "contribution");
|
||||
l.add_line(e, dr, Side::Debit, usd("1650")).unwrap();
|
||||
l.add_line(e, cr, Side::Credit, usd("1650")).unwrap();
|
||||
l.add_line(e, dr, Side::Debit, usd("1480")).unwrap();
|
||||
l.add_line(e, cr, Side::Credit, usd("1480")).unwrap();
|
||||
l.post_entry(e).unwrap();
|
||||
e
|
||||
};
|
||||
|
||||
@@ -1021,6 +1021,22 @@ fn the_registers_book_value_is_what_the_books_carry() {
|
||||
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
|
||||
/// entry crediting the asset's accumulated depreciation by exactly that;
|
||||
/// record_depreciation_run (replay) needs only the asset and entry.
|
||||
|
||||
@@ -14,10 +14,10 @@ use std::str::FromStr;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
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::{
|
||||
AccountId, AccountType, AssetBasis, AssetId, BasisRule, Currency, EntryId, Ledger, Money,
|
||||
NewAsset, Sec704cMethod, Side,
|
||||
NewAsset, NewSchedule, ScheduleConvention, ScheduleKind, Sec704cMethod, Side,
|
||||
};
|
||||
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.net_income.tax(), None, "tax depreciation is the CPA's");
|
||||
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.distributions.tax(), Some(d("300.00")));
|
||||
assert_eq!(row.ending.tax(), None);
|
||||
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.
|
||||
assert_eq!(
|
||||
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
|
||||
/// 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]
|
||||
fn cash_only_books_are_complete() {
|
||||
fn cash_only_books_leave_only_the_tax_income_open() {
|
||||
let mut b = books();
|
||||
let (cash, tobin, morgan, revenue) = (b.cash, b.tobin, b.morgan, b.revenue);
|
||||
post(
|
||||
@@ -323,9 +328,17 @@ fn cash_only_books_are_complete() {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tc.open.is_empty());
|
||||
for row in &tc.rows {
|
||||
assert_eq!(row.ending.tax(), Some(row.book.ending));
|
||||
assert!(row.ending.open.is_empty());
|
||||
for (row, income) in tc.rows.iter().zip(["50.01", "50.00"]) {
|
||||
let income = vec![Open::TaxNetIncome { book: d(income) }];
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -343,9 +356,9 @@ fn property_without_a_basis_is_open_never_guessed() {
|
||||
&mut b.l,
|
||||
date(2026, 1, 5),
|
||||
&[
|
||||
(prepaid, Side::Debit, "25.00"),
|
||||
(tobin, Side::Credit, "12.50"),
|
||||
(morgan, Side::Credit, "12.50"),
|
||||
(prepaid, Side::Debit, "36.00"),
|
||||
(tobin, Side::Credit, "18.00"),
|
||||
(morgan, Side::Credit, "18.00"),
|
||||
],
|
||||
);
|
||||
let tool = post(
|
||||
@@ -379,14 +392,14 @@ fn property_without_a_basis_is_open_never_guessed() {
|
||||
)
|
||||
.unwrap();
|
||||
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.open,
|
||||
vec![
|
||||
Open::PropertyBasis {
|
||||
entry: contract,
|
||||
book: d("12.50")
|
||||
book: d("18.00")
|
||||
},
|
||||
Open::PropertyBasis {
|
||||
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.reconciliation.property_without_basis, d("12.50"));
|
||||
assert_eq!(m.reconciliation.property_without_basis, d("18.00"));
|
||||
for row in &tc.rows {
|
||||
assert_eq!(
|
||||
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());
|
||||
}
|
||||
Reference in new issue
Block a user