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
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:
81 files changed
+9391
-2604
No files matched your search
@@ -9,38 +9,46 @@
|
||||
//! of [`crate::xlsx`] for XLSX).
|
||||
//!
|
||||
//! ```text
|
||||
//! GOAL: one list of what the company owns, its cost, depreciation, and
|
||||
//! book value, that a CPA can file, and that shows at a glance
|
||||
//! whether the register and the books agree.
|
||||
//! SOURCE: ledger_core::fixed_asset_register (INVARIANTS 1-4); CONVENTION
|
||||
//! GOAL: one list of what the company owns, its cost, depreciation due and
|
||||
//! posted, and book value, with its tax figures, that a CPA can
|
||||
//! file, and that shows at a glance whether the register and the
|
||||
//! books agree.
|
||||
//! SOURCE: ledger_core::fixed_asset_register (INVARIANTS 1-6); CONVENTION
|
||||
//! (layout, the statements' header and footer).
|
||||
//! INVARIANTS:
|
||||
//! 1. One line per register row, in the register's order, disposed ones
|
||||
//! included and marked "Disposed DATE, proceeds AMOUNT"; held ones
|
||||
//! "Active".
|
||||
//! 2. The totals are the register's own totals (held assets only),
|
||||
//! never re-summed here.
|
||||
//! "Active". Each shows its acquired and in-service dates.
|
||||
//! 2. Accumulated depreciation twice, never confused: scheduled (due by
|
||||
//! the date) and posted (what the books carry); book value is the
|
||||
//! posted figure's. The totals are the register's own (held assets
|
||||
//! only), never re-summed here.
|
||||
//! 3. Reconciliation: cost per register, per books, and their difference
|
||||
//! (unregistered_cost); accumulated depreciation per schedule, per
|
||||
//! books, and their difference (unposted_depreciation), exactly.
|
||||
//! books, and their difference (unposted_depreciation); from the
|
||||
//! assets' runs, and the books' difference from it
|
||||
//! (unattributed_accumulated), exactly.
|
||||
//! 4. An empty register says "No fixed assets" and still shows totals
|
||||
//! and the reconciliation (the books may carry unregistered cost).
|
||||
//! 5. Header and footer as on every statement: company, "Fixed Asset
|
||||
//! Register", "As of DATE"; basis, prepared on, journal seq.
|
||||
//! 6. PDF: deterministic bytes; every text is data, never markup.
|
||||
//! 7. Tax basis beside book value: every line carries its tax basis
|
||||
//! rule (in words: [`rule_label`], [`rule_cited`]), the contributor's
|
||||
//! basis, the tax basis, and the built-in gain (loss), each the
|
||||
//! register row's own (ledger_core::Asset::tax_basis,
|
||||
//! Asset::built_in_gain), never re-derived here; a line with none
|
||||
//! recorded says "Not recorded" and leaves the amounts blank.
|
||||
//! 8. The tax basis total is the register's total_tax_basis; when
|
||||
//! missing_tax_basis > 0 a line says "Tax basis not recorded for N
|
||||
//! asset(s)" (see [`missing_line`]).
|
||||
//! 9. The notes ([`notes`]) say that book value is fair market value at
|
||||
//! contribution and tax basis follows its rule, and cite each rule
|
||||
//! (IRC §1012(a), §723, Treas. Reg. §1.168(i)-4(b), IRC §704(c)).
|
||||
//! The PDF marks each rule in the tax table with its note's number.
|
||||
//! 7. Tax figures beside book value: every line carries its basis rule
|
||||
//! (in words: [`rule_label`], [`rule_cited`]), the value at
|
||||
//! contribution (marked [`FMV_NOT_RECORDED_MARK`] when read as the
|
||||
//! book cost), the contributor's adjusted basis, the §723 basis, the
|
||||
//! depreciable basis, the §704(c) built-in gain (loss), the §704(c)
|
||||
//! method, and who gave the facts, each the register row's own
|
||||
//! (ledger_core::Asset::basis_figures), never re-derived here; a line
|
||||
//! with none recorded says "Not recorded" and leaves them blank.
|
||||
//! 8. The tax totals are the register's total_tax_basis_723 and
|
||||
//! total_depreciable_basis; when missing_basis > 0 a line says "Tax
|
||||
//! basis not recorded for N asset(s)" (see [`missing_line`]).
|
||||
//! 9. The notes ([`notes`]) say what book value and the scheduled figure
|
||||
//! are, that the tax figures are at contribution, and cite each rule
|
||||
//! (IRC §1012(a), §723, §1016(a), Treas. Reg. §1.168(i)-4(b),
|
||||
//! §1.704-3(a)(3)(ii), IRC §704(c)(1)(C)). The PDF marks each rule in
|
||||
//! the tax table with its note's number.
|
||||
//! ```
|
||||
//!
|
||||
//! SOURCE (7-9): docs/spec/rules.md B-25;
|
||||
@@ -49,8 +57,8 @@
|
||||
use crate::pdf::PdfError;
|
||||
use crate::report::{ReportMeta, amount_parts, format_amount, long_date};
|
||||
use crate::xlsx::{Formats, XlsxError, footer, header, number};
|
||||
use ledger_core::TaxBasisRule;
|
||||
use ledger_core::reports::{AssetRegister, AssetRegisterRow};
|
||||
use ledger_core::{BasisRule, Sec704cMethod};
|
||||
use rust_decimal::Decimal;
|
||||
use rust_xlsxwriter::{Format, FormatAlign, Workbook};
|
||||
use std::sync::OnceLock;
|
||||
@@ -62,46 +70,57 @@ use typst_layout::PagedDocument;
|
||||
pub const TITLE: &str = "Fixed Asset Register";
|
||||
|
||||
/// Column headings, left to right.
|
||||
pub const COLUMNS: [&str; 10] = [
|
||||
pub const COLUMNS: [&str; 12] = [
|
||||
"Tag",
|
||||
"Description",
|
||||
"Serial",
|
||||
"Acquired",
|
||||
"In service",
|
||||
"Cost",
|
||||
"Salvage",
|
||||
"Life (months)",
|
||||
"Accumulated depreciation",
|
||||
"Depreciation scheduled",
|
||||
"Depreciation posted",
|
||||
"Book value",
|
||||
"Status",
|
||||
];
|
||||
|
||||
/// The PDF's tax basis table's headings, left to right.
|
||||
pub const TAX_COLUMNS: [&str; 7] = [
|
||||
/// The PDF's tax table's headings, left to right.
|
||||
pub const TAX_COLUMNS: [&str; 9] = [
|
||||
"Tag",
|
||||
"Description",
|
||||
"Tax basis rule",
|
||||
"Basis rule",
|
||||
"Book cost",
|
||||
"Contributor's basis",
|
||||
"Tax basis",
|
||||
"Value at contribution",
|
||||
"Contributor's adjusted basis",
|
||||
"§723 basis",
|
||||
"Depreciable basis",
|
||||
"Built-in gain (loss)",
|
||||
];
|
||||
|
||||
/// The XLSX headings, left to right: the book columns, the tax columns,
|
||||
/// then the status.
|
||||
pub const XLSX_COLUMNS: [&str; 14] = [
|
||||
pub const XLSX_COLUMNS: [&str; 21] = [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -109,42 +128,55 @@ pub const XLSX_COLUMNS: [&str; 14] = [
|
||||
pub const EMPTY: &str = "No fixed assets";
|
||||
|
||||
/// The tax table's title.
|
||||
pub const TAX_TITLE: &str = "Federal tax basis";
|
||||
pub const TAX_TITLE: &str = "Federal tax figures, at contribution or purchase";
|
||||
|
||||
/// The rule cell of an asset with no tax basis recorded.
|
||||
pub const NOT_RECORDED: &str = "Not recorded";
|
||||
|
||||
/// A rule in short words, for the table: "Cost", "Carryover",
|
||||
/// The mark on a value at contribution read as the book cost (recorded
|
||||
/// before the value was a fact): its note is [`notes`]' last.
|
||||
pub const FMV_NOT_RECORDED_MARK: &str = "*";
|
||||
|
||||
/// A rule in short words, for the table: "Purchase", "Carryover",
|
||||
/// "Personal-use conversion".
|
||||
pub fn rule_label(r: TaxBasisRule) -> &'static str {
|
||||
pub fn rule_label(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",
|
||||
}
|
||||
}
|
||||
|
||||
/// A rule in words with its citation: "Cost", "Carryover (IRC §723)",
|
||||
/// "Personal-use conversion (Treas. Reg. §1.168(i)-4(b))".
|
||||
pub fn rule_cited(r: TaxBasisRule) -> &'static str {
|
||||
/// A rule in words with its citation: "Purchase (IRC §1012)", "Carryover
|
||||
/// (IRC §723)", "Personal-use conversion (Treas. Reg. §1.168(i)-4(b))".
|
||||
pub fn rule_cited(r: BasisRule) -> &'static str {
|
||||
match r {
|
||||
TaxBasisRule::Cost => "Cost",
|
||||
TaxBasisRule::Carryover => "Carryover (IRC §723)",
|
||||
TaxBasisRule::PersonalUseConversion => {
|
||||
"Personal-use conversion (Treas. Reg. §1.168(i)-4(b))"
|
||||
}
|
||||
BasisRule::Purchase => "Purchase (IRC §1012)",
|
||||
BasisRule::Carryover723 => "Carryover (IRC §723)",
|
||||
BasisRule::PersonalUseConversion => "Personal-use conversion (Treas. Reg. §1.168(i)-4(b))",
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of a rule's note in [`notes`].
|
||||
pub fn rule_mark(r: TaxBasisRule) -> &'static str {
|
||||
pub fn rule_mark(r: BasisRule) -> &'static str {
|
||||
match r {
|
||||
TaxBasisRule::Cost => "1",
|
||||
TaxBasisRule::Carryover => "2",
|
||||
TaxBasisRule::PersonalUseConversion => "3",
|
||||
BasisRule::Purchase => "1",
|
||||
BasisRule::Carryover723 => "2",
|
||||
BasisRule::PersonalUseConversion => "3",
|
||||
}
|
||||
}
|
||||
|
||||
/// A §704(c) method in words with its paragraph: "Traditional (Treas.
|
||||
/// Reg. §1.704-3(b))", ...
|
||||
pub fn method_cited(m: Sec704cMethod) -> String {
|
||||
let words = match m {
|
||||
Sec704cMethod::Traditional => "Traditional",
|
||||
Sec704cMethod::TraditionalWithCurative => "Traditional with curative allocations",
|
||||
Sec704cMethod::Remedial => "Remedial",
|
||||
};
|
||||
format!("{words} ({})", m.citation())
|
||||
}
|
||||
|
||||
/// One note under the register.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Note {
|
||||
@@ -154,8 +186,9 @@ pub struct Note {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// The notes (INVARIANT 9): book value against tax basis, then each
|
||||
/// rule's citation and the built-in gain (loss).
|
||||
/// The notes (INVARIANT 9): book value and the scheduled figure, the tax
|
||||
/// figures, each rule's citation, the built-in gain (loss), and a value
|
||||
/// read as the book cost.
|
||||
pub fn notes() -> Vec<Note> {
|
||||
let n = |mark: &str, text: &str| Note {
|
||||
mark: mark.to_string(),
|
||||
@@ -164,31 +197,41 @@ pub fn notes() -> Vec<Note> {
|
||||
vec![
|
||||
n(
|
||||
"",
|
||||
"Book value is the fair market value at contribution for property a member contributed, \
|
||||
and the price paid for property the company bought; tax basis follows its rule and can \
|
||||
differ. Depreciation here is book depreciation; tax depreciation starts from the tax \
|
||||
basis and is the CPA's.",
|
||||
"Book value is cost less the depreciation the books carry (posted); scheduled is what \
|
||||
the depreciation schedule says is due by the date, and the difference is depreciation \
|
||||
not yet run. Property a member contributed is on the books at its fair market value \
|
||||
at contribution, property the company bought at its price. Depreciation here is book \
|
||||
depreciation. The tax figures are at contribution (or purchase): tax depreciation, \
|
||||
and what remains of a built-in gain or loss later, are the CPA's.",
|
||||
),
|
||||
n(
|
||||
rule_mark(TaxBasisRule::Cost),
|
||||
"Cost: property the company bought has a basis of its cost (IRC §1012(a)).",
|
||||
rule_mark(BasisRule::Purchase),
|
||||
"Purchase (IRC §1012): property the company bought has a basis of its cost, which tax \
|
||||
depreciation starts from.",
|
||||
),
|
||||
n(
|
||||
rule_mark(TaxBasisRule::Carryover),
|
||||
"Carryover (IRC §723): property a member contributes keeps the member's adjusted \
|
||||
basis at contribution (their cost less any depreciation they took).",
|
||||
rule_mark(BasisRule::Carryover723),
|
||||
"Carryover (IRC §723): the company's basis in property a member contributes is the \
|
||||
member's adjusted basis at contribution (cost plus improvements, less depreciation \
|
||||
allowed or allowable, IRC §1016(a)); tax depreciation starts from it.",
|
||||
),
|
||||
n(
|
||||
rule_mark(TaxBasisRule::PersonalUseConversion),
|
||||
"Personal-use conversion (Treas. Reg. §1.168(i)-4(b)): property converted from \
|
||||
personal use depreciates from the lesser of its fair market value and the member's \
|
||||
adjusted basis at conversion.",
|
||||
rule_mark(BasisRule::PersonalUseConversion),
|
||||
"Personal-use conversion (Treas. Reg. §1.168(i)-4(b)): the §723 basis as for \
|
||||
carryover; tax depreciation starts from the lesser of its fair market value and that \
|
||||
basis at conversion.",
|
||||
),
|
||||
n(
|
||||
"4",
|
||||
"Built-in gain (loss): fair market value at contribution less the contributor's basis \
|
||||
(IRC §704(c)(1)(A)); a built-in loss is taken into account only for the contributing \
|
||||
member (IRC §704(c)(1)(C)).",
|
||||
"Built-in gain (loss), IRC §704(c): fair market value less the §723 basis at \
|
||||
contribution (Treas. Reg. §1.704-3(a)(3)(ii)); a built-in loss counts only for the \
|
||||
contributing member (IRC §704(c)(1)(C)). The method chosen allocates it among the \
|
||||
members (Treas. Reg. §1.704-3(b)-(d)).",
|
||||
),
|
||||
n(
|
||||
FMV_NOT_RECORDED_MARK,
|
||||
"Value at contribution not recorded as a fact: read as the book cost, as it was \
|
||||
recorded before the value was asked for.",
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -213,6 +256,8 @@ pub struct AssetLine {
|
||||
pub description: String,
|
||||
/// Serial number, or empty.
|
||||
pub serial: String,
|
||||
/// Acquired date, ISO 8601.
|
||||
pub acquired: String,
|
||||
/// In-service date, ISO 8601.
|
||||
pub in_service: String,
|
||||
/// Capitalized cost.
|
||||
@@ -222,21 +267,33 @@ pub struct AssetLine {
|
||||
/// Useful life in months.
|
||||
pub life_months: u32,
|
||||
/// Accumulated depreciation by schedule (zero once disposed).
|
||||
pub accumulated: Decimal,
|
||||
/// Book value (zero once disposed).
|
||||
pub accumulated_scheduled: Decimal,
|
||||
/// Accumulated depreciation the books carry (zero once disposed).
|
||||
pub accumulated_posted: Decimal,
|
||||
/// Book value: cost less posted (zero once disposed).
|
||||
pub book_value: Decimal,
|
||||
/// "Active", or "Disposed 2026-09-30, proceeds 450.00".
|
||||
pub status: String,
|
||||
/// Disposed by the date (shown, not in the totals).
|
||||
pub disposed: bool,
|
||||
/// The tax basis rule, if recorded.
|
||||
pub tax_rule: Option<TaxBasisRule>,
|
||||
/// The basis rule, if recorded.
|
||||
pub basis_rule: Option<BasisRule>,
|
||||
/// Fair market 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 contributing member's adjusted basis (a contribution only).
|
||||
pub contributor_basis: Option<Decimal>,
|
||||
/// The depreciable federal tax basis, if recorded.
|
||||
pub tax_basis: Option<Decimal>,
|
||||
pub contributor_adjusted_basis: Option<Decimal>,
|
||||
/// The §723 basis (a contribution only).
|
||||
pub tax_basis_723: Option<Decimal>,
|
||||
/// The depreciable tax basis, if recorded.
|
||||
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, in words with its paragraph, if chosen.
|
||||
pub sec704c_method: Option<String>,
|
||||
/// Who gave the basis facts, when, and on what, if recorded.
|
||||
pub basis_source: Option<String>,
|
||||
}
|
||||
|
||||
/// Which register column a reconciliation amount sits under.
|
||||
@@ -244,7 +301,7 @@ pub struct AssetLine {
|
||||
pub enum ReconColumn {
|
||||
/// Under "Cost".
|
||||
Cost,
|
||||
/// Under "Accumulated depreciation".
|
||||
/// Under the accumulated depreciation columns.
|
||||
Accumulated,
|
||||
}
|
||||
|
||||
@@ -270,17 +327,17 @@ pub struct RegisterLayout {
|
||||
pub period: String,
|
||||
/// One per asset, in register order.
|
||||
pub lines: Vec<AssetLine>,
|
||||
/// Total cost, accumulated depreciation, and book value of the assets
|
||||
/// held.
|
||||
pub totals: [Decimal; 3],
|
||||
/// Total tax basis of the assets held that have one.
|
||||
pub tax_total: Decimal,
|
||||
/// [`missing_line`] of the register's missing_tax_basis.
|
||||
/// Total cost, accumulated depreciation scheduled, posted, and book
|
||||
/// value of the assets held.
|
||||
pub totals: [Decimal; 4],
|
||||
/// Total §723 basis and depreciable basis of the assets held.
|
||||
pub tax_totals: [Decimal; 2],
|
||||
/// [`missing_line`] of the register's missing_basis.
|
||||
pub missing: Option<String>,
|
||||
/// [`notes`].
|
||||
pub notes: Vec<Note>,
|
||||
/// Cost then accumulated depreciation: per register, per books,
|
||||
/// difference.
|
||||
/// Cost, then accumulated depreciation: per register, per books,
|
||||
/// differences.
|
||||
pub reconciliation: Vec<ReconLine>,
|
||||
}
|
||||
|
||||
@@ -293,6 +350,15 @@ fn status(a: &AssetRegisterRow) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Who gave an asset's basis facts: "Morgan Ellis, 2026-10-20: entry 14".
|
||||
pub fn source_text(s: &ledger_core::settings::Source) -> String {
|
||||
let mut t = format!("{}, {}", s.decided_by, s.decided_on);
|
||||
if let Some(c) = &s.citation {
|
||||
t.push_str(&format!(": {c}"));
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// Lay the register out (see the module spec).
|
||||
pub fn layout(r: &AssetRegister) -> RegisterLayout {
|
||||
let recon = |label: &str, amount, column, difference| ReconLine {
|
||||
@@ -311,23 +377,35 @@ pub fn layout(r: &AssetRegister) -> RegisterLayout {
|
||||
tag: a.tag.clone().unwrap_or_default(),
|
||||
description: a.description.clone(),
|
||||
serial: a.serial.clone().unwrap_or_default(),
|
||||
in_service: a.in_service.to_string(),
|
||||
acquired: a.acquired_date.to_string(),
|
||||
in_service: a.in_service_date.to_string(),
|
||||
cost: a.cost,
|
||||
salvage: a.salvage,
|
||||
life_months: a.useful_life_months,
|
||||
accumulated: a.accumulated,
|
||||
accumulated_scheduled: a.accumulated_scheduled,
|
||||
accumulated_posted: a.accumulated_posted,
|
||||
book_value: a.book_value,
|
||||
status: status(a),
|
||||
disposed: a.disposed.is_some(),
|
||||
tax_rule: a.tax_rule,
|
||||
contributor_basis: a.contributor_basis,
|
||||
tax_basis: a.tax_basis,
|
||||
built_in_gain: a.built_in_gain,
|
||||
basis_rule: a.basis_rule,
|
||||
fmv_at_contribution: a.fmv_at_contribution,
|
||||
fmv_recorded: a.fmv_recorded,
|
||||
contributor_adjusted_basis: a.contributor_adjusted_basis,
|
||||
tax_basis_723: a.tax_basis_723,
|
||||
depreciable_basis: a.depreciable_basis,
|
||||
built_in_gain_704c: a.built_in_gain_704c,
|
||||
sec704c_method: a.sec704c_method.map(method_cited),
|
||||
basis_source: a.basis_source.as_ref().map(source_text),
|
||||
})
|
||||
.collect(),
|
||||
totals: [r.total_cost, r.total_accumulated, r.total_book_value],
|
||||
tax_total: r.total_tax_basis,
|
||||
missing: missing_line(r.missing_tax_basis),
|
||||
totals: [
|
||||
r.total_cost,
|
||||
r.total_accumulated_scheduled,
|
||||
r.total_accumulated_posted,
|
||||
r.total_book_value,
|
||||
],
|
||||
tax_totals: [r.total_tax_basis_723, r.total_depreciable_basis],
|
||||
missing: missing_line(r.missing_basis),
|
||||
notes: notes(),
|
||||
reconciliation: vec![
|
||||
recon("Cost per register", r.total_cost, ReconColumn::Cost, false),
|
||||
@@ -340,7 +418,7 @@ pub fn layout(r: &AssetRegister) -> RegisterLayout {
|
||||
),
|
||||
recon(
|
||||
"Accumulated depreciation per schedule",
|
||||
r.total_accumulated,
|
||||
r.total_accumulated_scheduled,
|
||||
ReconColumn::Accumulated,
|
||||
false,
|
||||
),
|
||||
@@ -356,6 +434,24 @@ pub fn layout(r: &AssetRegister) -> RegisterLayout {
|
||||
ReconColumn::Accumulated,
|
||||
true,
|
||||
),
|
||||
recon(
|
||||
"Accumulated depreciation from the assets' runs",
|
||||
r.total_accumulated_posted,
|
||||
ReconColumn::Accumulated,
|
||||
false,
|
||||
),
|
||||
recon(
|
||||
"Accumulated depreciation per books",
|
||||
r.ledger_accumulated,
|
||||
ReconColumn::Accumulated,
|
||||
false,
|
||||
),
|
||||
recon(
|
||||
"Difference (not from runs)",
|
||||
r.unattributed_accumulated(),
|
||||
ReconColumn::Accumulated,
|
||||
true,
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -365,14 +461,15 @@ pub fn layout(r: &AssetRegister) -> RegisterLayout {
|
||||
/// Layout as every statement (crate::xlsx INVARIANT 4): company, title,
|
||||
/// and date on rows 1-3, headings ([`XLSX_COLUMNS`]) on row 5, one row
|
||||
/// per asset from row 6, amounts as numbers in the accounting format, the
|
||||
/// tax rule in words with its citation ([`rule_cited`]); then the totals
|
||||
/// row (double rule) with the tax basis total under Tax basis, the
|
||||
/// missing tax basis line when there is one, an empty row, "Reconciliation
|
||||
/// to the books" and its lines with amounts under Cost and Accumulated
|
||||
/// depreciation, an empty row, and "Notes" with [`notes`].
|
||||
/// basis rule and §704(c) method in words with their citations
|
||||
/// ([`rule_cited`], [`method_cited`]); then the totals row (double rule)
|
||||
/// with the §723 and depreciable basis totals under theirs, the missing
|
||||
/// basis line when there is one, an empty row, "Reconciliation to the
|
||||
/// books" and its lines with amounts under Cost and Depreciation
|
||||
/// scheduled, an empty row, and "Notes" with [`notes`].
|
||||
pub fn asset_register_xlsx(r: &AssetRegister, meta: &ReportMeta) -> Result<Vec<u8>, XlsxError> {
|
||||
const AMOUNTS: [u16; 7] = [4, 5, 7, 8, 10, 11, 12];
|
||||
const TOTALS: [u16; 4] = [4, 7, 8, 11];
|
||||
const AMOUNTS: [u16; 10] = [5, 6, 8, 9, 10, 12, 13, 14, 15, 16];
|
||||
const TOTALS: [u16; 6] = [5, 8, 9, 10, 14, 15];
|
||||
let l = layout(r);
|
||||
let mut workbook = Workbook::new();
|
||||
let sheet = workbook.add_worksheet();
|
||||
@@ -381,7 +478,7 @@ pub fn asset_register_xlsx(r: &AssetRegister, meta: &ReportMeta) -> Result<Vec<u
|
||||
let width = XLSX_COLUMNS.len() as u16;
|
||||
let mut row = header(sheet, &f, meta, &l.title, &l.period, width)?;
|
||||
for (i, h) in XLSX_COLUMNS.iter().enumerate() {
|
||||
let fmt = if AMOUNTS.contains(&(i as u16)) || i == 6 {
|
||||
let fmt = if AMOUNTS.contains(&(i as u16)) || i == 7 {
|
||||
&f.header_right
|
||||
} else {
|
||||
&f.header
|
||||
@@ -405,36 +502,58 @@ pub fn asset_register_xlsx(r: &AssetRegister, meta: &ReportMeta) -> Result<Vec<u
|
||||
sheet.write_string_with_format(row, 0, &a.tag, text)?;
|
||||
sheet.write_string_with_format(row, 1, &a.description, text)?;
|
||||
sheet.write_string_with_format(row, 2, &a.serial, text)?;
|
||||
sheet.write_string_with_format(row, 3, &a.in_service, text)?;
|
||||
number(sheet, row, 4, a.cost, amount)?;
|
||||
number(sheet, row, 5, a.salvage, amount)?;
|
||||
sheet.write_string_with_format(row, 3, &a.acquired, text)?;
|
||||
sheet.write_string_with_format(row, 4, &a.in_service, text)?;
|
||||
number(sheet, row, 5, a.cost, amount)?;
|
||||
number(sheet, row, 6, a.salvage, amount)?;
|
||||
sheet.write_number_with_format(
|
||||
row,
|
||||
6,
|
||||
7,
|
||||
f64::from(a.life_months),
|
||||
&text.clone().set_align(FormatAlign::Right),
|
||||
)?;
|
||||
number(sheet, row, 7, a.accumulated, amount)?;
|
||||
number(sheet, row, 8, a.book_value, amount)?;
|
||||
match a.tax_rule {
|
||||
Some(rule) => sheet.write_string_with_format(row, 9, rule_cited(rule), text)?,
|
||||
number(sheet, row, 8, a.accumulated_scheduled, amount)?;
|
||||
number(sheet, row, 9, a.accumulated_posted, amount)?;
|
||||
number(sheet, row, 10, a.book_value, amount)?;
|
||||
match a.basis_rule {
|
||||
Some(rule) => sheet.write_string_with_format(row, 11, rule_cited(rule), text)?,
|
||||
None => sheet.write_string_with_format(
|
||||
row,
|
||||
9,
|
||||
11,
|
||||
NOT_RECORDED,
|
||||
if a.disposed { text } else { &missing },
|
||||
)?,
|
||||
};
|
||||
for (c, v) in [
|
||||
(10, a.contributor_basis),
|
||||
(11, a.tax_basis),
|
||||
(12, a.built_in_gain),
|
||||
(12, a.fmv_at_contribution),
|
||||
(13, a.contributor_adjusted_basis),
|
||||
(14, a.tax_basis_723),
|
||||
(15, a.depreciable_basis),
|
||||
(16, a.built_in_gain_704c),
|
||||
] {
|
||||
if let Some(v) = v {
|
||||
number(sheet, row, c, v, amount)?;
|
||||
}
|
||||
}
|
||||
sheet.write_string_with_format(row, 13, &a.status, text)?;
|
||||
if let Some(m) = &a.sec704c_method {
|
||||
sheet.write_string_with_format(row, 17, m, text)?;
|
||||
}
|
||||
if let Some(src) = &a.basis_source {
|
||||
sheet.write_string_with_format(row, 18, src, text)?;
|
||||
}
|
||||
if a.fmv_at_contribution.is_some() {
|
||||
sheet.write_string_with_format(
|
||||
row,
|
||||
19,
|
||||
if a.fmv_recorded {
|
||||
"Yes"
|
||||
} else {
|
||||
"No: read as the book cost"
|
||||
},
|
||||
text,
|
||||
)?;
|
||||
}
|
||||
sheet.write_string_with_format(row, 20, &a.status, text)?;
|
||||
row += 1;
|
||||
}
|
||||
sheet.write_string_with_format(row, 0, "Total, assets held", &f.bold)?;
|
||||
@@ -443,7 +562,7 @@ pub fn asset_register_xlsx(r: &AssetRegister, meta: &ReportMeta) -> Result<Vec<u
|
||||
}
|
||||
for (c, d) in TOTALS
|
||||
.into_iter()
|
||||
.zip(l.totals.into_iter().chain([l.tax_total]))
|
||||
.zip(l.totals.into_iter().chain(l.tax_totals))
|
||||
{
|
||||
number(sheet, row, c, d, &f.dollars_total)?;
|
||||
}
|
||||
@@ -457,8 +576,8 @@ pub fn asset_register_xlsx(r: &AssetRegister, meta: &ReportMeta) -> Result<Vec<u
|
||||
row += 1;
|
||||
for line in &l.reconciliation {
|
||||
let col = match line.column {
|
||||
ReconColumn::Cost => 4,
|
||||
ReconColumn::Accumulated => 7,
|
||||
ReconColumn::Cost => 5,
|
||||
ReconColumn::Accumulated => 8,
|
||||
};
|
||||
let (label_fmt, amount_fmt) = if line.difference {
|
||||
(&f.bold, &f.dollars_sum)
|
||||
@@ -488,16 +607,23 @@ pub fn asset_register_xlsx(r: &AssetRegister, meta: &ReportMeta) -> Result<Vec<u
|
||||
(1, 40.0),
|
||||
(2, 20.0),
|
||||
(3, 12.0),
|
||||
(4, 14.0),
|
||||
(5, 12.0),
|
||||
(6, 9.0),
|
||||
(7, 16.0),
|
||||
(8, 14.0),
|
||||
(9, 30.0),
|
||||
(4, 12.0),
|
||||
(5, 14.0),
|
||||
(6, 12.0),
|
||||
(7, 9.0),
|
||||
(8, 16.0),
|
||||
(9, 16.0),
|
||||
(10, 14.0),
|
||||
(11, 14.0),
|
||||
(11, 30.0),
|
||||
(12, 14.0),
|
||||
(13, 34.0),
|
||||
(13, 16.0),
|
||||
(14, 14.0),
|
||||
(15, 14.0),
|
||||
(16, 16.0),
|
||||
(17, 30.0),
|
||||
(18, 30.0),
|
||||
(19, 16.0),
|
||||
(20, 34.0),
|
||||
] {
|
||||
sheet.set_column_width(c, w)?;
|
||||
}
|
||||
@@ -539,27 +665,35 @@ pub fn template_data(r: &AssetRegister, meta: &ReportMeta) -> serde_json::Value
|
||||
"tax_title": TAX_TITLE,
|
||||
"tax_columns": TAX_COLUMNS,
|
||||
"empty": EMPTY,
|
||||
"fmv_mark": FMV_NOT_RECORDED_MARK,
|
||||
"lines": l.lines.iter().map(|a| serde_json::json!({
|
||||
"tag": a.tag,
|
||||
"description": a.description,
|
||||
"serial": a.serial,
|
||||
"acquired": a.acquired,
|
||||
"in_service": a.in_service,
|
||||
"cost": amount(a.cost),
|
||||
"salvage": amount(a.salvage),
|
||||
"life": a.life_months.to_string(),
|
||||
"accumulated": amount(a.accumulated),
|
||||
"accumulated_scheduled": amount(a.accumulated_scheduled),
|
||||
"accumulated_posted": amount(a.accumulated_posted),
|
||||
"book_value": amount(a.book_value),
|
||||
"status": a.status,
|
||||
"disposed": a.disposed,
|
||||
"rule": a.tax_rule.map_or(NOT_RECORDED, rule_label),
|
||||
"rule_mark": a.tax_rule.map_or("", rule_mark),
|
||||
"recorded": a.tax_rule.is_some(),
|
||||
"contributor_basis": maybe(a.contributor_basis),
|
||||
"tax_basis": maybe(a.tax_basis),
|
||||
"built_in_gain": maybe(a.built_in_gain),
|
||||
"rule": a.basis_rule.map_or(NOT_RECORDED, rule_label),
|
||||
"rule_mark": a.basis_rule.map_or("", rule_mark),
|
||||
"recorded": a.basis_rule.is_some(),
|
||||
"fmv_at_contribution": maybe(a.fmv_at_contribution),
|
||||
"fmv_recorded": a.fmv_recorded,
|
||||
"contributor_adjusted_basis": maybe(a.contributor_adjusted_basis),
|
||||
"tax_basis_723": maybe(a.tax_basis_723),
|
||||
"depreciable_basis": maybe(a.depreciable_basis),
|
||||
"built_in_gain": maybe(a.built_in_gain_704c),
|
||||
"method": a.sec704c_method,
|
||||
"source": a.basis_source,
|
||||
})).collect::<Vec<_>>(),
|
||||
"totals": l.totals.iter().map(|d| amount(*d)).collect::<Vec<_>>(),
|
||||
"tax_total": amount(l.tax_total),
|
||||
"tax_totals": l.tax_totals.iter().map(|d| amount(*d)).collect::<Vec<_>>(),
|
||||
"missing": l.missing,
|
||||
"notes": l.notes.iter().map(|n| serde_json::json!({
|
||||
"mark": n.mark,
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
// The fixed asset register: every asset, its cost, depreciation, and book
|
||||
// value, then the totals and the reconciliation to the general ledger,
|
||||
// then each asset's federal tax basis beside its book cost, and the notes
|
||||
// that cite the tax basis rules.
|
||||
// The fixed asset register: every asset, its cost, depreciation scheduled
|
||||
// and posted, and book value, then the totals and the reconciliation to
|
||||
// the general ledger, then each asset's federal tax figures beside its
|
||||
// book cost, and the notes that cite the rules.
|
||||
//
|
||||
// Everything arrives as data in sys.inputs.data (JSON), laid out by
|
||||
// ledger-import::asset_register, with amounts already split into exact
|
||||
// parts. The template only typesets: no arithmetic, and no text is ever
|
||||
// parsed as markup.
|
||||
//
|
||||
// Shape: { company, title, period, footer, columns: [10 headings],
|
||||
// tax_title, tax_columns: [7 headings], empty,
|
||||
// lines: [{ tag, description, serial, in_service, cost, salvage,
|
||||
// life, accumulated, book_value, status, disposed,
|
||||
// rule, rule_mark, recorded, contributor_basis|null,
|
||||
// tax_basis|null, built_in_gain|null }],
|
||||
// totals: [cost, accumulated, book_value], tax_total,
|
||||
// Shape: { company, title, period, footer, columns: [12 headings],
|
||||
// tax_title, tax_columns: [9 headings], empty, fmv_mark,
|
||||
// lines: [{ tag, description, serial, acquired, in_service, cost,
|
||||
// salvage, life, accumulated_scheduled,
|
||||
// accumulated_posted, book_value, status, disposed,
|
||||
// rule, rule_mark, recorded, fmv_at_contribution|null,
|
||||
// fmv_recorded, contributor_adjusted_basis|null,
|
||||
// tax_basis_723|null, depreciable_basis|null,
|
||||
// built_in_gain|null, method|null, source|null }],
|
||||
// totals: [cost, scheduled, posted, book_value],
|
||||
// tax_totals: [tax_basis_723, depreciable_basis],
|
||||
// missing: text|null, notes: [{ mark, text }],
|
||||
// reconciliation: [{ label, amount, difference }] }
|
||||
// where every amount is { digits: "1,234.56", negative }.
|
||||
@@ -66,14 +70,14 @@
|
||||
[#open#sign#a.digits#close]
|
||||
}
|
||||
|
||||
// Columns: 4-8 are numbers (right-aligned).
|
||||
#let numeric = (4, 5, 6, 7, 8)
|
||||
// Columns: 5-10 are numbers (right-aligned).
|
||||
#let numeric = (5, 6, 7, 8, 9, 10)
|
||||
#let ncols = d.columns.len()
|
||||
#let full(body, ..args) = table.cell(colspan: ncols, ..args, body)
|
||||
|
||||
#table(
|
||||
// Number columns are fixed widths (headings wrap); text takes the rest.
|
||||
columns: (auto, 1.4fr, auto, auto, 6.4em, 5.6em, 6em, 7.4em, 6.4em, 1fr),
|
||||
columns: (auto, 1.4fr, auto, auto, auto, 6.2em, 5.6em, 5.6em, 7em, 7em, 6.2em, 1.2fr),
|
||||
stroke: none,
|
||||
inset: (x: 3.5pt, y: 3.4pt),
|
||||
align: (col, row) => (if col in numeric { right } else { left }) + (if row == 0 { bottom } else { horizon }),
|
||||
@@ -95,11 +99,13 @@
|
||||
t(l.tag),
|
||||
t(l.description),
|
||||
t(l.serial),
|
||||
t(l.acquired),
|
||||
t(l.in_service),
|
||||
a(l.cost),
|
||||
a(l.salvage),
|
||||
t(l.life),
|
||||
a(l.accumulated),
|
||||
a(l.accumulated_scheduled),
|
||||
a(l.accumulated_posted),
|
||||
a(l.book_value),
|
||||
t(l.status),
|
||||
)
|
||||
@@ -112,12 +118,13 @@
|
||||
let tot(x) = table.cell(stroke: s, text(weight: "bold", amount(x, currency: true)))
|
||||
let blank = table.cell(stroke: s, [])
|
||||
(
|
||||
table.cell(stroke: s, colspan: 4, text(weight: "bold", "Total, assets held")),
|
||||
table.cell(stroke: s, colspan: 5, text(weight: "bold", "Total, assets held")),
|
||||
tot(d.totals.at(0)),
|
||||
blank,
|
||||
blank,
|
||||
tot(d.totals.at(1)),
|
||||
tot(d.totals.at(2)),
|
||||
tot(d.totals.at(3)),
|
||||
blank,
|
||||
)
|
||||
},
|
||||
@@ -154,16 +161,19 @@
|
||||
)
|
||||
})
|
||||
|
||||
// Federal tax basis, asset by asset, beside the book cost it may differ
|
||||
// from; each rule marked with the number of the note that cites it.
|
||||
#let tax_numeric = (3, 4, 5, 6)
|
||||
// Federal tax figures, asset by asset, beside the book cost they may
|
||||
// differ from; each rule marked with the number of the note that cites
|
||||
// it, a value read as the book cost marked too; under the description,
|
||||
// the §704(c) method and who gave the facts.
|
||||
#let tax_numeric = (3, 4, 5, 6, 7, 8)
|
||||
#let tcols = d.tax_columns.len()
|
||||
#let maybe(x) = if x == none { [] } else { amount(x) }
|
||||
#v(12pt)
|
||||
#text(weight: "semibold", d.tax_title)
|
||||
#v(2pt)
|
||||
#table(
|
||||
columns: (auto, 1fr, auto, 7.4em, 7.4em, 7.4em, 7.4em),
|
||||
// Each amount column wide enough for its heading's longest word.
|
||||
columns: (auto, 1fr, auto, 6.2em, 8.6em, 7.6em, 6.4em, 7.2em, 7.6em),
|
||||
stroke: none,
|
||||
inset: (x: 3.5pt, y: 3.4pt),
|
||||
align: (col, row) => (if col in tax_numeric { right } else { left }) + (if row == 0 { bottom } else { horizon }),
|
||||
@@ -190,13 +200,29 @@
|
||||
} else {
|
||||
text(fill: rgb("#9a5b00"), emph(l.rule))
|
||||
}
|
||||
let fmv = if l.fmv_at_contribution == none {
|
||||
[]
|
||||
} else if l.fmv_recorded {
|
||||
amount(l.fmv_at_contribution)
|
||||
} else {
|
||||
[#super(d.fmv_mark)#amount(l.fmv_at_contribution)]
|
||||
}
|
||||
let about = (l.method, l.source).filter(x => x != none)
|
||||
(
|
||||
t(l.tag),
|
||||
t(l.description),
|
||||
t({
|
||||
l.description
|
||||
for x in about {
|
||||
linebreak()
|
||||
text(6.5pt, fill: quiet, x)
|
||||
}
|
||||
}),
|
||||
t(rule),
|
||||
t(amount(l.cost)),
|
||||
t(maybe(l.contributor_basis)),
|
||||
t(maybe(l.tax_basis)),
|
||||
t(fmv),
|
||||
t(maybe(l.contributor_adjusted_basis)),
|
||||
t(maybe(l.tax_basis_723)),
|
||||
t(maybe(l.depreciable_basis)),
|
||||
t(maybe(l.built_in_gain)),
|
||||
)
|
||||
})
|
||||
@@ -206,8 +232,9 @@
|
||||
let s = (top: firm)
|
||||
let blank = table.cell(stroke: s, [])
|
||||
(
|
||||
table.cell(stroke: s, colspan: 5, text(weight: "bold", "Total tax basis, assets held")),
|
||||
table.cell(stroke: s, text(weight: "bold", amount(d.tax_total, currency: true))),
|
||||
table.cell(stroke: s, colspan: 6, text(weight: "bold", "Total, assets held")),
|
||||
table.cell(stroke: s, text(weight: "bold", amount(d.tax_totals.at(0), currency: true))),
|
||||
table.cell(stroke: s, text(weight: "bold", amount(d.tax_totals.at(1), currency: true))),
|
||||
blank,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
Reference in new issue
Block a user