Repository navigation
Conversation
…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>
…herworld-dev#457) The forecast learned from calendar months whatever the budget start day, and counted a month its history only partly held as a whole one. Bank sync fetches 90 days, so the first month holds only its last days: five days of September, salary included and hardly any spending, made the average savings several times too high. With budget months chosen for the charts and reports, the forecast now learns from the complete budget months before the running one and projects budget months. In either mode a month counts only when every account that held money before its first transaction (an opening balance) has history from the month's start, give or take a weekend of no bookings. An account added after the history window bounds nothing, so adding one does not empty the forecast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r month Bills and recurring income only set floors under the projection, as monthly amounts: a new bill or a raise counted only once it outgrew the whole average, a yearly bonus counted a twelfth in every month, and one-off bills and income, as well as rows dated after today, not at all. A one-off bill for a car in December left December's balance 8,500 too high. The projection now adds what is scheduled to the month it falls in: bills on the Bills page's own schedule (BillService::occurrencesBetween), recurring income on its expected dates, and rows dated after today. What is still due this month counts in the first month projected. The rest of the history, without bill payments and the credits matching recurring income, is averaged as before, per month learned from, so nothing counts twice and a bonus that stopped shows as a falling line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a start day, the days left of the running budget month were folded
into the first projected month, which then held a month and a half of
bills and income. The rest of the running month is now its own first
projection, marked partial and labelled "{month} (rest)", with the
average spread over the days left and everything scheduled in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A credit counted as a recurring income's payment only when it held the pattern as one phrase. A pattern whose words the bank writes in another order, or with other characters between them, never matched, so the salary stayed in the average and came again on its date: the forecast showed about twice the income. The pattern now matches when the row holds every one of its words, in any order and case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orecast Income and bills with no account were kept only when the forecast was asked for without a list of accounts. The web UI always passes the accounts the user can see, so they were left out of every forecast it showed: a yearly bonus entered without an account never appeared. They are the user's own, so they now count whenever the forecast holds one of the user's own accounts, and stay out of one made only of accounts shared with them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service multiplies the exact average by the share of the month left; the test rounded the average first, which is a cent off on some days. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-grouping Rebuilt budget-app.js on the merged sources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
|
I gave this a run on a test instance with a year of history, a few bills and recurring income, in calendar months and with start days 26 and 10. The placement by month works, but a few things need sorting before I can merge it.
Smaller ones: the scheduled totals add up floats where the rest of the app goes through MoneyCalculator, and a bill that's overdue but not paid isn't in any column any more. The unit tests mock the rows, so none of this shows up there. An integration test with a real bill placeholder and an income marked received would catch the first and third. |
# Conflicts: # budget/CHANGELOG.md # budget/js/budget-app.js # budget/js/budget-app.js.map # budget/js/budget-reports.js.map # budget/lib/Controller/SettingController.php # budget/tests/js/reportMonthGrouping.test.js
From the review of otherworld-dev#472: - A bill's scheduled placeholder is no longer counted on top of the bill placed on its dates, and scheduled rows are not history. - Each account's partial months drop out on their own, so one new account no longer takes every other account's history with it. - A credit booked by Mark Received or auto-create (vendor = the income's name) leaves the average, and so does a bill payment that isn't linked to its bill but carries its name or pattern. - An income or bill without a pattern only claims the rows booked under its name, not any amount within a fifth. - A transfer bill to an account outside the forecast is spending, one from outside into it is income. - An overdue, unpaid bill counts in the first column, unless a row since its due date pays it. - Scheduled totals go through MoneyCalculator. ForecastScheduledItemsTest runs these against real rows: a bill with its placeholder, credits as Mark Received books them, unlinked payments, a new account, a transfer bill and overdue bills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the thorough run, all of these were real. Fixed in 4751e82 (dev merged in first, bundle rebuilt in 35435f1):
New integration test Unit, integration, Psalm and CS are green. |
The column for the rest of the running month took a share of the average by days. Spending isn't spread evenly: most of it leaves in the days after the salary comes in. With a start day of 27, on 7 November the forecast put 61% of a month's spending in the 19 days left, while the month it learned from spent 82% in its first 12, and the month looked to end deep in the red. The rest of the month now gets the income and spending the months learned from had after the same number of days into each, averaged over them. Bills, recurring income and rows dated after today stay on their dates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
One more fix from running it on my own data (98b009c): the column for the rest of the running month took a share of the average by days, but spending isn't spread evenly. Most of it leaves in the days after the salary comes in. With start day 27, on 7 November the forecast put 61% of a month's spending in the 19 days left, while the month it learned from had spent 82% in its first 12, so the month looked to end deep in the red. The rest of the month now gets the income and spending the months learned from had after the same number of days into each, averaged over them. Bills, recurring income and rows dated after today stay on their dates, and the whole months after it are unchanged. New integration test |
|
Checked the fixes on a test instance. The placeholder, the new savings account, Mark Received, the transfer bills and the overdue bill all work now, and the new integration tests pass here too. A few cases still count twice or drop out though:
Smaller ones: a salary that's more than 20% off one month (overtime) becomes all variable income and the trend shoots up, 2500/2500/3250 projected 5208 then 6833. And a bill with a generic pattern like AMAZON takes every Amazon purchase within 20% out of the average. A date window and one row per occurrence would sort both. Also the placeholder test counts scheduled rows across every user, so a leftover row from another test breaks it. Worth scoping to the test's account. |
- Each date a bill or recurring income fell on claims the one row near it (within RecurringIncomeService::expectedWindowDays()) that pays it: linked to it, booked under its name, or with every word of its pattern, whatever its amount, so overtime doesn't turn the salary into variable income with a rising trend. Without a pattern, a row in the item's own account within a fifth of its amount counts too, so a salary booked under the employer's name isn't counted twice. One row per date, so a pattern like AMAZON takes one purchase a month, not every one. Past dates go back before the bill was added (BillService::occurrencesBetween(..., sinceCreated: false)). - Only rows linked to the user's own bills leave the average. On an account shared with them the owner's bills aren't placed, and the owner's rent dropped out of their forecast altogether. - An overdue bill counts its latest date due only, not every month since nobody marked it paid. - The placeholder test counts scheduled rows on its own account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, all fair. Fixed in 10f6572:
New integration tests for each case (income under another name, bill under another name, overtime, AMAZON, shared account, a bill unmarked since three months ago). All seven fail against the previous version. "Income without a pattern" now checks that the pension under the bank's name is taken and the refund beside it stays. Unit, integration, Psalm and CS are green. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #457. Replaces #464, which I closed; this PR has its changes and the follow-ups below.
Builds on #458. It uses that PR's Months in dashboard charts and reports setting and
BudgetPeriod::monthsBetween(). Until #458 is merged, this branch includes its commits. The forecast commits start at feat(forecast): learn from budget months and complete months only.Why
The live forecast (
ForecastService::getLiveForecast(), the Forecast page and the dashboard tile) projects each month as the average of past months. Bills and recurring income only act as a floor under that average. In practice the projected balance was often wrong where it matters most: in the months where something you already know about happens.Planned one-off payments were invisible. Examples:
None of these counted, so the balance line ran on as if the money stayed in the account.
Past one-off payments were spread over every month. When you moved savings into an investment last spring, that amount went into the average and lowered every projected month a little. It never showed as one month that drops.
Irregular income was averaged too. A yearly bonus counted as a twelfth in every month, never in the month it is paid.
Partial months skewed the average. Bank sync fetches 90 days, so the first month holds only its last days. Five days of September with the salary and almost no spending made the average savings several times too high.
The start day was ignored. The forecast always used calendar months, so with a start day of 26 a salary paid on the 26th landed in the wrong month compared with the Budget page.
My aim: what you already know about shows in the month it happens, and the average covers only the everyday part that you can't plan.
What changes
TransactionMapper::getFirstTransactionDates()reads each account's first date in one grouped query.BillService::occurrencesBetween()), one-off bills included;partial: trueand labelled "{month} (rest)".RecurringIncomeService) is unchanged.There is a changelog entry under
## [Unreleased]and a separatechore(build)commit with the rebuiltbudget-app.jsandbudget-forecast.js. Strings added: "{month} (rest)" and one hint line on the Forecast page. I left the translation files alone.Testing
Unit tests (new):
ForecastScheduledTest, with:Also tests for the history window, complete-month detection and budget-period aggregation in
PatternAnalyzer.Updated tests:
ForecastServiceTest,ForecastHistoryWindowTest,UsersOwnDateTestand the integration testsForecastHistoryTestandForecastSpendingFloorTestnow expect the running month's column first.Full suites:
dashboardDateRange,importMappingRequiredFields); they time out ondevtoo and pass when run alone.cs:check, Psalm andnpm run lintare clean.Manual (Nextcloud 31), on a real data set with start day 27:
🤖 Generated with Claude Code