Skip to content

feat(reports): let dashboard charts and reports count budget months - #458

Closed
saesch83 wants to merge 6 commits into
otherworld-dev:devfrom
saesch83:feature/report-month-grouping
Closed

saesch83 wants to merge 6 commits into
otherworld-dev:devfrom
saesch83:feature/report-month-grouping

Conversation

@saesch83

Copy link
Copy Markdown
Contributor

Closes #457.

What it does

A new setting next to Budget Cycle Start Day, Months in dashboard charts and reports, offers Calendar months (the default) or Budget months. With budget months and start day 26, a salary on 26 September counts towards October in the charts and reports, the same as on the Budget page and in Category Details.

What it affects:

  • The dashboard's income vs. expenses trend chart. Its range covers whole budget months, also when switching the 3/6/12-month selector. The spending chart and top categories use the same range.
  • The Reports page: the summary's trend chart, cash flow by month, and the category × month matrix.
  • Exports and scheduled reports of those report types, which go through the same ReportService methods.
  • When budget months are on, a note under each of these says "Months are budget months starting on day N, as chosen in Settings."

Unchanged: year-over-year and the digest stay on calendar months. They call the aggregator and query methods directly, so they keep the default monthStartDay = 1.

How

  • Queries: TransactionReportQueries::getCashFlowByMonth(), getCashFlowByMonthByAccount(), getCategoryNetByMonth() and the trend wrappers take an optional int $monthStartDay = 1.
    • Above 1, they group by date (ReportScope::monthBucketExpr()) and rename each date to its budget month with BudgetPeriod::monthContaining() (ReportScope::toBudgetMonths()).
    • The existing mergeReportHalves() then adds up the dates of each month. A transaction has one date, so the summed COUNT(DISTINCT t.id) still counts each transaction once.
    • I did it this way because there is no portable SQL to cut a budget month out of a date on MySQL, PostgreSQL and SQLite.
  • Aggregator: ReportAggregator passes monthStartDay through and builds the month list with the new BudgetPeriod::monthsBetween(). period.monthStartDay and trends.monthStartDay tell the UI which kind of months it got.
  • Choosing per user: ReportService asks BudgetCarryoverService::reportMonthStartDay(), which returns the budget start day only when report_month_grouping is budget.

Also fixed

  1. Hero tiles' "% vs last month". It read the current period summary's trends, which are calendar buckets. With a start day those cut the current period in two, so the comparison disappeared or compared two halves of one period. The dashboard now fetches the previous budget period's summary and compares totals with totals.
  2. Trend months (separate commit). The trend series stepped a month at a time from the start date. A range like 26 Jul – 25 Oct lost October, and a range starting on 31 January skipped February. It now lists every month the range touches.

Testing

  • Unit tests:
    • PHP: query folding, the per-account variant, the category matrix, the aggregator month lists, the setting resolver and ReportService wiring.
    • JS: the setting helper, the note, the dashboard request ranges for both modes, and the hero comparison.
  • Full suites: 7,503 PHPUnit tests (1 skipped) and 2,245 vitest tests pass. Three vitest tests that time out only under full parallel load also time out on dev, and they pass when run alone. cs:check, Psalm and npm run lint are clean.
  • Psalm baseline: the existing baseline entry for getCategoryMonthlyReport() no longer matches its changed return statement. I corrected the @return shape (it was missing mixedCurrency) instead of re-baselining.
  • Manual (Nextcloud 31): I checked a real data set with start day 26.
    • Calendar and budget mode give identical totals and counts over the same range.
    • The months shift as expected.
    • The setting, the dashboard and the reports work in the browser with no console errors.

🤖 Generated with Claude Code

@otherworld-dev

Copy link
Copy Markdown
Owner

I ran this on a test instance with a start day of 26. In budget-month mode the numbers match the Budget page, splits and excluded accounts included, and calendar mode gives the same totals as before.

