P3: rename accounts, one set of period defaults, tax categories warn or refuse (defects 15, 16, 8)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
books.rename_account; account names trimmed, bounded, refused when a sibling's or TIN-shaped; books.show refuses what its target ignores. A report asked for without dates covers this fiscal year so far, everywhere (one rule for tools, descriptions and the viewer; the IIF and CPA package keep their own dates). policy.require_tax_category warns or refuses; a Form 4797 gain is shown apart on every return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
69 files changed
+8063
-570
No files matched your search
@@ -22,11 +22,37 @@ use rust_decimal::Decimal;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
/// Limit the register to these dates (set by drill-downs).
|
||||
pub range: Option<(NaiveDate, NaiveDate)>,
|
||||
/// Which dates the register shows.
|
||||
pub range: RegisterRange,
|
||||
pub show_empty: bool,
|
||||
}
|
||||
|
||||
/// Which dates an account's register shows.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RegisterRange {
|
||||
/// What a register opens on when nobody chose dates: the fiscal year
|
||||
/// to date (rules.md B-30, `Books::default_period`), its opening
|
||||
/// balance carried in.
|
||||
#[default]
|
||||
Default,
|
||||
/// Every date: the books' first entry through the later of today
|
||||
/// and the last ("Show all dates", `Books::all_dates`).
|
||||
All,
|
||||
/// These dates: a drill-down's, or a `books.show` target's.
|
||||
Dates(NaiveDate, NaiveDate),
|
||||
}
|
||||
|
||||
impl RegisterRange {
|
||||
/// The first and last day shown.
|
||||
pub fn dates(self, b: &Books) -> (NaiveDate, NaiveDate) {
|
||||
match self {
|
||||
RegisterRange::Default => b.default_period(),
|
||||
RegisterRange::All => b.all_dates(),
|
||||
RegisterRange::Dates(s, e) => (s, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_chart(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
let p = pal(ui.ctx());
|
||||
page(ui, "accounts", |ui| {
|
||||
@@ -152,7 +178,7 @@ pub fn show_chart(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
},
|
||||
);
|
||||
if let Some(id) = go {
|
||||
shell.state.accounts.range = None;
|
||||
shell.state.accounts.range = RegisterRange::Default;
|
||||
shell.go(Route::Account(id));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
@@ -165,12 +191,7 @@ pub fn show_account(ui: &mut Ui, b: &Books, id: u64, shell: &mut Shell<'_>) {
|
||||
let Some(a) = b.ledger.chart().get(id) else {
|
||||
return;
|
||||
};
|
||||
let first = b.kpis.first_entry.unwrap_or(b.today);
|
||||
let (s, e) = shell
|
||||
.state
|
||||
.accounts
|
||||
.range
|
||||
.unwrap_or((first.min(b.today), b.today));
|
||||
let (s, e) = shell.state.accounts.range.dates(b);
|
||||
page(ui, "account", |ui| {
|
||||
widgets::copyable(
|
||||
ui,
|
||||
@@ -226,8 +247,14 @@ pub fn show_account(ui: &mut Ui, b: &Books, id: u64, shell: &mut Shell<'_>) {
|
||||
))
|
||||
.font(theme::semibold(13.5)),
|
||||
);
|
||||
if shell.state.accounts.range.is_some() && ui.button("Show all dates").clicked() {
|
||||
shell.state.accounts.range = None;
|
||||
// "Show all dates" one click away from any other range; from
|
||||
// all dates, back to the fiscal year to date.
|
||||
if shell.state.accounts.range == RegisterRange::All {
|
||||
if ui.button("Fiscal year to date").clicked() {
|
||||
shell.state.accounts.range = RegisterRange::Default;
|
||||
}
|
||||
} else if ui.button("Show all dates").clicked() {
|
||||
shell.state.accounts.range = RegisterRange::All;
|
||||
}
|
||||
if ui
|
||||
.button(format!("{} Find in transactions", icon::LIST_BULLETS))
|
||||
@@ -238,8 +265,20 @@ pub fn show_account(ui: &mut Ui, b: &Books, id: u64, shell: &mut Shell<'_>) {
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
let Ok(gl) = general_ledger(&b.ledger, s, e) else {
|
||||
return;
|
||||
let gl = match general_ledger(&b.ledger, s, e) {
|
||||
Ok(gl) => gl,
|
||||
Err(why) => {
|
||||
// Never a blank page: say why these dates can't be shown.
|
||||
widgets::card(ui, |ui| {
|
||||
widgets::empty(
|
||||
ui,
|
||||
icon::WARNING,
|
||||
"These dates can't be shown",
|
||||
&format!("{why}. Choose other dates."),
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(acct) = gl.accounts.iter().find(|x| x.code == a.code) else {
|
||||
widgets::card(ui, |ui| {
|
||||
|
||||
@@ -764,7 +764,7 @@ fn reconciliation(ui: &mut Ui, b: &Books, r: &AssetRegister, shell: &mut Shell<'
|
||||
},
|
||||
);
|
||||
if let Some(id) = go {
|
||||
shell.state.accounts.range = None;
|
||||
shell.state.accounts.range = crate::pages::accounts::RegisterRange::Default;
|
||||
shell.go(Route::Account(id));
|
||||
}
|
||||
}
|
||||
@@ -927,7 +927,8 @@ pub fn show_asset(ui: &mut Ui, b: &Books, id: AssetId, shell: &mut Shell<'_>) {
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
shell.state.accounts.range = None;
|
||||
shell.state.accounts.range =
|
||||
crate::pages::accounts::RegisterRange::Default;
|
||||
go = Some(Route::Account(acct));
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
@@ -105,7 +105,7 @@ pub fn show(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
},
|
||||
);
|
||||
if r.interact(Sense::click()).clicked() {
|
||||
shell.state.accounts.range = None;
|
||||
shell.state.accounts.range = crate::pages::accounts::RegisterRange::Default;
|
||||
shell.go(Route::Account(*id));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -368,6 +368,20 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(Route::Account),
|
||||
),
|
||||
// Its code and both names as journaled: a later rename or
|
||||
// renumber never rewrites what this line said.
|
||||
"account.renamed" => ev(
|
||||
icon::TAG,
|
||||
format!(
|
||||
"Renamed {} from \u{201c}{}\u{201d} to \u{201c}{}\u{201d}",
|
||||
s(p, "code"),
|
||||
s(p, "previous"),
|
||||
s(p, "name")
|
||||
),
|
||||
p.get("account_id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(Route::Account),
|
||||
),
|
||||
"role.assigned" => {
|
||||
let role = s(p, "role");
|
||||
let control = ledger_core::control_account(role);
|
||||
@@ -792,11 +806,27 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
format!("Cut-off journal line set aside ({} bytes)", p["bytes"]),
|
||||
None,
|
||||
),
|
||||
"journal.format_set" => ev(
|
||||
icon::FILE_TEXT,
|
||||
format!("Journal format {} declared", p["format"]),
|
||||
None,
|
||||
),
|
||||
"journal.format_set" => {
|
||||
// The features in force with it (P3: account_renamed,
|
||||
// require_tax_category): what an earlier version can't read.
|
||||
let features: Vec<&str> = p["features"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().filter_map(|f| f.as_str()).collect())
|
||||
.unwrap_or_default();
|
||||
ev(
|
||||
icon::FILE_TEXT,
|
||||
if features.is_empty() {
|
||||
format!("Journal format {} declared", p["format"])
|
||||
} else {
|
||||
format!(
|
||||
"Journal format {} declared, with {}",
|
||||
p["format"],
|
||||
features.join(", ")
|
||||
)
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
"recurring.generated" => ev(
|
||||
icon::REPEAT,
|
||||
"Recurring draft created".into(),
|
||||
|
||||
@@ -281,7 +281,8 @@ fn spending_changes(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
},
|
||||
);
|
||||
if let Some(id) = go {
|
||||
shell.state.accounts.range = Some((ms - Months::new(3), b.today));
|
||||
shell.state.accounts.range =
|
||||
crate::pages::accounts::RegisterRange::Dates(ms - Months::new(3), b.today);
|
||||
shell.go(Route::Account(id));
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
|
||||
@@ -22,6 +22,10 @@ use ledger_core::{AccountId, AccountType, MemberCapital, tax_summary, vendor_109
|
||||
use rust_decimal::Decimal;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// The range a report covers. The default (a report nobody chose dates
|
||||
/// for) is the fiscal year to date: what an omitted date means
|
||||
/// everywhere (rules.md B-30, `Books::default_period`; a test pins that
|
||||
/// they agree).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum Range {
|
||||
ThisMonth,
|
||||
@@ -52,7 +56,7 @@ impl Range {
|
||||
(s - Months::new(12), s - Days::new(1))
|
||||
}
|
||||
Range::Last12Months => (ms - Months::new(11), t),
|
||||
Range::AllTime => (b.kpis.first_entry.unwrap_or(t).min(t), t),
|
||||
Range::AllTime => b.all_dates(),
|
||||
Range::Custom(s, e) => (s, e),
|
||||
}
|
||||
}
|
||||
@@ -70,7 +74,8 @@ impl Range {
|
||||
}
|
||||
}
|
||||
|
||||
const PRESETS: [Range; 7] = [
|
||||
/// The ranges the picker offers by name.
|
||||
pub const PRESETS: [Range; 7] = [
|
||||
Range::ThisMonth,
|
||||
Range::LastMonth,
|
||||
Range::ThisQuarter,
|
||||
@@ -354,7 +359,7 @@ fn col_title(s: NaiveDate, e: NaiveDate, columns: Columns) -> String {
|
||||
}
|
||||
|
||||
/// The most columns a split report shows.
|
||||
const MAX_COLUMNS: i64 = 240;
|
||||
const MAX_COLUMNS: i64 = ledger_store::ops::MAX_REPORT_COLUMNS;
|
||||
|
||||
/// How many columns splitting `s..=e` by `columns` makes, when that is
|
||||
/// more than [`MAX_COLUMNS`]; counted from the months, before building
|
||||
@@ -366,9 +371,8 @@ fn too_many_columns(s: NaiveDate, e: NaiveDate, columns: Columns) -> Option<i64>
|
||||
Columns::Quarter => 3,
|
||||
Columns::Year => 12,
|
||||
};
|
||||
let months = (i64::from(e.year()) - i64::from(s.year())) * 12 + i64::from(e.month())
|
||||
- i64::from(s.month());
|
||||
let n = months / per + 1;
|
||||
// The count books.show refuses past, so the two never disagree.
|
||||
let n = ledger_store::ops::report_columns(s, e, per);
|
||||
(n > MAX_COLUMNS).then_some(n)
|
||||
}
|
||||
|
||||
@@ -1028,6 +1032,19 @@ fn tax(b: &Books, s: NaiveDate, e: NaiveDate) -> Table {
|
||||
&ts.separately_stated,
|
||||
);
|
||||
}
|
||||
// Book amounts only: Form 4797 figures the tax gain from the tax
|
||||
// basis, and its split decides the line (ledger_core::tax).
|
||||
if !ts.form_4797.is_empty() {
|
||||
t.section("Gains and losses on business property (Form 4797)");
|
||||
for l in &ts.form_4797 {
|
||||
t.rows.push(Row {
|
||||
kind: Kind::Line,
|
||||
label: l.label.clone(),
|
||||
detail: l.accounts.join(", "),
|
||||
cells: vec![Cell::of(l.book), Cell::none()],
|
||||
});
|
||||
}
|
||||
}
|
||||
if !ts.uncategorized.is_empty() {
|
||||
t.section("Not categorized");
|
||||
for u in &ts.uncategorized {
|
||||
@@ -1056,6 +1073,9 @@ fn tax(b: &Books, s: NaiveDate, e: NaiveDate) -> Table {
|
||||
}
|
||||
let form = ts.form.map(fmt::tax_form).unwrap_or("not set");
|
||||
t.note(format!("Return: {form}. Deductible percentages applied (meals 50%, penalties 0%). A worksheet for the CPA, not a filing."));
|
||||
if !ts.form_4797.is_empty() {
|
||||
t.note("Gains and losses on business property are book amounts, in no total: the CPA figures them on Form 4797.");
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
@@ -1460,6 +1480,11 @@ fn report(ui: &mut Ui, b: &Books, kind: ReportKind, shell: &mut Shell<'_>, picke
|
||||
let s = widgets::date_picker(ui, "from", s);
|
||||
ui.label(RichText::new("to").color(p.muted));
|
||||
let e = widgets::date_picker(ui, "to", e);
|
||||
// The human's own pickers: a "from" set past the "to" reads
|
||||
// as the two in order. Never a books.show target's dates:
|
||||
// App::show takes only a resolved range in order (rules.md
|
||||
// B-30) and ignores any other, so this never changes what the
|
||||
// agent was told the viewer shows.
|
||||
st.range = Range::Custom(s.min(e), e.max(s));
|
||||
}
|
||||
if splittable {
|
||||
@@ -1987,7 +2012,7 @@ fn render(ui: &mut Ui, t: &Table, pdf: &PdfInfo, shell: &mut Shell<'_>) {
|
||||
start_pdf(ui.ctx(), st, t, pdf);
|
||||
}
|
||||
if let Some(d) = go {
|
||||
shell.state.accounts.range = Some((d.start, d.end));
|
||||
shell.state.accounts.range = crate::pages::accounts::RegisterRange::Dates(d.start, d.end);
|
||||
shell.go(Route::Account(d.account));
|
||||
}
|
||||
}
|
||||
@@ -2162,7 +2187,7 @@ fn gl(ui: &mut Ui, b: &Books, s: NaiveDate, e: NaiveDate, shell: &mut Shell<'_>)
|
||||
}
|
||||
if let Some(r) = go {
|
||||
if let Route::Account(_) = r {
|
||||
shell.state.accounts.range = Some((s, e));
|
||||
shell.state.accounts.range = crate::pages::accounts::RegisterRange::Dates(s, e);
|
||||
}
|
||||
shell.go(r);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user