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
@@ -2,15 +2,17 @@
|
||||
//! small design studio (single-member LLC) with client invoices, template
|
||||
//! sales paid out by a platform, contractors (one without a W-9), rent on a
|
||||
//! recurring schedule, a credit card, a laptop on depreciation, prepaid
|
||||
//! insurance, owner draws, a closed year, PDF receipts, and a few loose
|
||||
//! ends for the "needs attention" list. Deterministic for a given `today`.
|
||||
//! insurance, owner draws, a closed year, PDF receipts, bank statements
|
||||
//! with reconciliations (one voided and redone), documents linked to the
|
||||
//! company, an asset, and a party, and a few loose ends for the "needs
|
||||
//! attention" list. Deterministic for a given `today`.
|
||||
|
||||
use chrono::{Datelike, Days, Months, NaiveDate};
|
||||
use ledger_core::{
|
||||
AccountType, CloseTo, Currency, Form1099, Frequency, Money, NewAsset, NewRecurring,
|
||||
NewSchedule, ScheduleKind, Side, arap::DocumentLine,
|
||||
};
|
||||
use ledger_store::{Store, StoreError};
|
||||
use ledger_store::{Statement, Store, StoreError, Subject};
|
||||
use rust_decimal::Decimal;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -339,7 +341,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
|
||||
let alex = s.add_party("Alex Rivera", &[ledger_core::Role::Member])?;
|
||||
|
||||
// Formation documents.
|
||||
s.file_document(
|
||||
let (articles, _, _) = s.file_document(
|
||||
"formation",
|
||||
"Articles of Organization.pdf",
|
||||
"application/pdf",
|
||||
@@ -353,7 +355,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
|
||||
),
|
||||
Some("Filed with the state"),
|
||||
)?;
|
||||
s.file_document(
|
||||
let (ein_letter, _, _) = s.file_document(
|
||||
"tax",
|
||||
"IRS EIN confirmation.pdf",
|
||||
"application/pdf",
|
||||
@@ -894,5 +896,163 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
|
||||
month_index += 1;
|
||||
}
|
||||
let _ = (bank_fees, client_class);
|
||||
|
||||
// The company's papers, linked to it; the laptop's warranty and
|
||||
// photo, linked to the asset; Sam's contract, linked to Sam and the
|
||||
// company.
|
||||
s.link_attachment(articles, Subject::Company)?;
|
||||
s.link_attachment(ein_letter, Subject::Company)?;
|
||||
let (warranty, _, _) = s.file_document(
|
||||
"warranty",
|
||||
"MacBook Pro AppleCare.pdf",
|
||||
"application/pdf",
|
||||
&receipt_pdf(
|
||||
"AppleCare+ for Mac",
|
||||
&[
|
||||
"Agreement for MacBook Pro 14-inch".into(),
|
||||
format!("Coverage from {}", laptop_date.format("%B %-d, %Y")),
|
||||
"Three years of hardware coverage".into(),
|
||||
],
|
||||
),
|
||||
Some("Warranty, three years"),
|
||||
)?;
|
||||
s.link_attachment(warranty, Subject::Asset(asset))?;
|
||||
let sam_party = s.ledger().party_of_vendor(sam).expect("linked");
|
||||
let (contract_doc, _, _) = s.file_document(
|
||||
"contract",
|
||||
"Sam Okafor services agreement.pdf",
|
||||
"application/pdf",
|
||||
&receipt_pdf(
|
||||
"Independent Contractor Agreement",
|
||||
&[
|
||||
"Juniper Studio LLC and Sam Okafor".into(),
|
||||
"Illustration services, billed per project".into(),
|
||||
format!("Signed {}", (start + Months::new(1)).format("%B %-d, %Y")),
|
||||
],
|
||||
),
|
||||
Some("Signed agreement"),
|
||||
)?;
|
||||
s.link_attachment(contract_doc, Subject::Party(sam_party))?;
|
||||
s.link_attachment(contract_doc, Subject::Company)?;
|
||||
|
||||
// Statements and reconciliations: savings reconciled through last
|
||||
// month; checking through the month before (its first reconciliation
|
||||
// voided and redone against the bank's reissued statement, one check
|
||||
// outstanding at the first statement); the card never.
|
||||
let month_end = |m: NaiveDate| m + Months::new(1) - Days::new(1);
|
||||
let lines_on = |s: &Store, account: u64, through: NaiveDate| -> Vec<(u64, usize, NaiveDate)> {
|
||||
let mut out = Vec::new();
|
||||
for e in s.ledger().posted_entries() {
|
||||
if e.date() <= through {
|
||||
for (i, l) in e.lines().iter().enumerate() {
|
||||
if l.account == account && !s.ledger().is_cleared(e.id(), i) {
|
||||
out.push((e.id(), i, e.date()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|x| (x.2, x.0, x.1));
|
||||
out
|
||||
};
|
||||
let statement = |s: &mut Store,
|
||||
account: u64,
|
||||
name: &str,
|
||||
month: NaiveDate,
|
||||
note: &str,
|
||||
opening: Option<Decimal>|
|
||||
-> Result<u64, StoreError> {
|
||||
let end = month_end(month);
|
||||
// The bank's opening is its last closing: what had cleared by then.
|
||||
let opening = match opening {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
s.ledger()
|
||||
.reconciliation_report(account, month - Days::new(1), Decimal::ZERO)?
|
||||
.cleared_balance
|
||||
}
|
||||
};
|
||||
let closing = s
|
||||
.ledger()
|
||||
.reconciliation_report(account, end, Decimal::ZERO)?
|
||||
.cleared_balance;
|
||||
let (att, _, _) = s.file_document(
|
||||
"statement",
|
||||
&format!(
|
||||
"{name} statement {}{}.pdf",
|
||||
month.format("%Y-%m"),
|
||||
if note.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", note.to_lowercase())
|
||||
}
|
||||
),
|
||||
"application/pdf",
|
||||
&receipt_pdf(
|
||||
&format!("{name} statement"),
|
||||
&[
|
||||
format!(
|
||||
"{} through {}",
|
||||
month.format("%B %-d, %Y"),
|
||||
end.format("%B %-d, %Y")
|
||||
),
|
||||
format!("Opening balance: ${opening}"),
|
||||
format!("Closing balance: ${closing}"),
|
||||
note.to_string(),
|
||||
],
|
||||
),
|
||||
Some(&format!("{name}, {}", month.format("%B %Y"))),
|
||||
)?;
|
||||
s.record_statement(Statement {
|
||||
attachment_id: att,
|
||||
account_id: account,
|
||||
period_start: month,
|
||||
period_end: end,
|
||||
opening_balance: opening,
|
||||
closing_balance: closing,
|
||||
})?;
|
||||
Ok(att)
|
||||
};
|
||||
let last_month = this_month - Months::new(1);
|
||||
// Savings: every line through last month cleared.
|
||||
for (e, i, _) in lines_on(&s, savings, month_end(last_month)) {
|
||||
s.mark_cleared(e, i, true)?;
|
||||
}
|
||||
let st = statement(&mut s, savings, "Savings", last_month, "", None)?;
|
||||
s.complete_reconciliation(st)?;
|
||||
// Checking, three months back: the month's last check still out.
|
||||
let m1 = this_month - Months::new(3);
|
||||
let pending = lines_on(&s, checking, month_end(m1));
|
||||
let outstanding = pending.last().map(|x| (x.0, x.1));
|
||||
for (e, i, _) in pending {
|
||||
if Some((e, i)) != outstanding {
|
||||
s.mark_cleared(e, i, true)?;
|
||||
}
|
||||
}
|
||||
let first = statement(&mut s, checking, "Operating Checking", m1, "", None)?;
|
||||
let voided = s.complete_reconciliation(first)?;
|
||||
s.void_reconciliation(
|
||||
voided.id,
|
||||
"The bank reissued this statement; reconciled again against the corrected copy",
|
||||
)?;
|
||||
let reissued = statement(&mut s, checking, "Operating Checking", m1, "Reissued", None)?;
|
||||
let m1_closing = s
|
||||
.ledger()
|
||||
.reconciliation_report(checking, month_end(m1), Decimal::ZERO)?
|
||||
.cleared_balance;
|
||||
s.complete_reconciliation(reissued)?;
|
||||
// Two months back: everything through it cleared.
|
||||
let m2 = this_month - Months::new(2);
|
||||
for (e, i, _) in lines_on(&s, checking, month_end(m2)) {
|
||||
s.mark_cleared(e, i, true)?;
|
||||
}
|
||||
let second = statement(
|
||||
&mut s,
|
||||
checking,
|
||||
"Operating Checking",
|
||||
m2,
|
||||
"",
|
||||
Some(m1_closing),
|
||||
)?;
|
||||
s.complete_reconciliation(second)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user