Shared parties with exact shares, member capital statements, TINs for any party, K-1 worksheet

Entries may name several parties, each with an exact amount, a percent,
or a share derived from the members' capital lines or [[company.members]]
shares (entry.parties_set); shares always sum exactly. The 1099
worksheet counts a vendor's share. A member's party page is their
partner capital statement, from the same ledger_core::k1 source as the
CPA package.

books.set_tin stores any party's TIN (members' SSNs for K-1s) in the
vault (party.tin_set, last four in the books). The CPA package carries a
K-1 member worksheet (k1-members.csv, with a members' total that ties to
Schedule L) and the 1099 worksheet with full TINs, saved outside the
books only. files_jointly_with notes spouses' two K-1s land on one 1040.

Adversarial review fixes: reversed contributions no longer inflate K-1
contributions and distributions; a K-1 row never takes another party's
TIN; TIN detection covers dot/slash/dash separators; refusals redact
TINs; vendor tagging can't silently replace shared parties; reversals
follow re-apportioning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-15 18:20:18 -06:00
1 parent ef00ccc242
commit e1b2509c22
32 files changed
+7232 -375

No files matched your search

+77
View File
@@ -473,6 +473,30 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
};
ev(icon::IDENTIFICATION_CARD, text, pid.map(Route::Party))
}
"party.tin_set" => {
let pid = p.get("id").and_then(|v| v.as_u64());
let name = party_name(b, pid);
let text = match p.get("facts").filter(|f| !f.is_null()) {
Some(f) => {
let kind = match f.get("tin_type").and_then(|v| v.as_str()) {
Some("ssn") => "SSN",
Some("ein") => "EIN",
Some("itin") => "ITIN",
_ => "TIN",
};
let l4 = f.get("last4").and_then(|v| v.as_str()).unwrap_or("????");
if f.get("purged").is_some_and(|v| !v.is_null()) {
format!("Purged {name}'s {kind} (••••{l4}) from the vault")
} else {
format!(
"Taxpayer ID for {name} recorded: {kind} ending {l4}, kept in the vault"
)
}
}
None => format!("Taxpayer ID facts for {name} cleared"),
};
ev(icon::IDENTIFICATION_CARD, text, pid.map(Route::Party))
}
"party.w4_set" => {
let pid = p.get("id").and_then(|v| v.as_u64());
let name = party_name(b, pid);
@@ -491,6 +515,59 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
};
ev(icon::IDENTIFICATION_BADGE, text, pid.map(Route::Party))
}
"entry.parties_set" => {
let e = entry_id.unwrap_or(0);
let named: Vec<(u64, String)> = p
.get("parties")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|x| {
let id = x.get("party_id").and_then(|v| v.as_u64())?;
let share = match x.get("share") {
Some(Value::Object(o)) => o
.get("amount")
.and_then(|v| v.as_str())
.map(|a| {
rust_decimal::Decimal::from_str_exact(a)
.map(fmt::dollars)
.unwrap_or_else(|_| a.to_string())
})
.or_else(|| {
o.get("percent")
.and_then(|v| v.as_str())
.map(|pc| format!("{pc}%"))
}),
_ => None,
};
let name = party_name(b, Some(id));
Some((
id,
match share {
Some(sh) => format!("{name} ({sh})"),
None => name,
},
))
})
.collect();
Some(Event {
icon: icon::ADDRESS_BOOK,
text: if named.is_empty() {
format!("Cleared the parties of #{e}")
} else {
format!(
"Tagged #{e} with {}",
named
.iter()
.map(|(_, n)| n.as_str())
.collect::<Vec<_>>()
.join(" and ")
)
},
route: entry_id.map(Route::Entry),
also: named.first().map(|(id, _)| Route::Party(*id)),
})
}
"entry.party_set" => {
let pid = p.get("party_id").and_then(|v| v.as_u64());
let e = entry_id.unwrap_or(0);