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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 14:04:12 -06:00
1 parent 3a9aa92315
commit 2f999c6050
81 files changed
+9391 -2604

No files matched your search

+89 -25
View File
@@ -651,6 +651,26 @@ pub fn find(b: &Books) -> Vec<Attention> {
),
});
}
// Accumulated depreciation no asset's runs account for: a hand entry,
// or a run reversed after its asset left the books.
let unattributed = register
.as_ref()
.map_or(Decimal::ZERO, |r| r.unattributed_accumulated());
if !unattributed.is_zero() {
out.push(Attention {
severity: Severity::Problem,
title: format!(
"{} of accumulated depreciation not from any asset's runs",
fmt::dollars(unattributed.abs())
),
detail: "The accumulated depreciation accounts hold depreciation (or take it back) that no registered asset's depreciation run accounts for: an entry made by hand, or a run reversed after its asset was disposed.".into(),
target: Some(Route::Assets),
prompt: format!(
"In wanderbooks, the accumulated depreciation accounts differ by {} from what the assets' depreciation runs put there (books.export fixed_asset_register, unattributed_accumulated). Find the entries behind it in those accounts' registers and tell me how to fix them.",
fmt::dollars(unattributed)
),
});
}
let unregistered = register.map_or(Decimal::ZERO, |r| r.unregistered_cost());
if !unregistered.is_zero() {
let (title, why) = if unregistered > Decimal::ZERO {
@@ -681,16 +701,25 @@ pub fn find(b: &Books) -> Vec<Attention> {
),
});
}
for a in b.ledger.registered_assets().filter(|a| {
a.tag.is_none() && a.status == ledger_core::AssetStatus::Active && a.purchase_date <= today
}) {
let held = |a: &&ledger_core::Asset| {
a.status == ledger_core::AssetStatus::Active
&& b.ledger
.asset_acquired_date(a.id)
.is_some_and(|d| d <= today)
};
for a in b
.ledger
.registered_assets()
.filter(held)
.filter(|a| a.tag.is_none())
{
out.push(Attention {
severity: Severity::Todo,
title: format!("No tag: {}", a.description),
detail: format!(
"Asset {}, in service {}. A tag is the label on the thing itself.",
a.id,
fmt::date(a.purchase_date)
fmt::date(a.in_service_date)
),
target: Some(Route::Asset(a.id)),
prompt: format!(
@@ -700,27 +729,62 @@ pub fn find(b: &Books) -> Vec<Attention> {
});
}
for a in b.ledger.registered_assets().filter(|a| {
a.tax_basis().is_none()
&& a.status == ledger_core::AssetStatus::Active
&& a.purchase_date <= today
}) {
for a in b.ledger.registered_assets().filter(held) {
let name = a.tag.clone().unwrap_or_else(|| format!("asset {}", a.id));
out.push(Attention {
severity: Severity::Todo,
title: format!("No tax basis: {}", b.asset_label(a.id)),
detail: format!(
"Book cost {}, in service {}. The CPA needs its federal tax basis beside its book value.",
fmt::dollars(a.cost.amount),
fmt::date(a.purchase_date)
),
target: Some(Route::Asset(a.id)),
prompt: format!(
"In wanderbooks, record the tax basis of {name} (\"{}\", book cost {}) with books.set_asset_tax_basis. Ask me how the company got it: bought (cost), contributed by a member from business use (carryover, IRC §723: the member's adjusted basis), or converted by a member from personal use (personal-use conversion, Treas. Reg. §1.168(i)-4(b): the member's adjusted basis; the tax basis is the lesser of that and its fair market value). Then show me the asset.",
a.description,
fmt::dollars(a.cost.amount)
),
});
let Some(basis) = &a.basis else {
out.push(Attention {
severity: Severity::Todo,
title: format!("No tax basis: {}", b.asset_label(a.id)),
detail: format!(
"Book cost {}, in service {}. The CPA needs its federal tax basis beside its book value.",
fmt::dollars(a.cost.amount),
fmt::date(a.in_service_date)
),
target: Some(Route::Asset(a.id)),
prompt: format!(
"In wanderbooks, record the tax basis of {name} (\"{}\", book cost {}) with books.set_asset_tax_basis. Ask me how the company got it: bought (purchase), contributed by a member from business use (carryover_723, IRC §723: the member's adjusted basis), or contributed by a member from personal use (personal_use_conversion, Treas. Reg. §1.168(i)-4(b): the member's adjusted basis, depreciated from the lesser of that and its fair market value); for a contribution, its fair market value when contributed, and who says so. Then show me the asset.",
a.description,
fmt::dollars(a.cost.amount)
),
});
continue;
};
if !basis.rule.is_contribution() {
continue;
}
if !a.fmv_is_recorded() {
out.push(Attention {
severity: Severity::Todo,
title: format!("Confirm its value at contribution: {}", b.asset_label(a.id)),
detail: format!(
"Its basis was recorded before the value at contribution was asked, so the books read it as the book cost, {}. The §704(c) built-in gain or loss rests on it.",
fmt::dollars(a.cost.amount)
),
target: Some(Route::Asset(a.id)),
prompt: format!(
"In wanderbooks, {name} (\"{}\") was contributed with its value at contribution not recorded; the books read it as its book cost, {}. Ask me what it was worth when contributed, then record it with books.set_asset_tax_basis (rule {}, contributor_adjusted_basis {}, fmv_at_contribution, and basis_source: who says so, citing the contribution entry). Then show me the asset.",
a.description,
fmt::dollars(a.cost.amount),
basis.rule.key(),
basis
.contributor_adjusted_basis
.map(|v| v.to_string())
.unwrap_or_default()
),
});
}
if a.built_in_gain_704c().is_some_and(|g| !g.is_zero()) && basis.sec704c_method.is_none() {
out.push(Attention {
severity: Severity::Todo,
title: format!("Choose a §704(c) method with the CPA: {}", b.asset_label(a.id)),
detail: "It was contributed with a built-in gain or loss, and no §704(c) method (traditional, traditional with curative allocations, or remedial) is recorded for it.".into(),
target: Some(Route::Asset(a.id)),
prompt: format!(
"In wanderbooks, {name} (\"{}\") has a §704(c) built-in gain or loss and no §704(c) method. Ask me which method the CPA and I chose (traditional, traditional_with_curative, or remedial: Treas. Reg. §1.704-3(b)-(d)), then record it with books.set_asset_tax_basis, keeping its other facts as they are. Then show me the asset.",
a.description
),
});
}
}
use ledger_store::DiskCheck;
@@ -861,7 +925,7 @@ mod tests {
b.post(d(2026, 2, 1), cash, capital, 500_000);
b.s.register_asset(NewAsset {
description: "Laptop".into(),
purchase_date: d(2026, 2, 2),
in_service_date: d(2026, 2, 2),
cost: Money::new(rust_decimal::Decimal::new(100_000, 2), Currency::USD),
salvage_value: Money::new(rust_decimal::Decimal::ZERO, Currency::USD),
useful_life_months: 36,
+22 -25
View File
@@ -1238,11 +1238,11 @@ impl Books {
};
let amount = self.asset_register(through)?.unposted_depreciation();
let mut periods = std::collections::BTreeSet::new();
for a in self
.ledger
.registered_assets()
.filter(|a| a.purchase_date <= through)
{
for a in self.ledger.registered_assets().filter(|a| {
self.ledger
.asset_acquired_date(a.id)
.is_some_and(|d| d <= through)
}) {
if matches!(a.status, AssetStatus::Disposed { date, .. } if date <= through) {
continue;
}
@@ -2091,26 +2091,22 @@ fn asset_trails(ledger: &Ledger, journal: &[JournalLine]) -> HashMap<AssetId, As
continue;
};
t.events.push(i);
match line.line_type.as_str() {
"asset.depreciation_posted" => {
if let (Some(period), Some(entry), Some(amount)) = (
p.get("period").and_then(|v| v.as_str()),
u(p, "entry_id"),
p.get("amount")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<Decimal>().ok()),
) {
t.runs.push((entry, amount));
t.periods.push(period.to_string());
}
if line.line_type == "asset.disposed" {
t.disposal_entry = u(p, "entry_id");
if let Some(a) = asset {
gain_loss_accounts.insert(a, u(p, "gain_loss_account"));
}
"asset.disposed" => {
t.disposal_entry = u(p, "entry_id");
if let Some(a) = asset {
gain_loss_accounts.insert(a, u(p, "gain_loss_account"));
}
}
}
// Every run journaled, reversed or not, in the order recorded: the
// ledger's runs (each asset.depreciation_posted as replay recorded it),
// one reading of the journal, never a second copy read here.
for a in ledger.registered_assets() {
if let Some(t) = out.get_mut(&a.id) {
for run in ledger.depreciation_runs(a.id) {
t.runs.push((run.entry, run.amount));
t.periods.push(run.period.clone());
}
_ => {}
}
}
// The runs in effect, by month; a run reversed (and not re-instated)
@@ -2168,7 +2164,7 @@ fn asset_trails(ledger: &Ledger, journal: &[JournalLine]) -> HashMap<AssetId, As
{
continue;
}
let days = (e.date() - a.purchase_date).num_days().abs();
let days = (e.date() - a.in_service_date).num_days().abs();
if days > 31 {
continue;
}
@@ -2214,7 +2210,8 @@ fn disposal_true_up(
})
.sum();
}
ledger_core::accumulated_depreciation(asset, NaiveDate::MAX).map_or(Decimal::ZERO, |t| t - runs)
ledger_core::accumulated_scheduled_through(asset, NaiveDate::MAX)
.map_or(Decimal::ZERO, |t| t - runs)
}
/// Entry ids a journal line refers to.
+2 -8
View File
@@ -489,7 +489,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
)?;
let asset = s.register_asset(NewAsset {
description: "MacBook Pro 16\"".into(),
purchase_date: laptop_date,
in_service_date: laptop_date,
cost: usd(laptop),
salvage_value: usd(dec(400)),
useful_life_months: 36,
@@ -501,13 +501,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
s.set_asset_tag(asset, "JS-0001")?;
s.set_asset_serial(asset, Some("C02FK3XJQ6L4"))?;
// Bought by the company, so its federal tax basis is its cost.
s.set_asset_tax_basis(
asset,
ledger_core::TaxBasis {
rule: ledger_core::TaxBasisRule::Cost,
contributor_basis: None,
},
)?;
s.set_asset_basis(asset, ledger_core::AssetBasis::purchase())?;
// Prepaid insurance, spread over a year.
let ins_date = day(start + Months::new(2), 15);
+244 -111
View File
@@ -2,11 +2,14 @@
//! asset, reconciled to the books) and one asset completely (its label,
//! its schedule month by month, the entries and documents behind it).
//! Every number comes from `ledger-core` (`fixed_asset_register`,
//! `depreciation_schedule`, `Asset::tax_basis`, `Asset::built_in_gain`);
//! what is posted comes from the journal. Book value and federal tax
//! basis sit side by side and are never confused: book cost is what was
//! paid or, for a member's contribution, its fair market value; tax basis
//! follows its rule (docs/spec/rules.md B-25).
//! `depreciation_schedule`, `Asset::basis_figures`), what is posted too
//! (`Ledger::accumulated_posted`, its runs). Book value is what the books
//! carry (cost less the depreciation posted), never the schedule's figure,
//! which sits beside it as "scheduled" (B-24). Book value and the federal
//! tax figures sit side by side and are never confused: book cost is what
//! was paid or, for a member's contribution, its fair market value; the
//! §723 basis, the depreciable basis and the §704(c) built-in gain follow
//! the basis rule (docs/spec/rules.md B-25).
use super::page;
use crate::app::Shell;
@@ -18,7 +21,9 @@ use crate::widgets;
use chrono::{Datelike, NaiveDate};
use egui::{Color32, CornerRadius, Margin, RichText, Sense, Ui, vec2};
use egui_phosphor::regular as icon;
use ledger_core::{Asset, AssetId, AssetRegister, AssetRegisterRow, AssetStatus, TaxBasisRule};
use ledger_core::{
Asset, AssetId, AssetRegister, AssetRegisterRow, AssetStatus, BasisRule, Sec704cMethod,
};
use rust_decimal::Decimal;
/// The register's date (None: today), kept across navigation.
@@ -52,8 +57,9 @@ pub struct Notice {
/// 2. unposted_depreciation() < 0, unregistered_cost() != 0: a problem
/// each, with the exact amount.
/// 3. Both zero: one note that the register agrees with the books.
/// 4. missing_tax_basis > 0: a problem, last, from
/// [`missing_tax_notice`].
/// 4. unattributed_accumulated() != 0: a problem (accumulated
/// depreciation no asset's runs account for), with the exact amount.
/// 5. missing_basis > 0: a problem, last, from [`missing_tax_notice`].
/// ```
pub fn notices(b: &Books, r: &AssetRegister) -> Vec<Notice> {
let mut out = Vec::new();
@@ -125,7 +131,17 @@ pub fn notices(b: &Books, r: &AssetRegister) -> Vec<Notice> {
),
});
}
if unposted.is_zero() && unregistered.is_zero() {
let unattributed = r.unattributed_accumulated();
if !unattributed.is_zero() {
out.push(Notice {
problem: true,
text: format!(
"{} of accumulated depreciation in the books isn't from any asset's depreciation runs — ask the bookkeeper to find the entries behind it.",
fmt::dollars(unattributed)
),
});
}
if unposted.is_zero() && unregistered.is_zero() && unattributed.is_zero() {
out.push(Notice {
problem: false,
text: format!(
@@ -134,7 +150,7 @@ pub fn notices(b: &Books, r: &AssetRegister) -> Vec<Notice> {
),
});
}
if let Some(text) = missing_tax_notice(r.missing_tax_basis) {
if let Some(text) = missing_tax_notice(r.missing_basis) {
out.push(Notice {
problem: true,
text,
@@ -148,118 +164,141 @@ pub fn notices(b: &Books, r: &AssetRegister) -> Vec<Notice> {
pub fn missing_tax_notice(missing: usize) -> Option<String> {
(missing > 0).then(|| {
format!(
"Tax basis not recorded for {} — ask the bookkeeper to record {} (books.set_asset_tax_basis): bought by the company, contributed by a member from business use, or converted from personal use.",
"Tax basis not recorded for {} — ask the bookkeeper to record {} (books.set_asset_tax_basis): bought by the company, contributed by a member from business use, or contributed from personal use.",
fmt::count(missing, "asset", "assets"),
if missing == 1 { "it" } else { "each" }
)
})
}
/// A tax basis rule for a sentence: "cost", "carryover", "personal-use
/// A basis rule for a sentence: "purchase", "carryover", "personal-use
/// conversion".
pub fn rule_words(r: TaxBasisRule) -> &'static str {
pub fn rule_words(r: BasisRule) -> &'static str {
match r {
TaxBasisRule::Cost => "cost",
TaxBasisRule::Carryover => "carryover",
TaxBasisRule::PersonalUseConversion => "personal-use conversion",
BasisRule::Purchase => "purchase",
BasisRule::Carryover723 => "carryover",
BasisRule::PersonalUseConversion => "personal-use conversion",
}
}
/// What a rule means, in plain words, with the law it comes from.
pub fn rule_explained(r: TaxBasisRule) -> &'static str {
pub fn rule_explained(r: BasisRule) -> &'static str {
match r {
TaxBasisRule::Cost => {
BasisRule::Purchase => {
"Bought by the company: its tax basis is what it paid (IRC §1012(a))."
}
TaxBasisRule::Carryover => {
"Contributed by a member from business use: it keeps the member's adjusted basis — what they paid less any depreciation they took (IRC §723)."
BasisRule::Carryover723 => {
"Contributed by a member from business use: the company's basis is the member's adjusted basis — cost plus improvements, less depreciation allowed or allowable (IRC §723, §1016(a)) — and tax depreciation starts from it."
}
TaxBasisRule::PersonalUseConversion => {
"Contributed by a member from personal use: it depreciates from the lesser of its fair market value and the member's adjusted basis (Treas. Reg. §1.168(i)-4(b))."
BasisRule::PersonalUseConversion => {
"Contributed by a member from personal use: the company's basis is the member's adjusted basis (IRC §723); tax depreciation starts from the lesser of its fair market value and that basis (Treas. Reg. §1.168(i)-4(b))."
}
}
}
/// Book value against tax basis, in one sentence, for an asset under
/// `rule` (or none recorded).
pub fn book_vs_tax(rule: Option<TaxBasisRule>) -> &'static str {
/// Book value against the tax figures, in one sentence, for an asset
/// under `rule` (or none recorded).
pub fn book_vs_tax(rule: Option<BasisRule>) -> &'static str {
match rule {
Some(TaxBasisRule::Cost) => {
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 value is the fair market value at contribution; tax depreciation starts from the tax basis — the CPA's."
"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."
}
}
}
/// An asset's federal tax basis as the viewer shows it: every figure is
/// ledger-core's (`Asset::tax_basis`, `Asset::built_in_gain`), never
/// re-derived.
/// An asset's basis facts and tax figures as the viewer shows them: every
/// figure is ledger-core's (`Asset::basis_figures`), never re-derived.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaxFacts {
/// The rule, if recorded.
pub rule: Option<TaxBasisRule>,
pub rule: Option<BasisRule>,
/// The contributing member's adjusted basis (a contribution only).
pub contributor_basis: Option<Decimal>,
pub contributor_adjusted_basis: Option<Decimal>,
/// The value at contribution (a contribution only).
pub fmv_at_contribution: Option<Decimal>,
/// Whether that value was recorded (false: read as the book cost).
pub fmv_recorded: bool,
/// The §723 basis (a contribution only).
pub tax_basis_723: Option<Decimal>,
/// The depreciable tax basis, if recorded.
pub tax_basis: Option<Decimal>,
pub depreciable_basis: Option<Decimal>,
/// Built-in gain (+) or loss (−) at contribution, if any.
pub built_in_gain: Option<Decimal>,
pub built_in_gain_704c: Option<Decimal>,
/// The §704(c) method, if chosen.
pub sec704c_method: Option<Sec704cMethod>,
}
impl TaxFacts {
/// An asset's.
pub fn of(a: &Asset) -> Self {
let f = a.basis_figures().ok().flatten();
TaxFacts {
rule: a.tax.map(|t| t.rule),
contributor_basis: a.tax.and_then(|t| t.contributor_basis),
tax_basis: a.tax_basis(),
built_in_gain: a.built_in_gain(),
rule: a.basis.as_ref().map(|b| b.rule),
contributor_adjusted_basis: a.basis.as_ref().and_then(|b| b.contributor_adjusted_basis),
fmv_at_contribution: a.fmv_at_contribution(),
fmv_recorded: a.fmv_is_recorded(),
tax_basis_723: f.and_then(|f| f.tax_basis_723),
depreciable_basis: f.map(|f| f.depreciable_basis),
built_in_gain_704c: f.and_then(|f| f.built_in_gain_704c),
sec704c_method: a.basis.as_ref().and_then(|b| b.sec704c_method),
}
}
/// A register row's.
pub fn of_row(x: &AssetRegisterRow) -> Self {
TaxFacts {
rule: x.tax_rule,
contributor_basis: x.contributor_basis,
tax_basis: x.tax_basis,
built_in_gain: x.built_in_gain,
rule: x.basis_rule,
contributor_adjusted_basis: x.contributor_adjusted_basis,
fmv_at_contribution: x.fmv_at_contribution,
fmv_recorded: x.fmv_recorded,
tax_basis_723: x.tax_basis_723,
depreciable_basis: x.depreciable_basis,
built_in_gain_704c: x.built_in_gain_704c,
sec704c_method: x.sec704c_method,
}
}
/// "Carryover · contributor's basis $900.00", or "Tax basis not
/// recorded": the line under an asset in the register.
/// "Carryover · §723 basis $900.00", or "Tax basis not recorded": the
/// line under an asset in the register.
pub fn summary(&self) -> String {
match self.rule {
None => "Tax basis not recorded".into(),
Some(r) => {
let mut s = ledger_import::asset_register::rule_label(r).to_string();
if let Some(b) = self.contributor_basis {
s.push_str(&format!(" · contributor's basis {}", fmt::dollars(b)));
if let Some(b) = self.tax_basis_723 {
s.push_str(&format!(" · §723 basis {}", fmt::dollars(b)));
}
s
}
}
}
/// Everything, with the rule's citation, for a tooltip: "Tax basis
/// $900.00: Carryover (IRC §723); contributor's basis $1,000.00;
/// built-in gain $100.00." or "Tax basis not recorded."
/// Everything, with the rule's citation, for a tooltip: "Depreciable
/// basis $900.00: Carryover (IRC §723); §723 basis $1,000.00; value at
/// contribution $1,100.00; built-in gain $100.00." or "Tax basis not
/// recorded."
pub fn detail(&self) -> String {
let (Some(r), Some(t)) = (self.rule, self.tax_basis) else {
let (Some(r), Some(t)) = (self.rule, self.depreciable_basis) else {
return "Tax basis not recorded.".into();
};
let mut s = format!(
"Tax basis {}: {}",
"Depreciable basis {}: {}",
fmt::dollars(t),
ledger_import::asset_register::rule_cited(r)
);
if let Some(b) = self.contributor_basis {
s.push_str(&format!("; contributor's basis {}", fmt::dollars(b)));
if let Some(b) = self.tax_basis_723 {
s.push_str(&format!("; §723 basis {}", fmt::dollars(b)));
}
if let Some(g) = self.built_in_gain {
if let Some(v) = self.fmv_at_contribution {
s.push_str(&format!("; value at contribution {}", fmt::dollars(v)));
if !self.fmv_recorded {
s.push_str(" (the book cost: not recorded)");
}
}
if let Some(g) = self.built_in_gain_704c {
let (label, amount) = gain_or_loss(g);
s.push_str(&format!(
"; {} {}",
@@ -267,6 +306,12 @@ impl TaxFacts {
fmt::dollars(amount)
));
}
if let Some(m) = self.sec704c_method {
s.push_str(&format!(
"; §704(c) method {}",
ledger_import::asset_register::method_cited(m)
));
}
s.push('.');
s
}
@@ -481,26 +526,31 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
None,
),
1 => (
"Accumulated depreciation",
r.total_accumulated,
"Their book depreciation by schedule through the date.",
None,
"Depreciation posted",
r.total_accumulated_posted,
"The book depreciation the books carry for them on the date: their depreciation runs, less any reversed.",
(r.total_accumulated_scheduled != r.total_accumulated_posted).then(|| {
(
format!("{} scheduled", fmt::dollars(r.total_accumulated_scheduled)),
p.muted,
)
}),
),
2 => (
"Book value",
r.total_book_value,
"Cost less accumulated depreciation.",
"Cost less the depreciation posted: what the books carry.",
None,
),
_ => (
"Tax basis",
r.total_tax_basis,
"The federal tax basis of the assets held that have one recorded: what tax depreciation (the CPA's) starts from.",
(r.missing_tax_basis > 0).then(|| {
"Depreciable tax basis",
r.total_depreciable_basis,
"The federal depreciable basis of the assets held that have one recorded, at contribution or purchase: what tax depreciation (the CPA's) starts from.",
(r.missing_basis > 0).then(|| {
(
format!(
"{} not recorded",
fmt::count(r.missing_tax_basis, "asset", "assets")
fmt::count(r.missing_basis, "asset", "assets")
),
p.warning,
)
@@ -528,13 +578,15 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
widgets::empty(
ui,
icon::PACKAGE,
"Nothing in service yet",
&format!("No asset was in service by {}.", fmt::date(as_of)),
"Nothing acquired yet",
&format!("No asset was acquired by {}.", fmt::date(as_of)),
);
return;
}
let amt = widgets::AMOUNT_MIN + 24.0;
let fixed = 110.0 + 104.0 + amt * 5.0 + 190.0;
// The depreciable basis's heading is the longest.
let wide = amt + 36.0;
let fixed = 110.0 + 104.0 + amt * 4.0 + wide + 190.0;
let w = ui.available_width().max(fixed + 200.0);
widgets::min_width(ui, "asset_register", w, |ui| {
let head = |t: &str, cw: f32, right: bool| (t.to_string(), cw, right, p.muted);
@@ -546,10 +598,10 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
head("Description", 0.0, false),
head("In service", 104.0, false),
head("Cost", amt, true),
head("Accumulated", amt, true),
head("Posted", amt, true),
head("Book value", amt, true),
head("Tax basis", amt, true),
head("Built-in gain", amt, true),
head("Scheduled", amt, true),
head("Depreciable basis", wide, true),
head("Status", 190.0, false),
],
None,
@@ -572,7 +624,16 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
Some(s) => format!("Serial {s} · {}", tax.summary()),
None => tax.summary(),
};
let (tax_text, tax_color) = match tax.tax_basis {
let dates = if x.acquired_date != x.in_service_date {
format!(
"Acquired {}, in service {}.",
fmt::date(x.acquired_date),
fmt::date(x.in_service_date)
)
} else {
format!("Acquired and in service {}.", fmt::date(x.in_service_date))
};
let (tax_text, tax_color) = match tax.depreciable_basis {
_ if disposed => (String::new(), text),
Some(t) => (fmt::money(t), text),
None => ("not recorded".to_string(), p.warning),
@@ -588,17 +649,12 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
if x.tag.is_some() { text } else { p.warning },
),
(x.description.clone(), 0.0, false, text),
(fmt::date(x.in_service), 104.0, false, text),
(fmt::date(x.in_service_date), 104.0, false, text),
(money(x.cost), amt, true, text),
(money(x.accumulated), amt, true, text),
(money(x.accumulated_posted), amt, true, text),
(money(x.book_value), amt, true, text),
(tax_text, amt, true, tax_color),
(
x.built_in_gain.map(money).unwrap_or_default(),
amt,
true,
text,
),
(money(x.accumulated_scheduled), amt, true, p.muted),
(tax_text, wide, true, tax_color),
(status_text(x.disposed), 190.0, false, p.muted),
],
Some((sub.as_str(), p.faint)),
@@ -607,12 +663,12 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
);
let resp = resp.on_hover_text(if disposed {
format!(
"Open this asset (it cost {}; it left the books when disposed). {}",
"Open this asset (it cost {}; it left the books when disposed). {dates} {}",
fmt::dollars(x.cost),
tax.detail()
)
} else {
format!("Open this asset. {}", tax.detail())
format!("Open this asset. {dates} {}", tax.detail())
});
resp.context_menu(|ui| {
if let Some(t) = &x.tag {
@@ -622,8 +678,8 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
widgets::copy_item(ui, "Copy serial number", s);
}
widgets::copy_item(ui, "Copy book value", &x.book_value.to_string());
if let Some(t) = x.tax_basis {
widgets::copy_item(ui, "Copy tax basis", &t.to_string());
if let Some(t) = x.depreciable_basis {
widgets::copy_item(ui, "Copy depreciable basis", &t.to_string());
}
});
if resp.clicked() {
@@ -639,10 +695,15 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
(String::new(), 0.0, false, p.text),
(String::new(), 104.0, false, p.text),
(fmt::money(r.total_cost), amt, true, p.text),
(fmt::money(r.total_accumulated), amt, true, p.text),
(fmt::money(r.total_accumulated_posted), amt, true, p.text),
(fmt::money(r.total_book_value), amt, true, p.text),
(fmt::money(r.total_tax_basis), amt, true, p.text),
(String::new(), amt, true, p.text),
(
fmt::money(r.total_accumulated_scheduled),
amt,
true,
p.muted,
),
(fmt::money(r.total_depreciable_basis), wide, true, p.text),
(String::new(), 190.0, false, p.text),
],
None,
@@ -655,7 +716,7 @@ pub fn show_register(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
ui.add(
egui::Label::new(
RichText::new(
"Cost is what the company paid or, for a member's contribution, its fair market value then; the tax basis follows its rule (under each asset) and can differ. Tax depreciation starts from the tax basis — the CPA's.",
"Book value is cost less the depreciation posted (what the books carry); scheduled is what the schedule says is due by the date. Cost is what the company paid or, for a member's contribution, its fair market value then; the tax figures follow its rule (under each asset), at contribution or purchase, and can differ. Tax depreciation starts from the depreciable basis — the CPA's.",
)
.font(theme::regular(11.5))
.color(p.faint),
@@ -722,7 +783,7 @@ fn reconciliation(ui: &mut Ui, b: &Books, r: &AssetRegister, shell: &mut Shell<'
line(
ui,
"Accumulated depreciation by schedule",
r.total_accumulated,
r.total_accumulated_scheduled,
false,
p.text,
);
@@ -740,6 +801,22 @@ fn reconciliation(ui: &mut Ui, b: &Books, r: &AssetRegister, shell: &mut Shell<'
.is_ok_and(|due| due.amount <= Decimal::ZERO);
let color = if accruing { p.muted } else { diff_color(d) };
line(ui, "Difference (not yet posted)", d, true, color);
ui.add_space(8.0);
line(
ui,
"Accumulated depreciation from the assets' runs",
r.total_accumulated_posted,
false,
p.text,
);
let u = r.unattributed_accumulated();
line(
ui,
"Difference (in the books, not from runs)",
u,
true,
diff_color(u),
);
});
ui.add_space(8.0);
ui.horizontal_wrapped(|ui| {
@@ -1011,7 +1088,7 @@ pub fn show_asset(ui: &mut Ui, b: &Books, id: AssetId, shell: &mut Shell<'_>) {
|_| {},
|ui| {
let end = a
.purchase_date
.in_service_date
.checked_add_months(chrono::Months::new(a.useful_life_months))
.and_then(|d| d.pred_opt());
facts(ui, |ui, k| {
@@ -1024,8 +1101,15 @@ pub fn show_asset(ui: &mut Ui, b: &Books, id: AssetId, shell: &mut Shell<'_>) {
};
widgets::copyable(ui, rt, &d.to_string());
};
if let Some(acquired) = b.ledger.asset_acquired_date(a.id)
&& acquired != a.in_service_date
{
k(ui, "Acquired");
ui.label(fmt::date(acquired));
ui.end_row();
}
k(ui, "In service");
ui.label(fmt::date(a.purchase_date));
ui.label(fmt::date(a.in_service_date));
ui.end_row();
k(ui, "Cost");
money(ui, a.cost.amount, false);
@@ -1047,8 +1131,11 @@ pub fn show_asset(ui: &mut Ui, b: &Books, id: AssetId, shell: &mut Shell<'_>) {
ui.end_row();
match &today {
Ok(Some(r)) => {
k(ui, &format!("Accumulated, {}", fmt::date_short(b.today)));
money(ui, r.accumulated, false);
k(ui, &format!("Posted, {}", fmt::date_short(b.today)));
money(ui, r.accumulated_posted, false);
ui.end_row();
k(ui, &format!("Scheduled, {}", fmt::date_short(b.today)));
money(ui, r.accumulated_scheduled, false);
ui.end_row();
k(ui, "Book value today");
money(ui, r.book_value, true);
@@ -1061,9 +1148,6 @@ pub fn show_asset(ui: &mut Ui, b: &Books, id: AssetId, shell: &mut Shell<'_>) {
ui.end_row();
}
}
k(ui, "Posted in the books");
money(ui, trail.posted(), false);
ui.end_row();
if let AssetStatus::Disposed { date, proceeds } = &a.status {
k(ui, "Disposed");
ui.label(fmt::date(*date));
@@ -1146,7 +1230,9 @@ pub fn show_asset(ui: &mut Ui, b: &Books, id: AssetId, shell: &mut Shell<'_>) {
head("Month", 100.0, false),
head("Amount", amt, true),
head("Accumulated", amt, true),
head("Book value", amt, true),
// The schedule's cost less accumulated, never
// what the books carry (that is above).
head("Remaining", amt, true),
head("", 24.0, false),
head("In the books", 0.0, false),
],
@@ -1372,29 +1458,67 @@ fn tax_card(ui: &mut Ui, a: &Asset) {
k(ui, "Book cost");
money(ui, a.cost.amount, false);
ui.end_row();
if let Some(c) = t.contributor_basis {
k(ui, "Contributor's basis");
if let Some(v) = t.fmv_at_contribution {
k(ui, "Value at contribution");
ui.horizontal(|ui| {
money(ui, v, false);
if !t.fmv_recorded {
ui.label(
RichText::new(
"from book cost, recorded before the value was asked",
)
.font(theme::regular(12.0))
.color(p.warning),
);
}
});
ui.end_row();
}
if let Some(c) = t.contributor_adjusted_basis {
k(ui, "Contributor's adjusted basis");
money(ui, c, false);
ui.end_row();
}
if let Some(tb) = t.tax_basis {
k(ui, "Tax basis");
if let Some(b) = t.tax_basis_723 {
k(ui, "§723 basis");
money(ui, b, false);
ui.end_row();
}
if let Some(tb) = t.depreciable_basis {
k(ui, "Depreciable basis");
money(ui, tb, true);
ui.end_row();
}
if let Some(g) = t.built_in_gain {
if let Some(g) = t.built_in_gain_704c {
let (label, amount) = gain_or_loss(g);
k(ui, label);
k(ui, &format!("{label} §704(c)"));
money(ui, amount, false);
ui.end_row();
}
if rule.is_contribution() {
k(ui, "§704(c) method");
match t.sec704c_method {
Some(m) => {
ui.label(ledger_import::asset_register::method_cited(m));
}
None => {
ui.label(RichText::new("not chosen").color(p.warning));
}
}
ui.end_row();
}
if let Some(src) = a.basis.as_ref().and_then(|b| b.source.as_ref()) {
k(ui, "Facts from");
ui.label(ledger_import::asset_register::source_text(src));
ui.end_row();
}
});
ui.add_space(6.0);
let mut why = rule_explained(rule).to_string();
if t.built_in_gain.is_some_and(|g| g < Decimal::ZERO) {
why.push_str(" A built-in loss is taken into account only for the contributing member (IRC §704(c)(1)(C)).");
} else if t.built_in_gain.is_some_and(|g| g > Decimal::ZERO) {
why.push_str(" IRC §704(c) has the partnership take the built-in gain into account for the contributing member.");
if t.built_in_gain_704c.is_some_and(|g| g < Decimal::ZERO) {
why.push_str(" A built-in loss counts only for the contributing member; for the others the basis is treated as the value at contribution (IRC §704(c)(1)(C)).");
} else if t.built_in_gain_704c.is_some_and(|g| g > Decimal::ZERO) {
why.push_str(" IRC §704(c) has the company take the built-in gain into account for the contributing member.");
}
for line in [why.as_str(), book_vs_tax(Some(rule))] {
ui.add(
@@ -1451,7 +1575,7 @@ mod tests {
.map(|(cost, months)| {
b.s.register_asset(NewAsset {
description: format!("Thing {months}"),
purchase_date: d(2026, 1, 1),
in_service_date: d(2026, 1, 1),
cost: Money::new(cents(cost), Currency::USD),
salvage_value: Money::new(Decimal::ZERO, Currency::USD),
useful_life_months: months,
@@ -1501,17 +1625,26 @@ mod tests {
assert_eq!(none.color, positive);
}
/// One asset's page shows its own book value today, not another's.
/// One asset's page shows its own book value today, not another's:
/// what the books carry (cost less posted: nothing is posted, so its
/// cost), with the schedule's figure beside it, never as book value.
#[test]
fn an_assets_page_shows_its_own_book_value() {
for i in 0..2 {
let (_dir, books, ids) = two_assets(&format!("asset-page-{i}"));
let a = books.ledger.get_asset(ids[i]).unwrap().clone();
let want = ledger_core::book_value(&a, today()).unwrap();
let scheduled = ledger_core::accumulated_scheduled_through(&a, today()).unwrap();
assert!(scheduled > Decimal::ZERO, "the schedule has run ahead");
let (texts, _) = render(books, Route::Asset(ids[i]), |_| {});
assert_eq!(
beside(&texts, "Book value today").text,
fmt::dollars(want),
fmt::dollars(a.cost.amount),
"asset {i}"
);
let label = format!("Scheduled, {}", fmt::date_short(today()));
assert_eq!(
beside(&texts, &label).text,
fmt::dollars(scheduled),
"asset {i}"
);
}
+56 -36
View File
@@ -11,9 +11,8 @@ use crate::theme::{self, pal};
use crate::widgets;
use egui::{CornerRadius, RichText, Sense, Ui, vec2};
use egui_phosphor::regular as icon;
use ledger_core::AssetStatus;
use ledger_core::PeriodStatus;
use ledger_core::assets::{accumulated_depreciation, book_value};
use ledger_core::assets::accumulated_scheduled_through;
use ledger_core::{AssetStatus, PeriodStatus};
use rust_decimal::Decimal;
fn kv(ui: &mut Ui, k: &str, v: &str) {
@@ -356,18 +355,17 @@ fn assets(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
},
|ui| {
for a in &assets {
// What the books carry (runs in effect), so this ties to the
// balance sheet; the schedule may run ahead of the books.
let acc: Decimal = b.asset_trail(a.id).posted();
let bv = a.cost.amount - acc;
let scheduled = accumulated_depreciation(a, b.today);
let _ = book_value;
let dep = a.cost.amount - a.salvage_value.amount;
let frac: f32 = if dep.is_zero() {
1.0
} else {
(acc / dep).try_into().unwrap_or(0.0)
// What the books carry (its runs, less reversals:
// Ledger::accumulated_posted; zero once disposed, when the
// asset left the books), so this ties to the balance
// sheet; the schedule may run ahead of the books.
let disposed = match &a.status {
AssetStatus::Disposed { date, .. } if *date <= b.today => Some(*date),
_ => None,
};
let acc = b.ledger.accumulated_posted(a.id, b.today);
let scheduled = accumulated_scheduled_through(a, b.today);
let dep = a.cost.amount - a.salvage_value.amount;
ui.horizontal_wrapped(|ui| {
if widgets::link(
ui,
@@ -379,8 +377,8 @@ fn assets(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
}
ui.label(
RichText::new(format!(
"bought {} · {} months · {}",
fmt::date(a.purchase_date),
"in service {} · {} months · {}",
fmt::date(a.in_service_date),
a.useful_life_months,
match &a.status {
AssetStatus::Active => "active".to_string(),
@@ -392,29 +390,51 @@ fn assets(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
.color(p.muted),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(
RichText::new(format!("book value {}", fmt::dollars(bv)))
.font(theme::semibold(13.0)),
);
let text = match (&acc, disposed) {
(_, Some(_)) => format!("book value {}", fmt::dollars(Decimal::ZERO)),
(Ok(acc), None) => {
format!("book value {}", fmt::dollars(a.cost.amount - *acc))
}
(Err(e), None) => format!("book value can't be computed: {e}"),
};
ui.label(RichText::new(text).font(theme::semibold(13.0)));
});
});
let note = match scheduled {
Ok(s) if s > acc => {
format!(" · {} more scheduled through today", fmt::dollars(s - acc))
}
Ok(_) => String::new(),
// An earlier version's asset: said, never guessed.
Err(_) => " · its schedule can't be computed".to_string(),
};
progress(
ui,
frac,
&format!(
"{} of {} depreciated{note}",
fmt::dollars(acc),
fmt::dollars(dep)
match (&acc, disposed) {
(_, Some(date)) => progress(
ui,
1.0,
&format!("disposed {}: it left the books", fmt::date(date)),
),
);
(Ok(acc), None) => {
let frac: f32 = if dep.is_zero() {
1.0
} else {
(*acc / dep).try_into().unwrap_or(0.0)
};
let note = match scheduled {
Ok(s) if s > *acc => {
format!(
" · {} more scheduled through today",
fmt::dollars(s - *acc)
)
}
Ok(_) => String::new(),
// An earlier version's asset: said, never guessed.
Err(_) => " · its schedule can't be computed".to_string(),
};
progress(
ui,
frac,
&format!(
"{} of {} depreciated{note}",
fmt::dollars(*acc),
fmt::dollars(dep)
),
);
}
(Err(_), None) => {}
}
ui.add_space(8.0);
}
},
+65 -21
View File
@@ -60,41 +60,80 @@ fn attachment_name(b: &Books, id: u64) -> String {
.unwrap_or_else(|| format!("attachment {id}"))
}
/// "Tax basis of FL-0001: personal-use conversion, contributor's basis
/// $2,000.00 → tax basis $1,700.00". The tax basis is ledger-core's
/// (`Asset::tax_basis`) for the event's rule and basis on the asset's
/// book cost, which never changes once registered.
fn tax_basis_text(b: &Books, asset_id: Option<u64>, p: &Value) -> String {
/// "Tax basis of FL-0001: personal-use conversion, value $1,440.00,
/// contributor's adjusted basis $1,688.40: §723 basis $1,688.40,
/// 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
/// 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.
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 name = match (asset, asset_id) {
(Some(a), _) => a.tag.clone().unwrap_or_else(|| a.description.clone()),
(None, Some(id)) => format!("asset {id}"),
(None, None) => "an asset".to_string(),
};
let rule = ledger_core::TaxBasisRule::parse(s(p, "rule"));
let contributor: Option<rust_decimal::Decimal> = p
.get("contributor_basis")
.and_then(|v| v.as_str())
.and_then(|v| v.parse().ok());
let dec = |k: &str| -> Option<rust_decimal::Decimal> {
p.get(k)
.and_then(|v| v.as_str())
.and_then(|v| v.parse().ok())
};
let rule = if legacy {
ledger_core::BasisRule::parse_legacy(s(p, "rule"))
} else {
ledger_core::BasisRule::parse(s(p, "rule"))
};
let Some(rule) = rule else {
return format!("Tax basis of {name} recorded");
};
let tax = asset.and_then(|a| {
let adjusted = dec(if legacy {
"contributor_basis"
} else {
"contributor_adjusted_basis"
});
let fmv = (!legacy).then(|| dec("fmv_at_contribution")).flatten();
let method = p
.get("sec704c_method")
.and_then(|v| v.as_str())
.and_then(ledger_core::Sec704cMethod::parse);
let figures = asset.and_then(|a| {
let mut a = a.clone();
a.tax = Some(ledger_core::TaxBasis {
a.basis = Some(ledger_core::AssetBasis {
rule,
contributor_basis: contributor,
contributor_adjusted_basis: adjusted,
fmv_at_contribution: fmv,
sec704c_method: method,
source: None,
});
a.tax_basis()
a.basis_figures().ok().flatten()
});
let mut text = format!("Tax basis of {name}: {}", super::assets::rule_words(rule));
if let Some(c) = contributor {
text.push_str(&format!(", contributor's basis {}", fmt::dollars(c)));
if let Some(v) = fmv {
text.push_str(&format!(", value {}", fmt::dollars(v)));
}
match (tax, contributor.is_some()) {
(Some(t), true) => text.push_str(&format!(" → tax basis {}", fmt::dollars(t))),
(Some(t), false) => text.push_str(&format!(", tax basis {}", fmt::dollars(t))),
(None, _) => {}
if let Some(c) = adjusted {
text.push_str(&format!(
", contributor's adjusted basis {}",
fmt::dollars(c)
));
}
if let Some(f) = figures {
let mut parts = Vec::new();
if let Some(t) = f.tax_basis_723 {
parts.push(format!("§723 basis {}", fmt::dollars(t)));
}
parts.push(format!(
"depreciable basis {}",
fmt::dollars(f.depreciable_basis)
));
if let Some(g) = f.built_in_gain_704c {
let (label, amount) = super::assets::gain_or_loss(g);
parts.push(format!("{} {}", label.to_lowercase(), fmt::dollars(amount)));
}
text.push_str(&format!(": {}", parts.join(", ")));
}
if let Some(m) = method {
text.push_str(&format!("; §704(c) method {}", m.key().replace('_', " ")));
}
text
}
@@ -785,7 +824,12 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
}
"asset.tax_basis_set" => ev(
icon::SCALES,
tax_basis_text(b, asset_id, p),
tax_basis_text(b, asset_id, p, true),
asset_id.map(Route::Asset),
),
"asset.basis_set" => ev(
icon::SCALES,
tax_basis_text(b, asset_id, p, false),
asset_id.map(Route::Asset),
),
"asset.corrected" => ev(
+68 -35
View File
@@ -1156,20 +1156,21 @@ fn aging(
fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
let mut t = Table::new(vec![
"Cost".into(),
"Accumulated".into(),
"Scheduled".into(),
"Posted".into(),
"Book value".into(),
"Contributor's basis".into(),
"Tax basis".into(),
"Built-in gain (loss)".into(),
// Short enough for the columns: the note says what each is.
"§723 basis".into(),
"Depr. basis".into(),
"Built-in gain".into(),
]);
let r = match b.asset_register(as_of) {
Ok(r) => r,
Err(e) => return cant_compute(t, &e),
};
let none3 = || vec![Cell::none(), Cell::none(), Cell::none()];
if r.rows.is_empty() {
t.note(format!(
"No fixed asset was in service by {}.",
"No fixed asset was acquired by {}.",
fmt::date(as_of)
));
return t;
@@ -1184,7 +1185,10 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
if let Some(s) = &x.serial {
detail.push(format!("serial {s}"));
}
detail.push(format!("in service {}", fmt::date(x.in_service)));
if x.acquired_date != x.in_service_date {
detail.push(format!("acquired {}", fmt::date(x.acquired_date)));
}
detail.push(format!("in service {}", fmt::date(x.in_service_date)));
detail.push(fmt::count(x.useful_life_months as usize, "month", "months"));
if !x.salvage.is_zero() {
detail.push(format!("salvage {}", fmt::dollars(x.salvage)));
@@ -1197,6 +1201,22 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
),
None => "tax basis not recorded".into(),
});
if let Some(v) = tax.fmv_at_contribution {
detail.push(if tax.fmv_recorded {
format!("value at contribution {}", fmt::dollars(v))
} else {
format!(
"value at contribution {} (the book cost: not recorded)",
fmt::dollars(v)
)
});
}
if let Some(m) = tax.sec704c_method {
detail.push(format!(
"§704(c) method: {}",
ledger_import::asset_register::method_cited(m)
));
}
let opt = |v: Option<Decimal>| Cell {
value: v,
drill: None,
@@ -1208,15 +1228,16 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
super::assets::status_text(x.disposed),
fmt::dollars(x.cost)
));
[none3(), none3()].concat()
vec![Cell::none(); 7]
}
None => vec![
Cell::of(x.cost),
Cell::of(x.accumulated),
Cell::of(x.accumulated_scheduled),
Cell::of(x.accumulated_posted),
Cell::of(x.book_value),
opt(tax.contributor_basis),
opt(tax.tax_basis),
opt(tax.built_in_gain),
opt(tax.tax_basis_723),
opt(tax.depreciable_basis),
opt(tax.built_in_gain_704c),
],
};
t.rows.push(Row {
@@ -1231,10 +1252,11 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
"Total held",
vec![
Cell::of(r.total_cost),
Cell::of(r.total_accumulated),
Cell::of(r.total_accumulated_scheduled),
Cell::of(r.total_accumulated_posted),
Cell::of(r.total_book_value),
Cell::none(),
Cell::of(r.total_tax_basis),
Cell::of(r.total_tax_basis_723),
Cell::of(r.total_depreciable_basis),
Cell::none(),
],
);
@@ -1270,6 +1292,10 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
value: Some(r.ledger_accumulated),
drill: accum_drill,
},
Cell {
value: Some(r.ledger_accumulated),
drill: accum_drill,
},
Cell::of(r.ledger_cost - r.ledger_accumulated),
Cell::none(),
Cell::none(),
@@ -1282,7 +1308,8 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
vec![
Cell::of(-r.unregistered_cost()),
Cell::of(r.unposted_depreciation()),
Cell::of(-r.unregistered_cost() - r.unposted_depreciation()),
Cell::of(-r.unattributed_accumulated()),
Cell::of(r.total_book_value - (r.ledger_cost - r.ledger_accumulated)),
Cell::none(),
Cell::none(),
Cell::none(),
@@ -1292,7 +1319,7 @@ fn asset_register(b: &Books, as_of: NaiveDate) -> Table {
t.note(n.text);
}
t.note(
"Cost is what the company paid or, for a member's contribution, its fair market value then; the tax basis follows its rule and can differ. Built-in gain (loss) is that value less the contributor's basis (IRC §704(c)). Tax depreciation starts from the tax basis — the CPA's.",
"Book value is cost less the depreciation posted (what the books carry); scheduled is what the schedule says is due by the date. Cost is what the company paid or, for a member's contribution, its fair market value then; the tax figures follow its rule and can differ: the §723 basis is the member's adjusted basis (IRC §723), the depreciable basis (Depr. basis) what tax depreciation starts from, and the built-in gain, a loss in parentheses, the value less the §723 basis (IRC §704(c)). The tax figures are at contribution or purchase; tax depreciation is the CPA's.",
);
t
}
@@ -2389,10 +2416,11 @@ mod tests {
}
}
/// The fixed asset register report's tax columns are the register's:
/// each held asset's contributor's basis, tax basis, and built-in gain
/// (ledger-core's Asset::tax_basis and built_in_gain), the total its
/// total_tax_basis, and its rule (cited) in the detail.
/// The fixed asset register report's columns are the register's: each
/// held asset's cost, depreciation scheduled and posted, book value,
/// §723 basis, depreciable basis and built-in gain (ledger-core's
/// Asset::basis_figures), the totals its totals, and its rule (cited)
/// in the detail.
#[test]
fn asset_register_report_carries_tax_basis() {
let today = NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
@@ -2407,21 +2435,23 @@ mod tests {
.unwrap();
let r = ledger_core::fixed_asset_register(&b.ledger, today).unwrap();
assert!(!r.rows.is_empty(), "the demo has an asset");
assert_eq!(r.missing_tax_basis, 0, "the demo's asset has a tax basis");
assert_eq!(r.missing_basis, 0, "the demo's asset has a tax basis");
let t = asset_register(&b, today);
assert_eq!(
t.columns,
[
"Cost",
"Accumulated",
"Scheduled",
"Posted",
"Book value",
"Contributor's basis",
"Tax basis",
"Built-in gain (loss)"
"§723 basis",
"Depr. basis",
"Built-in gain"
]
);
for x in &r.rows {
let a = b.ledger.get_asset(x.asset_id).unwrap();
let f = a.basis_figures().unwrap().unwrap();
let row = t
.rows
.iter()
@@ -2432,22 +2462,25 @@ mod tests {
v,
[
Some(x.cost),
Some(x.accumulated),
Some(x.accumulated_scheduled),
Some(x.accumulated_posted),
Some(x.book_value),
a.tax.and_then(|t| t.contributor_basis),
a.tax_basis(),
a.built_in_gain(),
f.tax_basis_723,
Some(f.depreciable_basis),
f.built_in_gain_704c,
]
);
assert!(row.detail.contains("tax basis: Cost"), "{}", row.detail);
assert_eq!(x.book_value, x.cost - x.accumulated_posted);
assert!(row.detail.contains("tax basis: Purchase"), "{}", row.detail);
}
let total = t.rows.iter().find(|row| row.label == "Total held").unwrap();
assert_eq!(total.cells[4].value, Some(r.total_tax_basis));
assert_eq!(total.cells[4].value, r.rows[0].tax_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[2].value, Some(r.total_accumulated_posted));
assert!(
t.rows
.iter()
.all(|row| row.cells.is_empty() || row.cells.len() == 6)
.all(|row| row.cells.is_empty() || row.cells.len() == 7)
);
assert!(
t.rows
@@ -2527,7 +2560,7 @@ mod tests {
b.post_as("Bank loan", d(2026, 3, 27), cash, loan, 250_000);
b.s.register_asset(ledger_core::NewAsset {
description: "Tablet".into(),
purchase_date: d(2026, 3, 27),
in_service_date: d(2026, 3, 27),
cost: Money::new(Decimal::new(63_000, 2), ledger_core::Currency::USD),
salvage_value: Money::new(Decimal::ZERO, ledger_core::Currency::USD),
useful_life_months: 36,
+1 -1
View File
@@ -339,7 +339,7 @@ pub fn search(b: &Books, query: &str) -> Vec<Item> {
if !serial.is_empty() {
detail.push(format!("serial {serial}"));
}
detail.push(format!("in service {}", fmt::date(a.purchase_date)));
detail.push(format!("in service {}", fmt::date(a.in_service_date)));
out.push(Item {
icon: icon::PACKAGE,
title: b.asset_label(a.id),