feat: ledger-gui — a read-only, live desktop view of the books
egui/wgpu app for owners and their accountant: Overview, Cash, Profit, Transactions (with rendered receipts and journal history), Reports (statements with interval columns, comparisons, drill-down), Accounts, Documents, Company, Journal; Ctrl+K palette; back/forward; light/dark. Follows the journal live (~30 ms from write to screen). --demo opens a deterministic demo company, also used for screenshot tests. Engine: ledger-store Snapshot (read-only, incremental, tolerant of a half-written tail); ledger-core analysis (intervals, balance series, P&L by interval, cash flows); ledger-config extracted from ledger-mcp with the full company profile and cash_accounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
1 parent
bc3ab779e7
commit
c09325ef8c
52 files changed
+14050
-113
No files matched your search
@@ -0,0 +1,796 @@
|
||||
//! A demo company, built through the real engine: fifteen months of a
|
||||
//! 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`.
|
||||
|
||||
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 rust_decimal::Decimal;
|
||||
use std::path::Path;
|
||||
|
||||
/// The demo's `wanderbooks.toml`.
|
||||
pub const BOOKS_TOML: &str = r#"[company]
|
||||
name = "Juniper Studio LLC"
|
||||
entity_type = "LLC, single-member"
|
||||
tax_classification = "disregarded entity" # Schedule C
|
||||
fiscal_year_start = "01-01"
|
||||
formation_state = "OR"
|
||||
formed = "2025-06-02"
|
||||
ein = "00-0000000"
|
||||
address = "1200 NW Everett St, Portland, OR 97209"
|
||||
owner = "Alex Rivera"
|
||||
|
||||
[books]
|
||||
accounting_basis = "accrual"
|
||||
receipt_threshold = "25.00"
|
||||
close_into = "3000"
|
||||
close_sweep = ["3100"]
|
||||
cash_accounts = ["1000", "1010"]
|
||||
"#;
|
||||
|
||||
struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 = self
|
||||
.0
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
self.0 >> 33
|
||||
}
|
||||
/// Uniform in [lo, hi] dollars, to the cent.
|
||||
fn amount(&mut self, lo: u64, hi: u64) -> Decimal {
|
||||
let cents = lo * 100 + self.next() % ((hi - lo) * 100 + 1);
|
||||
Decimal::new(cents as i64, 2)
|
||||
}
|
||||
fn pick(&mut self, n: u64) -> u64 {
|
||||
self.next() % n
|
||||
}
|
||||
}
|
||||
|
||||
fn usd(d: Decimal) -> Money {
|
||||
Money::new(d, Currency::USD)
|
||||
}
|
||||
|
||||
fn dec(n: i64) -> Decimal {
|
||||
Decimal::from(n)
|
||||
}
|
||||
|
||||
fn day(month_start: NaiveDate, d: u32) -> NaiveDate {
|
||||
let last = (month_start + Months::new(1) - Days::new(1)).day();
|
||||
NaiveDate::from_ymd_opt(month_start.year(), month_start.month(), d.min(last)).expect("valid")
|
||||
}
|
||||
|
||||
/// A one-page PDF receipt with the given lines (Helvetica).
|
||||
pub fn receipt_pdf(title: &str, lines: &[String]) -> Vec<u8> {
|
||||
let esc = |s: &str| {
|
||||
s.replace('\\', "\\\\")
|
||||
.replace('(', "\\(")
|
||||
.replace(')', "\\)")
|
||||
};
|
||||
let mut content = String::new();
|
||||
content.push_str("BT /F2 22 Tf 56 760 Td (");
|
||||
content.push_str(&esc(title));
|
||||
content.push_str(") Tj ET\n");
|
||||
content.push_str("0.85 0.85 0.85 RG 56 742 m 556 742 l S\n");
|
||||
let mut y = 712;
|
||||
for l in lines {
|
||||
let (font, size) = if l.starts_with("TOTAL") {
|
||||
("F2", 13)
|
||||
} else {
|
||||
("F1", 11)
|
||||
};
|
||||
content.push_str(&format!(
|
||||
"BT /{font} {size} Tf 56 {y} Td ({}) Tj ET\n",
|
||||
esc(l)
|
||||
));
|
||||
y -= 20;
|
||||
}
|
||||
let objects = [
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>".to_string(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_string(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>".to_string(),
|
||||
format!("<< /Length {} >>\nstream\n{content}endstream", content.len()),
|
||||
];
|
||||
let mut pdf = b"%PDF-1.4\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objects.iter().enumerate() {
|
||||
offsets.push(pdf.len());
|
||||
pdf.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = pdf.len();
|
||||
pdf.extend_from_slice(
|
||||
format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes(),
|
||||
);
|
||||
for off in offsets {
|
||||
pdf.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
pdf.extend_from_slice(
|
||||
format!(
|
||||
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
|
||||
objects.len() + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
pdf
|
||||
}
|
||||
|
||||
fn receipt(vendor: &str, date: NaiveDate, item: &str, amount: Decimal, paid: &str) -> Vec<u8> {
|
||||
receipt_pdf(
|
||||
vendor,
|
||||
&[
|
||||
format!("Receipt {}", date.format("%B %-d, %Y")),
|
||||
format!(
|
||||
"Order {:08}",
|
||||
(date.num_days_from_ce() as u64 * 7919
|
||||
+ (amount * dec(100)).try_into().unwrap_or(0u64))
|
||||
% 100_000_000
|
||||
),
|
||||
String::new(),
|
||||
item.to_string(),
|
||||
format!("Amount: ${amount}"),
|
||||
String::new(),
|
||||
format!("TOTAL ${amount}"),
|
||||
format!("Paid with {paid}"),
|
||||
String::new(),
|
||||
"Thank you for your business.".into(),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the demo books in `dir` (which must not already hold books): the
|
||||
/// journal and blobs under `dir/books-data`, the settings in
|
||||
/// `dir/wanderbooks.toml`.
|
||||
pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
std::fs::write(dir.join("wanderbooks.toml"), BOOKS_TOML)?;
|
||||
let mut s = Store::create(&dir.join("books-data"), Currency::USD)?;
|
||||
let mut rng = Rng(0x5eed_1234);
|
||||
let start =
|
||||
NaiveDate::from_ymd_opt(today.year(), today.month(), 1).expect("valid") - Months::new(15);
|
||||
let this_month = NaiveDate::from_ymd_opt(today.year(), today.month(), 1).expect("valid");
|
||||
|
||||
let acct =
|
||||
|s: &mut Store, code: &str, name: &str, t, tax: Option<&str>| -> Result<u64, StoreError> {
|
||||
let id = s.add_account(code, name, t, None, false)?;
|
||||
if let Some(c) = tax {
|
||||
s.set_account_tax(id, Some(c), None)?;
|
||||
}
|
||||
Ok(id)
|
||||
};
|
||||
use AccountType::*;
|
||||
let checking = acct(&mut s, "1000", "Operating Checking", Asset, None)?;
|
||||
let savings = acct(&mut s, "1010", "Savings", Asset, None)?;
|
||||
let prepaid = acct(&mut s, "1300", "Prepaid Insurance", Asset, None)?;
|
||||
let equipment = acct(&mut s, "1500", "Computer Equipment", Asset, None)?;
|
||||
let accum = acct(&mut s, "1510", "Accumulated Depreciation", Asset, None)?;
|
||||
let card = acct(&mut s, "2100", "Business Credit Card", Liability, None)?;
|
||||
let capital = acct(&mut s, "3000", "Owner's Capital", Equity, None)?;
|
||||
let draws = acct(&mut s, "3100", "Owner's Draws", Equity, None)?;
|
||||
let _services = acct(
|
||||
&mut s,
|
||||
"4000",
|
||||
"Design Services",
|
||||
Revenue,
|
||||
Some("gross_receipts"),
|
||||
)?;
|
||||
let templates = acct(
|
||||
&mut s,
|
||||
"4100",
|
||||
"Template Sales",
|
||||
Revenue,
|
||||
Some("gross_receipts"),
|
||||
)?;
|
||||
let fees = acct(
|
||||
&mut s,
|
||||
"6000",
|
||||
"Platform Fees",
|
||||
Expense,
|
||||
Some("commissions_fees"),
|
||||
)?;
|
||||
let contract = acct(
|
||||
&mut s,
|
||||
"6050",
|
||||
"Contract Labor",
|
||||
Expense,
|
||||
Some("contract_labor"),
|
||||
)?;
|
||||
let software = acct(
|
||||
&mut s,
|
||||
"6100",
|
||||
"Software & Subscriptions",
|
||||
Expense,
|
||||
Some("office_expense"),
|
||||
)?;
|
||||
let rent = acct(
|
||||
&mut s,
|
||||
"6200",
|
||||
"Studio Rent",
|
||||
Expense,
|
||||
Some("rent_property"),
|
||||
)?;
|
||||
let insurance = acct(&mut s, "6300", "Insurance", Expense, Some("insurance"))?;
|
||||
let advertising = acct(&mut s, "6400", "Advertising", Expense, Some("advertising"))?;
|
||||
let meals = acct(&mut s, "6500", "Meals", Expense, Some("meals"))?;
|
||||
let travel = acct(&mut s, "6600", "Travel", Expense, Some("travel"))?;
|
||||
let supplies = acct(&mut s, "6700", "Office Supplies", Expense, Some("supplies"))?;
|
||||
let depreciation = acct(
|
||||
&mut s,
|
||||
"6800",
|
||||
"Depreciation",
|
||||
Expense,
|
||||
Some("depreciation"),
|
||||
)?;
|
||||
let bank_fees = acct(&mut s, "6900", "Bank Fees", Expense, None)?;
|
||||
let client_class = s.add_class("Client work")?;
|
||||
let template_class = s.add_class("Templates")?;
|
||||
|
||||
let sam = s.add_vendor("Sam Okafor")?;
|
||||
s.set_vendor_1099(sam, Some(Form1099::Nec), true)?;
|
||||
let riley = s.add_vendor("Riley Chen")?;
|
||||
s.set_vendor_1099(riley, Some(Form1099::Nec), false)?;
|
||||
let landlord = s.add_vendor("Everett Lofts LLC")?;
|
||||
s.set_vendor_1099(landlord, Some(Form1099::Misc), true)?;
|
||||
let customers: Vec<u64> = [
|
||||
"Fern & Field Coffee",
|
||||
"Bramble Outdoor Co.",
|
||||
"Northwind Clinics",
|
||||
"Tidepool Records",
|
||||
"Larkspur Bakery",
|
||||
]
|
||||
.iter()
|
||||
.map(|n| s.add_customer(n))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
// Formation documents.
|
||||
s.file_document(
|
||||
"formation",
|
||||
"Articles of Organization.pdf",
|
||||
"application/pdf",
|
||||
&receipt_pdf(
|
||||
"Articles of Organization",
|
||||
&[
|
||||
"Juniper Studio LLC".into(),
|
||||
"Oregon Secretary of State".into(),
|
||||
format!("Filed {}", start.format("%B %-d, %Y")),
|
||||
],
|
||||
),
|
||||
Some("Filed with the state"),
|
||||
)?;
|
||||
s.file_document(
|
||||
"tax",
|
||||
"IRS EIN confirmation.pdf",
|
||||
"application/pdf",
|
||||
&receipt_pdf(
|
||||
"EIN Assignment",
|
||||
&[
|
||||
"Juniper Studio LLC".into(),
|
||||
"Employer Identification Number assigned".into(),
|
||||
],
|
||||
),
|
||||
Some("CP 575"),
|
||||
)?;
|
||||
|
||||
// Helper: post a two-line entry with an optional receipt and vendor.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn post(
|
||||
s: &mut Store,
|
||||
date: NaiveDate,
|
||||
desc: &str,
|
||||
dr: u64,
|
||||
cr: u64,
|
||||
amount: Decimal,
|
||||
receipt: Option<(&str, Vec<u8>)>,
|
||||
vendor: Option<u64>,
|
||||
class: Option<u32>,
|
||||
) -> Result<u64, StoreError> {
|
||||
let e = s.create_entry(date, desc)?;
|
||||
s.add_line(e, dr, Side::Debit, usd(amount))?;
|
||||
s.add_line(e, cr, Side::Credit, usd(amount))?;
|
||||
if let Some(c) = class {
|
||||
s.set_line_class(e, 0, Some(c))?;
|
||||
s.set_line_class(e, 1, Some(c))?;
|
||||
}
|
||||
if let Some((name, bytes)) = receipt {
|
||||
s.attach_receipt(Some(e), name, "application/pdf", &bytes, None)?;
|
||||
}
|
||||
s.post_entry(e)?;
|
||||
if vendor.is_some() {
|
||||
s.set_entry_vendor(e, vendor)?;
|
||||
}
|
||||
Ok(e)
|
||||
}
|
||||
|
||||
post(
|
||||
&mut s,
|
||||
day(start, 2),
|
||||
"Owner contribution — startup capital",
|
||||
checking,
|
||||
capital,
|
||||
dec(15000),
|
||||
Some((
|
||||
"bank-transfer.pdf",
|
||||
receipt(
|
||||
"Transfer confirmation",
|
||||
day(start, 2),
|
||||
"Transfer from Alex Rivera personal checking",
|
||||
dec(15000),
|
||||
"ACH",
|
||||
),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Rent: a recurring entry, posted every month with its invoice; this
|
||||
// month's is left as a draft.
|
||||
s.register_recurring(NewRecurring {
|
||||
description: "Studio rent — Everett Lofts".into(),
|
||||
lines: vec![
|
||||
(rent, Side::Debit, usd(dec(1800))),
|
||||
(checking, Side::Credit, usd(dec(1800))),
|
||||
],
|
||||
frequency: Frequency::Monthly,
|
||||
start: day(start, 1),
|
||||
end: None,
|
||||
})?;
|
||||
|
||||
// Laptop and its depreciation.
|
||||
let laptop_date = day(start + Months::new(1), 9);
|
||||
let laptop = dec(3199);
|
||||
post(
|
||||
&mut s,
|
||||
laptop_date,
|
||||
"MacBook Pro 16\" — studio workstation",
|
||||
equipment,
|
||||
card,
|
||||
laptop,
|
||||
Some((
|
||||
"apple-store.pdf",
|
||||
receipt(
|
||||
"Apple Store",
|
||||
laptop_date,
|
||||
"MacBook Pro 16-inch, M-series",
|
||||
laptop,
|
||||
"Visa ending 4242",
|
||||
),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let asset = s.register_asset(NewAsset {
|
||||
description: "MacBook Pro 16\"".into(),
|
||||
purchase_date: laptop_date,
|
||||
cost: usd(laptop),
|
||||
salvage_value: usd(dec(400)),
|
||||
useful_life_months: 36,
|
||||
cost_account: equipment,
|
||||
accum_depr_account: accum,
|
||||
expense_account: depreciation,
|
||||
})?;
|
||||
|
||||
// Prepaid insurance, spread over a year.
|
||||
let ins_date = day(start + Months::new(2), 15);
|
||||
post(
|
||||
&mut s,
|
||||
ins_date,
|
||||
"General liability insurance — annual premium",
|
||||
prepaid,
|
||||
checking,
|
||||
dec(1440),
|
||||
Some((
|
||||
"hiscox-policy.pdf",
|
||||
receipt(
|
||||
"Hiscox",
|
||||
ins_date,
|
||||
"General liability, 12 months",
|
||||
dec(1440),
|
||||
"ACH",
|
||||
),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
s.register_schedule(NewSchedule {
|
||||
description: "General liability insurance".into(),
|
||||
kind: ScheduleKind::PrepaidExpense,
|
||||
total: usd(dec(1440)),
|
||||
start: ins_date,
|
||||
months: 12,
|
||||
balance_account: prepaid,
|
||||
pl_account: insurance,
|
||||
})?;
|
||||
|
||||
let mut open_invoices: Vec<(u64, NaiveDate, Decimal)> = Vec::new();
|
||||
let mut card_balance = Decimal::ZERO;
|
||||
let mut m = start;
|
||||
let mut month_index = 0;
|
||||
while m <= this_month {
|
||||
let current = m == this_month;
|
||||
let in_range = |d: NaiveDate| d <= today;
|
||||
// Software on the card.
|
||||
for (vendor, item, lo, hi, d) in [
|
||||
("Figma", "Figma Professional — monthly", 45, 45, 3),
|
||||
("Adobe", "Creative Cloud All Apps", 59, 60, 7),
|
||||
("Google Workspace", "Business Starter — 2 users", 28, 28, 11),
|
||||
] {
|
||||
let date = day(m, d);
|
||||
if !in_range(date) {
|
||||
continue;
|
||||
}
|
||||
let amt = rng.amount(lo, hi);
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
&format!("{vendor} — {item}"),
|
||||
software,
|
||||
card,
|
||||
amt,
|
||||
Some((
|
||||
&format!("{}-{}.pdf", vendor.to_lowercase().replace(' ', "-"), date),
|
||||
receipt(vendor, date, item, amt, "Visa ending 4242"),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
card_balance += amt;
|
||||
}
|
||||
// Advertising some months.
|
||||
if month_index % 3 == 1 {
|
||||
let date = day(m, 14);
|
||||
if in_range(date) {
|
||||
let amt = rng.amount(150, 420);
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Instagram ads — portfolio campaign",
|
||||
advertising,
|
||||
card,
|
||||
amt,
|
||||
Some((
|
||||
"meta-ads.pdf",
|
||||
receipt("Meta Ads", date, "Sponsored posts", amt, "Visa ending 4242"),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
card_balance += amt;
|
||||
}
|
||||
}
|
||||
// Meals and supplies.
|
||||
let date = day(m, 17);
|
||||
if in_range(date) {
|
||||
let amt = rng.amount(28, 96);
|
||||
let missing = current || month_index == 13;
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Client lunch — project kickoff",
|
||||
meals,
|
||||
card,
|
||||
amt,
|
||||
(!missing).then(|| {
|
||||
(
|
||||
"lunch.pdf",
|
||||
receipt(
|
||||
"Tasty n Alder",
|
||||
date,
|
||||
"Lunch for 2",
|
||||
amt,
|
||||
"Visa ending 4242",
|
||||
),
|
||||
)
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
card_balance += amt;
|
||||
}
|
||||
if month_index % 4 == 2 {
|
||||
let date = day(m, 21);
|
||||
if in_range(date) {
|
||||
let amt = rng.amount(40, 180);
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Paper, ink, and sketchbooks",
|
||||
supplies,
|
||||
card,
|
||||
amt,
|
||||
Some((
|
||||
"blick.pdf",
|
||||
receipt(
|
||||
"Blick Art Materials",
|
||||
date,
|
||||
"Supplies",
|
||||
amt,
|
||||
"Visa ending 4242",
|
||||
),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
card_balance += amt;
|
||||
}
|
||||
}
|
||||
if month_index == 7 {
|
||||
let date = day(m, 9);
|
||||
if in_range(date) {
|
||||
let amt = dec(1186) + rng.amount(0, 90);
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Flights and hotel — client workshop, Austin",
|
||||
travel,
|
||||
card,
|
||||
amt,
|
||||
Some((
|
||||
"alaska-air.pdf",
|
||||
receipt(
|
||||
"Alaska Airlines",
|
||||
date,
|
||||
"PDX-AUS round trip + 2 nights",
|
||||
amt,
|
||||
"Visa ending 4242",
|
||||
),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
card_balance += amt;
|
||||
}
|
||||
}
|
||||
// Pay the card in full on the 25th.
|
||||
let date = day(m, 25);
|
||||
if in_range(date) && card_balance > Decimal::ZERO {
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Credit card payment",
|
||||
card,
|
||||
checking,
|
||||
card_balance,
|
||||
Some((
|
||||
"card-statement.pdf",
|
||||
receipt(
|
||||
"Chase Ink",
|
||||
date,
|
||||
"Statement balance payment",
|
||||
card_balance,
|
||||
"ACH from Operating Checking",
|
||||
),
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
card_balance = Decimal::ZERO;
|
||||
}
|
||||
// Client invoices: two or three a month, net 30.
|
||||
for k in 0..(2 + rng.pick(2)) {
|
||||
let date = day(m, 5 + 9 * k as u32);
|
||||
if !in_range(date) {
|
||||
continue;
|
||||
}
|
||||
let cust = customers[rng.pick(customers.len() as u64) as usize];
|
||||
let amt = (rng.amount(1800, 7800) / dec(50)).round() * dec(50);
|
||||
let inv = s.create_invoice(
|
||||
cust,
|
||||
date,
|
||||
date + Days::new(30),
|
||||
vec![DocumentLine {
|
||||
description: [
|
||||
"Brand identity",
|
||||
"Website design",
|
||||
"Packaging",
|
||||
"Illustration set",
|
||||
"Design retainer",
|
||||
][rng.pick(5) as usize]
|
||||
.into(),
|
||||
amount: amt,
|
||||
}],
|
||||
)?;
|
||||
let entry = s.ledger().get_invoice(inv).map(|i| i.entry_id);
|
||||
if let Some(e) = entry {
|
||||
let _ = e;
|
||||
}
|
||||
open_invoices.push((inv, date, amt));
|
||||
}
|
||||
// Collect invoices 20-50 days out; leave the latest ones open and one overdue.
|
||||
let mut still_open = Vec::new();
|
||||
for (inv, date, amt) in open_invoices.drain(..) {
|
||||
let paid_on = (date + Days::new(20 + rng.pick(30))).max(day(m, 2));
|
||||
let overdue_one = month_index == 12 && inv % 2 == 0;
|
||||
if paid_on <= today && paid_on < m + Months::new(1) && !overdue_one {
|
||||
s.receive_payment(inv, paid_on, amt, checking)?;
|
||||
} else {
|
||||
still_open.push((inv, date, amt));
|
||||
}
|
||||
}
|
||||
open_invoices = still_open;
|
||||
// Template sales, paid out by the platform on the 1st for last month.
|
||||
if month_index > 1 {
|
||||
let date = day(m, 1);
|
||||
if in_range(date) {
|
||||
let gross = rng.amount(450, 1600);
|
||||
let fee = (gross * Decimal::new(10, 2)).round_dp(2) + Decimal::new(30, 2);
|
||||
let net = gross - fee;
|
||||
let e = s.create_entry(date, "Gumroad payout — template sales")?;
|
||||
s.add_line(e, checking, Side::Debit, usd(net))?;
|
||||
s.add_line(e, fees, Side::Debit, usd(fee))?;
|
||||
s.add_line(e, templates, Side::Credit, usd(gross))?;
|
||||
for i in 0..3 {
|
||||
s.set_line_class(e, i, Some(template_class))?;
|
||||
}
|
||||
s.attach_receipt(
|
||||
Some(e),
|
||||
"gumroad-payout.pdf",
|
||||
"application/pdf",
|
||||
&receipt_pdf(
|
||||
"Gumroad payout",
|
||||
&[
|
||||
format!("Payout {}", date.format("%B %-d, %Y")),
|
||||
format!("Gross sales: ${gross}"),
|
||||
format!("Fees: ${fee}"),
|
||||
format!("TOTAL ${net}"),
|
||||
"Deposited to checking ending 0187".into(),
|
||||
],
|
||||
),
|
||||
None,
|
||||
)?;
|
||||
s.post_entry(e)?;
|
||||
}
|
||||
}
|
||||
// Contractors.
|
||||
if month_index % 2 == 0 && month_index > 0 {
|
||||
let date = day(m, 28);
|
||||
if in_range(date) {
|
||||
let amt = (rng.amount(900, 2200) / dec(25)).round() * dec(25);
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Sam Okafor — illustration (invoice)",
|
||||
contract,
|
||||
checking,
|
||||
amt,
|
||||
Some((
|
||||
"sam-okafor-invoice.pdf",
|
||||
receipt(
|
||||
"Sam Okafor Illustration",
|
||||
date,
|
||||
"Illustration for client projects",
|
||||
amt,
|
||||
"ACH",
|
||||
),
|
||||
)),
|
||||
Some(sam),
|
||||
Some(client_class),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
if month_index == 11 || month_index == 13 {
|
||||
let date = day(m, 19);
|
||||
if in_range(date) {
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Riley Chen — front-end development",
|
||||
contract,
|
||||
checking,
|
||||
dec(1400),
|
||||
Some((
|
||||
"riley-chen.pdf",
|
||||
receipt(
|
||||
"Riley Chen",
|
||||
date,
|
||||
"Website build, milestone",
|
||||
dec(1400),
|
||||
"ACH",
|
||||
),
|
||||
)),
|
||||
Some(riley),
|
||||
Some(client_class),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
// Owner draws and savings.
|
||||
if month_index >= 3 {
|
||||
let date = day(m, 28);
|
||||
if in_range(date) {
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Owner draw",
|
||||
draws,
|
||||
checking,
|
||||
dec(3500),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
if month_index % 3 == 2 {
|
||||
let date = day(m, 26);
|
||||
if in_range(date) {
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Transfer to savings",
|
||||
savings,
|
||||
checking,
|
||||
dec(4000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
let date = day(m, 30);
|
||||
if in_range(date) && month_index % 5 == 4 {
|
||||
post(
|
||||
&mut s,
|
||||
date,
|
||||
"Wire fee",
|
||||
bank_fees,
|
||||
checking,
|
||||
dec(30),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
// Month-end: rent occurrences, depreciation, amortization.
|
||||
let month_end = m + Months::new(1) - Days::new(1);
|
||||
let through = if current { today } else { month_end };
|
||||
for (_, date, entry) in s.run_recurring(through)? {
|
||||
if current {
|
||||
continue; // this month's rent waits as a draft
|
||||
}
|
||||
s.attach_receipt(
|
||||
Some(entry),
|
||||
&format!("rent-{date}.pdf"),
|
||||
"application/pdf",
|
||||
&receipt(
|
||||
"Everett Lofts LLC",
|
||||
date,
|
||||
"Studio 4B rent",
|
||||
dec(1800),
|
||||
"ACH",
|
||||
),
|
||||
None,
|
||||
)?;
|
||||
s.post_entry(entry)?;
|
||||
s.set_entry_vendor(entry, Some(landlord))?;
|
||||
}
|
||||
if !current {
|
||||
if m >= laptop_date.with_day(1).expect("valid") {
|
||||
s.run_depreciation(asset, &m.format("%Y-%m").to_string())?;
|
||||
}
|
||||
s.run_schedules(month_end)?;
|
||||
}
|
||||
// Close each finished calendar year into capital, draws swept in.
|
||||
if m.month() == 12 && !current {
|
||||
let ys = NaiveDate::from_ymd_opt(m.year(), 1, 1).expect("valid");
|
||||
let p = s.create_period(&m.year().to_string(), ys.max(start), month_end)?;
|
||||
s.close_period_to(
|
||||
p,
|
||||
&CloseTo::Account {
|
||||
into: capital,
|
||||
sweep: vec![draws],
|
||||
},
|
||||
)?;
|
||||
}
|
||||
m = m + Months::new(1);
|
||||
month_index += 1;
|
||||
}
|
||||
let _ = (bank_fees, client_class);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user