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:
swaitsandClaude Opus 5.5 committed 2026-10-15 15:42:37 -06:00
1 parent a299077991
commit 08ce558ed3
41 files changed
+8463 -465

No files matched your search

+46 -3
View File
@@ -107,6 +107,19 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
shell.go(Route::Party(party.id));
}
}
if e.party_id.is_none()
&& let Some(why) = b.ledger.entry_no_party(e.id)
{
ui.add_space(4.0);
ui.add(
egui::Label::new(
RichText::new(format!("{} No counterparty: {why}", icon::USER_MINUS))
.color(p.muted),
)
.wrap(),
)
.on_hover_text("Marked as having no counterparty (entry.no_party)");
}
if let Some(&asset) = b.entry_assets.get(&e.id) {
ui.add_space(4.0);
if widgets::link(
@@ -268,18 +281,25 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
});
ui.add_space(18.0);
// Receipts.
// Receipts and documents: filed on it, linked to it, or linked
// to the invoice, bill, or payroll run it records; a withdrawn
// receipt filed on it stays, marked.
let subjects = b.entry_subjects(e.id);
let all_for_entry: Vec<&Attachment> = b
.attachments
.iter()
.filter(|a| a.entry_id == Some(e.id))
.filter(|a| {
a.entry_id == Some(e.id)
|| (a.withdrawn_at.is_none()
&& b.links_of(a.id).iter().any(|s| subjects.contains(s)))
})
.collect();
section(
ui,
if all_for_entry.len() == 1 {
"Receipt"
} else {
"Receipts"
"Receipts and documents"
},
);
if all_for_entry.is_empty() {
@@ -295,6 +315,29 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
}));
}
for a in all_for_entry {
let elsewhere: Vec<ledger_store::Subject> = b
.links_of(a.id)
.iter()
.copied()
.filter(|s| *s != ledger_store::Subject::Entry(e.id))
.collect();
if !elsewhere.is_empty() {
let mut go = None;
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(4.0, 4.0);
ui.label(
RichText::new("Linked to")
.font(theme::regular(11.5))
.color(p.faint),
);
for s in elsewhere {
super::documents::subject_chip(ui, b, s, &mut go);
}
});
if let Some(r) = go {
shell.go(r);
}
}
attachment_view(ui, b, a, shell, true);
ui.add_space(12.0);
}