P2 slice 6: the viewer
`wanderbooks gui [BOOK]` finds the data root, reads settings from the journal, the vault of the book, presence through ops/, Save a copy / Open on every document, Save the W-9, the Export menu, and colour schemes. One builder for the CPA package and the 1099 worksheet (ledger-package), shared by the server and the viewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
1 parent
b26c099a68
commit
5bc0633cdd
77 files changed
+5833
-2427
No files matched your search
@@ -0,0 +1,615 @@
|
||||
//! The CPA handoff package: every statement, ledger, and worksheet the CPA
|
||||
//! needs for the return, with the evidence behind the year's entries, in
|
||||
//! one zip.
|
||||
//!
|
||||
//! ```text
|
||||
//! GOAL: everything the CPA needs for the return in one zip, the K-1s'
|
||||
//! and 1099s' TINs included when asked, built the same by the
|
||||
//! server and the viewer.
|
||||
//! SOURCE: docs/mcp-interface.md; B-26; DECISION (owner, 2026-10-15):
|
||||
//! "it needs to be in the damn CPA package."
|
||||
//! INVARIANTS:
|
||||
//! 1. The statements (profit and loss, balance sheet, cash flow, members'
|
||||
//! equity with members, tax summary, trial balance) each as PDF and
|
||||
//! XLSX, for the fiscal year containing as_of, through as_of; the
|
||||
//! fixed asset register when there are fixed assets; the general
|
||||
//! ledger and journal (XLSX); the journal as IIF; the 1099 worksheet
|
||||
//! for as_of's calendar year (or why it can't be made); with members,
|
||||
//! the K-1 worksheet; a README first.
|
||||
//! 2. With members, k1-members.csv has one row per member: name, party,
|
||||
//! share, capital account, TIN and type (or last four without a TIN
|
||||
//! source), and the K-1 worksheet's capital figures (ledger_core::k1)
|
||||
//! for the fiscal year to date; the members' total row with more than
|
||||
//! one. The 1099 worksheet adds each payee's party, last four, TIN,
|
||||
//! and type with a TIN source.
|
||||
//! 3. A TIN the source can't give never fails the package: the row says
|
||||
//! why, and so do the README and tin_gaps (members, and 1099 payees
|
||||
//! that are reportable).
|
||||
//! 4. With include_receipts: every current file behind the year's
|
||||
//! posted entries, filed on them or linked to them, once per entry,
|
||||
//! each with a row in receipts/index.csv; and the company's
|
||||
//! documents, the files behind no entry (loose receipts aside).
|
||||
//! Withdrawn files never.
|
||||
//! 5. Read-only (crate INVARIANT 1): the package is returned, its files
|
||||
//! and the zip, and written nowhere.
|
||||
//! ```
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::{AccountId, Side, reports};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::books::PackageBooks;
|
||||
use crate::iif::export_iif;
|
||||
use crate::reports::{
|
||||
Settings, bs_statement, cash_flow_for, member_capitals, pnl_statement, report_members,
|
||||
report_meta, year_earnings,
|
||||
};
|
||||
use crate::text::{csv_escape, money, share_text};
|
||||
use crate::tins::{TinSource, tin_type_key};
|
||||
use crate::vendor_1099::vendor_1099_report;
|
||||
|
||||
/// What the package is built for.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PackageRequest<'a> {
|
||||
/// The day it is as of: the balance sheet's date, the end of every
|
||||
/// period report (the fiscal year to date).
|
||||
pub as_of: NaiveDate,
|
||||
/// The day it is prepared, for the reports' headers.
|
||||
pub prepared_on: NaiveDate,
|
||||
/// Cash account codes the cash flow statement reconciles to (empty:
|
||||
/// the settings' cash accounts).
|
||||
pub cash_accounts: &'a [String],
|
||||
/// Financing account codes (empty: the settings' financing accounts).
|
||||
pub financing_accounts: &'a [String],
|
||||
/// Investing account codes (empty: the settings' investing accounts).
|
||||
pub investing_accounts: &'a [String],
|
||||
/// Whether the receipts and the company's documents go in
|
||||
/// (INVARIANT 4).
|
||||
pub include_receipts: bool,
|
||||
/// Where the worksheets' full TINs come from: none, last fours only.
|
||||
pub tins: Option<TinSource<'a>>,
|
||||
}
|
||||
|
||||
/// A built package.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Package {
|
||||
/// The zip's name: `cpa-package-<as_of>.zip`.
|
||||
pub file_name: String,
|
||||
/// Every file in the zip, in its order: (name in the zip, bytes).
|
||||
pub files: Vec<(String, Vec<u8>)>,
|
||||
/// The zip.
|
||||
pub zip: Vec<u8>,
|
||||
/// With a TIN source: each member or reportable 1099 payee whose TIN
|
||||
/// the package lacks, and why (INVARIANT 3).
|
||||
pub tin_gaps: Vec<String>,
|
||||
/// The receipts in receipts/ (one per entry they are behind).
|
||||
pub receipt_count: usize,
|
||||
}
|
||||
|
||||
/// The package's file name for `as_of`.
|
||||
pub fn package_file_name(as_of: NaiveDate) -> String {
|
||||
format!("cpa-package-{as_of}.zip")
|
||||
}
|
||||
|
||||
/// Build the CPA package from `books` (INVARIANTS 1-5).
|
||||
pub fn build_package<B: PackageBooks + ?Sized>(
|
||||
books: &B,
|
||||
settings: &Settings<'_>,
|
||||
req: &PackageRequest<'_>,
|
||||
) -> Result<Package, String> {
|
||||
use ledger_import::{XlsxReport, report, report_to_xlsx, statement_pdf};
|
||||
let company = settings.company;
|
||||
let (as_of, today) = (req.as_of, req.prepared_on);
|
||||
let meta = report_meta(company, req.prepared_on, books.journal_seq());
|
||||
let ledger = books.ledger();
|
||||
let tins = req.tins;
|
||||
let mut tin_gaps: Vec<String> = Vec::new();
|
||||
|
||||
// Fiscal year to date, per the company's fiscal_year_start.
|
||||
let (fy_start, _) = company.fiscal_year_containing(as_of);
|
||||
let (fy_start, as_of_s) = (fy_start.to_string(), as_of.to_string());
|
||||
let xlsx = |r: XlsxReport<'_>| report_to_xlsx(&r, &meta).map_err(|e| e.to_string());
|
||||
let pdf = |st: &ledger_import::Statement| statement_pdf(st, &meta).map_err(|e| e.to_string());
|
||||
let fy = NaiveDate::parse_from_str(&fy_start, "%Y-%m-%d").map_err(|e| e.to_string())?;
|
||||
let members = report_members(company);
|
||||
|
||||
// The financial statements, each as PDF and XLSX.
|
||||
let mut statements: Vec<(&str, ledger_import::Statement)> = vec![
|
||||
(
|
||||
"profit-and-loss",
|
||||
report::profit_and_loss(&pnl_statement(
|
||||
ledger,
|
||||
Some(&fy_start),
|
||||
Some(&as_of_s),
|
||||
None,
|
||||
today,
|
||||
)?),
|
||||
),
|
||||
(
|
||||
"balance-sheet",
|
||||
report::balance_sheet(
|
||||
&bs_statement(ledger, Some(&as_of_s), today)?,
|
||||
year_earnings(ledger, company, as_of)?,
|
||||
&members,
|
||||
),
|
||||
),
|
||||
(
|
||||
"cash-flow",
|
||||
report::cash_flow(
|
||||
&cash_flow_for(
|
||||
ledger,
|
||||
settings,
|
||||
Some(&fy_start),
|
||||
Some(&as_of_s),
|
||||
req.cash_accounts,
|
||||
req.financing_accounts,
|
||||
req.investing_accounts,
|
||||
today,
|
||||
)?,
|
||||
&members,
|
||||
),
|
||||
),
|
||||
];
|
||||
let mut k1_csv: Option<(String, Vec<u8>)> = None;
|
||||
if !company.members.is_empty() {
|
||||
let capitals = member_capitals(ledger, &company.members)?;
|
||||
let me =
|
||||
ledger_core::members_equity(ledger, fy, as_of, &capitals).map_err(|e| e.to_string())?;
|
||||
statements.push(("members-equity", report::members_equity(&me)));
|
||||
// The K-1 worksheet: each member's capital account for the
|
||||
// year (Schedule K-1 Item L) and their TIN (INVARIANT 2).
|
||||
let cash_codes = if req.cash_accounts.is_empty() {
|
||||
settings.cash_accounts
|
||||
} else {
|
||||
req.cash_accounts
|
||||
};
|
||||
let cash: Vec<AccountId> = cash_codes
|
||||
.iter()
|
||||
.filter_map(|c| ledger.chart().find_by_code(c).map(|a| a.id))
|
||||
.collect();
|
||||
k1_csv = Some(
|
||||
match ledger_core::k1::k1_worksheet(ledger, fy, as_of, &capitals, &cash)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|rows| {
|
||||
// A row per member, in their order, or none: a row is
|
||||
// never paired with another member.
|
||||
if rows.len() == company.members.len() {
|
||||
Ok(rows)
|
||||
} else {
|
||||
Err(format!(
|
||||
"the K-1 worksheet has {} rows for {} members",
|
||||
rows.len(),
|
||||
company.members.len()
|
||||
))
|
||||
}
|
||||
}) {
|
||||
Ok(rows) => {
|
||||
let mut w = String::from("member,party_id,share,capital_account,");
|
||||
w.push_str(if tins.is_some() {
|
||||
"tin,tin_type,tin_note,"
|
||||
} else {
|
||||
"tin_last4,tin_type,"
|
||||
});
|
||||
w.push_str("beginning_capital,contributions_cash,contributions_property,net_income,distributions,other,ending_capital\n");
|
||||
for (row, m) in rows.iter().zip(&company.members) {
|
||||
let party = ledger_core::k1::member_party(ledger, &m.name);
|
||||
let facts = party
|
||||
.and_then(|id| ledger.get_party(id))
|
||||
.and_then(|p| p.tin.clone());
|
||||
let mut cells = vec![
|
||||
csv_escape(&m.name),
|
||||
party.map(|id| id.to_string()).unwrap_or_default(),
|
||||
share_text(m.share),
|
||||
m.capital_account.clone(),
|
||||
];
|
||||
match tins {
|
||||
Some(tins) => {
|
||||
let got = match party {
|
||||
None => Err(format!(
|
||||
"TIN not in the vault: no party named {:?} with the member role (books.add_party with role member, or books.set_party_roles, then books.set_tin)",
|
||||
m.name
|
||||
)),
|
||||
Some(id) => tins(id),
|
||||
};
|
||||
match got {
|
||||
Ok(t) => {
|
||||
cells.push(t.formatted());
|
||||
cells.push(tin_type_key(t.kind).into());
|
||||
cells.push(String::new());
|
||||
}
|
||||
Err(why) => {
|
||||
tin_gaps.push(format!("member {}: {why}", m.name));
|
||||
cells.push(String::new());
|
||||
cells.push(
|
||||
facts
|
||||
.map(|f| f.tin_type.key().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
cells.push(csv_escape(&why));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
cells.push(
|
||||
facts.as_ref().map(|f| f.last4.clone()).unwrap_or_default(),
|
||||
);
|
||||
cells.push(
|
||||
facts
|
||||
.map(|f| f.tin_type.key().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for v in [
|
||||
row.beginning,
|
||||
row.contributions_cash,
|
||||
row.contributions_property,
|
||||
row.net_income,
|
||||
row.distributions,
|
||||
row.other,
|
||||
row.ending,
|
||||
] {
|
||||
cells.push(money(v));
|
||||
}
|
||||
w.push_str(&cells.join(","));
|
||||
w.push('\n');
|
||||
}
|
||||
// The members together: ties to the 1065's Schedule L
|
||||
// capital and the statement of members' equity. Not
|
||||
// a K-1 (each member gets their own).
|
||||
if rows.len() > 1 {
|
||||
let sum = |f: fn(&ledger_core::k1::K1Row) -> Decimal| {
|
||||
rows.iter().map(f).sum::<Decimal>()
|
||||
};
|
||||
let share: Decimal = company.members.iter().map(|m| m.share).sum();
|
||||
let mut cells = vec![
|
||||
csv_escape("Total, all members (not a K-1)"),
|
||||
String::new(),
|
||||
share_text(share),
|
||||
String::new(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
];
|
||||
if tins.is_some() {
|
||||
cells.push(String::new());
|
||||
}
|
||||
for v in [
|
||||
sum(|r| r.beginning),
|
||||
sum(|r| r.contributions_cash),
|
||||
sum(|r| r.contributions_property),
|
||||
sum(|r| r.net_income),
|
||||
sum(|r| r.distributions),
|
||||
sum(|r| r.other),
|
||||
sum(|r| r.ending),
|
||||
] {
|
||||
cells.push(money(v));
|
||||
}
|
||||
w.push_str(&cells.join(","));
|
||||
w.push('\n');
|
||||
}
|
||||
("k1-members.csv".into(), w.into_bytes())
|
||||
}
|
||||
Err(e) => (
|
||||
"k1-members-NOT-INCLUDED.txt".into(),
|
||||
format!("{e}\n").into_bytes(),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
statements.push((
|
||||
"tax-summary",
|
||||
report::tax_summary(
|
||||
&ledger_core::tax_summary(ledger, fy, as_of, company.tax_form)
|
||||
.map_err(|e| e.to_string())?,
|
||||
),
|
||||
));
|
||||
statements.push((
|
||||
"trial-balance",
|
||||
report::trial_balance(&reports::trial_balance_as_of(ledger, as_of), as_of),
|
||||
));
|
||||
let mut files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for (name, st) in &statements {
|
||||
files.push((format!("{name}.pdf"), pdf(st)?));
|
||||
files.push((format!("{name}.xlsx"), xlsx(XlsxReport::Statement(st))?));
|
||||
}
|
||||
// The fixed asset register, when there are fixed assets.
|
||||
if ledger.registered_assets().next().is_some() {
|
||||
let r = ledger_core::fixed_asset_register(ledger, as_of).map_err(|e| e.to_string())?;
|
||||
files.push((
|
||||
"fixed-asset-register.pdf".into(),
|
||||
ledger_import::asset_register_pdf(&r, &meta).map_err(|e| e.to_string())?,
|
||||
));
|
||||
files.push((
|
||||
"fixed-asset-register.xlsx".into(),
|
||||
ledger_import::asset_register_xlsx(&r, &meta).map_err(|e| e.to_string())?,
|
||||
));
|
||||
}
|
||||
// The books behind them: by account, and entry by entry.
|
||||
let gl = ledger_core::general_ledger(ledger, fy, as_of).map_err(|e| e.to_string())?;
|
||||
files.push((
|
||||
"general-ledger.xlsx".into(),
|
||||
xlsx(XlsxReport::GeneralLedger(&gl))?,
|
||||
));
|
||||
files.push((
|
||||
"general-journal.xlsx".into(),
|
||||
xlsx(XlsxReport::GeneralJournal(ledger, fy, as_of))?,
|
||||
));
|
||||
// 1099 worksheet for the calendar year of as_of.
|
||||
let year = chrono::Datelike::year(&as_of);
|
||||
let mut worksheet_1099 = false;
|
||||
match vendor_1099_report(ledger, settings, year, None) {
|
||||
Ok(r1099) => {
|
||||
let mut w = String::from("vendor,form,w9_on_file,paid,reportable");
|
||||
if tins.is_some() {
|
||||
w.push_str(",party_id,tin_last4,tin,tin_type,tin_note");
|
||||
}
|
||||
w.push('\n');
|
||||
for v in &r1099.vendors {
|
||||
w.push_str(&format!(
|
||||
"\"{}\",{},{},{},{}",
|
||||
v.name.replace('"', "\"\""),
|
||||
v.form.map(|f| f.label()).unwrap_or(""),
|
||||
v.tin_on_file,
|
||||
money(v.paid),
|
||||
v.reportable
|
||||
));
|
||||
// The payee's full TIN from the source (INVARIANT 2).
|
||||
if let Some(tins) = tins {
|
||||
let party = ledger.party_of_vendor(v.vendor_id);
|
||||
let last4 = party.and_then(|id| ledger.get_party(id)).and_then(|p| {
|
||||
p.w9.as_ref()
|
||||
.map(|f| f.tin_last4.clone())
|
||||
.or_else(|| p.tin.as_ref().map(|t| t.last4.clone()))
|
||||
});
|
||||
let got = match party {
|
||||
None => Err("TIN not in the vault: no party for this vendor".into()),
|
||||
Some(id) => tins(id),
|
||||
};
|
||||
let (tin, kind, note) = match got {
|
||||
Ok(t) => (
|
||||
t.formatted(),
|
||||
tin_type_key(t.kind).to_string(),
|
||||
String::new(),
|
||||
),
|
||||
Err(why) => {
|
||||
if v.reportable {
|
||||
tin_gaps.push(format!("1099 payee {}: {why}", v.name));
|
||||
}
|
||||
(String::new(), String::new(), why)
|
||||
}
|
||||
};
|
||||
w.push_str(&format!(
|
||||
",{},{},{tin},{kind},{}",
|
||||
party.map(|id| id.to_string()).unwrap_or_default(),
|
||||
last4.unwrap_or_default(),
|
||||
csv_escape(¬e)
|
||||
));
|
||||
}
|
||||
w.push('\n');
|
||||
}
|
||||
files.push((format!("1099-worksheet-{year}.csv"), w.into_bytes()));
|
||||
worksheet_1099 = true;
|
||||
}
|
||||
// The threshold isn't known for this year: never guess it
|
||||
// (rules.md B-19); say so in the package instead.
|
||||
Err(e) => files.push((
|
||||
format!("1099-worksheet-{year}-NOT-INCLUDED.txt"),
|
||||
format!("{e}\n").into_bytes(),
|
||||
)),
|
||||
}
|
||||
// The evidence: every current file behind the year's posted
|
||||
// entries, filed on them or linked to them (a file linked to an
|
||||
// entry counts as its receipt), one copy per entry with a row in
|
||||
// the index; and the company's documents, the files behind no
|
||||
// entry (INVARIANT 4).
|
||||
let mut receipt_count = 0;
|
||||
let mut document_count = 0;
|
||||
// Each file's name in the zip is its own: a second `r.pdf` is
|
||||
// `r (2).pdf` (compared ignoring case, so it unpacks the same on every
|
||||
// system), and the index names the file as it is stored.
|
||||
let mut used: std::collections::HashSet<String> =
|
||||
files.iter().map(|(n, _)| n.to_lowercase()).collect();
|
||||
let mut unique = |name: String| -> String {
|
||||
let (stem, ext) = match name.rsplit_once('.') {
|
||||
Some((s, e)) if !s.ends_with('/') && !s.is_empty() => (s.to_string(), format!(".{e}")),
|
||||
_ => (name.clone(), String::new()),
|
||||
};
|
||||
let mut n = 1;
|
||||
let mut candidate = name;
|
||||
while !used.insert(candidate.to_lowercase()) {
|
||||
n += 1;
|
||||
candidate = format!("{stem} ({n}){ext}");
|
||||
}
|
||||
candidate
|
||||
};
|
||||
if req.include_receipts {
|
||||
let clean = |s: &str| -> String {
|
||||
s.chars()
|
||||
.map(|c| {
|
||||
if c.is_alphanumeric() || ".-_ ".contains(c) {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let mut index = String::from("entry_id,date,description,amount,file,sha256\n");
|
||||
for a in books.all_attachments().map_err(|e| e.to_string())? {
|
||||
if a.withdrawn_at.is_some() {
|
||||
continue;
|
||||
}
|
||||
let mut behind: Vec<u64> = a.entry_id.into_iter().collect();
|
||||
for sub in books.links_of(a.id).map_err(|e| e.to_string())? {
|
||||
if let ledger_store::Subject::Entry(e) = sub
|
||||
&& !behind.contains(&e)
|
||||
{
|
||||
behind.push(e);
|
||||
}
|
||||
}
|
||||
behind.sort_unstable();
|
||||
let bytes = books.attachment_bytes(a.id).map_err(|e| e.to_string())?;
|
||||
if behind.is_empty() {
|
||||
if a.category != "receipts" {
|
||||
files.push((
|
||||
unique(format!(
|
||||
"documents/{}/{}",
|
||||
clean(&a.category),
|
||||
clean(&a.file_name)
|
||||
)),
|
||||
bytes,
|
||||
));
|
||||
document_count += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for e in behind.iter().filter_map(|&e| ledger.get_entry(e)) {
|
||||
if e.status() != ledger_core::EntryStatus::Posted
|
||||
|| e.date() < fy
|
||||
|| e.date() > as_of
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let name = unique(format!(
|
||||
"receipts/{}-entry-{}-{}",
|
||||
e.date(),
|
||||
e.id(),
|
||||
clean(&a.file_name)
|
||||
));
|
||||
let amount: Decimal = e
|
||||
.lines()
|
||||
.iter()
|
||||
.filter(|l| l.side == Side::Debit)
|
||||
.map(|l| l.amount.amount)
|
||||
.sum();
|
||||
index.push_str(&format!(
|
||||
"{},{},\"{}\",{},{},{}\n",
|
||||
e.id(),
|
||||
e.date(),
|
||||
e.description().replace('"', "\"\""),
|
||||
money(amount),
|
||||
name,
|
||||
a.blob_sha
|
||||
));
|
||||
files.push((name, bytes.clone()));
|
||||
receipt_count += 1;
|
||||
}
|
||||
}
|
||||
files.push(("receipts/index.csv".into(), index.into_bytes()));
|
||||
}
|
||||
let k1_included = k1_csv
|
||||
.as_ref()
|
||||
.is_some_and(|(name, _)| name == "k1-members.csv");
|
||||
if let Some(k1) = k1_csv {
|
||||
files.push(k1);
|
||||
}
|
||||
let seq = books.journal_seq();
|
||||
files.push((
|
||||
"general-journal.iif".into(),
|
||||
export_iif(ledger, settings.cash_accounts)?.into_bytes(),
|
||||
));
|
||||
let include_tins = tins.is_some();
|
||||
let readme = format!(
|
||||
"{name} — books package for the CPA\n\nFiscal year to date: {fy_start} through {as_of_s}. {basis} basis.\nKept in wanderbooks: double-entry books from an append-only, hash-chained journal ({seq} events, verified).\n\nStatements (PDF and XLSX): profit and loss, balance sheet, cash flow, {members}tax summary, trial balance.\nLedgers (XLSX): general ledger by account; general journal entry by entry.\ngeneral-journal.iif: every posted entry, for import into QuickBooks Desktop.\n{worksheet}{k1}{receipts}{tins}\nThe tax summary maps accounts to return categories; it's a worksheet, not a filing. Questions go to the owners, who can ask their bookkeeping agent to trace any number to its entries and receipts.\n",
|
||||
name = company.name,
|
||||
basis = company.basis,
|
||||
members = if company.members.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"members' equity, "
|
||||
},
|
||||
worksheet = if worksheet_1099 {
|
||||
format!(
|
||||
"1099-worksheet-{year}.csv: cash paid per vendor marked for a 1099, against the year's threshold{}.\n",
|
||||
if include_tins {
|
||||
", with each payee's TIN"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"1099-worksheet-{year}-NOT-INCLUDED.txt: why the year's 1099 worksheet isn't here.\n"
|
||||
)
|
||||
},
|
||||
k1 = if company.members.is_empty() {
|
||||
String::new()
|
||||
} else if !k1_included {
|
||||
"k1-members-NOT-INCLUDED.txt: why the members' K-1 worksheet isn't here.\n".to_string()
|
||||
} else {
|
||||
let mut k = format!(
|
||||
"k1-members.csv: each member's capital account for the year (Schedule K-1 Item L: beginning, contributions in cash and in property, share of income, distributions, ending), with their {}{}.\n",
|
||||
if include_tins {
|
||||
"taxpayer ID"
|
||||
} else {
|
||||
"taxpayer ID's last four"
|
||||
},
|
||||
if company.members.len() > 1 {
|
||||
"; the last row totals the members (not a K-1) to tie to Schedule L and the statement of members' equity"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
let members = &company.members;
|
||||
for (i, m) in members.iter().enumerate() {
|
||||
if let Some(j) = &m.files_jointly_with
|
||||
&& members[..i].iter().all(|o| o.name != *j)
|
||||
{
|
||||
k.push_str(&format!(
|
||||
"{} and {j} file a joint individual return: each receives their own Schedule K-1; both are reported on the one Form 1040 (Schedule E, Part II).\n",
|
||||
m.name
|
||||
));
|
||||
}
|
||||
}
|
||||
k
|
||||
},
|
||||
tins = if include_tins {
|
||||
let mut t = String::from(
|
||||
"\nThis package holds full taxpayer identification numbers: keep it out of email and shared folders, and delete copies once filed.\n",
|
||||
);
|
||||
if !tin_gaps.is_empty() {
|
||||
t.push_str("TINs not included:\n");
|
||||
for g in &tin_gaps {
|
||||
t.push_str(&format!("- {g}\n"));
|
||||
}
|
||||
}
|
||||
t
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
receipts = if req.include_receipts {
|
||||
format!(
|
||||
"receipts/: the {receipt_count} receipts behind the period's entries; index.csv ties each to its entry, date, amount, and SHA-256.\n{}",
|
||||
if document_count > 0 {
|
||||
"documents/: the company's formation, legal, and tax documents.\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
);
|
||||
files.insert(0, ("README.txt".into(), readme.into_bytes()));
|
||||
|
||||
// Zip it.
|
||||
let mut zip_buf = Vec::new();
|
||||
{
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut zip_buf));
|
||||
let opts = zip::write::SimpleFileOptions::default();
|
||||
for (name, bytes) in &files {
|
||||
zip.start_file(name.as_str(), opts)
|
||||
.map_err(|e| e.to_string())?;
|
||||
use std::io::Write;
|
||||
zip.write_all(bytes).map_err(|e| e.to_string())?;
|
||||
}
|
||||
zip.finish().map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(Package {
|
||||
file_name: package_file_name(as_of),
|
||||
files,
|
||||
zip: zip_buf,
|
||||
tin_gaps,
|
||||
receipt_count,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user