A few things before I merge:

  • The js bundle wasn't rebuilt, so in a real install the setting doesn't do anything yet. Could you run npm run build and commit the js/ changes?
  • The hint says money on or after the start day counts towards the next month. That's only true for start days after the 15th - with a start day of 10, June runs 10 Jun to 9 Jul. Something like "the months run from your start day, the same as on the Budget page" covers both.
  • Category Details already calls this "Budget periods" / "Calendar months" (In bar charts, only the month should be included. #447). Could you use "Budget periods" here too so they read the same?
  • Could you add a line under ## [Unreleased] in CHANGELOG.md, including the trend fix (last month dropping off, February skipped from the 31st), since that changes calendar mode too?
  • The "% vs last month" on the hero tiles never showed before, so now everyone gets it, and on the 1st it compares one day with a whole month ("down 100%"). Could it compare against the same number of days of the last period?

I'll regenerate the .pot when I merge.

saesch83 and others added 5 commits November 1, 2026 20:04
…therworld-dev#457)

A new setting next to the Budget Cycle Start Day chooses whether the
dashboard charts and the month-by-month reports (income vs. expenses
trend, cash flow, category by month, and their exports) count calendar
months, the default, or the user's budget months. With budget months a
salary on the 26th counts towards the next month, as on the Budget page.

The queries group by date and fold the rows into budget months in PHP,
since a budget month can't be cut out of a date in SQL all supported
databases run. Year-over-year and the digest keep calendar months.

The hero tiles' "vs last month" now compares the current budget period
with the whole previous one. It used the summary's calendar-month
buckets, which with a start day cut the current period in two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The income vs. expenses trend stepped a month at a time from the start
date, so a range starting later in the month than it ends lost its last
month (26 Jul - 25 Oct showed no October), and one starting on 31 January
skipped February.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… months

With budget months chosen for the reports, "This Month" still meant the
calendar month, so with start day 26 the report showed two budget months:
1-25 October as October and 26-31 October as November, with the money
split between them. The presets now cover whole budget months: This
Month is the running budget month, Last 3 Months three of them, and Year
to Date and Last Year start with the budget month of January.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Call the choice "Budget periods", as Category Details does (otherworld-dev#447), and
  word the setting's hint so it holds for any start day.
- Compare the hero tiles' "% vs last month" with the same number of days
  of the previous period, so the 1st no longer reads "down 100%".
- Add the feature and both fixes to the changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@saesch83
saesch83 force-pushed the feature/report-month-grouping branch from 3eaf49b to 4b8ea3f Compare November 1, 2026 19:33
@saesch83

saesch83 commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for testing it! All five are in:

  • Bundle: rebased on current dev and rebuilt; budget-app.js and budget-reports.js are committed in a separate chore(build) commit.
  • Hint: now reads "… count calendar months or budget periods. Budget periods run from your start day, the same as on the Budget page." That holds for any start day.
  • Naming: the option is Budget periods, as in Category Details, and the note under the charts says "Months are budget periods starting on day N, as chosen in Settings."
  • Changelog: a new ## [Unreleased] section with the feature, the trend fix (last month dropping off, February skipped from the 31st) and the hero tile fix.
  • "% vs last month": the tiles now compare this period so far with the same number of days of the previous one, so on the 1st it's one day against one day. On the period's last day, or when the previous period is shorter, the whole previous period counts. The logic is in comparablePreviousRange() in formatters.js, with tests.

PHPUnit, vitest, lint, cs:check and Psalm are green.

…-grouping

Rebuilt budget-app.js on the merged sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saesch83 added a commit to saesch83/Budget that referenced this pull request Nov 6, 2026
Brings in upstream/dev through otherworld-dev#458's branch; budget-app.js rebuilt on
the merged sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
otherworld-dev pushed a commit that referenced this pull request Nov 7, 2026
…458)

With a Budget Cycle Start Day set, Settings gets Months in dashboard charts
and reports, with Calendar months (the default) or Budget periods. The month
grouping lives in ReportScope, so splits and excluded accounts and categories
are handled the same as before. The trend charts keep their last month, and
the hero tiles' % vs last month compares the same number of days of the
previous period. Closes #457.
otherworld-dev added a commit that referenced this pull request Nov 7, 2026
@otherworld-dev

Copy link
Copy Markdown
Owner

Merged on dev as 9e0a869 (squashed), so I'm closing this one by hand. I checked all five here, the bundle matches the source, the hint and "Budget periods" read right, the changelog's in, and the tiles compare like with like on the 1st with start days 1, 10, 26 and 31. I've regenerated the .pot as well.

One small thing for a follow-up if you fancy it - on the last day of a period it switches to the whole previous one, so on 28 Feb it compares 28 days with all 31 of January. today > current.end in comparablePreviousRange() would keep it even.

#472 is built on this branch, so it will want dev merging in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard charts and reports ignore the Budget Cycle Start Day (calendar vs. budget months)

2 participants