P3 review: asset bases and the tax-basis K-1
Schema 13 is as shipped and the asset columns are schema 14 (shipped migrations pinned by hash); the tax-basis K-1 leaves net income open (only the return has it) and takes a contributed prepaid plan at its book amount as its section 723 basis; a disposal of contributed property names its 704(c) gain or loss; aliases compare by meaning; fixture amounts made up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
24 files changed
+860
-127
No files matched your search
@@ -4,4 +4,4 @@
|
||||
- Never file the package, or a file from it, into the books.
|
||||
- tin_gaps lists each member or payee whose TIN it lacks and why: collect them (books.set_tin, books.set_w9) and export again.
|
||||
- Everything in it is as of its as_of: the 1099 worksheet counts the year's payments through that day, and general-journal.iif (the QuickBooks Desktop file) every posted entry from the first through it, closing entries left out. That file is plain ASCII within QuickBooks Desktop's limits: warnings (and the README) say what it writes differently from the books ("Café" as "Cafe", a name cut to 31 characters, a subaccount of another type at the top level, the memos grouped); tell the human. It names no customer or vendor: the README lists the entries on accounts receivable or payable, where QuickBooks may ask the CPA for one.
|
||||
- With members, the K-1 worksheet is two files: k1-members.csv (each member's capital on the books, with their TIN) and k1-tax-basis-capital.csv (the same capital on the tax basis, as Schedule K-1 Item L reports it: property contributed at the member's adjusted basis, IRC §723, and the reconciliation from book to tax capital). A line the books can't know (tax depreciation, the basis of property no asset records) is left open with what the books have: tell the human the CPA supplies it; never fill it in.
|
||||
- With members, the K-1 worksheet is two files: k1-members.csv (each member's capital on the books, with their TIN) and k1-tax-basis-capital.csv (the same capital on the tax basis, as Schedule K-1 Item L reports it: property contributed at the member's adjusted basis, IRC §723, a prepaid contract handed over at its book amount for the CPA to confirm, and the reconciliation from book to tax capital). A line the books can't know (the share of income as figured for tax, always; tax depreciation; a disposal's tax gain; the basis of property no asset records) is left open with what the books have: tell the human the CPA supplies it; never fill it in.
|
||||
@@ -6,5 +6,5 @@
|
||||
- Two dates: the in-service date (in_service_date: the day it is ready and available for its use) starts depreciation; the acquired date is the date of the entry that put it on the books (its purchase or a member's contribution: give it as acquisition_entry, dated on or before the in-service date), and puts it on the register. Something bought before it is used is on the register from the purchase and depreciates from the day it serves.
|
||||
- Two accumulated figures, never confused: what the books carry (accumulated_posted: its depreciation runs, less any reversed) and what the schedule says is due by a date (accumulated_scheduled_through). Book value is cost less what the books carry; the difference is depreciation not yet run.
|
||||
- Every asset gets a tag for the label on the thing itself (books.register_asset generates the next one, like FA-0001, unless the human has their own); record the manufacturer's serial number too, change either with books.label_asset, and print label sheets with books.asset_labels (give the human the PDF; print at 100% on Avery 5160/8160). Asset tools take the tag in place of the id.
|
||||
- Record every asset's tax basis beside its book value (books.set_asset_tax_basis, or basis_rule on books.register_asset): purchase for something bought (basis is its cost); for a member's contribution, carryover_723 (their business property) or personal_use_conversion (their personal property), with the member's adjusted basis (cost plus improvements, less depreciation allowed or allowable) and its fair market value when contributed: ask the human for both and who says so (basis_source), never assume the value is the cost. Three figures follow, each named for what it is: the §723 basis (the member's adjusted basis), the depreciable basis (that basis; for personal use, the lesser of it and the value), and the §704(c) built-in gain or loss (value less §723 basis; a loss counts only for the contributing member). They are at contribution; tax depreciation, and what remains of the gain or loss later, are the CPA's. Record the §704(c) method (traditional, traditional_with_curative, remedial) once chosen with the CPA. Book value stays fair market value.
|
||||
- Record every asset's tax basis beside its book value (books.set_asset_tax_basis, or basis_rule on books.register_asset): purchase for something bought (basis is its cost); for a member's contribution, carryover_723 (their business property) or personal_use_conversion (their personal property), with the member's adjusted basis (cost plus improvements, less depreciation allowed or allowable) and its fair market value when contributed: ask the human for both and who says so (basis_source), never assume the value is the cost. Three figures follow, each named for what it is: the §723 basis (the member's adjusted basis), the depreciable basis (that basis; for personal use, the lesser of it and the value), and the §704(c) built-in gain or loss (value less §723 basis; a loss counts only for the contributing member). They are at contribution; tax depreciation, and what remains of the gain or loss later, are the CPA's. Record the §704(c) method (traditional, traditional_with_curative, remedial) once chosen with the CPA. The book cost of a contribution is its fair market value at contribution; book value is that cost less the depreciation posted.
|
||||
- books.export fixed_asset_register is the register for the CPA (posted and scheduled, every tax figure, reconciled to the books); the CPA package's k1-tax-basis-capital.csv carries each member's capital on the tax basis beside the book figures, with what only the CPA can supply left open. books.show asset opens one in the viewer.
|
||||
@@ -1438,16 +1438,19 @@ fn settings_problems(ledger: &Ledger, cfg: &Settings) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// A parameter and its deprecated alias (one release): the value given,
|
||||
/// noting when it came by the alias; both given and different is refused.
|
||||
/// noting when it came by the alias; both given and meaning different
|
||||
/// values (by `same`: an amount, a day or a rule however written) is
|
||||
/// refused.
|
||||
fn aliased<'a>(
|
||||
name: &str,
|
||||
value: Option<&'a str>,
|
||||
alias: &str,
|
||||
old: Option<&'a str>,
|
||||
notes: &mut Vec<String>,
|
||||
same: fn(&str, &str) -> bool,
|
||||
) -> Result<Option<&'a str>, String> {
|
||||
match (value, old) {
|
||||
(Some(v), Some(o)) if v != o => Err(format!(
|
||||
(Some(v), Some(o)) if !same(v, o) => Err(format!(
|
||||
"{name} {v:?} and its old name {alias} {o:?} disagree: give {name} only"
|
||||
)),
|
||||
(Some(v), _) => Ok(Some(v)),
|
||||
@@ -1459,6 +1462,35 @@ fn aliased<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Two amounts given for one figure mean the same (`"100"` and
|
||||
/// `"100.00"`); text that isn't an amount compares as text.
|
||||
fn same_amount(a: &str, b: &str) -> bool {
|
||||
match (a.trim().parse::<Decimal>(), b.trim().parse::<Decimal>()) {
|
||||
(Ok(x), Ok(y)) => x == y,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two days given for one date mean the same.
|
||||
fn same_day(a: &str, b: &str) -> bool {
|
||||
match (parse_date(a), parse_date(b)) {
|
||||
(Ok(x), Ok(y)) => x == y,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two basis rules mean the same, by wire or pre-P3 name
|
||||
/// (`"carryover"` and `"carryover_723"`).
|
||||
fn same_rule(a: &str, b: &str) -> bool {
|
||||
let rule = |r: &str| {
|
||||
ledger_core::BasisRule::parse(r).or_else(|| ledger_core::BasisRule::parse_legacy(r))
|
||||
};
|
||||
match (rule(a), rule(b)) {
|
||||
(Some(x), Some(y)) => x == y,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
/// A basis rule from a tool: its wire name, or its pre-P3 name (an alias
|
||||
/// for one release, noted).
|
||||
fn parse_basis_rule(rule: &str, notes: &mut Vec<String>) -> Result<ledger_core::BasisRule, String> {
|
||||
@@ -5609,7 +5641,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.correct_asset",
|
||||
title = "Correct a fixed asset",
|
||||
description = "Correct a registered fixed asset's cost, salvage value, useful life, or in-service date (never before its acquisition entry's date) when it was registered wrong, with the reason; omitted fields keep their values. Refused while the books carry depreciation for it (a run posted and not reversed: correct with entries then). Its tag, serial, links, and basis record stay (a contribution's value and built-in gain are as contributed); correct the entry that put it on the books too, so the books and the register agree (books.export fixed_asset_register shows any difference). Journaled with the old figures.",
|
||||
description = "Correct a registered fixed asset's cost, salvage value, useful life, or in-service date (never before its acquisition entry's date) when it was registered wrong, with the reason; omitted fields keep their values. Refused while the books carry depreciation for it (a run posted and not reversed: correct with entries then). Its tag, serial, links and basis record stay (a recorded value at contribution never moves; a pre-P3 basis reads it as the book cost, so its built-in gain moves, noted); correct the entry that put it on the books too (books.export fixed_asset_register shows any difference). Journaled with the old figures.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = true,
|
||||
@@ -5628,6 +5660,7 @@ impl BooksServer {
|
||||
"in_service",
|
||||
p.in_service.as_deref(),
|
||||
&mut notes,
|
||||
same_day,
|
||||
)?;
|
||||
let mut store = self.lock()?;
|
||||
let id = resolve_asset_ref(&store, &p.asset)?;
|
||||
@@ -5648,11 +5681,19 @@ impl BooksServer {
|
||||
None => a.in_service_date,
|
||||
},
|
||||
};
|
||||
let before = a.built_in_gain_704c();
|
||||
store
|
||||
.correct_asset(id, fix, &p.reason)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let ledger = store.ledger();
|
||||
let a = ledger.get_asset(id).expect("resolved");
|
||||
if a.built_in_gain_704c() != before {
|
||||
notes.push(format!(
|
||||
"its basis was recorded before P3 with no value at contribution, which reads as the book cost, so its §704(c) built-in gain moved with the cost ({} to {}): record the value at contribution with books.set_asset_tax_basis",
|
||||
before.map_or("none".into(), money),
|
||||
a.built_in_gain_704c().map_or("none".into(), money)
|
||||
));
|
||||
}
|
||||
Ok(Json(AssetSummaryOne {
|
||||
asset_id: id,
|
||||
tag: a.tag.clone(),
|
||||
@@ -5691,6 +5732,7 @@ impl BooksServer {
|
||||
"contributor_basis",
|
||||
p.contributor_basis.as_deref(),
|
||||
&mut notes,
|
||||
same_amount,
|
||||
)?;
|
||||
let basis = parse_asset_basis(
|
||||
&p.rule,
|
||||
@@ -6256,6 +6298,7 @@ impl BooksServer {
|
||||
"purchase_date",
|
||||
p.purchase_date.as_deref(),
|
||||
&mut notes,
|
||||
same_day,
|
||||
)?
|
||||
.ok_or("in_service_date is required: the day it was placed in service")?,
|
||||
)?;
|
||||
@@ -6316,6 +6359,7 @@ impl BooksServer {
|
||||
"tax_basis_rule",
|
||||
p.tax_basis_rule.as_deref(),
|
||||
&mut notes,
|
||||
same_rule,
|
||||
)?;
|
||||
let adjusted = aliased(
|
||||
"contributor_adjusted_basis",
|
||||
@@ -6323,6 +6367,7 @@ impl BooksServer {
|
||||
"contributor_basis",
|
||||
p.contributor_basis.as_deref(),
|
||||
&mut notes,
|
||||
same_amount,
|
||||
)?;
|
||||
let basis = match rule {
|
||||
Some(rule) => Some(parse_asset_basis(
|
||||
@@ -9235,7 +9280,7 @@ impl BooksServer {
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
let mut csv = String::from(
|
||||
"tag,description,serial,acquired,in_service,cost,salvage,useful_life_months,accumulated_scheduled,accumulated_posted,book_value,status,proceeds,basis_rule,contributor_adjusted_basis,fmv_at_contribution,fmv_recorded,tax_basis_723,depreciable_basis,built_in_gain_704c,sec704c_method,basis_source\n",
|
||||
"tag,description,serial,acquired,in_service,cost,salvage,useful_life_months,accumulated_scheduled,accumulated_posted,book_value,status,proceeds,basis_rule,contributor_adjusted_basis,fmv_at_contribution,fmv_recorded,tax_basis_723,depreciable_basis,built_in_gain_704c,sec704c_1c,sec704c_method,basis_source\n",
|
||||
);
|
||||
for a in &r.rows {
|
||||
let cells = [
|
||||
@@ -9266,6 +9311,11 @@ impl BooksServer {
|
||||
a.tax_basis_723.map(money).unwrap_or_default(),
|
||||
a.depreciable_basis.map(money).unwrap_or_default(),
|
||||
a.built_in_gain_704c.map(money).unwrap_or_default(),
|
||||
// IRC §704(c)(1)(C): a built-in loss counts only for the
|
||||
// contributor; blank for a purchase or no basis.
|
||||
a.built_in_gain_704c
|
||||
.map(|g| (g < Decimal::ZERO).to_string())
|
||||
.unwrap_or_default(),
|
||||
a.sec704c_method
|
||||
.map(|m| m.key())
|
||||
.unwrap_or_default()
|
||||
@@ -9276,7 +9326,7 @@ impl BooksServer {
|
||||
csv.push('\n');
|
||||
}
|
||||
csv.push_str(&format!(
|
||||
"TOTAL,,,,,{},,,{},{},{},,,,,,,{},{},,,",
|
||||
"TOTAL,,,,,{},,,{},{},{},,,,,,,{},{},,,,",
|
||||
money(r.total_cost),
|
||||
money(r.total_accumulated_scheduled),
|
||||
money(r.total_accumulated_posted),
|
||||
|
||||
@@ -1079,6 +1079,34 @@ fn basis_rule_legacy_names_are_aliases() {
|
||||
let _ = t2;
|
||||
}
|
||||
|
||||
/// A name and its alias that mean the same, however written, agree (P3
|
||||
/// review, 2026-10-21: they were compared as text, so "100" and "100.00",
|
||||
/// or carryover and carryover_723, were refused as disagreeing).
|
||||
#[test]
|
||||
fn a_name_and_its_alias_that_mean_the_same_agree() {
|
||||
let mut m = books("alias-same");
|
||||
let (_, t) = m.register_ok(json!({}));
|
||||
let r = m.ok(
|
||||
"books.set_asset_tax_basis",
|
||||
json!({"asset": t, "rule": "carryover_723", "contributor_adjusted_basis": "900",
|
||||
"contributor_basis": "900.00", "fmv_at_contribution": "1200.00"}),
|
||||
);
|
||||
assert_eq!(r["basis"]["contributor_adjusted_basis"], "900.00", "{r}");
|
||||
m.register_ok(
|
||||
json!({"basis_rule": "carryover_723", "tax_basis_rule": "carryover",
|
||||
"contributor_adjusted_basis": "400.00", "contributor_basis": "400",
|
||||
"fmv_at_contribution": "1200.00"}),
|
||||
);
|
||||
m.register_ok(json!({"basis_rule": "purchase", "tax_basis_rule": "cost"}));
|
||||
assert_eq!(m.assets().len(), 3);
|
||||
let why = m.refuse(
|
||||
"books.set_asset_tax_basis",
|
||||
json!({"asset": t, "rule": "carryover_723", "contributor_adjusted_basis": "900.00",
|
||||
"contributor_basis": "900.01", "fmv_at_contribution": "1200.00"}),
|
||||
);
|
||||
assert!(why.contains("disagree"), "{why}");
|
||||
}
|
||||
|
||||
/// A contribution's fair market value is a fact the human gives, never
|
||||
/// assumed from the cost (review R11): without it the call is refused.
|
||||
#[test]
|
||||
@@ -1507,6 +1535,7 @@ fn export_fixed_asset_register_carries_posted_scheduled_and_704c() {
|
||||
"tax_basis_723",
|
||||
"depreciable_basis",
|
||||
"built_in_gain_704c",
|
||||
"sec704c_1c",
|
||||
"sec704c_method",
|
||||
"basis_source"
|
||||
]
|
||||
@@ -1526,7 +1555,14 @@ fn export_fixed_asset_register_carries_posted_scheduled_and_704c() {
|
||||
assert_eq!(canon(&r[col("tax_basis_723")]), "650.25");
|
||||
assert_eq!(canon(&r[col("built_in_gain_704c")]), "549.75");
|
||||
assert_eq!(r[col("fmv_recorded")], "true");
|
||||
assert_eq!(r[col("sec704c_1c")], "false", "a built-in gain");
|
||||
assert_eq!(
|
||||
csv_row("CONV-1")[col("sec704c_1c")],
|
||||
"true",
|
||||
"a built-in loss"
|
||||
);
|
||||
let r = csv_row("BOUGHT-1");
|
||||
assert_eq!(r[col("sec704c_1c")], "", "a purchase");
|
||||
assert_eq!(canon(&r[col("accumulated_posted")]), "100");
|
||||
assert_eq!(canon(&r[col("book_value")]), "1100");
|
||||
assert_eq!(r[col("contributor_adjusted_basis")], "");
|
||||
@@ -1731,3 +1767,54 @@ fn a_skipped_month_is_caught_up_and_said() {
|
||||
);
|
||||
assert!(err.contains("can't be reversed"), "{err}");
|
||||
}
|
||||
|
||||
/// A basis recorded before P3 has no value at contribution and reads it as
|
||||
/// the book cost, so correcting the cost moves its built-in gain: the
|
||||
/// result says so (P3 review, 2026-10-21: the tool text said the built-in
|
||||
/// gain was "as contributed" whatever the record). By hand: cost 1,200.00,
|
||||
/// basis 400.00, gain 800.00; cost corrected to 1,100.00, gain 700.00.
|
||||
#[test]
|
||||
fn correcting_a_legacy_contributions_cost_says_its_built_in_gain_moved() {
|
||||
let mut m = books("legacy-correct");
|
||||
let (id, t) = m.register_ok(json!({}));
|
||||
let dir = m.books_dir.take().unwrap();
|
||||
let data = m.data.clone();
|
||||
drop(m);
|
||||
let mut w = ledger_store::JournalWriter::open(&data.join("journal.jsonl")).unwrap();
|
||||
w.append(
|
||||
"asset.tax_basis_set",
|
||||
json!({"asset_id": id, "rule": "carryover", "contributor_basis": "400.00"}),
|
||||
)
|
||||
.unwrap();
|
||||
drop(w);
|
||||
let toml = dir.join("wanderbooks.toml");
|
||||
let mut m = Mcp::start(&dir, &toml);
|
||||
m.books_dir = Some(dir);
|
||||
assert_eq!(m.asset(id)["basis"]["built_in_gain_704c"], "800.00");
|
||||
let r = m.ok(
|
||||
"books.correct_asset",
|
||||
json!({"asset": t, "cost": "1100.00", "reason": "invoice misread"}),
|
||||
);
|
||||
assert_eq!(r["basis"]["built_in_gain_704c"], "700.00", "{r}");
|
||||
let notes = r["notes"].to_string();
|
||||
assert!(
|
||||
notes.contains("moved with the cost (800.00 to 700.00)"),
|
||||
"{notes}"
|
||||
);
|
||||
// A recorded value never moves, and says nothing.
|
||||
m.ok(
|
||||
"books.set_asset_tax_basis",
|
||||
json!({"asset": t, "rule": "carryover_723", "contributor_adjusted_basis": "400.00",
|
||||
"fmv_at_contribution": "1100.00"}),
|
||||
);
|
||||
let r = m.ok(
|
||||
"books.correct_asset",
|
||||
json!({"asset": t, "cost": "1000.00", "reason": "invoice misread again"}),
|
||||
);
|
||||
assert_eq!(r["basis"]["built_in_gain_704c"], "700.00", "{r}");
|
||||
assert!(
|
||||
r.get("notes")
|
||||
.is_none_or(|n| n.as_array().is_some_and(|a| a.is_empty())),
|
||||
"{r}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user