Parties, attachment links, statements, reconciliations (store), TIN vault crate

Parties: one directory for vendors, contractors, customers, employees,
members, platforms, agencies, banks, landlords (ledger-core parties.rs).
W-9/W-4 facts without the number (last four only; any nine-digit run in a
party field is refused, Treas. Reg. 301.6109-4). Vendor and customer
records link to parties on replay; core tags every bill, invoice, payment,
and reversal with its counterparty. Journal events party.*,
entry.party_set; payroll lines name the employee party.

MCP: books.add_party, update_party, set_party_roles, set_w9, set_w4,
parties, party, set_entry_party; draft_entry party; run_payroll employee;
show party. Viewer: Parties directory and party pages, search, journal
text, W-9 attention, demo data.

One definition of a party's money (ledger_core::parties::activity) for
MCP and viewer. 1099 amounts count payments from cash and financing
accounts (a member paying for the company counts; a card doesn't; a
prepayment counts when made): vendor_1099_report_with, B-19.

Store: attachment links to any subject, statements, reconciliations
(schema v11), with replay and projection. ledger-vault: encrypted TIN and
W-9 records (ChaCha20-Poly1305, key file outside the books).

Spec-blind tests for parties (core, store, MCP, viewer) and attachments.

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 14:43:40 -06:00
1 parent 2a188bd039
commit a299077991
54 files changed
+10961 -178

No files matched your search

+40 -8
View File
@@ -1,6 +1,7 @@
//! Ctrl+K: jump to anything. Pages, reports, accounts, entries (by text,
//! amount, or #id), fixed assets (by tag, serial, or description), vendors,
//! and documents, ranked by a simple fuzzy match.
//! amount, or #id), fixed assets (by tag, serial, or description), parties
//! (vendors and customers among them, by name or display name), and
//! documents, ranked by a simple fuzzy match.
use crate::books::Books;
use crate::fmt;
@@ -224,6 +225,15 @@ pub fn fuzzy(hay: &str, query: &str) -> Option<i32> {
/// tag, serial, and description; a query found whole in its tag or
/// serial (ignoring case) adds 500, in its description 50.
/// 3. Every asset result opens Route::Asset(its id).
/// INVARIANTS (parties):
/// 4. A party whose name or display name equals the trimmed query,
/// ignoring case, scores 1500: above an entry's exact #id, below an
/// exact asset tag.
/// 5. Otherwise a party is listed when the query fuzzily matches its
/// name and display name; a query found whole in either (ignoring
/// case) adds 500.
/// 6. Every party result opens Route::Party(its id); archived parties
/// are found too, and say so.
/// ```
pub fn search(b: &Books, query: &str) -> Vec<Item> {
let q = query.trim();
@@ -329,13 +339,34 @@ pub fn search(b: &Books, query: &str) -> Vec<Item> {
});
}
}
for v in b.ledger.list_vendors() {
if let Some(score) = fuzzy(&v.name, q) {
for p in b.ledger.parties() {
let display = p.display_name.as_deref().unwrap_or("");
let exact = |t: &str| !t.is_empty() && t.trim().to_lowercase() == ql;
let whole = |t: &str| !t.is_empty() && t.to_lowercase().contains(&ql);
let score = if exact(&p.name) || exact(display) {
Some(1500)
} else {
fuzzy(&format!("{} {display}", p.name), q).map(|s| {
s + if whole(&p.name) || whole(display) {
500
} else {
0
}
})
};
if let Some(score) = score {
let mut detail = vec![crate::pages::parties::roles_text(p)];
if p.display_name.is_some() {
detail.push(p.name.clone());
}
if p.archived {
detail.push("archived".into());
}
out.push(Item {
icon: icon::STOREFRONT,
title: v.name.clone(),
detail: "Vendor".into(),
route: Route::Company,
icon: crate::pages::parties::party_icon(p),
title: p.label().to_string(),
detail: detail.join(" · "),
route: Route::Party(p.id),
score,
});
}
@@ -378,6 +409,7 @@ pub fn describe(b: &Books, r: &Route) -> String {
.map(|a| a.file_name.clone())
.unwrap_or_default(),
Route::Asset(id) => b.asset_label(*id),
Route::Party(id) => b.party_label(*id),
Route::JournalEvent(seq) => format!("Event {seq}"),
_ => String::new(),
}