Skip to content

web: balance sheet, income statement, and cashflow pages, and one linking scheme - #2747

Open
acinader wants to merge 12 commits into
hledgerorg:mainfrom
acinader:web-reports
Open

acinader wants to merge 12 commits into
hledgerorg:mainfrom
acinader:web-reports

Conversation

@acinader

Copy link
Copy Markdown
Contributor

web: balance sheet, income statement, and cashflow pages, and one linking scheme

Follows #2739. Adds the remaining standard reports to hledger-web, and
makes every figure in every report a link to the register it is
derived from.

The pages

/balancesheet, /balancesheetequity, /incomestatement, and
/cashflow show the commands' reports, computed from the same specs,
as a table with a section per subreport and the net total as its
footer. The sidebar links to the first three under Journal; each report
page links to all five in a "Report:" row, with an "Interval:" row for
the columns. The balance report gains a "Show: Balance changes | Ending
balances" row: the balance command's --change and -H/--historical
accumulation modes, through an accum parameter.

The links

  • Every figure links to a register whose final balance is the figure:
    the account's transactions in that period. The register accepts
    accum=historical, in which its running balance starts from the
    account's balance before the period rather than from zero, so it ends
    on the ending balance the report showed. That starting balance
    appears as the oldest row, "Balance brought forward", itself a link
    to the transactions before the period, and the balance column's
    heading switches between the two modes.
  • A section's total links to the register of its account types, the
    net total to all of them. Sections shown with the opposite sign to
    the register's (liabilities, equity, revenues) say so in the link's
    title.
  • Column headings link to the same report for that period. Journal and
    register dates link to that day's entries. Sidebar amounts link where
    the account names do. Register rows carry the journal's entry ids.

These links are built once, in Hledger.Cli.Anchor, so -O html --base-url output gets them too. Without --base-url the CLI's output
is byte-identical: verified for 29 report shapes across bal, bs, bse,
is, cf, print, register, and budget in html, fods, csv, and tsv.

Fixes found on the way

  • --base-url weekly headings linked to date:2025-W03, which nothing
    parses; other headings dropped their last day, since a query's end
    date is exclusive. Terms now come from showDateSpanForQuery.
  • A register's historical starting balance ignored a date2: start.
  • A register restricted by type: showed blank To/From cells.
  • The sidebar's links ended in an encoded space when the search was
    empty.

Known limits

A transaction whose postings carry their own dates can make a period's
register differ from the figure: the report counts each posting in its
own period, the register shows whole transactions. Both manuals say so.
A depth-clipped row that spans two account types cannot reconcile with
any register.

Related issues

Partly addresses #589: the report pages offer the yearly, quarterly,
monthly, weekly, and daily intervals, each cell links to its account
and subperiod's register, and the balance page gains the -H toggle
discussed there; the journal and register are still not grouped by
period. Partly addresses #200: item 5's balance sheet and income
statement views, as pages linked from the sidebar and filtered by the
search. See also #1913 (the web register's To/From column selects
postings by a type: term and then drops it for naming the other side;
-r is unchanged), #1854 (the register's new "Balance brought forward"
row is the -H start balance, filtered by amt: as there), #1337 (one row
per transaction is kept; the figure-vs-register difference for
posting-dated postings is documented as a limit), and #2073 (an empty
statement page says no accounts of the report's types were found and
links to the account-types docs; no CLI check is added).

Commits

  1. dev: lib: cell titles, query-safe date spans, and the register's starting balance
  2. fix: cli: --base-url links carry period terms that parse back to the same period
  3. fix: lib: an account register's historical start balance honors a date2: start date
  4. imp: cli: link every balance report figure to the register it is derived from
  5. dev: cli: export the compound balance report pieces hledger-web needs
  6. imp: web: ending balances on the balance page, and a register that starts from them
  7. imp: cli: a statement's account links carry the section's account type
  8. imp: web: balance sheet, income statement, and cashflow pages
  9. imp: web: one linking scheme for the journal, register, and sidebar
  10. imp: web: link the reports from the sidebar
  11. doc: cli: when a linked register can differ from the figure
  12. imp: web: the sidebar's Journal link keeps the search

How to test

Manual test plan: https://gist.github.com/acinader/88ff463f19c33a8858ed5645a98a5620.
Automated: hledger test, the 1947 shelltests, hledger-web --test
(76 examples), and the browser suite (34).

AI usage: Claude Fable 5.1. About 0.35M output tokens in the main
session (the per-commit estimates in the commit messages sum to ~165k;
the rest went to planning, review triage, test plans, and drafting),
plus the subagent runs below, which produced findings, not code. Their
figures are the harness's per-run totals and do not separate input
from output.

Subagent run Tokens
Design pass, the report pages 3.30M
Design pass, the linking scheme 3.25M
Review of commit 4, two rounds 3.06M
Reviews of commits 6, 8, 9, and 10 2.45M
Sweep of the issue tracker 2.34M
Subagents in all 14.40M

