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

+183 -102
View File
@@ -4,10 +4,10 @@
//! INVARIANT it checks.
use chrono::NaiveDate;
use ledger_core::{AssetRegister, AssetRegisterRow, TaxBasisRule};
use ledger_core::{AssetRegister, AssetRegisterRow, BasisRule, Sec704cMethod};
use ledger_import::asset_register::{
layout, missing_line, notes, rule_cited, rule_label, rule_mark, template_data,
typeset_asset_register,
FMV_NOT_RECORDED_MARK, layout, method_cited, missing_line, notes, rule_cited, rule_label,
rule_mark, template_data, typeset_asset_register,
};
use ledger_import::report::ReportMeta;
use ledger_import::{PdfError, asset_register_pdf};
@@ -31,68 +31,95 @@ fn meta() -> ReportMeta {
}
}
fn row(id: u64, tag: &str, desc: &str, cost: i64, acc: i64) -> AssetRegisterRow {
/// A row: `scheduled` the schedule's accumulated depreciation, `posted`
/// what the books carry; book value is cost less posted.
fn row(id: u64, tag: &str, desc: &str, cost: i64, scheduled: i64, posted: i64) -> AssetRegisterRow {
AssetRegisterRow {
asset_id: id,
tag: Some(tag.into()),
description: desc.into(),
serial: None,
in_service: d(2026, 1, 15),
acquired_date: d(2026, 1, 15),
in_service_date: d(2026, 1, 15),
cost: cents(cost),
salvage: Decimal::ZERO,
useful_life_months: 36,
accumulated: cents(acc),
book_value: cents(cost - acc),
accumulated_scheduled: cents(scheduled),
accumulated_posted: cents(posted),
book_value: cents(cost - posted),
disposed: None,
tax_rule: None,
contributor_basis: None,
tax_basis: None,
built_in_gain: None,
basis_rule: None,
contributor_adjusted_basis: None,
fmv_at_contribution: None,
fmv_recorded: false,
tax_basis_723: None,
depreciable_basis: None,
built_in_gain_704c: None,
sec704c_1c: false,
sec704c_method: None,
basis_source: None,
}
}
/// Bought: tax basis is cost.
/// Bought: depreciable basis is cost.
fn bought(mut r: AssetRegisterRow) -> AssetRegisterRow {
r.tax_rule = Some(TaxBasisRule::Cost);
r.tax_basis = Some(r.cost);
r.basis_rule = Some(BasisRule::Purchase);
r.depreciable_basis = Some(r.cost);
r
}
/// Contributed under `rule` with the member's basis `basis` (cents); tax
/// basis and built-in gain as ledger_core::Asset computes them.
fn contributed(mut r: AssetRegisterRow, rule: TaxBasisRule, basis: i64) -> AssetRegisterRow {
/// Contributed under `rule` with the member's adjusted basis `basis`
/// (cents) and its value `fmv` (cents; None: recorded before P3, read as
/// the book cost); figures as ledger_core::kernel::basis_figures gives
/// them.
fn contributed(
mut r: AssetRegisterRow,
rule: BasisRule,
basis: i64,
fmv: Option<i64>,
) -> AssetRegisterRow {
let b = cents(basis);
r.tax_rule = Some(rule);
r.contributor_basis = Some(b);
r.tax_basis = Some(match rule {
TaxBasisRule::PersonalUseConversion => b.min(r.cost),
_ => b,
});
r.built_in_gain = Some(r.cost - b);
let v = fmv.map_or(r.cost, cents);
let f = ledger_core::kernel::basis_figures(rule, r.cost, b, v).unwrap();
r.basis_rule = Some(rule);
r.contributor_adjusted_basis = Some(b);
r.fmv_at_contribution = Some(v);
r.fmv_recorded = fmv.is_some();
r.tax_basis_723 = f.tax_basis_723;
r.depreciable_basis = Some(f.depreciable_basis);
r.built_in_gain_704c = f.built_in_gain_704c;
r.sec704c_1c = f.built_in_gain_704c.is_some_and(|g| g < Decimal::ZERO);
r
}
fn register(rows: Vec<AssetRegisterRow>) -> AssetRegister {
let held = || rows.iter().filter(|r| r.disposed.is_none());
let total_cost = held().map(|r| r.cost).sum();
let total_accumulated = held().map(|r| r.accumulated).sum();
let total_tax_basis = held().filter_map(|r| r.tax_basis).sum();
let missing_tax_basis = held().filter(|r| r.tax_basis.is_none()).count();
let total_cost: Decimal = held().map(|r| r.cost).sum();
let total_accumulated_scheduled: Decimal = held().map(|r| r.accumulated_scheduled).sum();
let total_accumulated_posted: Decimal = held().map(|r| r.accumulated_posted).sum();
AssetRegister {
total_tax_basis,
missing_tax_basis,
as_of: d(2026, 10, 15),
total_book_value: total_cost - total_accumulated,
total_depreciable_basis: held().filter_map(|r| r.depreciable_basis).sum(),
total_tax_basis_723: held().filter_map(|r| r.tax_basis_723).sum(),
missing_basis: held().filter(|r| r.depreciable_basis.is_none()).count(),
total_book_value: total_cost - total_accumulated_posted,
ledger_cost: total_cost,
ledger_accumulated: total_accumulated - cents(1234),
ledger_accumulated: total_accumulated_scheduled - cents(1234),
total_cost,
total_accumulated,
total_accumulated_scheduled,
total_accumulated_posted,
rows,
}
}
/// Hand-worked: held cost 2,500.00 + 1,299.00 + 1,750.00 + 800.00 =
/// 6,349.00; scheduled 527.78 + 324.75 = 852.53; posted 500.00 + 324.75 =
/// 824.75; book value 6,349.00 − 824.75 = 5,524.25; §723 basis 900.00 +
/// 2,000.00 = 2,900.00; depreciable basis 2,500.00 + 900.00 + 1,750.00 =
/// 5,150.00; the books carry 852.53 − 12.34 = 840.19 accumulated, so
/// 12.34 isn't yet posted and 840.19 − 824.75 = 15.44 isn't from runs.
fn sample() -> AssetRegister {
let mut disposed = row(3, "FL-0003", "Old monitor", 40000, 0);
let mut disposed = row(3, "FL-0003", "Old monitor", 40000, 0, 0);
disposed.book_value = Decimal::ZERO;
disposed.disposed = Some((d(2026, 9, 30), cents(4500)));
let mut laptop = row(
@@ -101,27 +128,41 @@ fn sample() -> AssetRegister {
"Framework Laptop 13 (Ryzen 7 7840U, 64 GB, 4 TB)",
250000,
52778,
50000,
);
laptop.serial = Some("FRANBMCP0A1234".into());
laptop.salvage = cents(10000);
laptop.acquired_date = d(2026, 1, 10);
// A member's business laptop: carryover basis, built-in gain, its
// value recorded, the traditional method chosen, the facts' source.
let mut air = contributed(
row(2, "FL-0002", "MacBook Air", 129900, 32475, 32475),
BasisRule::Carryover723,
90000,
Some(129900),
);
air.sec704c_method = Some(Sec704cMethod::Traditional);
air.basis_source = Some(ledger_core::settings::Source {
decided_by: "Morgan Ellis".into(),
decided_on: d(2026, 10, 20),
citation: Some("entry 7".into()),
note: None,
});
register(vec![
bought(laptop),
// A member's business laptop: carryover basis, built-in gain.
contributed(
row(2, "FL-0002", "MacBook Air", 129900, 32475),
TaxBasisRule::Carryover,
90000,
),
air,
bought(disposed),
// A member's personal camera: worth less than its basis, so tax
// basis is the fair market value and there's a built-in loss.
// A member's personal camera, recorded before P3 (value read as
// the book cost): worth less than its basis, so the depreciable
// basis is the value and there's a built-in loss.
contributed(
row(4, "FL-0004", "Sony A7 IV camera", 175000, 0),
TaxBasisRule::PersonalUseConversion,
row(4, "FL-0004", "Sony A7 IV camera", 175000, 0, 0),
BasisRule::PersonalUseConversion,
200000,
None,
),
// No tax basis recorded yet.
row(5, "FL-0005", "Standing desk", 80000, 0),
row(5, "FL-0005", "Standing desk", 80000, 0, 0),
])
}
@@ -157,8 +198,10 @@ fn pdf_is_deterministic() {
assert_eq!(a, asset_register_pdf(&r, &meta()).unwrap());
}
/// INVARIANTS 1, 2, 3, 5: header, every asset, the status of each, the
/// register's totals, and the reconciliation with both differences.
/// INVARIANTS 1, 2, 3, 5: header, every asset with its acquired and
/// in-service dates, the status of each, depreciation scheduled and
/// posted, the register's totals, and the reconciliation with its three
/// differences.
#[test]
fn pdf_shows_assets_totals_and_reconciliation() {
let t = all_text(&sample(), &meta());
@@ -166,22 +209,26 @@ fn pdf_shows_assets_totals_and_reconciliation() {
"Fernhill Labs LLC",
"Fixed Asset Register",
"As of October 15, 2026",
"ACQUIRED",
"IN SERVICE",
"DEPRECIATION SCHEDULED",
"DEPRECIATION POSTED",
"FL-0001",
"FRANBMCP0A1234",
"2026-01-10 2026-01-15",
"MacBook Air",
"Active",
"Disposed 2026-09-30, proceeds 45.00",
"Total, assets held",
"6,349.00", // cost held: 2,500.00 + 1,299.00 + 1,750.00 + 800.00
"852.53", // accumulated held
"5,496.47", // book value held: 6,349.00 - 852.53
"$6,349.00 $852.53 $824.75 $5,524.25",
"Cost per register",
"Cost per books",
"Difference (unregistered cost)",
"Accumulated depreciation per schedule",
"Accumulated depreciation per books",
"Difference (not yet posted)",
"12.34",
"Difference (not yet posted) $12.34",
"Accumulated depreciation from the assets' runs",
"Difference (not from runs) $15.44",
"Journal seq 314",
] {
assert!(t.contains(s), "{s:?} missing from {t:?}");
@@ -189,45 +236,58 @@ fn pdf_shows_assets_totals_and_reconciliation() {
// The disposed asset's cost (400.00) is shown but in no total.
assert!(t.contains("400.00"));
let l = layout(&sample());
assert_eq!(l.totals, [cents(634900), cents(85253), cents(549647)]);
assert_eq!(
l.totals,
[cents(634900), cents(85253), cents(82475), cents(552425)]
);
}
/// INVARIANTS 7-9: the tax table shows each rule in short words, the
/// contributor's basis, the tax basis, the built-in gain and loss, the
/// register's tax basis total, the missing line, and the notes with
/// their citations.
/// value at contribution (marked when read as the book cost), the
/// contributor's adjusted basis, the §723 basis, the depreciable basis,
/// the built-in gain and loss, the method and the facts' source under the
/// description, the register's tax totals, the missing line, and the
/// notes with their citations.
#[test]
fn pdf_shows_tax_basis() {
fn pdf_shows_723_and_depreciable_basis() {
let r = sample();
let t = all_text(&r, &meta());
for s in [
"Federal tax basis",
"TAX BASIS RULE",
"CONTRIBUTOR'S BASIS",
"Federal tax figures, at contribution or purchase",
"BASIS RULE",
"VALUE AT CONTRIBUTION",
"CONTRIBUTOR'S ADJUSTED BASIS",
"§723 BASIS",
"DEPRECIABLE BASIS",
"BUILT-IN GAIN (LOSS)",
"Carryover",
"Personal-use conversion",
"900.00", // MacBook: contributor's basis = tax basis
"399.00", // its built-in gain
"2,000.00", // camera: contributor's basis
"1,750.00", // its tax basis: the fair market value
"(250.00)", // its built-in loss: 1,750.00 - 2,000.00
"Not recorded",
"Total tax basis, assets held",
"5,150.00", // 2,500.00 + 900.00 + 1,750.00; the desk has none
// MacBook: cost, value, adjusted basis, §723, depreciable, gain.
"Carryover 2 1,299.00 1,299.00 900.00 900.00 900.00 399.00",
"Traditional (Treas. Reg. §1.704-3(b))",
"Morgan Ellis, 2026-10-20: entry 7",
// Camera: value read as cost (marked), basis 2,000.00, the lesser.
"Personal-use conversion 3 1,750.00 * 1,750.00 2,000.00 2,000.00 1,750.00 (250.00)",
"Standing desk Not recorded 800.00",
"Total, assets held $2,900.00 $5,150.00",
"Tax basis not recorded for 1 asset",
"Notes",
"Book value is the fair market value at contribution",
"IRC §1012(a)",
"Book value is cost less the depreciation the books carry",
"IRC §1012",
"IRC §723",
"IRC §1016(a)",
"Treas. Reg. §1.168(i)-4(b)",
// The PDF wraps "§1.704-3(a)(3)(ii)" between its parentheses.
"Treas. Reg. §1.704-3(a) (3)(ii)",
"IRC §704(c)(1)(C)",
"Value at contribution not recorded as a fact",
] {
assert!(t.contains(s), "{s:?} missing from {t:?}");
}
let l = layout(&r);
assert_eq!(l.tax_total, r.total_tax_basis);
assert_eq!(l.tax_total, cents(515000));
assert_eq!(
l.tax_totals,
[r.total_tax_basis_723, r.total_depreciable_basis]
);
assert_eq!(l.tax_totals, [cents(290000), cents(515000)]);
assert_eq!(
l.missing.as_deref(),
Some("Tax basis not recorded for 1 asset")
@@ -235,10 +295,19 @@ fn pdf_shows_tax_basis() {
assert_eq!(l.notes, notes());
// Each line carries its row's tax fields unchanged.
for (line, row) in l.lines.iter().zip(&r.rows) {
assert_eq!(line.tax_rule, row.tax_rule);
assert_eq!(line.contributor_basis, row.contributor_basis);
assert_eq!(line.tax_basis, row.tax_basis);
assert_eq!(line.built_in_gain, row.built_in_gain);
assert_eq!(line.basis_rule, row.basis_rule);
assert_eq!(line.fmv_at_contribution, row.fmv_at_contribution);
assert_eq!(line.fmv_recorded, row.fmv_recorded);
assert_eq!(
line.contributor_adjusted_basis,
row.contributor_adjusted_basis
);
assert_eq!(line.tax_basis_723, row.tax_basis_723);
assert_eq!(line.depreciable_basis, row.depreciable_basis);
assert_eq!(line.built_in_gain_704c, row.built_in_gain_704c);
assert_eq!(line.sec704c_method, row.sec704c_method.map(method_cited));
assert_eq!(line.accumulated_posted, row.accumulated_posted);
assert_eq!(line.accumulated_scheduled, row.accumulated_scheduled);
}
}
@@ -246,11 +315,11 @@ fn pdf_shows_tax_basis() {
/// (a disposed one without doesn't count), and the plural reads right.
#[test]
fn missing_line_only_when_missing() {
let mut gone = row(1, "FL-0001", "Old monitor", 40000, 0);
let mut gone = row(1, "FL-0001", "Old monitor", 40000, 0, 0);
gone.book_value = Decimal::ZERO;
gone.disposed = Some((d(2026, 9, 30), cents(4500)));
let r = register(vec![bought(row(2, "FL-0002", "Desk", 50000, 0)), gone]);
assert_eq!(r.missing_tax_basis, 0);
let r = register(vec![bought(row(2, "FL-0002", "Desk", 50000, 0, 0)), gone]);
assert_eq!(r.missing_basis, 0);
assert_eq!(layout(&r).missing, None);
assert!(!all_text(&r, &meta()).contains("Tax basis not recorded"));
assert_eq!(missing_line(0), None);
@@ -260,36 +329,46 @@ fn missing_line_only_when_missing() {
);
}
/// INVARIANT 7: the rules in words, short and with their citations.
/// INVARIANT 7: the rules in words, short and with their citations; the
/// methods with their paragraphs.
#[test]
fn rule_words() {
use TaxBasisRule::*;
use BasisRule::*;
assert_eq!(
[Cost, Carryover, PersonalUseConversion].map(rule_label),
["Cost", "Carryover", "Personal-use conversion"]
[Purchase, Carryover723, PersonalUseConversion].map(rule_label),
["Purchase", "Carryover", "Personal-use conversion"]
);
assert_eq!(
[Cost, Carryover, PersonalUseConversion].map(rule_cited),
[Purchase, Carryover723, PersonalUseConversion].map(rule_cited),
[
"Cost",
"Purchase (IRC §1012)",
"Carryover (IRC §723)",
"Personal-use conversion (Treas. Reg. §1.168(i)-4(b))"
]
);
assert_eq!(
Sec704cMethod::ALL.map(method_cited),
[
"Traditional (Treas. Reg. §1.704-3(b))".to_string(),
"Traditional with curative allocations (Treas. Reg. §1.704-3(c))".to_string(),
"Remedial (Treas. Reg. §1.704-3(d))".to_string(),
]
);
}
/// INVARIANT 9: the notes number the rules 1, 2, 3 (the general note
/// first and unnumbered, the built-in gain note 4), each rule's note
/// cites that rule, and the PDF marks each recorded line's rule with its
/// note's number; a line with no rule recorded has no mark.
/// first and unnumbered, the built-in gain note 4, the value read as cost
/// "*"), each rule's note cites that rule, and the PDF marks each
/// recorded line's rule with its note's number; a line with no rule
/// recorded has no mark.
#[test]
fn rule_marks_match_their_notes() {
use TaxBasisRule::*;
let rules = [Cost, Carryover, PersonalUseConversion];
use BasisRule::*;
let rules = [Purchase, Carryover723, PersonalUseConversion];
assert_eq!(rules.map(rule_mark), ["1", "2", "3"]);
let n = notes();
let marks: Vec<&str> = n.iter().map(|n| n.mark.as_str()).collect();
assert_eq!(marks, ["", "1", "2", "3", "4"]);
assert_eq!(marks, ["", "1", "2", "3", "4", FMV_NOT_RECORDED_MARK]);
for rule in rules {
let note = n.iter().find(|n| n.mark == rule_mark(rule)).unwrap();
let cites = format!("{}: ", rule_cited(rule));
@@ -308,15 +387,15 @@ fn rule_marks_match_their_notes() {
// The PDF: the mark right after the rule, the note under its number.
let t = all_text(&r, &meta());
for s in [
"(Ryzen 7 7840U, 64 GB, 4 TB) Cost 1 2,500.00",
"MacBook Air Carryover 2 1,299.00",
"Old monitor Cost 1 400.00",
"(Ryzen 7 7840U, 64 GB, 4 TB) Purchase 1 2,500.00",
"Carryover 2 1,299.00",
"Old monitor Purchase 1 400.00",
"Sony A7 IV camera Personal-use conversion 3 1,750.00",
"Standing desk Not recorded 800.00",
"1 Cost: property the company bought",
"2 Carryover (IRC §723): property a member contributes",
"3 Personal-use conversion (Treas. Reg. §1.168(i)-4(b)): property converted",
"4 Built-in gain (loss):",
"1 Purchase (IRC §1012): property the company bought",
"2 Carryover (IRC §723): the company's basis in property a member contributes",
"3 Personal-use conversion (Treas. Reg. §1.168(i)-4(b)): the §723 basis",
"4 Built-in gain (loss), IRC §704(c):",
] {
assert!(t.contains(s), "{s:?} missing from {t:?}");
}
@@ -334,6 +413,7 @@ fn empty_register_renders() {
"No fixed assets",
"Total, assets held",
"Difference (not yet posted)",
"Difference (not from runs)",
] {
assert!(t.contains(s), "{s:?} missing from {t:?}");
}
@@ -342,7 +422,7 @@ fn empty_register_renders() {
/// INVARIANT 6: markup in every text field prints literally and never runs.
#[test]
fn text_is_data() {
let mut a = row(1, "#FL-*1*", "#panic() *bold* $x$ <l> @r [x] \\", 100, 0);
let mut a = row(1, "#FL-*1*", "#panic() *bold* $x$ <l> @r [x] \\", 100, 0, 0);
a.serial = Some("_u_ // /*".into());
let m = ReportMeta {
company: "#co *Inc*".into(),
@@ -357,7 +437,7 @@ fn text_is_data() {
for s in [
"#panic()", "*", "_", "$", "[", "]", "\\", "#", "<a>", "@a", "//", "/*", "\"", "`",
] {
let mut a = row(1, s, s, 100, 0);
let mut a = row(1, s, s, 100, 0, 0);
a.serial = Some(s.into());
let m = ReportMeta {
company: s.into(),
@@ -379,6 +459,7 @@ fn long_register_paginates_landscape() {
&format!("Asset {i}"),
100_000,
1_000,
1_000,
)
})
.collect();
+185 -118
View File
@@ -4005,7 +4005,7 @@ fn cash_flow_adjustments_appear_only_when_there_are_some() {
assert_eq!(section_lines(&plain, "Noncash")[0].label, "None");
l.register_asset(ledger_core::NewAsset {
description: "Laptop".into(),
purchase_date: d(2026, 1, 3),
in_service_date: d(2026, 1, 3),
cost: usd(900),
salvage_value: usd(0),
useful_life_months: 36,
@@ -4488,56 +4488,87 @@ fn tax_summary_optional_sections() {
/// A register as ledger_core::fixed_asset_register would return it: two
/// assets held, one disposed (shown, in no total), and books that differ
/// from the register on both counts. Tax basis: the laptop a member
/// converted from personal use (worth less than their basis: tax basis is
/// its value, a built-in loss), the desk with none recorded, the monitor
/// a carryover with a built-in gain (disposed, in no total).
/// from the register on all three counts. Tax figures: the laptop a
/// member contributed from personal use (worth less than their basis:
/// depreciable basis is its value, a built-in loss), the desk with none
/// recorded, the monitor a carryover recorded before its value was asked
/// (read as its cost) with a built-in gain (disposed, in no total).
/// Hand-worked totals of the held: cost 2,500.00 + 1,200.00 = 3,700.00;
/// scheduled 527.78 + 250.00 = 777.78; posted 500.00 + 250.00 = 750.00;
/// book value 3,700.00 − 750.00 = 2,950.00; §723 basis 2,800.00;
/// depreciable basis 2,500.00. The books: cost 4,000.00 (300.00
/// unregistered), accumulated 700.00 (77.78 not yet posted, −50.00 not
/// from runs).
fn asset_register_fixture() -> ledger_core::AssetRegister {
use ledger_core::AssetRegisterRow;
let row = |id, tag: Option<&str>, desc: &str, serial: Option<&str>, cost, acc, disposed| {
AssetRegisterRow {
asset_id: id,
tag: tag.map(Into::into),
description: desc.into(),
serial: serial.map(Into::into),
in_service: d(2026, 1, 15),
cost: cents(cost),
salvage: cents(10000),
useful_life_months: 36,
accumulated: cents(acc),
book_value: cents(cost - acc),
disposed,
tax_rule: None,
contributor_basis: None,
tax_basis: None,
built_in_gain: None,
}
};
let taxed = |mut r: AssetRegisterRow, rule, basis: Decimal, tax: Decimal| {
r.tax_rule = Some(rule);
r.contributor_basis = Some(basis);
r.tax_basis = Some(tax);
r.built_in_gain = Some(r.cost - basis);
let row =
|id, tag: Option<&str>, desc: &str, serial: Option<&str>, cost, sched, posted, disposed| {
AssetRegisterRow {
asset_id: id,
tag: tag.map(Into::into),
description: desc.into(),
serial: serial.map(Into::into),
acquired_date: d(2026, 1, 10),
in_service_date: d(2026, 1, 15),
cost: cents(cost),
salvage: cents(10000),
useful_life_months: 36,
accumulated_scheduled: cents(sched),
accumulated_posted: cents(posted),
book_value: cents(cost - posted),
disposed,
basis_rule: None,
contributor_adjusted_basis: None,
fmv_at_contribution: None,
fmv_recorded: false,
tax_basis_723: None,
depreciable_basis: None,
built_in_gain_704c: None,
sec704c_1c: false,
sec704c_method: None,
basis_source: None,
}
};
let taxed = |mut r: AssetRegisterRow, rule, basis: Decimal, fmv: Option<Decimal>| {
let v = fmv.unwrap_or(r.cost);
let f = ledger_core::kernel::basis_figures(rule, r.cost, basis, v).unwrap();
r.basis_rule = Some(rule);
r.contributor_adjusted_basis = Some(basis);
r.fmv_at_contribution = Some(v);
r.fmv_recorded = fmv.is_some();
r.tax_basis_723 = f.tax_basis_723;
r.depreciable_basis = Some(f.depreciable_basis);
r.built_in_gain_704c = f.built_in_gain_704c;
r.sec704c_1c = f.built_in_gain_704c.is_some_and(|g| g < Decimal::ZERO);
r
};
let mut laptop = taxed(
row(
1,
Some("FL-0001"),
"Framework Laptop 13",
Some("FRAN123"),
250000,
52778,
50000,
None,
),
ledger_core::BasisRule::PersonalUseConversion,
cents(280000),
Some(cents(250000)),
);
laptop.sec704c_method = Some(ledger_core::Sec704cMethod::Traditional);
laptop.basis_source = Some(ledger_core::settings::Source {
decided_by: "Tobin Harlow".into(),
decided_on: d(2026, 10, 20),
citation: Some("entry 4".into()),
note: None,
});
ledger_core::AssetRegister {
as_of: d(2026, 10, 15),
rows: vec![
taxed(
row(
1,
Some("FL-0001"),
"Framework Laptop 13",
Some("FRAN123"),
250000,
52778,
None,
),
ledger_core::TaxBasisRule::PersonalUseConversion,
cents(280000),
cents(250000),
),
row(2, None, "Desk", None, 120000, 25000, None),
laptop,
row(2, None, "Desk", None, 120000, 25000, 25000, None),
{
let mut r = row(
3,
@@ -4546,22 +4577,20 @@ fn asset_register_fixture() -> ledger_core::AssetRegister {
None,
40000,
0,
0,
Some((d(2026, 9, 30), cents(4500))),
);
r.book_value = Decimal::ZERO;
taxed(
r,
ledger_core::TaxBasisRule::Carryover,
cents(30000),
cents(30000),
)
taxed(r, ledger_core::BasisRule::Carryover723, cents(30000), None)
},
],
total_cost: cents(370000),
total_accumulated: cents(77778),
total_book_value: cents(292222),
total_tax_basis: cents(250000),
missing_tax_basis: 1,
total_accumulated_scheduled: cents(77778),
total_accumulated_posted: cents(75000),
total_book_value: cents(295000),
total_depreciable_basis: cents(250000),
total_tax_basis_723: cents(280000),
missing_basis: 1,
ledger_cost: cents(400000),
ledger_accumulated: cents(70000),
}
@@ -4581,10 +4610,11 @@ fn text_cell(cells: &BTreeMap<(u32, u32), Cell>, at: (u32, u32)) -> String {
}
}
/// asset_register INVARIANTS 1-3 and 5, read back from the workbook's XML:
/// header, headings, one row per asset in order (disposed marked), the
/// register's own totals, and the reconciliation under Cost and
/// Accumulated depreciation.
/// asset_register INVARIANTS 1-3, 5 and 7-9, read back from the
/// workbook's XML: header, headings, one row per asset in order (disposed
/// marked) with its acquired and in-service dates, depreciation scheduled
/// and posted, its tax figures, method and source; the register's own
/// totals, and the reconciliation's three differences.
#[test]
fn asset_register_xlsx_cells() {
let r = asset_register_fixture();
@@ -4593,75 +4623,99 @@ fn asset_register_xlsx_cells() {
assert_eq!(text_cell(&c, (1, 0)), "Spec Test LLC");
assert_eq!(text_cell(&c, (2, 0)), "Fixed Asset Register");
assert_eq!(text_cell(&c, (3, 0)), "As of October 15, 2026");
let heads: Vec<String> = (0..14).map(|i| text_cell(&c, (5, i))).collect();
let heads: Vec<String> = (0..21).map(|i| text_cell(&c, (5, i))).collect();
assert_eq!(
heads,
[
"TAG",
"DESCRIPTION",
"SERIAL",
"ACQUIRED",
"IN SERVICE",
"COST",
"SALVAGE",
"LIFE (MONTHS)",
"ACCUMULATED DEPRECIATION",
"DEPRECIATION SCHEDULED",
"DEPRECIATION POSTED",
"BOOK VALUE",
"TAX BASIS RULE",
"CONTRIBUTOR'S BASIS",
"TAX BASIS",
"BASIS RULE",
"VALUE AT CONTRIBUTION",
"CONTRIBUTOR'S ADJUSTED BASIS",
"§723 BASIS",
"DEPRECIABLE BASIS",
"BUILT-IN GAIN (LOSS)",
"ALLOCATION METHOD",
"BASIS FACTS FROM",
"VALUE RECORDED",
"STATUS"
]
);
assert_eq!(text_cell(&c, (6, 0)), "FL-0001");
assert_eq!(text_cell(&c, (6, 1)), "Framework Laptop 13");
assert_eq!(text_cell(&c, (6, 2)), "FRAN123");
assert_eq!(text_cell(&c, (6, 3)), "2026-01-15");
assert_eq!(number_cell(&c, (6, 4)), dec("2500.00"));
assert_eq!(number_cell(&c, (6, 5)), dec("100.00"));
assert_eq!(number_cell(&c, (6, 6)), dec("36"));
assert_eq!(number_cell(&c, (6, 7)), dec("527.78"));
assert_eq!(number_cell(&c, (6, 8)), dec("1972.22"));
// Tax basis (INVARIANT 7): rule in words with its citation, the
// contributor's basis, tax basis, built-in loss as a negative.
assert_eq!(text_cell(&c, (6, 3)), "2026-01-10");
assert_eq!(text_cell(&c, (6, 4)), "2026-01-15");
assert_eq!(number_cell(&c, (6, 5)), dec("2500.00"));
assert_eq!(number_cell(&c, (6, 6)), dec("100.00"));
assert_eq!(number_cell(&c, (6, 7)), dec("36"));
assert_eq!(number_cell(&c, (6, 8)), dec("527.78"));
assert_eq!(number_cell(&c, (6, 9)), dec("500.00"));
assert_eq!(number_cell(&c, (6, 10)), dec("2000.00"));
// Tax figures (INVARIANT 7): rule in words with its citation, value,
// adjusted basis, §723 basis, depreciable basis (the lesser), the
// built-in loss as a negative, the method, the source.
assert_eq!(
text_cell(&c, (6, 9)),
text_cell(&c, (6, 11)),
"Personal-use conversion (Treas. Reg. §1.168(i)-4(b))"
);
assert_eq!(number_cell(&c, (6, 10)), dec("2800.00"));
assert_eq!(number_cell(&c, (6, 11)), dec("2500.00"));
assert_eq!(number_cell(&c, (6, 12)), dec("-300.00"));
assert_eq!(text_cell(&c, (6, 13)), "Active");
assert_eq!(number_cell(&c, (6, 12)), dec("2500.00"));
assert_eq!(number_cell(&c, (6, 13)), dec("2800.00"));
assert_eq!(number_cell(&c, (6, 14)), dec("2800.00"));
assert_eq!(number_cell(&c, (6, 15)), dec("2500.00"));
assert_eq!(number_cell(&c, (6, 16)), dec("-300.00"));
assert_eq!(
text_cell(&c, (6, 17)),
"Traditional (Treas. Reg. §1.704-3(b))"
);
assert_eq!(text_cell(&c, (6, 18)), "Tobin Harlow, 2026-10-20: entry 4");
assert_eq!(text_cell(&c, (6, 19)), "Yes");
assert_eq!(text_cell(&c, (6, 20)), "Active");
// The desk: no tag, no serial, no tax basis recorded.
assert_eq!(text_cell(&c, (7, 1)), "Desk");
assert_eq!(number_cell(&c, (7, 4)), dec("1200.00"));
assert_eq!(number_cell(&c, (7, 7)), dec("250.00"));
assert_eq!(number_cell(&c, (7, 8)), dec("950.00"));
assert_eq!(text_cell(&c, (7, 9)), "Not recorded");
for col in 10..=12 {
assert_eq!(number_cell(&c, (7, 5)), dec("1200.00"));
assert_eq!(number_cell(&c, (7, 8)), dec("250.00"));
assert_eq!(number_cell(&c, (7, 9)), dec("250.00"));
assert_eq!(number_cell(&c, (7, 10)), dec("950.00"));
assert_eq!(text_cell(&c, (7, 11)), "Not recorded");
for col in 12..=19 {
assert!(
!c.contains_key(&(7, col)),
"desk has no tax amount in {col}"
"desk has nothing in tax column {col}"
);
}
assert_eq!(text_cell(&c, (7, 13)), "Active");
assert_eq!(text_cell(&c, (7, 20)), "Active");
assert_eq!(text_cell(&c, (8, 0)), "FL-0003");
assert_eq!(text_cell(&c, (8, 9)), "Carryover (IRC §723)");
assert_eq!(number_cell(&c, (8, 10)), dec("300.00"));
assert_eq!(number_cell(&c, (8, 11)), dec("300.00"));
assert_eq!(number_cell(&c, (8, 12)), dec("100.00"));
assert_eq!(text_cell(&c, (8, 11)), "Carryover (IRC §723)");
assert_eq!(number_cell(&c, (8, 12)), dec("400.00"));
assert_eq!(number_cell(&c, (8, 13)), dec("300.00"));
assert_eq!(number_cell(&c, (8, 14)), dec("300.00"));
assert_eq!(number_cell(&c, (8, 15)), dec("300.00"));
assert_eq!(number_cell(&c, (8, 16)), dec("100.00"));
assert_eq!(text_cell(&c, (8, 19)), "No: read as the book cost");
assert_eq!(
text_cell(&c, (8, 13)),
text_cell(&c, (8, 20)),
"Disposed 2026-09-30, proceeds 45.00"
);
assert_eq!(number_cell(&c, (8, 8)), Decimal::ZERO);
assert_eq!(number_cell(&c, (8, 10)), Decimal::ZERO);
// Totals: the register's (held assets only), not a re-sum of the rows.
assert_eq!(text_cell(&c, (9, 0)), "Total, assets held");
assert_eq!(number_cell(&c, (9, 4)), dec("3700.00"));
assert_eq!(number_cell(&c, (9, 7)), dec("777.78"));
assert_eq!(number_cell(&c, (9, 8)), dec("2922.22"));
assert_eq!(number_cell(&c, (9, 11)), dec("2500.00"));
for col in [5, 10, 12] {
assert_eq!(number_cell(&c, (9, 5)), dec("3700.00"));
assert_eq!(number_cell(&c, (9, 8)), dec("777.78"));
assert_eq!(number_cell(&c, (9, 9)), dec("750.00"));
assert_eq!(number_cell(&c, (9, 10)), dec("2950.00"));
assert_eq!(number_cell(&c, (9, 14)), dec("2800.00"));
assert_eq!(number_cell(&c, (9, 15)), dec("2500.00"));
for col in [6, 7, 12, 13, 16] {
assert!(!c.contains_key(&(9, col)), "no total in {col}");
}
// INVARIANT 8: the missing tax basis line.
@@ -4669,19 +4723,27 @@ fn asset_register_xlsx_cells() {
// Reconciliation.
assert_eq!(text_cell(&c, (12, 0)), "Reconciliation to the books");
let recon = [
(13, "Cost per register", 4, "3700.00"),
(14, "Cost per books", 4, "4000.00"),
(15, "Difference (unregistered cost)", 4, "300.00"),
(17, "Accumulated depreciation per schedule", 7, "777.78"),
(18, "Accumulated depreciation per books", 7, "700.00"),
(19, "Difference (not yet posted)", 7, "77.78"),
(13, "Cost per register", 5, "3700.00"),
(14, "Cost per books", 5, "4000.00"),
(15, "Difference (unregistered cost)", 5, "300.00"),
(17, "Accumulated depreciation per schedule", 8, "777.78"),
(18, "Accumulated depreciation per books", 8, "700.00"),
(19, "Difference (not yet posted)", 8, "77.78"),
(
21,
"Accumulated depreciation from the assets' runs",
8,
"750.00",
),
(22, "Accumulated depreciation per books", 8, "700.00"),
(23, "Difference (not from runs)", 8, "-50.00"),
];
for (row, label, col, amt) in recon {
assert_eq!(text_cell(&c, (row, 0)), label);
assert_eq!(number_cell(&c, (row, col)), dec(amt), "{label}");
}
// INVARIANT 9: the notes, numbered as the PDF marks them.
assert_eq!(text_cell(&c, (21, 0)), "Notes");
assert_eq!(text_cell(&c, (25, 0)), "Notes");
let notes = ledger_import::asset_register::notes();
for (i, n) in notes.iter().enumerate() {
let want = if n.mark.is_empty() {
@@ -4689,19 +4751,21 @@ fn asset_register_xlsx_cells() {
} else {
format!("{}. {}", n.mark, n.text)
};
assert_eq!(text_cell(&c, (22 + i as u32, 0)), want);
assert_eq!(text_cell(&c, (26 + i as u32, 0)), want);
}
assert!(
text_cell(&c, (22, 0)).starts_with("Book value is the fair market value at contribution")
text_cell(&c, (26, 0))
.starts_with("Book value is cost less the depreciation the books carry")
);
for (row, start) in [
(23, "1. Cost: "),
(24, "2. Carryover (IRC §723): "),
(27, "1. Purchase (IRC §1012): "),
(28, "2. Carryover (IRC §723): "),
(
25,
29,
"3. Personal-use conversion (Treas. Reg. §1.168(i)-4(b)): ",
),
(26, "4. Built-in gain (loss): "),
(30, "4. Built-in gain (loss), IRC §704(c): "),
(31, "*. Value at contribution not recorded"),
] {
assert!(text_cell(&c, (row, 0)).starts_with(start), "note {start:?}");
}
@@ -4721,16 +4785,16 @@ fn asset_register_xlsx_headings_and_totals_rule() {
let x = ledger_import::asset_register_xlsx(&asset_register_fixture(), &meta()).unwrap();
let c = sheet_cells(&x);
let styles = cell_styles(&x);
let right_aligned = [4, 5, 6, 7, 8, 10, 11, 12];
for col in 0..14 {
let right_aligned = [5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16];
for col in 0..21 {
let right = styles[&(5, col)].halign.as_deref() == Some("right");
assert_eq!(right, right_aligned.contains(&col), "heading column {col}");
}
assert_eq!(text_cell(&c, (9, 0)), "Total, assets held");
assert!(!styles[&(9, 0)].top_border, "the label isn't ruled");
let total = &styles[&(9, 4)];
let total = &styles[&(9, 5)];
assert!(total.top_border, "{total:?}");
for col in 1..14 {
for col in 1..21 {
assert_eq!(
styles.get(&(9, col)),
Some(total),
@@ -4738,7 +4802,7 @@ fn asset_register_xlsx_headings_and_totals_rule() {
);
}
for row in 6..=8 {
for col in 0..14 {
for col in 0..21 {
let ruled = styles.get(&(row, col)).is_some_and(|s| s.top_border);
assert!(!ruled, "asset row {row} column {col}");
}
@@ -4753,15 +4817,18 @@ fn asset_register_xlsx_empty() {
as_of: d(2026, 10, 15),
rows: vec![],
total_cost: Decimal::ZERO,
total_accumulated: Decimal::ZERO,
total_accumulated_scheduled: Decimal::ZERO,
total_accumulated_posted: Decimal::ZERO,
total_book_value: Decimal::ZERO,
total_tax_basis: Decimal::ZERO,
missing_tax_basis: 0,
total_depreciable_basis: Decimal::ZERO,
total_tax_basis_723: Decimal::ZERO,
missing_basis: 0,
ledger_cost: cents(99900),
ledger_accumulated: Decimal::ZERO,
};
let c = sheet_cells(&ledger_import::asset_register_xlsx(&r, &meta()).unwrap());
assert_eq!(number_cell(&c, (7, 11)), Decimal::ZERO);
assert_eq!(number_cell(&c, (7, 14)), Decimal::ZERO);
assert_eq!(number_cell(&c, (7, 15)), Decimal::ZERO);
assert!(
!c.values()
.any(|v| matches!(v, Cell::Text(t) if t.starts_with("Tax basis not recorded"))),
@@ -4769,7 +4836,7 @@ fn asset_register_xlsx_empty() {
);
assert_eq!(text_cell(&c, (6, 0)), "No fixed assets");
assert_eq!(text_cell(&c, (7, 0)), "Total, assets held");
assert_eq!(number_cell(&c, (7, 4)), Decimal::ZERO);
assert_eq!(number_cell(&c, (7, 5)), Decimal::ZERO);
assert_eq!(text_cell(&c, (12, 0)), "Difference (unregistered cost)");
assert_eq!(number_cell(&c, (12, 4)), dec("999.00"));
assert_eq!(number_cell(&c, (12, 5)), dec("999.00"));
}