Attachments, statements, reconciliations, TIN vault, counterparty policy
MCP: books.link_attachment, unlink_attachment, attachments; link_to on attach_receipt and file_document; record_statement, statements; books.reconcile previews and, confirmed against a recorded statement, records the reconciliation; void_reconciliation, reconciliations; import_bank_csv links its source file; digest lists unreconciled accounts. TIN vault: books.set_w9 takes the TIN and the form into the encrypted vault (last four in the books); books.vendor_1099 include_tins and save_csv_to for the CPA's copy; books.get_w9; books.purge_tin. A leak test scans every tool output, the journal, the database, and the blobs. [books] parties = off | suggest | require (default suggest): every preview names the entry's party or suggests likely ones; require refuses to post until it names one or is marked as having none (entry.no_party). Viewer: documents on every subject page; statements and reconciliation history per account; reconciliation pages; reconcile and no-counterparty attention items; party money by period (paid out, received in, net, what for, by month); Needs attention gets its own full-width section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
a299077991
commit
08ce558ed3
41 files changed
+8463
-465
No files matched your search
@@ -93,25 +93,26 @@ fn tax_basis_text(b: &Books, asset_id: Option<u64>, p: &Value) -> String {
|
||||
}
|
||||
|
||||
/// What an attachment link's subject (`{"kind": .., "id": ..}`) is, in
|
||||
/// words, and where it opens.
|
||||
/// words (Books::subject_label), and where it opens
|
||||
/// (Books::subject_route).
|
||||
fn subject_text(b: &Books, subject: Option<&Value>) -> (String, Option<Route>) {
|
||||
let kind = subject
|
||||
.and_then(|s| s.get("kind"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let id = subject.and_then(|s| s.get("id")).and_then(|v| v.as_u64());
|
||||
match (kind, id) {
|
||||
("entry", Some(e)) => (format!("#{e}"), Some(Route::Entry(e))),
|
||||
("account", Some(a)) => (b.account_label(a), Some(Route::Account(a))),
|
||||
("asset", Some(a)) => (b.asset_label(a), Some(Route::Asset(a))),
|
||||
("party", Some(p)) => (b.party_label(p), Some(Route::Party(p))),
|
||||
("company", _) => ("the company".into(), Some(Route::Company)),
|
||||
("payroll_run", Some(i)) => (format!("payroll run {i}"), None),
|
||||
(k, Some(i)) if !k.is_empty() => (format!("{} {i}", k.replace('_', " ")), None),
|
||||
_ => ("a subject".into(), None),
|
||||
let parsed =
|
||||
subject.and_then(|v| serde_json::from_value::<ledger_store::Subject>(v.clone()).ok());
|
||||
match parsed {
|
||||
Some(ledger_store::Subject::Company) => ("the company".into(), Some(Route::Company)),
|
||||
Some(sub) => (b.subject_label(sub), b.subject_route(sub)),
|
||||
None => ("a subject".into(), None),
|
||||
}
|
||||
}
|
||||
|
||||
/// A date from the payload, in words ("Sep 30, 2026"), or as written.
|
||||
fn date_text(p: &Value, k: &str) -> String {
|
||||
let t = s(p, k);
|
||||
t.parse::<chrono::NaiveDate>()
|
||||
.map(fmt::date)
|
||||
.unwrap_or_else(|_| t.to_string())
|
||||
}
|
||||
|
||||
/// A party's label, or "a party" when the event names none.
|
||||
fn party_name(b: &Books, id: Option<u64>) -> String {
|
||||
match id {
|
||||
@@ -272,10 +273,10 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
Some(Event {
|
||||
icon: icon::FILE_TEXT,
|
||||
text: format!(
|
||||
"Statement for {}, {} to {}: closing balance {closing}",
|
||||
"Recorded {}'s statement, {} – {}: closing balance {closing}",
|
||||
account.map_or("an account".into(), |a| b.account_label(a)),
|
||||
s(p, "period_start"),
|
||||
s(p, "period_end")
|
||||
date_text(p, "period_start"),
|
||||
date_text(p, "period_end")
|
||||
),
|
||||
route: Some(Route::Document(att)),
|
||||
also: account.map(Route::Account),
|
||||
@@ -287,25 +288,48 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
.parse()
|
||||
.map(fmt::dollars)
|
||||
.unwrap_or_default();
|
||||
let lines = p
|
||||
.get("cleared")
|
||||
.and_then(|v| v.as_array())
|
||||
.map_or(0, |v| v.len());
|
||||
Some(Event {
|
||||
icon: icon::CHECK_SQUARE,
|
||||
text: format!(
|
||||
"Reconciled {} through {} at {balance}",
|
||||
"Reconciled {} through {} at {balance} ({} cleared)",
|
||||
account.map_or("an account".into(), |a| b.account_label(a)),
|
||||
s(p, "statement_date")
|
||||
date_text(p, "statement_date"),
|
||||
fmt::count(lines, "line", "lines")
|
||||
),
|
||||
route: account.map(Route::Account),
|
||||
also: p
|
||||
.get("attachment_id")
|
||||
route: p
|
||||
.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(Route::Document),
|
||||
.map(Route::Reconciliation),
|
||||
also: account.map(Route::Account),
|
||||
})
|
||||
}
|
||||
"reconciliation.voided" => {
|
||||
let r = p
|
||||
.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(|i| b.reconciliation(i));
|
||||
Some(Event {
|
||||
icon: icon::PROHIBIT,
|
||||
text: match r {
|
||||
Some(r) => format!(
|
||||
"Voided the reconciliation of {} through {}: {}",
|
||||
b.account_label(r.account_id),
|
||||
fmt::date(r.statement_date),
|
||||
s(p, "reason")
|
||||
),
|
||||
None => format!("Voided reconciliation {id}: {}", s(p, "reason")),
|
||||
},
|
||||
route: p
|
||||
.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(Route::Reconciliation),
|
||||
also: r.map(|r| Route::Account(r.account_id)),
|
||||
})
|
||||
}
|
||||
"reconciliation.voided" => ev(
|
||||
icon::PROHIBIT,
|
||||
format!("Voided reconciliation {id}: {}", s(p, "reason")),
|
||||
None,
|
||||
),
|
||||
"attachment.withdrawn" => ev(
|
||||
icon::ARCHIVE,
|
||||
format!("Withdrew {}: {}", attachment_name(b, att), s(p, "reason")),
|
||||
@@ -697,6 +721,15 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
),
|
||||
entry_id.map(Route::Entry),
|
||||
),
|
||||
"entry.no_party" => ev(
|
||||
icon::USER_MINUS,
|
||||
format!(
|
||||
"Entry #{} marked as having no counterparty: {}",
|
||||
entry_id.unwrap_or(0),
|
||||
s(p, "reason")
|
||||
),
|
||||
entry_id.map(Route::Entry),
|
||||
),
|
||||
"line.cleared" => ev(
|
||||
icon::CHECK_SQUARE,
|
||||
format!("Cleared a line of #{}", entry_id.unwrap_or(0)),
|
||||
@@ -933,11 +966,14 @@ fn detail(ui: &mut Ui, b: &Books, line: &JournalLine, shell: &mut Shell<'_>) {
|
||||
for r in ev.route.iter().chain(&ev.also) {
|
||||
ui.add_space(6.0);
|
||||
if ui
|
||||
.button(format!(
|
||||
"{} Open {}",
|
||||
icon::ARROW_SQUARE_OUT,
|
||||
crate::palette::describe(b, r)
|
||||
))
|
||||
.button(format!("{} Open {}", icon::ARROW_SQUARE_OUT, {
|
||||
let d = crate::palette::describe(b, r);
|
||||
if d.is_empty() {
|
||||
r.section().title().to_string()
|
||||
} else {
|
||||
d
|
||||
}
|
||||
}))
|
||||
.clicked()
|
||||
{
|
||||
shell.go(r.clone());
|
||||
|
||||
Reference in new issue
Block a user