…ting balance

Additions for report linking in hledger-web. No output changes.

- Spreadsheet cells gain a cellTitle field. The HTML writer renders it
  as the link's title attribute when the cell has an anchor.

- showDateSpanForQuery renders a date span as a period expression that
  parses back to the same span. showDateSpan is for display: it prints
  inclusive end dates and ISO week names, so a span used as a date:
  term came back a day short, or, for a week, not at all.

- accountTransactionsReportWithStart returns the balance the running
  total starts from, alongside the report, so a register can show it
  as a balance brought forward. accountTransactionsReport is unchanged.

- transactionRegisterDateExtra is transactionRegisterDate given the
  account types, so a type: term in the report query matches postings
  as it does in the report itself. The report sorts its rows with it.

AI usage: Claude Fable 5.1, ~8k output tokens.
…same period

The links that -O html --base-url adds to balance reports used the
column heading's text as the register's date: term. That text is for
display: a week is an ISO week name, which the query parser does not
read ("date:2025-W03" gave a date parse error), and any other span
ends on its inclusive last day, which a query reads as exclusive, so
the register was a day short. The term is now the span as a period
expression, from showDateSpanForQuery; the heading text is unchanged.

Also, an inacct: term in the query is no longer copied into the
links, which name their own account; the register reads the first
inacct: term, so the copy made every account link open the same
register.

A shelltest pins the link shapes; none passed --base-url before.

AI usage: Claude Fable 5.1, ~6k output tokens.
…e2: start date

With historical balances, the account transactions report starts its
running balance from the postings before the query's start date. It
looked for that date only among the query's date: terms, or with
--date2 only among its date2: terms, so a register for date2:2025-02
without --date2 started from zero while listing the postings dated by
secondary date. Now the start date comes from whichever kind of date
term the query has, preferring the report's kind, and the prior
postings are selected by the same kind of date.

AI usage: Claude Fable 5.1, ~3k output tokens.
…ved from

With -O html or -O fods and --base-url, the balance reports' account
names and column headings linked to registers; their figures did not,
except in the tidy layout. Now, in the default layout, every figure
links to the register that derives it: the account's transactions in
that column's period, whose final balance is the figure. A total links
to the register of everything in the report's query for the period, a
row total to the whole report span, and in balancesheet, incomestatement,
cashflow, and balancesheetequity a section's total to the register of
the section's account types, and the net total to all the sections'.
Their column headings link too. Figures that look zero have an empty
register, and figures that are not sums of postings (--count,
--valuechange, --gain, --percent) have none, so they stay plain.

The links match the report they are in:

- A historical report's links carry accum=historical, asking the
  register for its historical mode, so that its running balance starts
  from the balance before the period and ends on the figure. (The
  parameter is for hledger-web's register, which learns it in a later
  change; until then it is ignored.) A cumulative figure links from the
  report's start to its period's end.

- A report using secondary dates (--date2) links with date2: terms.

- In list mode, a parent shown alongside its subaccounts sums only its
  own postings, so its links use inacctonly:. A depth-clipped row, or a
  parent whose subaccounts have no rows, sums them, and keeps inacct:.

- An account's link covers the span its row sums over: the report's
  period in a single-period report (bal -p 2025 --base-url linked the
  whole history), or the columns' whole span when one was asked for.

- A section shown with normally negative accounts, such as the income
  statement's revenues, says in its links' titles that the register
  shows the figures with the opposite sign.

Every link carries a title saying where it leads, which browsers show
on hover. The links assume the register runs with the report's other
options, such as valuation, as hledger-web's does. Without --base-url
the output is unchanged.

Hledger.Cli.Anchor gains LinkOpts and the builders that take it;
setAccountAnchor, dateCell, and dateSpanCell keep their signatures.
Balance exports multiBalanceTotalsRows and reportLinkOpts, and the
compound report renderer takes its subreport specs, so that it renders
each section with the section's options.

AI usage: Claude Fable 5.1, ~45k output tokens.
No output changes. For balance sheet, income statement, and cashflow
pages in hledger-web:

- The four report specs (balancesheetSpec and so on) are exported,
  with type signatures.

- compoundBalanceReportTitle is the report title as a pure function of
  the spec, the report options, the accumulation override, and the
  report. The span in a change report's title now comes from the
  report's own columns rather than from recomputing the report span
  from the journal; they are the same span.

- compoundBalanceReportAsSpreadsheetParts renders the report's parts
  as spreadsheet cells, in a record: the heading cells, each section's
  title, body rows, and totals rows, and the net rows. The CLI's
  compoundBalanceReportAsSpreadsheet lays them out as one table; a web
  page can mark them up as table sections.

- applySubreportTitles and allCommoditiesFromSubreports are exported.

AI usage: Claude Fable 5.1, ~8k output tokens.
…arts from them

The balance page shows balance changes. It now offers ending balances
too, through an accum parameter (accum=historical; accum=change is the
default) and a "Show: Balance changes | Ending balances" row of links
above the report. In that mode the column headings are the periods'
end dates, and the heading names the mode and the period, as the
multi-period page's heading does.

