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
797 lines
26 KiB
Rust
797 lines
26 KiB
Rust
//! 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(())
|
|
}
|