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
@@ -163,6 +163,22 @@ proptest! {
|
||||
fy = fy - Months::new(12);
|
||||
}
|
||||
prop_assert_eq!(b.fiscal_ytd(), (fy, t));
|
||||
// One default period everywhere (rules.md B-30): what a report or
|
||||
// register opens on is the fiscal year to date, the server's
|
||||
// resolver's own answer for no dates, and party totals count this
|
||||
// calendar year.
|
||||
prop_assert_eq!(b.default_period(), (fy, t));
|
||||
prop_assert_eq!(b.periods().statement(None, None), Ok((fy, t)));
|
||||
prop_assert_eq!(ledger_gui::pages::reports::Range::default().dates(&b), (fy, t));
|
||||
prop_assert_eq!(
|
||||
ledger_gui::pages::accounts::RegisterRange::default().dates(&b),
|
||||
(fy, t)
|
||||
);
|
||||
let jan1 = NaiveDate::from_ymd_opt(t.year(), 1, 1).unwrap();
|
||||
prop_assert_eq!(
|
||||
b.party_span(ledger_gui::books::PartyPeriod::ThisYear).0,
|
||||
jan1
|
||||
);
|
||||
let ytd = pl(fy, t);
|
||||
prop_assert_eq!(k.net_ytd, ytd.total_revenue - ytd.total_expenses);
|
||||
let first = p.entries.iter().map(|e| t - Days::new(e.0)).min();
|
||||
@@ -669,3 +685,135 @@ fn more_origins_and_parties() {
|
||||
assert_eq!(r.origin, "Payroll taxes");
|
||||
assert!(!r.missing_receipt, "a remittance isn't a hand-made entry");
|
||||
}
|
||||
|
||||
/// Defect 8, repros 1.4.2 and 1.4.3 (rules.md B-32): Needs attention lists
|
||||
/// every revenue and expense account without a tax category, with
|
||||
/// activity or not: one never used, one used only last fiscal year; never
|
||||
/// a header, an account with a category, or an asset. Info under warn,
|
||||
/// Todo under refuse.
|
||||
#[test]
|
||||
fn needs_attention_flags_every_uncategorized_pl_account_defect_8() {
|
||||
let today = NaiveDate::from_ymd_opt(2026, 10, 20).unwrap();
|
||||
let d = dir("attention-tax");
|
||||
let mut s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
|
||||
let cash = s
|
||||
.add_account("1000", "Checking", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let ops = s
|
||||
.add_account("6000", "Operating", AccountType::Expense, None, true)
|
||||
.unwrap();
|
||||
s.add_account("6150", "Software", AccountType::Expense, Some(ops), false)
|
||||
.unwrap();
|
||||
let hosting = s
|
||||
.add_account("6160", "Hosting", AccountType::Expense, Some(ops), false)
|
||||
.unwrap();
|
||||
s.add_account_with_tax(
|
||||
"4000",
|
||||
"Sales",
|
||||
AccountType::Revenue,
|
||||
None,
|
||||
false,
|
||||
Some(("gross_receipts", None)),
|
||||
)
|
||||
.unwrap();
|
||||
let e = s
|
||||
.create_entry(NaiveDate::from_ymd_opt(2025, 6, 1).unwrap(), "last year")
|
||||
.unwrap();
|
||||
let m = Money::new(Decimal::new(1_200, 2), Currency::USD);
|
||||
s.add_line(e, hosting, Side::Debit, m).unwrap();
|
||||
s.add_line(e, cash, Side::Credit, m).unwrap();
|
||||
s.post_entry(e).unwrap();
|
||||
journal_settings(
|
||||
&mut s,
|
||||
"[company]\nname = \"Fernhill Labs LLC\"\n[books]\ncash_accounts = [\"1000\"]\n",
|
||||
);
|
||||
let source = Source {
|
||||
data_dir: d.join("books-data"),
|
||||
today: Some(today),
|
||||
};
|
||||
let item = |b: &ledger_gui::books::Books| {
|
||||
ledger_gui::attention::find(b)
|
||||
.into_iter()
|
||||
.find(|a| a.title.contains("without a tax category"))
|
||||
};
|
||||
let b = data::load_once(&source).unwrap();
|
||||
let a = item(&b).expect("listed");
|
||||
assert_eq!(a.title, "2 accounts without a tax category");
|
||||
assert!(
|
||||
a.detail.starts_with("6150 Software, 6160 Hosting. Each revenue and expense account maps to a line of the return"),
|
||||
"{}",
|
||||
a.detail
|
||||
);
|
||||
assert_eq!(a.severity, ledger_gui::books::Severity::Info);
|
||||
assert!(
|
||||
a.prompt.contains("books.tax_categories") && a.prompt.contains("books.set_tax_category")
|
||||
);
|
||||
// Under refuse, a to-do.
|
||||
s.change_settings(
|
||||
&[ledger_core::settings::Change {
|
||||
key: "policy.require_tax_category".into(),
|
||||
value: Some(serde_json::json!("refuse")),
|
||||
}],
|
||||
&ledger_core::settings::Source {
|
||||
decided_by: "Tobin Harlow".into(),
|
||||
decided_on: today,
|
||||
citation: None,
|
||||
note: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let b = data::load_once(&source).unwrap();
|
||||
assert_eq!(
|
||||
item(&b).unwrap().severity,
|
||||
ledger_gui::books::Severity::Todo
|
||||
);
|
||||
// Categorized: no item at all.
|
||||
for code in ["6150", "6160"] {
|
||||
let id = s.ledger().chart().find_by_code(code).unwrap().id;
|
||||
s.set_account_tax(id, Some("other_deductions"), None)
|
||||
.unwrap();
|
||||
}
|
||||
let b = data::load_once(&source).unwrap();
|
||||
assert!(item(&b).is_none());
|
||||
}
|
||||
|
||||
/// "Show all dates" is every date (P3 settled in design, rules.md B-30):
|
||||
/// the register's and the reports' all-dates range runs from the first
|
||||
/// posted entry through the later of today and the last, so an entry
|
||||
/// dated after today is never left out of it (as party totals' "All time"
|
||||
/// already ends); with no such entry it ends today.
|
||||
#[test]
|
||||
fn all_dates_holds_an_entry_dated_after_today() {
|
||||
use ledger_gui::pages::accounts::RegisterRange;
|
||||
use ledger_gui::pages::reports::Range;
|
||||
let today = NaiveDate::from_ymd_opt(2026, 10, 20).unwrap();
|
||||
let d = dir("all-dates-future");
|
||||
let mut s = Store::create(&d.join("books-data"), Currency::USD).unwrap();
|
||||
let cash = s
|
||||
.add_account("1000", "Checking", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let capital = s
|
||||
.add_account("3000", "Capital", AccountType::Equity, None, false)
|
||||
.unwrap();
|
||||
let source = Source {
|
||||
data_dir: d.join("books-data"),
|
||||
today: Some(today),
|
||||
};
|
||||
let post = |s: &mut Store, day: NaiveDate| {
|
||||
let e = s.create_entry(day, "x").unwrap();
|
||||
let m = Money::new(Decimal::new(500, 2), Currency::USD);
|
||||
s.add_line(e, cash, Side::Debit, m).unwrap();
|
||||
s.add_line(e, capital, Side::Credit, m).unwrap();
|
||||
s.post_entry(e).unwrap();
|
||||
};
|
||||
let first = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
|
||||
post(&mut s, first);
|
||||
let b = data::load_once(&source).unwrap();
|
||||
assert_eq!(b.all_dates(), (first, today));
|
||||
let later = NaiveDate::from_ymd_opt(2026, 12, 1).unwrap();
|
||||
post(&mut s, later);
|
||||
let b = data::load_once(&source).unwrap();
|
||||
assert_eq!(b.kpis.last_entry, Some(later));
|
||||
assert_eq!(RegisterRange::All.dates(&b), (first, later));
|
||||
assert_eq!(Range::AllTime.dates(&b), (first, later));
|
||||
}
|
||||
Reference in new issue
Block a user