P3: party totals by what moved the money, once (defects 14, 11, 24)
Each party's part of an entry is signed as its own account would show it, with a direction and its cash and in-kind amounts; a reversal is its original negated. Totals on a cash or all basis: for a member, all equals the members' equity statement and cash the K-1's cash. Money is cash, cards and the financing accounts, and a liability credit never pays its own creditor (creditor_of), so accruals and declared distributions count once. tin_last4 from the TIN or W-9 facts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
52 files changed
+7535
-560
No files matched your search
@@ -94,8 +94,9 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
|
||||
};
|
||||
ui.add_space(4.0);
|
||||
let mut text = format!("{} {}", super::parties::party_icon(party), party.label());
|
||||
if several {
|
||||
text.push_str(&format!(" · {}", share_text(share)));
|
||||
let what = share_text(share, several, e.reversal_of);
|
||||
if !what.is_empty() {
|
||||
text.push_str(&format!(" · {what}"));
|
||||
}
|
||||
if widgets::link(ui, RichText::new(text).color(p.accent))
|
||||
.on_hover_text(format!(
|
||||
@@ -511,23 +512,71 @@ pub fn render(ui: &mut Ui, b: &Books, a: &Attachment, shell: &mut Shell<'_>, wid
|
||||
}
|
||||
}
|
||||
|
||||
/// One party's share of an entry, in words: "$50.00 (derived)", "$70.00",
|
||||
/// "$33.34 (33.333%)", or the share alone when it doesn't resolve.
|
||||
pub fn share_text(s: &crate::books::EntryParty) -> String {
|
||||
let how = match s.share {
|
||||
ledger_core::Share::Amount(_) => None,
|
||||
ledger_core::Share::Percent(p) => Some(format!("{}%", p.normalize())),
|
||||
ledger_core::Share::Derived => Some("derived".to_string()),
|
||||
/// A party's part of an entry in words (rules.md B-35): which way it
|
||||
/// went and how much ("paid $64.00", "received $740.00, in kind"), what of
|
||||
/// it is still owed, the share it came from when the entry names several
|
||||
/// ("(50%)", "(derived)"), and a reversal said as one ("paid $1,480.00
|
||||
/// (reversal of #12)"), so a property contribution and its reversal never
|
||||
/// read alike. A part that moved nothing with the party (a bill, a close)
|
||||
/// says so. Empty for a sole party whose part doesn't resolve.
|
||||
pub fn share_text(
|
||||
s: &crate::books::EntryParty,
|
||||
several: bool,
|
||||
reversal_of: Option<ledger_core::EntryId>,
|
||||
) -> String {
|
||||
use ledger_core::shares::Direction;
|
||||
let how = if several {
|
||||
match s.share {
|
||||
ledger_core::Share::Amount(_) => None,
|
||||
ledger_core::Share::Percent(p) => Some(format!("{}%", p.normalize())),
|
||||
ledger_core::Share::Derived => Some("derived".to_string()),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match (s.resolved, how) {
|
||||
(Some(v), Some(h)) => format!("{} ({h})", fmt::dollars(v.abs())),
|
||||
(Some(v), None) => fmt::dollars(v.abs()),
|
||||
(None, Some(h)) => h,
|
||||
(None, None) => match s.share {
|
||||
let mut t = match (s.resolved, s.direction) {
|
||||
(Some(v), Some(d)) => {
|
||||
let amount = fmt::dollars(v.abs());
|
||||
let mut t = match d {
|
||||
Direction::Paid => format!("paid {amount}"),
|
||||
Direction::Received => format!("received {amount}"),
|
||||
Direction::Neither => format!("{amount}, no money moved"),
|
||||
};
|
||||
let cash = s.cash.unwrap_or_default();
|
||||
let in_kind = s.in_kind.unwrap_or_default();
|
||||
if !in_kind.is_zero() {
|
||||
if in_kind == v {
|
||||
t.push_str(", in kind");
|
||||
} else {
|
||||
t.push_str(&format!(", {} in kind", fmt::dollars(in_kind.abs())));
|
||||
}
|
||||
}
|
||||
let owed = v - cash - in_kind;
|
||||
if d != Direction::Neither && !owed.is_zero() {
|
||||
t.push_str(&format!(", {} still owed", fmt::dollars(owed.abs())));
|
||||
}
|
||||
t
|
||||
}
|
||||
_ if several => match s.share {
|
||||
ledger_core::Share::Amount(a) => fmt::dollars(a),
|
||||
_ => String::new(),
|
||||
},
|
||||
_ => String::new(),
|
||||
};
|
||||
let mut notes: Vec<String> = how.into_iter().collect();
|
||||
if s.resolved.is_some()
|
||||
&& let Some(o) = reversal_of
|
||||
{
|
||||
notes.push(format!("reversal of #{o}"));
|
||||
}
|
||||
if !notes.is_empty() {
|
||||
if t.is_empty() {
|
||||
t = notes.join(", ");
|
||||
} else {
|
||||
t.push_str(&format!(" ({})", notes.join(", ")));
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -483,6 +483,21 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
};
|
||||
ev(icon::USER_GEAR, text, pid.map(Route::Party))
|
||||
}
|
||||
"party.creditor_set" => {
|
||||
let pid = p.get("id").and_then(|v| v.as_u64());
|
||||
let name = party_name(b, pid);
|
||||
let account = p
|
||||
.get("account_id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|a| b.account_name(a))
|
||||
.unwrap_or_else(|| "an account".to_string());
|
||||
let text = if p.get("on").and_then(|v| v.as_bool()).unwrap_or(true) {
|
||||
format!("{name} is now owed on {account}")
|
||||
} else {
|
||||
format!("{name} is no longer owed on {account}")
|
||||
};
|
||||
ev(icon::USER_GEAR, text, pid.map(Route::Party))
|
||||
}
|
||||
"party.w9_set" => {
|
||||
let pid = p.get("id").and_then(|v| v.as_u64());
|
||||
let name = party_name(b, pid);
|
||||
|
||||
@@ -564,7 +564,7 @@ pub fn show_list(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(format!(
|
||||
"{period_label}: Paid out is what each party's posted entries moved out of cash, cards, and what's owed back to whoever paid for the company; Received in is what came back in; Net is paid out less received (a plus means more came in). Entries counts the posted ones in the period. The 1099 amount, on each party's page, is the 1099 worksheet's."
|
||||
"{period_label}, cash basis: Paid out is the money each party's posted entries paid it, from cash, a card, or a member or lender paying for the company; Received in is the money that came in from it; Net is paid out less received (a plus means more came in). Property contributed or distributed in kind is on each party's page. Entries counts the posted ones in the period. The 1099 amount, on each party's page, is the 1099 worksheet's."
|
||||
))
|
||||
.font(theme::regular(11.5))
|
||||
.color(p.faint),
|
||||
@@ -577,6 +577,21 @@ pub fn show_list(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The party's totals with property in kind (basis all, rules.md B-35
|
||||
/// item 4), when it gave or took any in the period: "With property
|
||||
/// contributed or distributed: paid $X, received $Y". None when nothing
|
||||
/// moved in kind (the cash totals are then the whole story).
|
||||
pub fn in_kind_text(flow: &crate::books::PartyFlow) -> Option<String> {
|
||||
if flow.paid_in_kind.is_zero() && flow.received_in_kind.is_zero() {
|
||||
return None;
|
||||
}
|
||||
Some(format!(
|
||||
"With property contributed or distributed: paid {}, received {}",
|
||||
fmt::dollars(flow.paid + flow.paid_in_kind),
|
||||
fmt::dollars(flow.received + flow.received_in_kind)
|
||||
))
|
||||
}
|
||||
|
||||
/// A number tile on a party's page: label, amount, how it's computed,
|
||||
/// and the line under it.
|
||||
/// (label, value, how, sub); the value None when it can't be computed
|
||||
@@ -673,14 +688,14 @@ pub fn show_party(ui: &mut Ui, b: &Books, id: PartyId, shell: &mut Shell<'_>) {
|
||||
tiles.push((
|
||||
"Paid out".into(),
|
||||
Some(flow.paid),
|
||||
format!("{label}, {span_words}: what this party's posted entries moved out of cash, cards, and what's owed back to whoever paid for the company (net of refunds and reversals of payments)."),
|
||||
Some((label.clone(), p.muted)),
|
||||
format!("{label}, {span_words}, cash basis: the money this party's posted entries paid it, from cash, a card, or a member or lender paying for the company (net of reversals of payments; a refund is received in). A bill counts once it is paid."),
|
||||
Some((format!("{label} · cash basis"), p.muted)),
|
||||
));
|
||||
tiles.push((
|
||||
"Received in".into(),
|
||||
Some(flow.received),
|
||||
format!("{label}, {span_words}: what this party's posted entries brought into cash and those accounts (net of reversals)."),
|
||||
Some((label.clone(), p.muted)),
|
||||
format!("{label}, {span_words}, cash basis: the money this party's posted entries brought in (net of reversals)."),
|
||||
Some((format!("{label} · cash basis"), p.muted)),
|
||||
));
|
||||
let net = flow.net();
|
||||
tiles.push((
|
||||
@@ -771,6 +786,21 @@ pub fn show_party(ui: &mut Ui, b: &Books, id: PartyId, shell: &mut Shell<'_>) {
|
||||
r.context_menu(|ui| widgets::copy_item(ui, "Copy amount", &v.to_string()));
|
||||
}
|
||||
});
|
||||
// Basis all (rules.md B-35 item 4): property in kind, when any.
|
||||
if let Some(text) = in_kind_text(&flow) {
|
||||
ui.add_space(6.0);
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(text)
|
||||
.font(theme::regular(12.5))
|
||||
.color(p.muted),
|
||||
)
|
||||
.wrap(),
|
||||
)
|
||||
.on_hover_text(
|
||||
"Basis all (books.party basis \"all\"): the money above plus property contributed or distributed in kind, as the statement of members' equity counts it.",
|
||||
);
|
||||
}
|
||||
|
||||
// A member's page is their capital account statement too.
|
||||
if let Some(st) = b.capital_statement(id, span) {
|
||||
@@ -939,6 +969,23 @@ fn facts_card(ui: &mut Ui, b: &Books, party: &Party, go: &mut Option<Route>) {
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
if !party.creditor_of.is_empty() {
|
||||
k(ui, "Owed on");
|
||||
ui.vertical(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
for &a in &party.creditor_of {
|
||||
if widgets::link(ui, RichText::new(b.account_name(a)).color(p.accent))
|
||||
.on_hover_text(
|
||||
"What the company owes this party itself on (its card, its loan, what the members advanced): a charge or fee credited to it never counts as paying them.",
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
*go = Some(Route::Account(a));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
if let Some(v) = party.vendor {
|
||||
k(ui, "Vendor record");
|
||||
if widgets::link(ui, RichText::new(format!("vendor {v}")).color(p.accent))
|
||||
@@ -1229,7 +1276,7 @@ fn entries_card(ui: &mut Ui, b: &Books, info: &PartyInfo, go: &mut Option<Route>
|
||||
.collect();
|
||||
sub = format!(
|
||||
"Their share, {} of {}, with {} · {sub}",
|
||||
super::entry::share_text(mine),
|
||||
super::entry::share_text(mine, true, e.reversal_of),
|
||||
fmt::dollars_or(e.amount),
|
||||
others.join(" & ")
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user