The register accepts the same parameter. With accum=historical its
running balance is the account's balance rather than a total of the
transactions shown: it starts from the balance brought forward from
before the query's start date, which is shown as the oldest row,
linking to the transactions before the period. Its balance column's
heading, "Period Total" or "Historical Total", switches between the
two modes. A figure on a historical report links to its register in
this mode, so the balance ends on the figure clicked.

Column headings link to this report for their period, in place of the
register for it: a heading narrows the report, and each figure already
opens the register it is derived from. The interval links are now
labeled "Interval:", with "None" for a single column.

The search form and its clear button keep the page's period and
accumulation mode on the report page, and the mode on the register, so
that a search does not reset them. The report page hides zero-balance
accounts when the sidebar does (the e key's cookie), not only with -E.
A register restricted by a type: term shows each transaction's other
accounts, instead of blank cells.

Hledger.Web.ReportPage holds what report pages share: resolving the
period, accum, search, and zero-item settings against the startup
options, and rendering a report as the page's table, with a table
section per report section.

AI usage: Claude Fable 5.1, ~40k output tokens.
In the html and fods output of balancesheet, incomestatement, and the
other compound reports with --base-url, each section's rows now link
their account and figures with the section's type: term, as the
section's totals already did. A subaccount declared with a different
type than its parent is left out of the parent's figure, and now out of
the register the figure links to.

AI usage: Claude Fable 5.1, ~3k output tokens.
New pages show the reports of the command line's balancesheet,
balancesheetequity, incomestatement, and cashflow commands, at
/balancesheet, /balancesheetequity, /incomestatement, and /cashflow.
Each is the command's report, computed from the same spec, for the
page's search and period, laid out as a table with a section per
subreport: the section's title row, its accounts, and its total under a
hairline, with the net total as the table's footer. Ending balances for
the balance sheets and changes for the other two, as the commands show;
an accum parameter overrides that, and the heading then says so, as the
command's does.

Every figure links to the register it is derived from, in the mode
that makes the register's final balance the figure; a section's total
links to the register of the section's account types, and the net
total to all of them. Where a section shows figures with the opposite
sign to the register's, as the balance sheet's liabilities and the
income statement's revenues, the link's title says so. Column headings
link to the same report for the period.

A "Report:" row above each report, and above the balance report, links
the five report pages to one another, keeping the search and period.
No other page links to them yet. A journal with no accounts of the
report's types gets an explanation and a pointer to how account types
are found, rather than an empty table.

The search form keeps a statement's accum override, as it keeps the
balance report's. The register's mode is also read when it is opened
from a page whose default differs.

AI usage: Claude Fable 5.1, ~30k output tokens.
A name opens its own view, a number opens the register that derives it,
a date narrows the view to that day, and an entry is found by one id on
either page.

- A journal entry's date links to the journal narrowed to that day,
  scrolled to the entry, as a register row's date already did; the
  register's date link now narrows to the day too, in place of any date
  terms in the search.

- Register rows carry the journal's entry ids (transaction-FILE-INDEX)
  instead of bare numbers, so a link to an entry means the same thing on
  both pages; the chart's points name entries the same way.

- The sidebar's amounts link where their account names do, and its
  total, when a search narrows it, to the register of that search. An
  amount that looks zero has an empty register, and no link.

- The sidebar's balances are computed without the startup interval, so
  they cover the search's own span, as the registers they open do.

- Selecting a range on the register chart makes a date term in the
  form the query reads, YYYY-MM-DD..YYYY-MM-DD, with the end exclusive.

- An empty search no longer adds a trailing empty term to the sidebar's
  links.

AI usage: Claude Fable 5.1, ~12k output tokens.
Under the sidebar's Journal link, rows for the balance sheet, income
statement, and cashflow statement, in the Journal link's style, the
one shown in bold. The balance sheet with equity and the balance report
are one click away, in the report pages' own Report row. The rows carry
what that row carries: the search, minus any account term, which the
reports ignore, and a report page's period.

The manual's report section and the help dialog say so, and the manual
no longer calls the report pages unlinked.

AI usage: Claude Fable 5.1, ~6k output tokens.
A transaction whose postings carry dates of their own is counted by
the balance reports posting by posting, in each posting's period, while
a register shows the whole transaction in the period of the posting
that matched. The balance command's note on --base-url links says so,
as hledger-web's manual does.

AI usage: Claude Fable 5.1, ~1k output tokens.
Every other link in the sidebar and on the pages keeps the current
search; the Journal link dropped it, as a way out of a search. The
search form's clear button is that way out, so the Journal link now
keeps the search too, minus any account term, which the journal page
does not use. The j key still opens the plain journal.

AI usage: Claude Fable 5.1, ~1k output tokens.
@simonmichael simonmichael added the web The hledger-web tool. label Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

web The hledger-web tool.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants