From b01fd8fe5ba754382193b57e7b93c4fdf87fc71a Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:17:22 -0700 Subject: [PATCH 01/13] dev: lib: cell titles, query-safe date spans, and the register's starting 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. --- hledger-lib/Hledger/Data/Dates.hs | 33 +++++++++++++ .../Reports/AccountTransactionsReport.hs | 48 +++++++++++++++++-- hledger-lib/Hledger/Write/Html.hs | 9 +++- hledger-lib/Hledger/Write/Spreadsheet.hs | 9 +++- 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/hledger-lib/Hledger/Data/Dates.hs b/hledger-lib/Hledger/Data/Dates.hs index aa884799333..fdb99d0041a 100644 --- a/hledger-lib/Hledger/Data/Dates.hs +++ b/hledger-lib/Hledger/Data/Dates.hs @@ -46,6 +46,7 @@ module Hledger.Data.Dates ( showDateSpanAbbrev, showDateSpanAbbrevWith, showDateSpanFull, + showDateSpanForQuery, elapsedSeconds, prevday, periodexprp, @@ -170,6 +171,38 @@ showDateSpanFull (DateSpan mb me) = start = maybe "" (formatTime defaultTimeLocale "%F" . fromEFDay) mb end = maybe "" (formatTime defaultTimeLocale "%F" . addDays (-1) . fromEFDay) me +-- | Render a datespan as a period expression that parses back to the +-- same span, for a date: query term or a period parameter. Standard +-- calendar periods take their compact form (2025, 2025Q1, 2025-01, +-- 2025-01-15); any other span is written out with its exclusive end +-- date, which is how the period expression syntax reads one +-- (2025-01-13..2025-01-20); open ends stay open (2025-01-01.., +-- ..2025-02-01); the unbounded span is "". +-- +-- 'showDateSpan' is for display: it prints inclusive end dates and ISO +-- week names, which a query would read as a day short or not at all. +-- +-- >>> showDateSpanForQuery $ DateSpan (Just $ Exact $ fromGregorian 2025 1 1) (Just $ Exact $ fromGregorian 2026 1 1) +-- "2025" +-- >>> showDateSpanForQuery $ DateSpan (Just $ Exact $ fromGregorian 2025 1 13) (Just $ Exact $ fromGregorian 2025 1 20) +-- "2025-01-13..2025-01-20" +-- >>> showDateSpanForQuery $ DateSpan (Just $ Exact $ fromGregorian 2025 1 15) (Just $ Exact $ fromGregorian 2025 2 15) +-- "2025-01-15..2025-02-15" +-- >>> showDateSpanForQuery $ DateSpan (Just $ Exact $ fromGregorian 2025 1 1) Nothing +-- "2025-01-01.." +-- >>> showDateSpanForQuery $ DateSpan Nothing (Just $ Exact $ fromGregorian 2025 2 1) +-- "..2025-02-01" +-- >>> showDateSpanForQuery nulldatespan +-- "" +showDateSpanForQuery :: DateSpan -> Text +showDateSpanForQuery spn = case dateSpanAsPeriod spn of + WeekPeriod b -> T.pack $ iso b <> ".." <> iso (addDays 7 b) + PeriodBetween b e -> T.pack $ iso b <> ".." <> iso e + PeriodTo e -> T.pack $ ".." <> iso e + PeriodAll -> "" + p -> showPeriod p -- a day, month, quarter, or year, or an open end + where iso = formatTime defaultTimeLocale "%F" + -- | Get the current local date. getCurrentDay :: IO Day getCurrentDay = localDay . zonedTimeToLocalTime <$> getZonedTime diff --git a/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs b/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs index 9eb47ffa9ea..97d0c355cf1 100644 --- a/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs +++ b/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs @@ -10,8 +10,10 @@ module Hledger.Reports.AccountTransactionsReport ( AccountTransactionsReport, AccountTransactionsReportItem, accountTransactionsReport, + accountTransactionsReportWithStart, accountTransactionsReportItems, transactionRegisterDate, + transactionRegisterDateExtra, triOrigTransaction, triDate, triAmount, @@ -28,7 +30,7 @@ import Data.List.Extra (nubSort) import Data.Maybe (catMaybes) import Data.Ord (Down(..), comparing) import Data.Text qualified as T -import Data.Time.Calendar (Day) +import Data.Time.Calendar (Day, fromGregorian) import Hledger.Data import Hledger.Query @@ -99,7 +101,15 @@ triCommodityAmount c = filterMixedAmountByCommodity c . triAmount triCommodityBalance c = filterMixedAmountByCommodity c . triBalance accountTransactionsReport :: ReportSpec -> Journal -> Query -> AccountTransactionsReport -accountTransactionsReport rspec@ReportSpec{_rsReportOpts=ropts} j thisacctq = items +accountTransactionsReport rspec j = snd . accountTransactionsReportWithStart rspec j + +-- | The account transactions report, and the balance its running total +-- starts from: zero, or, with historical balances and a start date in +-- the query, the sum of the account's postings before that date (with +-- --average, their average per transaction). A register can show the +-- latter as a balance brought forward. +accountTransactionsReportWithStart :: ReportSpec -> Journal -> Query -> (MixedAmount, AccountTransactionsReport) +accountTransactionsReportWithStart rspec@ReportSpec{_rsReportOpts=ropts} j thisacctq = (startbal, items) where -- A depth limit should not affect the account transactions report; it should show all transactions in/below this account. -- Queries on currency or amount are also ignored at this stage; they are handled earlier, before valuation. @@ -169,7 +179,7 @@ accountTransactionsReport rspec@ReportSpec{_rsReportOpts=ropts} j thisacctq = it -- sort by the transaction's register date, then index, for accurate starting balance . dbg5With (("ts4:\n"++).pshowTransactions.map snd) . sortBy (comparing (Down . fst) <> comparing (Down . tindex . snd)) - . map (\t -> (transactionRegisterDate wd reportq thisacctq t, t)) + . map (\t -> (transactionRegisterDateExtra (journalAccountType j) wd reportq thisacctq t, t)) . map (if invert_ ropts then (\t -> t{tpostings = map postingNegateMainAmount $ tpostings t}) else id) $ jtxns acctJournal @@ -227,11 +237,17 @@ accountTransactionsReportItem reportq thisacctq runningcalc signfn accttypefn (i -- - the transaction date, or its secondary date if --date2 was used. -- transactionRegisterDate :: WhichDate -> Query -> Query -> Transaction -> Day -transactionRegisterDate wd reportq thisacctq t +transactionRegisterDate = transactionRegisterDateExtra (const Nothing) + +-- | Like 'transactionRegisterDate', but given the accounts' types, so +-- that a type: term in the report query matches postings here as it +-- does in the report itself. +transactionRegisterDateExtra :: (AccountName -> Maybe AccountType) -> WhichDate -> Query -> Query -> Transaction -> Day +transactionRegisterDateExtra accttypefn wd reportq thisacctq t | not $ null thisacctps = minimum $ map (postingDateOrDate2 wd) thisacctps | otherwise = transactionDateOrDate2 wd t where - reportps = tpostings $ filterTransactionPostings reportq t + reportps = tpostings $ filterTransactionPostingsExtra accttypefn reportq t thisacctps = filter (matchesPosting thisacctq) reportps -- -- | Generate a short readable summary of some postings, like @@ -290,4 +306,26 @@ filterAccountTransactionsReportByCommodity comm = -- tests tests_AccountTransactionsReport = testGroup "AccountTransactionsReport" [ + testCase "accountTransactionsReportWithStart" $ do + let checking = Acct $ toRegex' "assets:bank:checking" + fromJune = Date $ DateSpan (Just $ Exact $ fromGregorian 2008 6 1) Nothing + rspec accum = defreportspec{_rsQuery=fromJune, _rsReportOpts=defreportopts{balanceaccum_=accum}} + (histstart, histitems) = accountTransactionsReportWithStart (rspec Historical) samplejournal checking + (start, items) = accountTransactionsReportWithStart (rspec PerPeriod) samplejournal checking + -- with historical balances, the running balance starts from the postings before the start date + showMixedAmount histstart @?= "$1.00" + map (showMixedAmount . triBalance) histitems @?= ["$1.00", "$2.00", "$1.00", "$2.00"] + -- otherwise from zero + showMixedAmount start @?= "0" + map (showMixedAmount . triBalance) items @?= ["0", "$1.00", "0", "$1.00"] + + ,testCase "transactionRegisterDateExtra" $ do + let t = nulltransaction{tdate=fromGregorian 2008 1 1, tpostings=[ + ("assets:bank:checking" `post` usd 1){pdate=Just $ fromGregorian 2008 1 5} + ,"income:salary" `post` usd (-1)]} + checking = Acct $ toRegex' "assets:bank:checking" + assettypes a = if "assets" `T.isPrefixOf` a then Just Asset else Nothing + -- a type: term in the report query matches the posting only when the account types are known + transactionRegisterDate PrimaryDate (Type [Asset]) checking t @?= fromGregorian 2008 1 1 + transactionRegisterDateExtra assettypes PrimaryDate (Type [Asset]) checking t @?= fromGregorian 2008 1 5 ] diff --git a/hledger-lib/Hledger/Write/Html.hs b/hledger-lib/Hledger/Write/Html.hs index d32050026d2..dcbaa9c9cc3 100644 --- a/hledger-lib/Hledger/Write/Html.hs +++ b/hledger-lib/Hledger/Write/Html.hs @@ -97,7 +97,10 @@ formatCell cell = let content = if Text.null $ cellAnchor cell then str - else (H.a ! A.href (H.textValue $ cellAnchor cell)) str in + else foldl (!) H.a + (A.href (H.textValue $ cellAnchor cell) : + [A.title (H.textValue $ cellTitle cell) | not $ Text.null $ cellTitle cell]) + str in -- Mark date cells with a "date" class, so eg wrapping within dates -- can be prevented with css; borders are classes too. let class_ = @@ -190,6 +193,10 @@ tests_Hledger_Write_Html = testGroup "Write.Html" [ @?= "$1, 2 €" -- links, totals, borders cell (str "a") {cellAnchor = "register?q=a&b"} @?= "a" + cell (str "a") {cellAnchor = "register?q=a", cellTitle = "Show \"a\""} + @?= "a" + -- a title without an anchor has nothing to describe + cell (str "a") {cellTitle = "t"} @?= "a" cell (str "Total:") {cellStyle = Body Total, cellBorder = Spr.noBorder {Spr.borderTop = Spr.DoubleLine}} @?= "Total:" cell (Spr.headerCell "h" :: Cell Spr.NumLines Text) {cellClass = Spr.Class "account", cellBorder = Spr.noBorder {Spr.borderBottom = Spr.SingleLine}} diff --git a/hledger-lib/Hledger/Write/Spreadsheet.hs b/hledger-lib/Hledger/Write/Spreadsheet.hs index f8f2ea16986..9a861dfa822 100644 --- a/hledger-lib/Hledger/Write/Spreadsheet.hs +++ b/hledger-lib/Hledger/Write/Spreadsheet.hs @@ -149,6 +149,10 @@ data Cell border text = cellStyle :: Style, cellSpan :: Span, cellAnchor :: Text, + -- | A whole-phrase description of where 'cellAnchor' leads, for + -- writers that can attach one to a link (HTML's title attribute). + -- Ignored when there is no anchor. + cellTitle :: Text, cellClass :: Class, -- | The cell content split into parts to be joined with ", ": -- individual amounts of a multi-commodity amount, for writers @@ -159,8 +163,8 @@ data Cell border text = } instance Functor (Cell border) where - fmap f (Cell typ border style span anchor class_ parts content) = - Cell typ border style span anchor class_ (map f parts) (f content) + fmap f (Cell typ border style span anchor title class_ parts content) = + Cell typ border style span anchor title class_ (map f parts) (f content) defaultCell :: (Lines border) => text -> Cell border text defaultCell text = @@ -170,6 +174,7 @@ defaultCell text = cellStyle = Body Item, cellSpan = NoSpan, cellAnchor = mempty, + cellTitle = mempty, cellClass = Class mempty, cellParts = [], cellContent = text From 3c8eae165d9e5bb4a5ea06c69033ad341b8a0f44 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:38:45 -0700 Subject: [PATCH 02/13] fix: cli: --base-url links carry period terms that parse back to the 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. --- hledger/Hledger/Cli/Anchor.hs | 40 +++++++++++----- hledger/test/balance/base-url.test | 77 ++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 hledger/test/balance/base-url.test diff --git a/hledger/Hledger/Cli/Anchor.hs b/hledger/Hledger/Cli/Anchor.hs index af0c90b36f2..afba2fce758 100644 --- a/hledger/Hledger/Cli/Anchor.hs +++ b/hledger/Hledger/Cli/Anchor.hs @@ -20,7 +20,7 @@ import Hledger.Write.Spreadsheet qualified as Spr import Hledger.Write.Spreadsheet (headerCell) import Hledger.Utils.IO (error') import Hledger.Utils.Text (quoteIfSpaced) -import Hledger.Data.Dates (showDateSpan, showDateSpanFull, showDate) +import Hledger.Data.Dates (showDateSpan, showDateSpanFull, showDateSpanForQuery, showDate) import Hledger.Data.Types (DateSpan) import Hledger.Reports.ReportOptions (PeriodTitles(..)) @@ -61,36 +61,52 @@ removeDates = filter (\term_ -> not $ Text.isPrefixOf "date:" term_ || Text.isPrefixOf "date2:" term_) +-- | Drop the terms naming the register's account. A link names the +-- account it is for; a leftover term would come first and win. +removeInacct :: [Text] -> [Text] +removeInacct = + filter (\term_ -> + not $ Text.isPrefixOf "inacct:" term_ || Text.isPrefixOf "inacctonly:" term_) + +-- | Restrict the query to the given period expression, if it is not +-- empty (the unbounded span), in place of any date terms it had. replaceDate :: Text -> [Text] -> [Text] -replaceDate prd query = "date:"<>prd : removeDates query +replaceDate prd query = ["date:"<>prd | not $ Text.null prd] ++ removeDates query +-- | A column heading for the period, linking to the register for it. +-- The link's date term is the span as a period expression, which can +-- differ from the heading text: a query reads the end date of +-- 2025-01-15..2025-02-14 as exclusive, and reads no week names. headerDateSpanCell :: PeriodTitles -> Maybe Text -> [Text] -> DateSpan -> Spr.Cell () Text headerDateSpanCell ph base query spn = - let prd = renderPeriodHeading ph spn in - (headerCell prd) { - Spr.cellAnchor = composeAnchor base $ replaceDate prd query + (headerCell $ renderPeriodHeading ph spn) { + Spr.cellAnchor = + composeAnchor base $ replaceDate (showDateSpanForQuery spn) (removeInacct query) } +-- | A cell showing a date or period, linking to the account's register +-- restricted to the given period expression. dateQueryCell :: (Spr.Lines border) => - Maybe Text -> [Text] -> Text -> Text -> Spr.Cell border Text -dateQueryCell base query acct dateTerm = - (Spr.defaultCell dateTerm) { + Maybe Text -> [Text] -> Text -> Text -> Text -> Spr.Cell border Text +dateQueryCell base query acct label dateTerm = + (Spr.defaultCell label) { Spr.cellAnchor = - composeAnchor base $ "inacct:"<>acct : replaceDate dateTerm query + composeAnchor base $ "inacct:"<>acct : replaceDate dateTerm (removeInacct query) } dateCell :: (Spr.Lines border) => Maybe Text -> [Text] -> Text -> Day -> Spr.Cell border Text -dateCell base query acct = dateQueryCell base query acct . showDate +dateCell base query acct d = dateQueryCell base query acct (showDate d) (showDate d) dateSpanCell :: (Spr.Lines border) => PeriodTitles -> Maybe Text -> [Text] -> Text -> DateSpan -> Spr.Cell border Text -dateSpanCell ph base query acct = dateQueryCell base query acct . renderPeriodHeading ph +dateSpanCell ph base query acct spn = + dateQueryCell base query acct (renderPeriodHeading ph spn) (showDateSpanForQuery spn) -- | Render a DateSpan as a period heading according to the requested style. renderPeriodHeading :: PeriodTitles -> DateSpan -> Text @@ -100,4 +116,4 @@ renderPeriodHeading PTCompact = showDateSpan setAccountAnchor :: Maybe Text -> [Text] -> Text -> Spr.Cell border text -> Spr.Cell border text setAccountAnchor base query acct cell = - cell {Spr.cellAnchor = composeAnchor base $ "inacct:"<>acct : query} + cell {Spr.cellAnchor = composeAnchor base $ "inacct:"<>acct : removeInacct query} diff --git a/hledger/test/balance/base-url.test b/hledger/test/balance/base-url.test new file mode 100644 index 00000000000..622e05b9665 --- /dev/null +++ b/hledger/test/balance/base-url.test @@ -0,0 +1,77 @@ +# * balance report links with --base-url + +# ** 1. Column headings link to the register for the column's period, as a +# period expression that reads back as the same span: a week is written +# out with its exclusive end date, not as an ISO week name. +< +2025-01-15 a + assets:cash 10 + income + +2025-01-22 b + assets:cash 20 + income + +$ hledger -f- bal -W -O html --base-url '' +> /2025-W03= 0 + +# ** 2. The heading keeps its display form; only the link's term changes. +< +2025-01-15 a + assets:cash 10 + income + +2025-02-20 b + assets:cash 20 + income + +$ hledger -f- bal -M --period-titles=dates -O html --base-url '' +> /2025-01-01\.\.2025-01-31= 0 + +# ** 3. A span that is not a calendar period keeps its last day: the +# heading shows the inclusive end date, the term the exclusive one. +< +2025-01-15 a + assets:cash 10 + income + +2025-02-20 b + assets:cash 20 + income + +$ hledger -f- bal -M -p 2025-01-15..2025-03-01 -O html --base-url '' +> /2025-01-15\.\.2025-02-14= 0 + +# ** 4. The same term in the tidy layout's period column. +< +2025-01-15 a + assets:cash 10 + income + +$ hledger -f- bal -W --layout=tidy -O html --base-url '' +> /2025-W03= 0 + +# ** 5. An inacct: term in the query is not carried into the links, which +# name their own account. +< +2025-01-15 a + assets:cash 10 + income + +$ hledger -f- bal -O html --base-url '' inacct:assets +> /assets:cash= 0 + +# ** 6. Without --base-url, no links. +< +2025-01-15 a + assets:cash 10 + income + +$ hledger -f- bal -W -O html +> !/= 0 From 0a64bd4d68e8a464c66d39b4f0952a6ddfe182e8 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:11:29 -0700 Subject: [PATCH 03/13] fix: lib: an account register's historical start balance honors a date2: 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. --- .../Reports/AccountTransactionsReport.hs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs b/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs index 97d0c355cf1..4ea23bdc1d8 100644 --- a/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs +++ b/hledger-lib/Hledger/Reports/AccountTransactionsReport.hs @@ -25,9 +25,10 @@ module Hledger.Reports.AccountTransactionsReport ( ) where +import Data.Foldable (asum) import Data.List (mapAccumR, nub, partition, sortBy) import Data.List.Extra (nubSort) -import Data.Maybe (catMaybes) +import Data.Maybe (catMaybes, fromMaybe) import Data.Ord (Down(..), comparing) import Data.Text qualified as T import Data.Time.Calendar (Day, fromGregorian) @@ -167,11 +168,15 @@ accountTransactionsReportWithStart rspec@ReportSpec{_rsReportOpts=ropts} j thisa numpriorts = length priorpss priorsum = sumPostings $ concat priorpss priorq = dbg5 "priorq" $ And [thisacctq, tostartdateq, datelessreportq] - tostartdateq = - case mstartdate of - Just _ -> Date (DateSpan Nothing (Exact <$> mstartdate)) - Nothing -> None -- no start date specified, there are no prior postings - mstartdate = queryStartDate (date2_ ropts) reportq + -- The postings before the report start, by whichever kind of date + -- the query's start date is: the report's kind if it has one, + -- else the other, so that a date2: term selects the prior postings + -- by secondary date as it selects the report's postings. + -- With no start date, there are no prior postings. + tostartdateq = fromMaybe None $ asum [cutoff (date2_ ropts), cutoff (not $ date2_ ropts)] + cutoff secondary = + (if secondary then Date2 else Date) . DateSpan Nothing . Just . Exact + <$> queryStartDate secondary reportq datelessreportq = filterQuery (not . queryIsDateOrDate2) reportq items = @@ -318,6 +323,10 @@ tests_AccountTransactionsReport = testGroup "AccountTransactionsReport" [ -- otherwise from zero showMixedAmount start @?= "0" map (showMixedAmount . triBalance) items @?= ["0", "$1.00", "0", "$1.00"] + -- a date2: start date counts too, by secondary date (here the same days) + let fromJune2 = Date2 $ DateSpan (Just $ Exact $ fromGregorian 2008 6 1) Nothing + (histstart2, _) = accountTransactionsReportWithStart (rspec Historical){_rsQuery=fromJune2} samplejournal checking + showMixedAmount histstart2 @?= "$1.00" ,testCase "transactionRegisterDateExtra" $ do let t = nulltransaction{tdate=fromGregorian 2008 1 1, tpostings=[ From 35fbce8c1b79ef8c781182880470d81bdedebd20 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:29:09 -0700 Subject: [PATCH 04/13] imp: cli: link every balance report figure to the register it is derived 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. --- hledger/Hledger/Cli/Anchor.hs | 205 +++++++++++--- hledger/Hledger/Cli/Commands/Balance.hs | 28 +- hledger/Hledger/Cli/Commands/Balance.md | 6 +- .../Hledger/Cli/Commands/Balance/Internal.hs | 106 ++++++- hledger/Hledger/Cli/CompoundBalanceCommand.hs | 104 ++++--- hledger/test/balance/base-url.test | 268 +++++++++++++++++- hledger/test/balancesheet.test | 74 +++++ hledger/test/incomestatement.test | 58 ++++ 8 files changed, 744 insertions(+), 105 deletions(-) diff --git a/hledger/Hledger/Cli/Anchor.hs b/hledger/Hledger/Cli/Anchor.hs index afba2fce758..37e72ef2277 100644 --- a/hledger/Hledger/Cli/Anchor.hs +++ b/hledger/Hledger/Cli/Anchor.hs @@ -1,11 +1,31 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-| +Links from report cells to hledger-web's register: for hledger-web's own +report pages, and for HTML and FODS output with --base-url. A figure +links to the register that derives it, an account name to the account's +register, a period heading to the register for that period. +-} module Hledger.Cli.Anchor ( + LinkOpts(..), + defaultLinkOpts, + linkParams, + composeAnchor, + composeAnchorWith, + accountTerm, + dateTerm, + removeDates, + removeInacct, + withLink, setAccountAnchor, + setAccountAnchorWith, dateCell, dateSpanCell, + dateSpanCellWith, + totalDateSpanCell, headerDateSpanCell, renderPeriodHeading, + amountPhrase, ) where import Data.Text qualified as Text @@ -20,21 +40,66 @@ import Hledger.Write.Spreadsheet qualified as Spr import Hledger.Write.Spreadsheet (headerCell) import Hledger.Utils.IO (error') import Hledger.Utils.Text (quoteIfSpaced) -import Hledger.Data.Dates (showDateSpan, showDateSpanFull, showDateSpanForQuery, showDate) -import Hledger.Data.Types (DateSpan) -import Hledger.Reports.ReportOptions (PeriodTitles(..)) - - -registerQueryUrl :: [Text] -> Text -registerQueryUrl query = +import Hledger.Data.Dates (showDateSpan, showDateSpanFull, showDateSpanForQuery, showDate, nulldatespan) +import Hledger.Data.Types (DateSpan(..)) +import Hledger.Reports.ReportOptions (PeriodTitles(..), BalanceAccumulation(..)) + + +-- | What a report's register links need to know about the report, +-- besides its query and the base url. +data LinkOpts = LinkOpts { + loAccum :: BalanceAccumulation, + -- ^ With historical balances, the register starts from the balance + -- before the period, so that it ends on the figure in the cell. + loSpan :: DateSpan, + -- ^ The whole report span. A cumulative figure's register runs from its start. + loDate2 :: Bool, + -- ^ Does the report use secondary dates ? Then so must the register. + loIncludesSubs :: Bool, + -- ^ Does the row's figure include subaccounts (inacct:) or not (inacctonly:) ? + loNegated :: Bool + -- ^ Are the figures shown with the opposite sign to the register's ? +} + +defaultLinkOpts :: LinkOpts +defaultLinkOpts = LinkOpts PerPeriod nulldatespan False True False + +-- | The query parameters a link carries besides q: the register's +-- accumulation mode, when it is not the register's default. +linkParams :: LinkOpts -> [(Text, Text)] +linkParams lo = [("accum", "historical") | loAccum lo == Historical] + +-- | The span a figure's register covers: the period, or, for a +-- cumulative figure, the report's start to the period's end. +linkSpan :: LinkOpts -> DateSpan -> DateSpan +linkSpan lo spn@(DateSpan _ e) = + case loAccum lo of + Cumulative -> let DateSpan s _ = loSpan lo in DateSpan s e + _ -> spn + +-- | The query term naming the account a link is for. +accountTerm :: Bool -> Text -> Text +accountTerm includesSubs acct = + (if includesSubs then "inacct:" else "inacctonly:") <> acct + +-- | A date: term for the span (date2: for a report using secondary +-- dates), or none for the unbounded span. +dateTerm :: Bool -> DateSpan -> [Text] +dateTerm date2 spn = + [(if date2 then "date2:" else "date:") <> t | let t = showDateSpanForQuery spn, not $ Text.null t] + +registerQueryUrl :: [(Text, Text)] -> [Text] -> Text +registerQueryUrl params query = Uri.render $ [UriQQ.uri|register|] { Uri.uriQuery = - [Uri.QueryParam [UriQQ.queryKey|q|] $ - fromMaybe (error' "register URI query construction failed") $ - Uri.mkQueryValue $ Text.unwords $ - map quoteIfSpaced $ filter (not . Text.null) query] + Uri.QueryParam [UriQQ.queryKey|q|] + (queryValue $ Text.unwords $ map quoteIfSpaced $ filter (not . Text.null) query) + : [Uri.QueryParam (queryKey k) (queryValue v) | (k, v) <- params] } + where + queryKey = fromMaybe (error' "register URI query construction failed") . Uri.mkQueryKey + queryValue = fromMaybe (error' "register URI query construction failed") . Uri.mkQueryValue {- | >>> composeAnchor Nothing ["date:2024"] @@ -49,11 +114,19 @@ registerQueryUrl query = "foo/register?q=date:2024" -} composeAnchor :: Maybe Text -> [Text] -> Text -composeAnchor Nothing _ = mempty -composeAnchor (Just baseUrl) query = +composeAnchor = composeAnchorWith [] + +{- | Like 'composeAnchor', with query parameters after q. + +>>> composeAnchorWith [("accum","historical")] (Just "") ["date:2024"] +"register?q=date:2024&accum=historical" +-} +composeAnchorWith :: [(Text, Text)] -> Maybe Text -> [Text] -> Text +composeAnchorWith _ Nothing _ = mempty +composeAnchorWith params (Just baseUrl) query = baseUrl <> (if all (('/'==) . snd) $ Text.unsnoc baseUrl then "" else "/") <> - registerQueryUrl query + registerQueryUrl params query -- cf. Web.Widget.Common removeDates :: [Text] -> [Text] @@ -61,59 +134,103 @@ removeDates = filter (\term_ -> not $ Text.isPrefixOf "date:" term_ || Text.isPrefixOf "date2:" term_) --- | Drop the terms naming the register's account. A link names the --- account it is for; a leftover term would come first and win. +-- | Drop the terms naming an account for the register. A link names +-- the account it is for, and the register reads only the first such term. removeInacct :: [Text] -> [Text] removeInacct = filter (\term_ -> not $ Text.isPrefixOf "inacct:" term_ || Text.isPrefixOf "inacctonly:" term_) --- | Restrict the query to the given period expression, if it is not --- empty (the unbounded span), in place of any date terms it had. -replaceDate :: Text -> [Text] -> [Text] -replaceDate prd query = ["date:"<>prd | not $ Text.null prd] ++ removeDates query +-- | Give a cell a link and its title, unless the link is empty (no base url). +withLink :: Text -> Text -> Spr.Cell border text -> Spr.Cell border text +withLink anchor title cell + | Text.null anchor = cell + | otherwise = cell {Spr.cellAnchor = anchor, Spr.cellTitle = title} + +-- | The title of a link from a figure to the register that derives it, +-- with a note when the report shows figures with the opposite sign. +amountPhrase :: LinkOpts -> Bool -> Text +amountPhrase lo isTotal = + (case (loAccum lo, isTotal) of + (Historical, False) -> "Show the transactions behind this balance" + (Historical, True) -> "Show the transactions behind this total" + (_, False) -> "Show the transactions that make up this amount" + (_, True) -> "Show the transactions that make up this total") + <> (if loNegated lo then ", which the register shows with the opposite sign" else "") -- | A column heading for the period, linking to the register for it. -- The link's date term is the span as a period expression, which can -- differ from the heading text: a query reads the end date of -- 2025-01-15..2025-02-14 as exclusive, and reads no week names. headerDateSpanCell :: - PeriodTitles -> Maybe Text -> [Text] -> DateSpan -> Spr.Cell () Text -headerDateSpanCell ph base query spn = - (headerCell $ renderPeriodHeading ph spn) { - Spr.cellAnchor = - composeAnchor base $ replaceDate (showDateSpanForQuery spn) (removeInacct query) - } - + Bool -> PeriodTitles -> Maybe Text -> [Text] -> DateSpan -> Spr.Cell () Text +headerDateSpanCell date2 ph base query spn = + withLink + (composeAnchor base $ dateTerm date2 spn ++ removeDates (removeInacct query)) + "Show the transactions in this period" $ + headerCell $ renderPeriodHeading ph spn + +-- | A cell showing a period in an account's row, linking to the +-- account's register for that period. +dateSpanCellWith :: + (Spr.Lines border) => + LinkOpts -> PeriodTitles -> Maybe Text -> [Text] -> Text -> DateSpan -> Spr.Cell border Text +dateSpanCellWith lo ph base query acct spn = + withLink + (composeAnchorWith (linkParams lo) base $ + accountTerm (loIncludesSubs lo) acct : + dateTerm (loDate2 lo) (linkSpan lo spn) ++ removeDates (removeInacct query)) + "Show transactions affecting this account in this period" $ + Spr.defaultCell $ renderPeriodHeading ph spn --- | A cell showing a date or period, linking to the account's register --- restricted to the given period expression. -dateQueryCell :: +dateSpanCell :: (Spr.Lines border) => - Maybe Text -> [Text] -> Text -> Text -> Text -> Spr.Cell border Text -dateQueryCell base query acct label dateTerm = - (Spr.defaultCell label) { - Spr.cellAnchor = - composeAnchor base $ "inacct:"<>acct : replaceDate dateTerm (removeInacct query) - } + PeriodTitles -> Maybe Text -> [Text] -> Text -> DateSpan -> Spr.Cell border Text +dateSpanCell = dateSpanCellWith defaultLinkOpts +-- | A cell showing a date in an account's row, linking to the account's +-- register for that day. dateCell :: (Spr.Lines border) => Maybe Text -> [Text] -> Text -> Day -> Spr.Cell border Text -dateCell base query acct d = dateQueryCell base query acct (showDate d) (showDate d) - -dateSpanCell :: +dateCell base query acct d = + withLink + (composeAnchor base $ + accountTerm True acct : ("date:" <> showDate d) : removeDates (removeInacct query)) + "Show transactions affecting this account on this date" $ + Spr.defaultCell $ showDate d + +-- | A cell for a period in a totals row, linking to the register of +-- everything in the report's query, restricted by the given terms (a +-- section's account types, say), for that period. +totalDateSpanCell :: (Spr.Lines border) => - PeriodTitles -> Maybe Text -> [Text] -> Text -> DateSpan -> Spr.Cell border Text -dateSpanCell ph base query acct spn = - dateQueryCell base query acct (renderPeriodHeading ph spn) (showDateSpanForQuery spn) + LinkOpts -> PeriodTitles -> Maybe Text -> [Text] -> [Text] -> DateSpan -> Spr.Cell border Text +totalDateSpanCell lo ph base terms query spn = + withLink + (composeAnchorWith (linkParams lo) base $ + terms ++ dateTerm (loDate2 lo) (linkSpan lo spn) ++ removeDates (removeInacct query)) + (amountPhrase lo True) $ + Spr.defaultCell $ renderPeriodHeading ph spn -- | Render a DateSpan as a period heading according to the requested style. renderPeriodHeading :: PeriodTitles -> DateSpan -> Text renderPeriodHeading PTDates = showDateSpanFull renderPeriodHeading PTCompact = showDateSpan +-- | Link an account's cell to the account's register, restricted to the +-- given query. The query says which dates; the link options say whether +-- subaccounts are included and how the register accumulates. +setAccountAnchorWith :: + LinkOpts -> Maybe Text -> [Text] -> Text -> Spr.Cell border text -> Spr.Cell border text +setAccountAnchorWith lo base query acct = + withLink + (composeAnchorWith (linkParams lo) base $ + accountTerm (loIncludesSubs lo) acct : removeInacct query) + (if loIncludesSubs lo + then "Show transactions affecting this account and subaccounts" + else "Show transactions affecting this account but not subaccounts") + setAccountAnchor :: Maybe Text -> [Text] -> Text -> Spr.Cell border text -> Spr.Cell border text -setAccountAnchor base query acct cell = - cell {Spr.cellAnchor = composeAnchor base $ "inacct:"<>acct : removeInacct query} +setAccountAnchor = setAccountAnchorWith defaultLinkOpts diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index 04aa625c8af..70cda8867db 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -305,7 +305,8 @@ import Hledger.Utils.I18n qualified as I18n import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.Utils -import Hledger.Cli.Anchor (setAccountAnchor, renderPeriodHeading) +import Hledger.Cli.Anchor (linkParams, composeAnchorWith, dateTerm, removeInacct, + withLink, setAccountAnchor, setAccountAnchorWith, renderPeriodHeading, amountPhrase) import Hledger.Cli.Commands.Balance.Internal import Hledger.Write.Csv (CSV, printCSV, printTSV) import Hledger.Write.Ods (printFods) @@ -682,18 +683,35 @@ balanceReportAsSpreadsheetParts fmt opts (items, total) = _ -> [hCell "amount" "balance"] allCommodities = S.toAscList $ foldMap (\(_,_,_,ma) -> maCommodities ma) items + shownParents = shownParentsOf [name | (name, _, _, _) <- items] + -- The report's period, from -b/-e/-p. A date: query term is in the + -- query string already, and the report intersected the two. + periodterms = if period_ opts == PeriodAll then [] else dateTerm (date2_ opts) $ periodAsDateSpan $ period_ opts + linkquery = periodterms ++ querystring_ opts rows :: RowClass -> BalanceReportItem -> [NonEmpty (Ods.Cell Ods.NumLines Text)] rows rc (name, dispName, dep, ma) = - let accountCell = - setAccountAnchor + let lo = rowLinkOpts opts [] shownParents name + accountCell = + setAccountAnchorWith lo (guard (rc==Value) >> balance_base_url_ opts) - (querystring_ opts) name $ + linkquery name $ (\c -> c{Ods.cellClass = accountClass}) $ cell $ case rc of Total -> dispName -- show the total row heading as is; --drop etc. don't apply (#2688) Value -> renderBalanceAcct opts nbsp (name, dispName, dep) in + -- The figure links where the account name does, or, in the total + -- row, to the register of everything in the report; a figure that + -- looks zero has an empty register, and no link. + let linkFigure + | not (figuresLink opts) || mixedAmountLooksZero ma = id + | rc == Value = withLink (Ods.cellAnchor accountCell) (amountPhrase lo False) + | otherwise = + withLink + (composeAnchorWith (linkParams lo) (balance_base_url_ opts) $ + periodterms ++ removeInacct (querystring_ opts)) + (amountPhrase lo True) in addRowSpanHeaderNE accountCell $ case layout_ opts of LayoutBareWide -> @@ -708,7 +726,7 @@ balanceReportAsSpreadsheetParts fmt opts (items, total) = LayoutBare -> map (\a -> [cell $ acommodity a, renderAmount rc $ mixedAmount a]) . amounts $ mixedAmountStripCosts ma - _ -> [[renderAmount rc ma]] + _ -> [[linkFigure $ renderAmount rc ma]] renderAmount rc mixedAmt = wbToText <$> cellFromMixedAmount bopts (amountClass rc, mixedAmt) diff --git a/hledger/Hledger/Cli/Commands/Balance.md b/hledger/Hledger/Cli/Commands/Balance.md index 5e5a04815f4..4b2d8b5df53 100644 --- a/hledger/Hledger/Cli/Commands/Balance.md +++ b/hledger/Hledger/Cli/Commands/Balance.md @@ -994,7 +994,11 @@ As noted in [Output format](#output-format), if you choose HTML output (by using `-O html` or `-o somefile.html`), you can create a `hledger.css` file in the same directory to customise the report's appearance. -The HTML and [FODS] output formats can generate hyperlinks to a `hledger-web` register view for each account and period. +The HTML and [FODS] output formats can generate hyperlinks to `hledger-web` register views: +an account name links to the account's transactions, a period heading to the period's transactions, +and, in the default layout, a figure to the transactions it is made of, in a register whose final +balance is that figure (a historical balance report asks the register for its historical mode). +Each link says where it leads in its title, which browsers show on hover. E.g. if your `hledger-web` server is reachable at `http://localhost:5000` then you might run the `balance` command with the extra option `--base-url=http://localhost:5000`. You can also produce relative links, like `--base-url="some/path"` or `--base-url=""`.) diff --git a/hledger/Hledger/Cli/Commands/Balance/Internal.hs b/hledger/Hledger/Cli/Commands/Balance/Internal.hs index a1a642695c8..d01805770c2 100644 --- a/hledger/Hledger/Cli/Commands/Balance/Internal.hs +++ b/hledger/Hledger/Cli/Commands/Balance/Internal.hs @@ -14,7 +14,8 @@ import Text.Tabular.AsciiWide (Header(..), Properties(..), Table(..), concatTabl import Hledger.Utils.I18n qualified as I18n import Hledger -import Hledger.Cli.Anchor (setAccountAnchor, dateSpanCell, headerDateSpanCell, renderPeriodHeading) +import Hledger.Cli.Anchor (LinkOpts(..), dateTerm, removeDates, withLink, setAccountAnchorWith, + dateSpanCellWith, totalDateSpanCell, headerDateSpanCell, renderPeriodHeading, amountPhrase) import Hledger.Write.Spreadsheet (rawTableContent, headerCell, addHeaderBorders, addRowSpanHeader, cellFromMixedAmount, cellsFromMixedAmount) @@ -64,6 +65,38 @@ headerWithoutBorders = map (\c -> c {Ods.cellBorder = Ods.noBorder}) simpleDateSpanCell :: PeriodTitles -> DateSpan -> Ods.Cell Ods.NumLines Text simpleDateSpanCell ph = Ods.defaultCell . renderPeriodHeading ph +-- | Do this report's figures link to registers? Only when they are +-- sums of postings, which is what a register shows. +figuresLink :: ReportOpts -> Bool +figuresLink ropts = balancecalc_ ropts == CalcChange && not (percent_ ropts) + +-- | The register link options of a report's rows: its accumulation +-- mode, the span its columns cover, and whether it shows figures with +-- the sign opposite to the register's (as the compound reports' negated +-- sections do). +reportLinkOpts :: ReportOpts -> [DateSpan] -> LinkOpts +reportLinkOpts ropts colspans = LinkOpts { + loAccum = balanceaccum_ ropts, + loSpan = spansSpan colspans, + loDate2 = date2_ ropts, + loIncludesSubs = True, + loNegated = normalbalance_ ropts == Just NormallyNegative +} + +-- | The link options of one account's row: whether its figures include +-- the subaccounts. In tree mode every row does. In list mode a row +-- excludes them only when some of them have rows of their own; a +-- depth-clipped row, or a parent whose children are not shown, sums them. +rowLinkOpts :: ReportOpts -> [DateSpan] -> S.Set AccountName -> AccountName -> LinkOpts +rowLinkOpts ropts colspans shownParents acct = + (reportLinkOpts ropts colspans) { + loIncludesSubs = tree_ ropts || acct `S.notMember` shownParents + } + +-- | The accounts that have a subaccount among the given rows. +shownParentsOf :: [AccountName] -> S.Set AccountName +shownParentsOf = S.fromList . concatMap parentAccountNames + addTotalBorders :: (Functor f) => [f (Ods.Cell border text)] -> [f (Ods.Cell Ods.NumLines text)] @@ -170,7 +203,9 @@ multiBalanceRowAsCellBuilders :: multiBalanceRowAsCellBuilders bopts ropts@ReportOpts{..} colspans allCommodities rc renderDateSpanCell (PeriodicReportRow _acct as rowtot rowavg) = case layout_ of - LayoutWide width -> [fmap (cellFromMixedAmount bopts{displayMaxWidth=width}) clsamts] + LayoutWide width -> + [zipWith linkFigure spanclsamts $ + map (cellFromMixedAmount bopts{displayMaxWidth=width}) clsamts] LayoutTall -> paddedTranspose Ods.emptyCell . map (cellsFromMixedAmount bopts{displayMaxWidth=Nothing}) $ clsamts @@ -193,10 +228,27 @@ multiBalanceRowAsCellBuilders bopts ropts@ReportOpts{..} colspans allCommodities cs = if all mixedAmountLooksZero allamts then [""] else S.toList $ foldMap maCommodities allamts classified = map ((,) (amountClass rc)) as allamts = map snd clsamts - clsamts = (if not summary_only_ then classified else []) ++ - [(rowTotalClass rc, rowtot) | - multiBalanceHasTotalsColumn ropts && not (null as)] ++ - [(rowAverageClass rc, rowavg) | average_ && not (null as)] + clsamts = map snd spanclsamts + -- Each figure with its class, and the span it covers: a column's, the + -- whole report's for a row total, none for an average. The text + -- renderer passes no spans. + spanclsamts = + (if not summary_only_ then zip (map Just colspans ++ repeat Nothing) classified else []) ++ + [(Just (spansSpan colspans) <* guard (not $ null colspans), (rowTotalClass rc, rowtot)) | + multiBalanceHasTotalsColumn ropts && not (null as)] ++ + [(Nothing, (rowAverageClass rc, rowavg)) | average_ && not (null as)] + -- In the wide layout each figure links to the register it is derived + -- from, through the row's date span cell for the span it covers. A + -- figure that looks zero has an empty register, and a figure that is + -- not a sum of postings has none, so neither links. A totals row's + -- date span cell carries its own title. + linkFigure (Just spn, (_, amt)) c + | figuresLink ropts && not (mixedAmountLooksZero amt) = + let dsCell = renderDateSpanCell spn in + withLink (Ods.cellAnchor dsCell) + (if rc == Total then Ods.cellTitle dsCell + else amountPhrase (reportLinkOpts ropts colspans) False) c + linkFigure _ c = c addDateColumns spn@(DateSpan s e) remCols = (wbFromText <$> renderDateSpanCell spn) : wbDate (maybe "" showEFDate s) : @@ -227,7 +279,7 @@ balanceSubReportAsSpreadsheetParts :: [[Ods.Cell Ods.NumLines Text]]) balanceSubReportAsSpreadsheetParts fmt opts@ReportOpts{..} allCommodities (PeriodicReport colspans items tr) = - (allHeaders, concatMap fullRowAsTexts items, addTotalBorders totalrows) + (allHeaders, concatMap fullRowAsTexts items, totalrows) where accountCell label = (Ods.defaultCell label) {Ods.cellClass = accountClass} hCell cls label = (headerCell label) {Ods.cellClass = cls} @@ -251,24 +303,48 @@ balanceSubReportAsSpreadsheetParts fmt opts@ReportOpts{..} -- stylesheet can align them with the figures below (cf amountClass). amountHeader c = c{Ods.cellClass = amountClass Value} dateHeaders = - (if not summary_only_ then map (amountHeader . headerDateSpanCell period_titles_ balance_base_url_ querystring_) colspans else [] )++ + (if not summary_only_ then map (amountHeader . headerDateSpanCell date2_ period_titles_ balance_base_url_ querystring_) colspans else [] )++ [hCell (rowTotalClass Value) "total" | multiBalanceHasTotalsColumn opts] ++ [hCell (rowAverageClass Value) "average" | average_] + shownParents = shownParentsOf $ map prrFullName items + -- An account's link covers the span its row sums: the columns', which + -- can be wider than the span asked for. When none was asked for, the + -- register is left unrestricted, as the report is. + rowquery = + [t | requestedSpan, t <- dateTerm date2_ $ spansSpan colspans] ++ removeDates querystring_ + where + requestedSpan = period_ /= PeriodAll || any isDateTerm querystring_ + isDateTerm t = "date:" `T.isPrefixOf` t || "date2:" `T.isPrefixOf` t fullRowAsTexts row = addRowSpanHeader anchorCell $ - rowAsText Value (dateSpanCell period_titles_ balance_base_url_ querystring_ acctName) row + map (map (fmap wbToText)) $ + multiBalanceRowAsCellBuilders fmt opts colspans allCommodities Value + (dateSpanCellWith lo period_titles_ balance_base_url_ querystring_ acctName) row where acctName = prrFullName row + lo = rowLinkOpts opts colspans shownParents acctName anchorCell = - setAccountAnchor balance_base_url_ querystring_ acctName $ + setAccountAnchorWith lo balance_base_url_ rowquery acctName $ accountCell $ renderPeriodicAcct opts nbsp row totalrows = if no_total_ then [] - else addRowSpanHeader (accountCell totalRowHeadingSpreadsheet) $ - rowAsText Total (simpleDateSpanCell period_titles_) tr - rowAsText rc dsCell = - map (map (fmap wbToText)) . - multiBalanceRowAsCellBuilders fmt opts colspans allCommodities rc dsCell + else multiBalanceTotalsRows fmt opts (reportLinkOpts opts colspans) [] + allCommodities colspans totalRowHeadingSpreadsheet tr + +-- | The totals row of a multi-column report, as one or more rows of +-- spreadsheet cells: the label, then a total per column, each linking +-- to the register of everything in the report's query, plus the given +-- terms, for that column. +multiBalanceTotalsRows :: + AmountFormat -> ReportOpts -> LinkOpts -> [Text] -> [CommoditySymbol] -> [DateSpan] -> + Text -> PeriodicReportRow a MixedAmount -> [[Ods.Cell Ods.NumLines Text]] +multiBalanceTotalsRows fmt opts@ReportOpts{..} lo extraquery allCommodities colspans label row = + addTotalBorders $ + addRowSpanHeader ((Ods.defaultCell label) {Ods.cellClass = accountClass}) $ + map (map (fmap wbToText)) $ + multiBalanceRowAsCellBuilders fmt opts colspans allCommodities Total + (totalDateSpanCell lo period_titles_ balance_base_url_ extraquery querystring_) + row tidyColumnLabels :: [Text] tidyColumnLabels = diff --git a/hledger/Hledger/Cli/CompoundBalanceCommand.hs b/hledger/Hledger/Cli/CompoundBalanceCommand.hs index 4606c326f1b..afb3bb18fce 100644 --- a/hledger/Hledger/Cli/CompoundBalanceCommand.hs +++ b/hledger/Hledger/Cli/CompoundBalanceCommand.hs @@ -17,7 +17,6 @@ module Hledger.Cli.CompoundBalanceCommand ( import Control.Monad (guard, unless, void) import Data.Bifunctor (second) import Data.Foldable (traverse_) -import Data.Function ((&)) import Data.List.NonEmpty (NonEmpty((:|))) import Data.Maybe (fromMaybe, mapMaybe, maybeToList) import Safe (atMay) @@ -38,6 +37,7 @@ import Text.Tabular.AsciiWide as Tabular hiding (render) import Hledger.Utils.I18n qualified as I18n import Hledger +import Hledger.Cli.Anchor (LinkOpts(..), headerDateSpanCell) import Hledger.Cli.Commands.Balance import Hledger.Cli.CliOptions import Hledger.Cli.Utils (unsupportedOutputFormatError, writeOutputLazyText, warnIfLargeMultiPeriodReport) @@ -215,13 +215,13 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportspec_=r -- render appropriately render = case outputFormatFromOpts opts of "txt" -> compoundBalanceReportAsText ropts' - "csv" -> printCSV . compoundBalanceReportAsCsv ropts' - "tsv" -> printTSV . compoundBalanceReportAsCsv ropts' - "html" -> (<>"\n") . htmlAsLazyText . compoundBalanceReportAsHtml ropts' + "csv" -> printCSV . compoundBalanceReportAsCsv ropts' cbcqueries + "tsv" -> printTSV . compoundBalanceReportAsCsv ropts' cbcqueries + "html" -> (<>"\n") . htmlAsLazyText . compoundBalanceReportAsHtml ropts' cbcqueries "fods" -> printFods IO.localeEncoding . fmap (second NonEmpty.toList) . uncurry Map.singleton . compoundBalanceReportAsSpreadsheet - oneLineNoCostFmt "Account" (Just "") ropts' + oneLineNoCostFmt "Account" (Just "") ropts' cbcqueries "json" -> toJsonText x -> error' $ unsupportedOutputFormatError x @@ -339,12 +339,13 @@ compoundBalanceReportAsText ropts (CompoundPeriodicReport title _colspans subrep -- Subreports' CSV is concatenated, with the headings rows replaced by a -- subreport title row, and an overall title row, one headings row, and an -- optional overall totals row is added. -compoundBalanceReportAsCsv :: ReportOpts -> CompoundPeriodicReport DisplayName MixedAmount -> CSV -compoundBalanceReportAsCsv ropts cbr = +compoundBalanceReportAsCsv :: + ReportOpts -> [CBCSubreportSpec DisplayName] -> CompoundPeriodicReport DisplayName MixedAmount -> CSV +compoundBalanceReportAsCsv ropts specs cbr = let spreadsheet = snd $ snd $ compoundBalanceReportAsSpreadsheet - machineFmt "Account" Nothing ropts cbr + machineFmt "Account" Nothing ropts specs cbr title = cbrTitle cbr titleRows | T.null title = [] | otherwise = @@ -354,11 +355,12 @@ compoundBalanceReportAsCsv ropts cbr = titleRows ++ NonEmpty.toList spreadsheet -- | Render a compound balance report as HTML. -compoundBalanceReportAsHtml :: ReportOpts -> CompoundPeriodicReport DisplayName MixedAmount -> Html -compoundBalanceReportAsHtml ropts cbr = +compoundBalanceReportAsHtml :: + ReportOpts -> [CBCSubreportSpec DisplayName] -> CompoundPeriodicReport DisplayName MixedAmount -> Html +compoundBalanceReportAsHtml ropts specs cbr = let (title, (_fixed, cells)) = compoundBalanceReportAsSpreadsheet - oneLineNoCostFmt "" (Just nbsp) ropts cbr + oneLineNoCostFmt "" (Just nbsp) ropts specs cbr in do -- the builtin styles, then the optional user stylesheet so it can override them H.style $ preEscapedToHtml $ stylesheet $ @@ -373,12 +375,15 @@ compoundBalanceReportAsHtml ropts cbr = -- Do not use `styledTableHtml` here since that leads to nested ``s. H.table $ nl <> (traverse_ formatRow $ fmap (map (fmap toHtml)) cells) --- | Render a compound balance report as Spreadsheet. +-- | Render a compound balance report as Spreadsheet. The subreport specs +-- it was made from say how each section's figures link: the section's +-- account types restrict its totals' registers, and a section shown with +-- normally negative accounts says so in its links' titles. compoundBalanceReportAsSpreadsheet :: AmountFormat -> T.Text -> Maybe T.Text -> - ReportOpts -> CompoundPeriodicReport DisplayName MixedAmount -> + ReportOpts -> [CBCSubreportSpec DisplayName] -> CompoundPeriodicReport DisplayName MixedAmount -> (T.Text, ((Int, Int), NonEmpty [Spr.Cell Spr.NumLines T.Text])) -compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts cbr = +compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts specs cbr = let CompoundPeriodicReport title colspans subreports totalrow = cbr leadingHeaders = @@ -387,17 +392,22 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts cbr = LayoutTidy -> map Spr.headerCell tidyColumnLabels LayoutBare -> [Spr.headerCell "Commodity"] _ -> [] - dataHeaders = + -- A period heading links to the register for that period; its text + -- is the period's end date in a historical report. + dataHeaderCells = (guard (layout_ ropts /= LayoutTidy) >>) $ - map - -- column headings stay English in these formats, month names included - (reportPeriodName ropts{translations_ = I18n.noTranslations} colspans) - (if not (summary_only_ ropts) then colspans else []) ++ - (guard (multiBalanceHasTotalsColumn ropts) >> ["Total"]) ++ - (guard (average_ ropts) >> ["Average"]) + [ (headerDateSpanCell (date2_ ropts) (period_titles_ ropts) (balance_base_url_ ropts) (querystring_ ropts) spn) { + Spr.cellBorder = Spr.noBorder, + -- column headings stay English in these formats, month names included + Spr.cellContent = reportPeriodName ropts{translations_ = I18n.noTranslations} colspans spn + } + | not (summary_only_ ropts), spn <- colspans ] ++ + (guard (multiBalanceHasTotalsColumn ropts) >> [Spr.headerCell "Total"]) ++ + (guard (average_ ropts) >> [Spr.headerCell "Average"]) + dataHeaders = map Spr.cellContent dataHeaderCells headerrow = leadingHeaders ++ - concatMap (Spr.horizontalSpan subColumns . Spr.headerCell) dataHeaders + concatMap (Spr.horizontalSpan subColumns) dataHeaderCells blankrow = fmap (Spr.horizontalSpan headerrow . Spr.defaultCell) maybeBlank @@ -407,14 +417,37 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts cbr = _ -> [()] allCommodities = allCommoditiesFromSubreports subreports + -- The account type codes a section is selected by, which its totals' + -- registers need; a section not selected by type gets no such links. + typeCodes q = case q of + Type ts -> Just $ T.concat $ map (T.pack . show) ts + _ -> Nothing + typeTerm = fmap ("type:" <>) . typeCodes + -- Is a section shown with its figures negated, as normally negative + -- accounts are ? + negated spec = normalbalance_ (cbcsubreportoptions spec ropts) == Just NormallyNegative + unlinked = map (map (\c -> c {Spr.cellAnchor = mempty, Spr.cellTitle = mempty})) + linkedWhen mterm rows = maybe (unlinked rows) (const rows) mterm + -- Make rows for a subreport: its title row, not the headings row, -- the data rows, any totals row, and a blank row for whitespace. + -- The section is rendered with its own report options, so that its + -- links say when its figures are negated, and its totals link to the + -- registers of its account types. subreportrows :: - (T.Text, MultiBalanceReport, Bool) -> [[Spr.Cell Spr.NumLines T.Text]] - subreportrows (subreporttitle, mbr, _increasestotal) = + (CBCSubreportSpec DisplayName, (T.Text, MultiBalanceReport, Bool)) -> [[Spr.Cell Spr.NumLines T.Text]] + subreportrows (spec, (subreporttitle, mbr, _increasestotal)) = let - (_, bodyrows, mtotalsrows) = - balanceSubReportAsSpreadsheetParts fmt ropts allCommodities mbr + sropts = cbcsubreportoptions spec ropts + (_, bodyrows, _) = + balanceSubReportAsSpreadsheetParts fmt sropts{no_total_ = True} allCommodities mbr + sectionterm = typeTerm $ cbcsubreportquery spec + mtotalsrows + | no_total_ ropts = [] + | otherwise = + linkedWhen sectionterm $ + multiBalanceTotalsRows fmt sropts (reportLinkOpts sropts (prDates mbr)) (maybeToList sectionterm) + allCommodities (prDates mbr) totalRowHeadingSpreadsheet (prTotals mbr) accountCell = (Spr.defaultCell subreporttitle) { Spr.cellStyle = Spr.Body Spr.Total, @@ -434,21 +467,22 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts cbr = mtotalsrows ++ maybeToList blankrow + -- The net total's register is that of all the sections' account + -- types. It shows the net with the opposite sign when a section's + -- negation is not undone by subtracting it, as an income statement's + -- revenues are added negated. + netterm = fmap (("type:" <>) . T.concat) $ traverse (typeCodes . cbcsubreportquery) specs + netnegated = any (\spec -> negated spec /= not (cbcsubreportincreasestotal spec)) specs totalrows = if no_total_ ropts || length subreports == 1 then [] else - multiBalanceRowAsCellBuilders fmt ropts colspans allCommodities - Total (simpleDateSpanCell $ period_titles_ ropts) totalrow - -- make a table of rendered lines of the report totals row - & map (map (fmap wbToText)) - & Spr.addRowSpanHeader - ((Spr.defaultCell "Net:") {Spr.cellClass = accountClass}) - -- insert a headings column, with Net: on the first line only - & addTotalBorders -- marking the first row for special styling + linkedWhen netterm $ + multiBalanceTotalsRows fmt ropts (reportLinkOpts ropts colspans){loNegated = netnegated} + (maybeToList netterm) allCommodities colspans "Net:" totalrow in (title, ((1, multiBalanceReportNumHeaderColumns $ layout_ ropts), - headerrow :| concatMap subreportrows subreports ++ totalrows)) + headerrow :| concatMap subreportrows (zip specs subreports) ++ totalrows)) -- | All commodities appearing in any of these subreports, sorted. -- Used as the commodity column order for LayoutBareWide across the whole diff --git a/hledger/test/balance/base-url.test b/hledger/test/balance/base-url.test index 622e05b9665..db2d473b9ba 100644 --- a/hledger/test/balance/base-url.test +++ b/hledger/test/balance/base-url.test @@ -13,7 +13,7 @@ income $ hledger -f- bal -W -O html --base-url '' -> /2025-W03 /2025-W03= 0 # ** 2. The heading keeps its display form; only the link's term changes. @@ -27,7 +27,7 @@ $ hledger -f- bal -W -O html --base-url '' income $ hledger -f- bal -M --period-titles=dates -O html --base-url '' -> /2025-01-01\.\.2025-01-31 /2025-01-01\.\.2025-01-31= 0 # ** 3. A span that is not a calendar period keeps its last day: the @@ -42,7 +42,7 @@ $ hledger -f- bal -M --period-titles=dates -O html --base-url '' income $ hledger -f- bal -M -p 2025-01-15..2025-03-01 -O html --base-url '' -> /2025-01-15\.\.2025-02-14 /2025-01-15\.\.2025-02-14= 0 # ** 4. The same term in the tidy layout's period column. @@ -52,7 +52,7 @@ $ hledger -f- bal -M -p 2025-01-15..2025-03-01 -O html --base-url '' income $ hledger -f- bal -W --layout=tidy -O html --base-url '' -> /2025-W03 /2025-W03= 0 # ** 5. An inacct: term in the query is not carried into the links, which @@ -63,7 +63,7 @@ $ hledger -f- bal -W --layout=tidy -O html --base-url '' income $ hledger -f- bal -O html --base-url '' inacct:assets -> /assets:cash /assets:cash= 0 # ** 6. Without --base-url, no links. @@ -75,3 +75,261 @@ $ hledger -f- bal -O html --base-url '' inacct:assets $ hledger -f- bal -W -O html > !/= 0 + +# ** 7. Each figure links to the register that derives it: the account's +# transactions in the column's period. +< +2025-01-10 salary + assets:bank:checking 100 + income:salary + +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank:checking + +$ hledger -f- bal -M -O html --base-url '' +> // +>= 0 + +# ** 8. A list-mode parent whose subaccount has its own row shows its own +# postings only, and links with inacctonly:. +< +2025-01-10 salary + assets:bank:checking 100 + income:salary + +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank:checking + +$ hledger -f- bal -M -O html --base-url '' +> /assets:bank= 0 + +# ** 9. Its figures likewise. +< +2025-01-10 salary + assets:bank:checking 100 + income:salary + +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank:checking + +$ hledger -f- bal -M -O html --base-url '' +> // +>= 0 + +# ** 10. A figure that looks zero has an empty register, and no link. +< +2025-01-10 salary + assets:bank:checking 100 + income:salary + +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank:checking + +$ hledger -f- bal -M -O html --base-url '' +> !/inacctonly:assets:bank\+date:2025-02/ +>= 0 + +# ** 11. The totals row links each column's total to the register of +# everything in the report's query for that period. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M -O html --base-url '' expenses +> // +>= 0 + +# ** 12. A row total links to the whole report span. (An average has no +# register, so no link.) +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M -T -A -O html --base-url '' +> // +>= 0 + +# ** 12b. The average has none. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M -T -A -O html --base-url '' +> !/class="amount rowaverage">= 0 + +# ** 13. With historical balances, the links ask the register for its +# historical mode, so that its running balance ends on the figure. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M -H -O html --base-url '' +> // +>= 0 + +# ** 14. The account name's link too. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M -H -O html --base-url '' +> /assets:bank= 0 + +# ** 15. A cumulative figure's register runs from the report's start. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M --cumulative -O html --base-url '' +> // +>= 0 + +# ** 15b. The first column's figure links to the first period alone. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + assets:bank -10 + expenses:food + +$ hledger -f- bal -M --cumulative -O html --base-url '' +> // +>= 0 + +# ** 16. Figures that are not sums of postings do not link. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +$ hledger -f- bal -M --count -O html --base-url '' +> !/assets:bank= 0 + +# ** 18. A depth-clipped row sums its subaccounts, and links with inacct:. +< +2025-01-20 fee + assets:bank:checking -5 + expenses:fees + +$ hledger -f- bal depth:1 -O html --base-url '' +> /assets= 0 + +# ** 19. In a single-period report the figures link to the account's +# register for the report period. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -p 2025-01 -O html --base-url '' expenses +> // +>= 0 + +# ** 20. And the total to everything's. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -p 2025-01 -O html --base-url '' expenses +> // +>= 0 + +# ** 21. FODS output carries the same links. +< +2025-01-20 fee + assets:bank -5 + expenses:fees + +$ hledger -f- bal -M -O fods --base-url '' +> // +>= 0 + +# ** 22. A report using secondary dates asks the register to use them too. +< +2025-01-31=2025-02-02 late fee + assets:bank -7 + expenses:fees + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f- bal -M --date2 -O html --base-url '' +> // +>= 0 diff --git a/hledger/test/balancesheet.test b/hledger/test/balancesheet.test index 3011a626496..2ac8f6ffc22 100644 --- a/hledger/test/balancesheet.test +++ b/hledger/test/balancesheet.test @@ -376,3 +376,77 @@ Balance Sheet 2020-01-01 =============++============ Liabilities || -------------++------------ + +# ** 16. With --base-url, a section total links to the register of the +# section's account types, in the report's historical mode. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - bs -M -O html --base-url '' +> // +>= 0 + +# ** 17. A figure in a section shown with normally negative accounts says +# the register shows it with the opposite sign. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - bs -M -O html --base-url '' +> // +>= 0 + +# ** 18. The net total links to the register of all the sections' types. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - bs -M -O html --base-url '' +> // +>= 0 + +# ** 19. A period heading links to the register for that period. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - bs -M -O html --base-url '' +> /
2025-01-31= 0 diff --git a/hledger/test/incomestatement.test b/hledger/test/incomestatement.test index 0906036efd9..ac088e50863 100644 --- a/hledger/test/incomestatement.test +++ b/hledger/test/incomestatement.test @@ -355,3 +355,61 @@ Income Statement 2025-01-01 ------------++------------ expenses:y || 0 expenses:x || 1 + +# ** 12. With --base-url, a revenues figure says the register shows it +# with the opposite sign, and a revenues total links to the revenue +# accounts' register. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - is -M -O html --base-url '' +> // +>= 0 + +# ** 13. An expenses total links to the expense accounts' register, with +# no note: it shows as the register does. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - is -M -O html --base-url '' +> // +>= 0 + +# ** 14. The net total is revenues minus expenses, the opposite of the +# register's sum of both. +< +2025-01-10 salary + assets:bank 100 + income:salary + +2025-01-25 loan + assets:bank 50 + liabilities:loan + +2025-02-15 lunch + expenses:food 10 + assets:bank + +$ hledger -f - is -M -O html --base-url '' +> // +>= 0 From 4de68066572ce0f52d1df55ea3e961aed874efee Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:41:06 -0700 Subject: [PATCH 05/13] dev: cli: export the compound balance report pieces hledger-web needs 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. --- hledger/Hledger/Cli/Commands/Balancesheet.hs | 2 + .../Cli/Commands/Balancesheetequity.hs | 2 + hledger/Hledger/Cli/Commands/Cashflow.hs | 2 + .../Hledger/Cli/Commands/Incomestatement.hs | 2 + hledger/Hledger/Cli/CompoundBalanceCommand.hs | 261 +++++++++++------- 5 files changed, 162 insertions(+), 107 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Balancesheet.hs b/hledger/Hledger/Cli/Commands/Balancesheet.hs index b967386b9b0..a7d65fd1add 100644 --- a/hledger/Hledger/Cli/Commands/Balancesheet.hs +++ b/hledger/Hledger/Cli/Commands/Balancesheet.hs @@ -11,6 +11,7 @@ The @balancesheet@ command prints a simple balance sheet. module Hledger.Cli.Commands.Balancesheet ( balancesheetmode ,balancesheet + ,balancesheetSpec ) where import System.Console.CmdArgs.Explicit @@ -20,6 +21,7 @@ import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.CompoundBalanceCommand +balancesheetSpec :: CompoundBalanceCommandSpec balancesheetSpec = CompoundBalanceCommandSpec { cbcdoc = $(embedFileRelative "Hledger/Cli/Commands/Balancesheet.txt"), -- TRANSLATORS: the report title, with its reporting interval if any. Each is a diff --git a/hledger/Hledger/Cli/Commands/Balancesheetequity.hs b/hledger/Hledger/Cli/Commands/Balancesheetequity.hs index d0b644aae73..a622c0bc069 100644 --- a/hledger/Hledger/Cli/Commands/Balancesheetequity.hs +++ b/hledger/Hledger/Cli/Commands/Balancesheetequity.hs @@ -12,6 +12,7 @@ The @balancesheetequity@ command prints a simple balance sheet. module Hledger.Cli.Commands.Balancesheetequity ( balancesheetequitymode ,balancesheetequity + ,balancesheetequitySpec ) where import System.Console.CmdArgs.Explicit @@ -21,6 +22,7 @@ import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.CompoundBalanceCommand +balancesheetequitySpec :: CompoundBalanceCommandSpec balancesheetequitySpec = CompoundBalanceCommandSpec { cbcdoc = $(embedFileRelative "Hledger/Cli/Commands/Balancesheetequity.txt"), -- TRANSLATORS: the report title, with its reporting interval if any. Each is a diff --git a/hledger/Hledger/Cli/Commands/Cashflow.hs b/hledger/Hledger/Cli/Commands/Cashflow.hs index b70e45bd589..570db9dc8bb 100644 --- a/hledger/Hledger/Cli/Commands/Cashflow.hs +++ b/hledger/Hledger/Cli/Commands/Cashflow.hs @@ -15,6 +15,7 @@ cash flows.) module Hledger.Cli.Commands.Cashflow ( cashflowmode ,cashflow + ,cashflowSpec ) where import System.Console.CmdArgs.Explicit @@ -24,6 +25,7 @@ import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.CompoundBalanceCommand +cashflowSpec :: CompoundBalanceCommandSpec cashflowSpec = CompoundBalanceCommandSpec { cbcdoc = $(embedFileRelative "Hledger/Cli/Commands/Cashflow.txt"), -- TRANSLATORS: the report title, with its reporting interval if any. Each is a diff --git a/hledger/Hledger/Cli/Commands/Incomestatement.hs b/hledger/Hledger/Cli/Commands/Incomestatement.hs index 456c43d0429..740fa86699a 100644 --- a/hledger/Hledger/Cli/Commands/Incomestatement.hs +++ b/hledger/Hledger/Cli/Commands/Incomestatement.hs @@ -11,6 +11,7 @@ The @incomestatement@ command prints a simple income statement (profit & loss re module Hledger.Cli.Commands.Incomestatement ( incomestatementmode ,incomestatement + ,incomestatementSpec ) where import System.Console.CmdArgs.Explicit @@ -20,6 +21,7 @@ import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.CompoundBalanceCommand +incomestatementSpec :: CompoundBalanceCommandSpec incomestatementSpec = CompoundBalanceCommandSpec { cbcdoc = $(embedFileRelative "Hledger/Cli/Commands/Incomestatement.txt"), -- TRANSLATORS: the report title, with its reporting interval if any. Each is a diff --git a/hledger/Hledger/Cli/CompoundBalanceCommand.hs b/hledger/Hledger/Cli/CompoundBalanceCommand.hs index afb3bb18fce..222b93c3515 100644 --- a/hledger/Hledger/Cli/CompoundBalanceCommand.hs +++ b/hledger/Hledger/Cli/CompoundBalanceCommand.hs @@ -12,6 +12,11 @@ module Hledger.Cli.CompoundBalanceCommand ( CompoundBalanceCommandSpec(..) ,compoundBalanceCommandMode ,compoundBalanceCommand + ,compoundBalanceReportTitle + ,applySubreportTitles + ,CompoundBalanceReportParts(..) + ,compoundBalanceReportAsSpreadsheetParts + ,allCommoditiesFromSubreports ) where import Control.Monad (guard, unless, void) @@ -138,78 +143,25 @@ compoundBalanceCommandMode CompoundBalanceCommandSpec{..} = -- | Generate a runnable command from a compound balance command specification. compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> IO ()) -compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportspec_=rspec, rawopts_=rawopts} j = do +compoundBalanceCommand spec@CompoundBalanceCommandSpec{..} opts@CliOpts{reportspec_=rspec, rawopts_=rawopts} j = do warnIfLargeMultiPeriodReport rspec j writeOutputLazyText opts $ render $ styleAmounts styles cbr where styles = journalCommodityStylesWith HardRounding j - ropts@ReportOpts{..} = _rsReportOpts rspec + ropts = _rsReportOpts rspec -- use the default balance type for this report, unless the user overrides mbalanceAccumulationOverride = balanceAccumulationOverride rawopts balanceaccumulation = fromMaybe cbcaccum mbalanceAccumulationOverride -- Set balance type in the report options. ropts' = ropts{balanceaccum_=balanceaccumulation} - tr = I18n.tr translations_ - trf = I18n.trf translations_ - - -- TRANSLATORS: the report title, eg "Monthly Balance Sheet 2024 (Historical Ending Balances), valued at period ends". - -- {clarification} brings its own leading space when present. - title = trf "{report} {dates}{clarification}{valuation}" - [ ("report", tr $ cbctitle $ titleInterval interval_) - , ("dates", titledatestr) - , ("clarification", maybe "" (" " <>) mtitleclarification) - , ("valuation", valuationdesc) - ] - where - - -- XXX #1078 the title of ending balance reports - -- (Historical) should mention the end date(s) shown as - -- column heading(s) (not the date span of the transactions). - -- Also the dates should not be simplified (it should show - -- "2008/01/01-2008/12/31", not "2008"). - titledatestr = case balanceaccumulation of - Historical -> showEndDates enddates - _ -> showDateSpan requestedspan - where - enddates = map (addDays (-1)) . mapMaybe spanEnd $ cbrDates cbr -- these spans will always have a definite end date - requestedspan = fst $ reportSpan j rspec - - -- when user overrides, add an indication to the report title - -- Do we need to deal with overridden BalanceCalculation? - mtitleclarification = case (balancecalc_, balanceaccumulation, mbalanceAccumulationOverride) of - (CalcValueChange, PerPeriod, _ ) -> Just $ tr "(Period-End Value Changes)" - (CalcValueChange, Cumulative, _ ) -> Just $ tr "(Cumulative Period-End Value Changes)" - (CalcGain, PerPeriod, _ ) -> Just $ tr "(Incremental Gain)" - (CalcGain, Cumulative, _ ) -> Just $ tr "(Cumulative Gain)" - (CalcGain, Historical, _ ) -> Just $ tr "(Historical Gain)" - (_, _, Just PerPeriod ) -> Just $ tr "(Balance Changes)" - (_, _, Just Cumulative) -> Just $ tr "(Cumulative Ending Balances)" - (_, _, Just Historical) -> Just $ tr "(Historical Ending Balances)" - _ -> Nothing - - valuationdesc = - (case conversionop_ of - Just ToCost -> tr ", converted to cost" - _ -> "") - <> (case value_ of - Just (AtThen _mc) -> tr ", valued at posting date" - Just (AtEnd _mc) | changingValuation -> "" - Just (AtEnd _mc) -> tr ", valued at period ends" - Just (AtNow _mc) -> tr ", current value" - Just (AtDate today _mc) -> trf ", valued at {date}" [("date", showDate today)] - Nothing -> "") - - changingValuation = case (balancecalc_, balanceaccum_) of - (CalcValueChange, PerPeriod) -> True - (CalcValueChange, Cumulative) -> True - _ -> False + tr = I18n.tr (translations_ ropts) -- make a CompoundBalanceReport. The default heading is the auto-generated - -- title above; --title=TEXT overrides it (and =empty suppresses). + -- title; --title=TEXT overrides it (and =empty suppresses). -- --subreport-titles=A|B|... overrides per-subreport titles. cbr' = compoundBalanceReport rspec{_rsReportOpts=ropts'} j cbcqueries cbr = applySubreportTitles ropts' $ - cbr'{cbrTitle = effectiveTitle ropts' title + cbr'{cbrTitle = effectiveTitle ropts' $ compoundBalanceReportTitle spec ropts mbalanceAccumulationOverride cbr' ,cbrSubreports = [ (tr t, r, b) | (t, r, b) <- cbrSubreports cbr' ]} -- render appropriately @@ -225,6 +177,70 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportspec_=r "json" -> toJsonText x -> error' $ unsupportedOutputFormatError x +-- | The title of a compound balance report: the interval and report +-- name, the dates, a note when the accumulation mode was overridden or +-- the calculation is not the plain one, and the valuation. The report +-- options are the command line's, with any accumulation override given +-- separately, since the title says which it was. +compoundBalanceReportTitle :: + CompoundBalanceCommandSpec -> ReportOpts -> Maybe BalanceAccumulation -> CompoundPeriodicReport a b -> T.Text +compoundBalanceReportTitle CompoundBalanceCommandSpec{cbctitle=cbctitle, cbcaccum=cbcaccum} ReportOpts{..} mbalanceAccumulationOverride cbr = + -- TRANSLATORS: the report title, eg "Monthly Balance Sheet 2024 (Historical Ending Balances), valued at period ends". + -- {clarification} brings its own leading space when present. + trf "{report} {dates}{clarification}{valuation}" + [ ("report", tr $ cbctitle $ titleInterval interval_) + , ("dates", titledatestr) + , ("clarification", maybe "" (" " <>) mtitleclarification) + , ("valuation", valuationdesc) + ] + where + tr = I18n.tr translations_ + trf = I18n.trf translations_ + balanceaccumulation = fromMaybe cbcaccum mbalanceAccumulationOverride + + -- XXX #1078 the title of ending balance reports + -- (Historical) should mention the end date(s) shown as + -- column heading(s) (not the date span of the transactions). + -- Also the dates should not be simplified (it should show + -- "2008/01/01-2008/12/31", not "2008"). + titledatestr = case balanceaccumulation of + Historical -> showEndDates enddates + _ -> showDateSpan columnsspan + where + enddates = map (addDays (-1)) . mapMaybe spanEnd $ cbrDates cbr -- these spans will always have a definite end date + -- the span the columns cover: the requested span, widened to whole intervals + columnsspan = spansSpan $ cbrDates cbr + + -- when user overrides, add an indication to the report title + -- Do we need to deal with overridden BalanceCalculation? + mtitleclarification = case (balancecalc_, balanceaccumulation, mbalanceAccumulationOverride) of + (CalcValueChange, PerPeriod, _ ) -> Just $ tr "(Period-End Value Changes)" + (CalcValueChange, Cumulative, _ ) -> Just $ tr "(Cumulative Period-End Value Changes)" + (CalcGain, PerPeriod, _ ) -> Just $ tr "(Incremental Gain)" + (CalcGain, Cumulative, _ ) -> Just $ tr "(Cumulative Gain)" + (CalcGain, Historical, _ ) -> Just $ tr "(Historical Gain)" + (_, _, Just PerPeriod ) -> Just $ tr "(Balance Changes)" + (_, _, Just Cumulative) -> Just $ tr "(Cumulative Ending Balances)" + (_, _, Just Historical) -> Just $ tr "(Historical Ending Balances)" + _ -> Nothing + + valuationdesc = + (case conversionop_ of + Just ToCost -> tr ", converted to cost" + _ -> "") + <> (case value_ of + Just (AtThen _mc) -> tr ", valued at posting date" + Just (AtEnd _mc) | changingValuation -> "" + Just (AtEnd _mc) -> tr ", valued at period ends" + Just (AtNow _mc) -> tr ", current value" + Just (AtDate today _mc) -> trf ", valued at {date}" [("date", showDate today)] + Nothing -> "") + + changingValuation = case (balancecalc_, balanceaccum_) of + (CalcValueChange, PerPeriod) -> True + (CalcValueChange, Cumulative) -> True + _ -> False + -- | Apply --subreport-titles overrides to a compound report's subreports. -- A `|`-separated argument overrides the corresponding subreport titles, in -- order; subreports beyond the supplied list keep their default title. An @@ -375,17 +391,88 @@ compoundBalanceReportAsHtml ropts specs cbr = -- Do not use `styledTableHtml` here since that leads to nested ``s. H.table $ nl <> (traverse_ formatRow $ fmap (map (fmap toHtml)) cells) --- | Render a compound balance report as Spreadsheet. The subreport specs --- it was made from say how each section's figures link: the section's --- account types restrict its totals' registers, and a section shown with --- normally negative accounts says so in its links' titles. +-- | Render a compound balance report as Spreadsheet: the heading row, +-- then each section's title row, body rows, totals rows, and a blank row +-- for whitespace if wanted, then the net total rows. compoundBalanceReportAsSpreadsheet :: AmountFormat -> T.Text -> Maybe T.Text -> ReportOpts -> [CBCSubreportSpec DisplayName] -> CompoundPeriodicReport DisplayName MixedAmount -> (T.Text, ((Int, Int), NonEmpty [Spr.Cell Spr.NumLines T.Text])) compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts specs cbr = let - CompoundPeriodicReport title colspans subreports totalrow = cbr + CompoundBalanceReportParts{..} = compoundBalanceReportAsSpreadsheetParts fmt accountLabel ropts specs cbr + dataHeaders = map Spr.cellContent cbrpDataHeaders + headerrow = + cbrpLeadingHeaders ++ + concatMap (Spr.horizontalSpan subColumns) cbrpDataHeaders + + blankrow = + fmap (Spr.horizontalSpan headerrow . Spr.defaultCell) maybeBlank + subColumns = + case layout_ ropts of + LayoutBareWide -> void allCommodities + _ -> [()] + allCommodities = allCommoditiesFromSubreports $ cbrSubreports cbr + + sectionrows (subreporttitle, bodyrows, totalsrows) = + let + accountCell = + (Spr.defaultCell subreporttitle) { + Spr.cellStyle = Spr.Body Spr.Total, + Spr.cellClass = accountClass + } + titleRows + | T.null subreporttitle = [] + | otherwise = + [case layout_ ropts of + LayoutBareWide -> + accountCell : + map Spr.headerCell (dataHeaders >> allCommodities) + _ -> Spr.horizontalSpan headerrow accountCell] + in + titleRows ++ + bodyrows ++ + totalsrows ++ + maybeToList blankrow + + in (cbrTitle cbr, + ((1, multiBalanceReportNumHeaderColumns $ layout_ ropts), + headerrow :| concatMap sectionrows cbrpSections ++ cbrpNetRows)) + +-- | The parts of a compound balance report as spreadsheet cells, to be +-- laid out as one table, or as a table with sections as hledger-web does. +data CompoundBalanceReportParts = CompoundBalanceReportParts { + cbrpLeadingHeaders :: [Spr.Cell Spr.NumLines T.Text], + -- ^ The heading row's first cell(s): the account column's, and the + -- tidy and bare layouts' extra columns'. + cbrpDataHeaders :: [Spr.Cell Spr.NumLines T.Text], + -- ^ Then a heading per data column: each period, linking to the + -- register for it, then the row total and average when shown. + -- In the bare-wide layout each of these spans the commodity columns. + cbrpSections :: [(T.Text, [[Spr.Cell Spr.NumLines T.Text]], [[Spr.Cell Spr.NumLines T.Text]])], + -- ^ Each section's title, body rows, and totals rows. + cbrpNetRows :: [[Spr.Cell Spr.NumLines T.Text]] + -- ^ The net total rows; none when there is one section or no totals. +} + +-- | Render a compound balance report's parts as spreadsheet cells. The +-- subreport specs it was made from say how each section's figures link: +-- a section is rendered with its own report options, so that its links +-- say when its figures are negated, and its totals link to the registers +-- of its account types. +compoundBalanceReportAsSpreadsheetParts :: + AmountFormat -> T.Text -> + ReportOpts -> [CBCSubreportSpec DisplayName] -> CompoundPeriodicReport DisplayName MixedAmount -> + CompoundBalanceReportParts +compoundBalanceReportAsSpreadsheetParts fmt accountLabel ropts specs cbr = + CompoundBalanceReportParts { + cbrpLeadingHeaders = leadingHeaders, + cbrpDataHeaders = dataHeaderCells, + cbrpSections = zipWith section specs subreports, + cbrpNetRows = netrows + } + where + CompoundPeriodicReport _ colspans subreports totalrow = cbr leadingHeaders = Spr.headerCell accountLabel : case layout_ ropts of @@ -404,17 +491,6 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts specs cbr = | not (summary_only_ ropts), spn <- colspans ] ++ (guard (multiBalanceHasTotalsColumn ropts) >> [Spr.headerCell "Total"]) ++ (guard (average_ ropts) >> [Spr.headerCell "Average"]) - dataHeaders = map Spr.cellContent dataHeaderCells - headerrow = - leadingHeaders ++ - concatMap (Spr.horizontalSpan subColumns) dataHeaderCells - - blankrow = - fmap (Spr.horizontalSpan headerrow . Spr.defaultCell) maybeBlank - subColumns = - case layout_ ropts of - LayoutBareWide -> void allCommodities - _ -> [()] allCommodities = allCommoditiesFromSubreports subreports -- The account type codes a section is selected by, which its totals' @@ -429,43 +505,18 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts specs cbr = unlinked = map (map (\c -> c {Spr.cellAnchor = mempty, Spr.cellTitle = mempty})) linkedWhen mterm rows = maybe (unlinked rows) (const rows) mterm - -- Make rows for a subreport: its title row, not the headings row, - -- the data rows, any totals row, and a blank row for whitespace. - -- The section is rendered with its own report options, so that its - -- links say when its figures are negated, and its totals link to the - -- registers of its account types. - subreportrows :: - (CBCSubreportSpec DisplayName, (T.Text, MultiBalanceReport, Bool)) -> [[Spr.Cell Spr.NumLines T.Text]] - subreportrows (spec, (subreporttitle, mbr, _increasestotal)) = - let + section spec (subreporttitle, mbr, _increasestotal) = (subreporttitle, bodyrows, totalsrows) + where sropts = cbcsubreportoptions spec ropts (_, bodyrows, _) = balanceSubReportAsSpreadsheetParts fmt sropts{no_total_ = True} allCommodities mbr sectionterm = typeTerm $ cbcsubreportquery spec - mtotalsrows + totalsrows | no_total_ ropts = [] | otherwise = linkedWhen sectionterm $ multiBalanceTotalsRows fmt sropts (reportLinkOpts sropts (prDates mbr)) (maybeToList sectionterm) allCommodities (prDates mbr) totalRowHeadingSpreadsheet (prTotals mbr) - accountCell = - (Spr.defaultCell subreporttitle) { - Spr.cellStyle = Spr.Body Spr.Total, - Spr.cellClass = accountClass - } - titleRows - | T.null subreporttitle = [] - | otherwise = - [case layout_ ropts of - LayoutBareWide -> - accountCell : - map Spr.headerCell (dataHeaders >> allCommodities) - _ -> Spr.horizontalSpan headerrow accountCell] - in - titleRows ++ - bodyrows ++ - mtotalsrows ++ - maybeToList blankrow -- The net total's register is that of all the sections' account -- types. It shows the net with the opposite sign when a section's @@ -473,17 +524,13 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts specs cbr = -- revenues are added negated. netterm = fmap (("type:" <>) . T.concat) $ traverse (typeCodes . cbcsubreportquery) specs netnegated = any (\spec -> negated spec /= not (cbcsubreportincreasestotal spec)) specs - totalrows = + netrows = if no_total_ ropts || length subreports == 1 then [] else linkedWhen netterm $ multiBalanceTotalsRows fmt ropts (reportLinkOpts ropts colspans){loNegated = netnegated} (maybeToList netterm) allCommodities colspans "Net:" totalrow - in (title, - ((1, multiBalanceReportNumHeaderColumns $ layout_ ropts), - headerrow :| concatMap subreportrows (zip specs subreports) ++ totalrows)) - -- | All commodities appearing in any of these subreports, sorted. -- Used as the commodity column order for LayoutBareWide across the whole -- compound report; it must cover every row rendered, including the totals From 1003d3872690dc911932e244d7d96b89805db3de Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:20:18 -0700 Subject: [PATCH 06/13] imp: web: ending balances on the balance page, and a register that starts 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. --- hledger-lib/Hledger/Write/Html.hs | 2 +- hledger-lib/locale/de.po | 68 ++-- hledger-lib/locale/hledger.pot | 349 ++++++++++--------- hledger-web/Hledger/Web/App.hs | 44 ++- hledger-web/Hledger/Web/Handler/BalanceR.hs | 128 +++---- hledger-web/Hledger/Web/Handler/RegisterR.hs | 57 ++- hledger-web/Hledger/Web/ReportPage.hs | 175 ++++++++++ hledger-web/Hledger/Web/Test.hs | 133 ++++++- hledger-web/Hledger/Web/Widget/Common.hs | 82 +++-- hledger-web/hledger-web.cabal | 2 + hledger-web/hledger-web.m4.md | 29 +- hledger-web/package.yaml | 1 + hledger-web/templates/balance-links.hamlet | 6 +- hledger-web/templates/default-layout.hamlet | 4 +- hledger-web/templates/register.hamlet | 19 +- 15 files changed, 761 insertions(+), 338 deletions(-) create mode 100644 hledger-web/Hledger/Web/ReportPage.hs diff --git a/hledger-lib/Hledger/Write/Html.hs b/hledger-lib/Hledger/Write/Html.hs index dcbaa9c9cc3..510821c00f8 100644 --- a/hledger-lib/Hledger/Write/Html.hs +++ b/hledger-lib/Hledger/Write/Html.hs @@ -4,7 +4,7 @@ hledger-web's pages are made of. They render "Hledger.Write.Spreadsheet" tables as HTML tables: the CLI's @-O html@ output uses 'styledTableHtml' and 'titledTableHtml', and -hledger-web's report pages use 'formatRow' inside their own table markup. +hledger-web's report pages use 'formatCell' inside their own table markup. blaze's text renderer writes everything on one line, so for human readability we inject raw newlines between elements (see 'nl'). -} diff --git a/hledger-lib/locale/de.po b/hledger-lib/locale/de.po index ace2dba68ea..d290ccfb850 100644 --- a/hledger-lib/locale/de.po +++ b/hledger-lib/locale/de.po @@ -622,9 +622,6 @@ msgstr "Der Periodenausdruck konnte nicht gelesen werden:" msgid "Report:" msgstr "Bericht:" -msgid "Balance" -msgstr "Salden" - msgid "Yearly" msgstr "Jährlich" @@ -640,24 +637,6 @@ msgstr "Wöchentlich" msgid "Daily" msgstr "Täglich" -msgid "Show the balance report" -msgstr "Saldenbericht anzeigen" - -msgid "Show the yearly balance report" -msgstr "Jährlichen Saldenbericht anzeigen" - -msgid "Show the quarterly balance report" -msgstr "Vierteljährlichen Saldenbericht anzeigen" - -msgid "Show the monthly balance report" -msgstr "Monatlichen Saldenbericht anzeigen" - -msgid "Show the weekly balance report" -msgstr "Wöchentlichen Saldenbericht anzeigen" - -msgid "Show the daily balance report" -msgstr "Täglichen Saldenbericht anzeigen" - msgctxt "column heading" msgid "Date" msgstr "Datum" @@ -770,3 +749,50 @@ msgstr "Einheit" #. The register command's spreadsheet sheet name. msgid "Register" msgstr "Register" + +#. hledger-web: the balance page's interval and accumulation links, and the register's balance mode. +msgid "Interval:" +msgstr "Intervall:" + +msgctxt "interval" +msgid "None" +msgstr "Keins" + +msgid "Show one column for the whole period" +msgstr "Eine Spalte für den ganzen Zeitraum anzeigen" + +msgid "Show a column per year" +msgstr "Eine Spalte pro Jahr anzeigen" + +msgid "Show a column per quarter" +msgstr "Eine Spalte pro Quartal anzeigen" + +msgid "Show a column per month" +msgstr "Eine Spalte pro Monat anzeigen" + +msgid "Show a column per week" +msgstr "Eine Spalte pro Woche anzeigen" + +msgid "Show a column per day" +msgstr "Eine Spalte pro Tag anzeigen" + +msgid "Show:" +msgstr "Anzeigen:" + +msgid "Ending balances" +msgstr "Endsalden" + +msgid "Show how much each balance changed in each period" +msgstr "Anzeigen, wie sich jeder Saldo in jeder Periode verändert hat" + +msgid "Show each balance at the end of each period, including everything before it" +msgstr "Jeden Saldo am Ende jeder Periode anzeigen, einschließlich allem davor" + +msgid "Unknown balance accumulation mode:" +msgstr "Unbekannter Kumulierungsmodus:" + +msgid "Show the running balance from the start of this period" +msgstr "Den laufenden Saldo ab Beginn dieses Zeitraums anzeigen" + +msgid "Show the running balance including everything before this period" +msgstr "Den laufenden Saldo einschließlich allem vor diesem Zeitraum anzeigen" diff --git a/hledger-lib/locale/hledger.pot b/hledger-lib/locale/hledger.pot index d2fddf89a33..912406724e1 100644 --- a/hledger-lib/locale/hledger.pot +++ b/hledger-lib/locale/hledger.pot @@ -13,539 +13,540 @@ msgstr "" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:174 +#: hledger-lib/Hledger/Utils/I18n.hs:183 msgctxt "month" msgid "January" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:174 +#: hledger-lib/Hledger/Utils/I18n.hs:183 msgctxt "month" msgid "February" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:174 +#: hledger-lib/Hledger/Utils/I18n.hs:183 msgctxt "month" msgid "March" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:175 +#: hledger-lib/Hledger/Utils/I18n.hs:184 msgctxt "month" msgid "April" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:175 +#: hledger-lib/Hledger/Utils/I18n.hs:184 msgctxt "month" msgid "May" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:175 +#: hledger-lib/Hledger/Utils/I18n.hs:184 msgctxt "month" msgid "June" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:176 +#: hledger-lib/Hledger/Utils/I18n.hs:185 msgctxt "month" msgid "July" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:176 +#: hledger-lib/Hledger/Utils/I18n.hs:185 msgctxt "month" msgid "August" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:176 +#: hledger-lib/Hledger/Utils/I18n.hs:185 msgctxt "month" msgid "September" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:177 +#: hledger-lib/Hledger/Utils/I18n.hs:186 msgctxt "month" msgid "October" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:177 +#: hledger-lib/Hledger/Utils/I18n.hs:186 msgctxt "month" msgid "November" msgstr "" #. stand-alone (nominative) month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:177 +#: hledger-lib/Hledger/Utils/I18n.hs:186 msgctxt "month" msgid "December" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:181 +#: hledger-lib/Hledger/Utils/I18n.hs:190 msgctxt "month abbrev" msgid "Jan" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:181 +#: hledger-lib/Hledger/Utils/I18n.hs:190 msgctxt "month abbrev" msgid "Feb" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:181 +#: hledger-lib/Hledger/Utils/I18n.hs:190 msgctxt "month abbrev" msgid "Mar" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:182 +#: hledger-lib/Hledger/Utils/I18n.hs:191 msgctxt "month abbrev" msgid "Apr" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:182 +#: hledger-lib/Hledger/Utils/I18n.hs:191 msgctxt "month abbrev" msgid "May" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:182 +#: hledger-lib/Hledger/Utils/I18n.hs:191 msgctxt "month abbrev" msgid "Jun" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:183 +#: hledger-lib/Hledger/Utils/I18n.hs:192 msgctxt "month abbrev" msgid "Jul" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:183 +#: hledger-lib/Hledger/Utils/I18n.hs:192 msgctxt "month abbrev" msgid "Aug" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:183 +#: hledger-lib/Hledger/Utils/I18n.hs:192 msgctxt "month abbrev" msgid "Sep" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:184 +#: hledger-lib/Hledger/Utils/I18n.hs:193 msgctxt "month abbrev" msgid "Oct" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:184 +#: hledger-lib/Hledger/Utils/I18n.hs:193 msgctxt "month abbrev" msgid "Nov" msgstr "" #. short stand-alone month names, as used in a column heading. -#: hledger-lib/Hledger/Utils/I18n.hs:184 +#: hledger-lib/Hledger/Utils/I18n.hs:193 msgctxt "month abbrev" msgid "Dec" msgstr "" #. the report title, eg "Monthly Balance Sheet 2024 (Historical Ending Balances), valued at period ends". {clarification} brings its own leading space when present. -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:151 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:190 #, python-brace-format msgid "{report} {dates}{clarification}{valuation}" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:174 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:217 msgid "(Period-End Value Changes)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:175 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:218 msgid "(Cumulative Period-End Value Changes)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:176 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:219 msgid "(Incremental Gain)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:177 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:220 msgid "(Cumulative Gain)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:178 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:221 msgid "(Historical Gain)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:179 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:222 msgid "(Balance Changes)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:180 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:223 msgid "(Cumulative Ending Balances)" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:181 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:224 msgid "(Historical Ending Balances)" msgstr "" #. the budget report title, eg "Budget performance in 2024, valued at period ends:". -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:186 hledger/Hledger/Cli/Commands/Balance.hs:903 hledger/Hledger/Cli/Commands/Balance.hs:1111 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:229 hledger/Hledger/Cli/Commands/Balance.hs:828 hledger/Hledger/Cli/Commands/Balance.hs:914 msgid ", converted to cost" msgstr "" #. the budget report title, eg "Budget performance in 2024, valued at period ends:". -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:189 hledger/Hledger/Cli/Commands/Balance.hs:906 hledger/Hledger/Cli/Commands/Balance.hs:1114 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:232 hledger/Hledger/Cli/Commands/Balance.hs:831 hledger/Hledger/Cli/Commands/Balance.hs:917 msgid ", valued at posting date" msgstr "" #. the budget report title, eg "Budget performance in 2024, valued at period ends:". -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:191 hledger/Hledger/Cli/Commands/Balance.hs:908 hledger/Hledger/Cli/Commands/Balance.hs:1115 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:234 hledger/Hledger/Cli/Commands/Balance.hs:833 hledger/Hledger/Cli/Commands/Balance.hs:918 msgid ", valued at period ends" msgstr "" #. the budget report title, eg "Budget performance in 2024, valued at period ends:". -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:192 hledger/Hledger/Cli/Commands/Balance.hs:909 hledger/Hledger/Cli/Commands/Balance.hs:1116 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:235 hledger/Hledger/Cli/Commands/Balance.hs:834 hledger/Hledger/Cli/Commands/Balance.hs:919 msgid ", current value" msgstr "" #. the budget report title, eg "Budget performance in 2024, valued at period ends:". -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:193 hledger/Hledger/Cli/Commands/Balance.hs:910 hledger/Hledger/Cli/Commands/Balance.hs:1117 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:236 hledger/Hledger/Cli/Commands/Balance.hs:835 hledger/Hledger/Cli/Commands/Balance.hs:920 #, python-brace-format msgid ", valued at {date}" msgstr "" -#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:308 +#: hledger/Hledger/Cli/CompoundBalanceCommand.hs:330 msgid "Net:" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:413 +#: hledger/Hledger/Cli/Commands/Balance.hs:408 msgid "Budget Report" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:426 +#: hledger/Hledger/Cli/Commands/Balance.hs:422 msgid "Multi-period Balance Report" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:438 +#: hledger/Hledger/Cli/Commands/Balance.hs:434 msgid "Balance Report" msgstr "" #. the multi-period balance report title, eg "Balance changes in 2024, valued at period ends:". -#: hledger/Hledger/Cli/Commands/Balance.hs:889 +#: hledger/Hledger/Cli/Commands/Balance.hs:814 #, python-brace-format msgid "{report} in {dates}{valuation}:" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:893 +#: hledger/Hledger/Cli/Commands/Balance.hs:818 msgid "Period-end value changes" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:894 +#: hledger/Hledger/Cli/Commands/Balance.hs:819 msgid "Cumulative period-end value changes" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:895 +#: hledger/Hledger/Cli/Commands/Balance.hs:820 msgid "Incremental gain" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:896 +#: hledger/Hledger/Cli/Commands/Balance.hs:821 msgid "Cumulative gain" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:897 +#: hledger/Hledger/Cli/Commands/Balance.hs:822 msgid "Historical gain" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:898 +#. the label before a report's accumulation mode links, and the links' text and tooltips. +#: hledger/Hledger/Cli/Commands/Balance.hs:823 hledger-web/Hledger/Web/Widget/Common.hs:141 msgid "Balance changes" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:899 +#: hledger/Hledger/Cli/Commands/Balance.hs:824 msgid "Ending balances (cumulative)" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:900 +#: hledger/Hledger/Cli/Commands/Balance.hs:825 msgid "Ending balances (historical)" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:961 hledger/Hledger/Cli/Commands/Balance.hs:1151 +#. the budget report title, eg "Budget performance in 2024, valued at period ends:". +#: hledger/Hledger/Cli/Commands/Balance.hs:910 +#, python-brace-format +msgid "Budget performance in {dates}{valuation}:" +msgstr "" + +#: hledger/Hledger/Cli/Commands/Balance.hs:955 hledger/Hledger/Cli/Commands/Balance/Internal.hs:155 msgctxt "column heading" msgid "Commodity" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:971 hledger/Hledger/Cli/Commands/Balance.hs:1153 hledger-web/Hledger/Web/Handler/RegisterR.hs:58 +#: hledger/Hledger/Cli/Commands/Balance.hs:957 hledger/Hledger/Cli/Commands/Balance/Internal.hs:165 hledger-web/Hledger/Web/Handler/RegisterR.hs:69 msgctxt "column heading" msgid "Total" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:972 hledger/Hledger/Cli/Commands/Balance.hs:1154 +#: hledger/Hledger/Cli/Commands/Balance.hs:958 hledger/Hledger/Cli/Commands/Balance/Internal.hs:166 msgctxt "column heading" msgid "Average" msgstr "" -#. the budget report title, eg "Budget performance in 2024, valued at period ends:". -#: hledger/Hledger/Cli/Commands/Balance.hs:1107 -#, python-brace-format -msgid "Budget performance in {dates}{valuation}:" -msgstr "" - #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:28 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:30 msgid "Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:29 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:31 msgid "Daily Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:30 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:32 msgid "Weekly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:31 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:33 msgid "Biweekly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:32 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:34 msgid "Monthly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:33 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:35 msgid "Bimonthly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:34 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:36 msgid "Quarterly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:35 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:37 msgid "Half-yearly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:36 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:38 msgid "Yearly Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:37 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:39 msgid "Biennial Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:38 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:40 msgid "Periodic Balance Sheet" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:41 hledger/Hledger/Cli/Commands/Balancesheetequity.hs:42 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:43 hledger/Hledger/Cli/Commands/Balancesheetequity.hs:44 msgid "Assets" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheet.hs:48 hledger/Hledger/Cli/Commands/Balancesheetequity.hs:49 +#: hledger/Hledger/Cli/Commands/Balancesheet.hs:50 hledger/Hledger/Cli/Commands/Balancesheetequity.hs:51 msgid "Liabilities" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:29 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:31 msgid "Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:30 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:32 msgid "Daily Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:31 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:33 msgid "Weekly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:32 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:34 msgid "Biweekly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:33 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:35 msgid "Monthly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:34 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:36 msgid "Bimonthly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:35 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:37 msgid "Quarterly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:36 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:38 msgid "Half-yearly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:37 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:39 msgid "Yearly Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:38 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:40 msgid "Biennial Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:39 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:41 msgid "Periodic Balance Sheet With Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:56 +#: hledger/Hledger/Cli/Commands/Balancesheetequity.hs:58 msgid "Equity" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:32 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:34 msgid "Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:33 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:35 msgid "Daily Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:34 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:36 msgid "Weekly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:35 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:37 msgid "Biweekly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:36 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:38 msgid "Monthly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:37 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:39 msgid "Bimonthly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:38 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:40 msgid "Quarterly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:39 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:41 msgid "Half-yearly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:40 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:42 msgid "Yearly Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:41 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:43 msgid "Biennial Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:42 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:44 msgid "Periodic Cashflow Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Cashflow.hs:45 +#: hledger/Hledger/Cli/Commands/Cashflow.hs:47 msgid "Cash flows" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:28 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:30 msgid "Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:29 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:31 msgid "Daily Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:30 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:32 msgid "Weekly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:31 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:33 msgid "Biweekly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:32 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:34 msgid "Monthly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:33 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:35 msgid "Bimonthly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:34 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:36 msgid "Quarterly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:35 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:37 msgid "Half-yearly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:36 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:38 msgid "Yearly Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:37 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:39 msgid "Biennial Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:38 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:40 msgid "Periodic Income Statement" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:41 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:43 msgid "Revenues" msgstr "" #. the report title, with its reporting interval if any. Each is a whole phrase, so put the words in the order and form your language needs. -#: hledger/Hledger/Cli/Commands/Incomestatement.hs:48 +#: hledger/Hledger/Cli/Commands/Incomestatement.hs:50 msgid "Expenses" msgstr "" -#: hledger/Hledger/Cli/Commands/Register.hs:115 +#: hledger/Hledger/Cli/Commands/Register.hs:114 msgid "Register" msgstr "" @@ -589,28 +590,33 @@ msgstr "" msgid "All accounts" msgstr "" +#: hledger-web/Hledger/Web/ReportPage.hs:124 +msgid "Could not parse the period expression:" +msgstr "" + +#: hledger-web/Hledger/Web/ReportPage.hs:128 +msgid "Unknown balance accumulation mode:" +msgstr "" + #: hledger-web/Hledger/Web/Handler/AddR.hs:65 msgid "Transaction added." msgstr "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:38 hledger-web/Hledger/Web/Handler/JournalR.hs:30 hledger-web/Hledger/Web/Handler/RegisterR.hs:39 +#: hledger-web/Hledger/Web/Handler/BalanceR.hs:33 hledger-web/Hledger/Web/Handler/JournalR.hs:30 hledger-web/Hledger/Web/Handler/RegisterR.hs:44 #, python-brace-format msgid "{title}, filtered" msgstr "" #. the browser tab title of this page. -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:48 +#: hledger-web/Hledger/Web/Handler/BalanceR.hs:39 msgid "balance - hledger-web" msgstr "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:54 hledger-web/Hledger/Web/Handler/BalanceR.hs:99 +#. the browser tab title of this page. +#: hledger-web/Hledger/Web/Handler/BalanceR.hs:42 hledger-web/Hledger/Web/Handler/BalanceR.hs:74 msgid "Balance report" msgstr "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:56 -msgid "Could not parse the period expression:" -msgstr "" - #: hledger-web/Hledger/Web/Handler/EditR.hs:44 hledger-web/Hledger/Web/Handler/UploadR.hs:57 #, python-brace-format msgid "Failed to load journal: {error}" @@ -644,27 +650,35 @@ msgstr "" msgid "journal - hledger-web" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:36 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:41 msgid "all accounts" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:38 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:43 #, python-brace-format msgid "{account} (excluding subaccounts)" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:56 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:67 msgctxt "column heading" msgid "Historical Total" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:57 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:68 msgctxt "column heading" msgid "Period Total" msgstr "" +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:73 +msgid "Show the running balance from the start of this period" +msgstr "" + +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:74 +msgid "Show the running balance including everything before this period" +msgstr "" + #. the browser tab title of this page. -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:62 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:90 msgid "register - hledger-web" msgstr "" @@ -731,69 +745,90 @@ msgstr "" msgid "Postings validation failed" msgstr "" -#. the label before the balance page's report links. -#: hledger-web/Hledger/Web/Widget/Common.hs:101 -msgid "Report:" +#. the label before a report's interval links. +#: hledger-web/Hledger/Web/Widget/Common.hs:110 +msgid "Interval:" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:108 -msgid "Balance" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:116 +msgid "Show one column for the whole period" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:108 -msgid "Show the balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:116 +msgctxt "interval" +msgid "None" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:109 +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:117 msgid "Yearly" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:109 -msgid "Show the yearly balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:117 +msgid "Show a column per year" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:110 +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:118 msgid "Quarterly" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:110 -msgid "Show the quarterly balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:118 +msgid "Show a column per quarter" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:111 +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:119 msgid "Monthly" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:111 -msgid "Show the monthly balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:119 +msgid "Show a column per month" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:112 +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:120 msgid "Weekly" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:112 -msgid "Show the weekly balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:120 +msgid "Show a column per week" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:113 +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:121 msgid "Daily" msgstr "" -#. the balance page's report links: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:113 -msgid "Show the daily balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:121 +msgid "Show a column per day" +msgstr "" + +#. the label before a report's accumulation mode links, and the links' text and tooltips. +#: hledger-web/Hledger/Web/Widget/Common.hs:140 +msgid "Show:" +msgstr "" + +#. the label before a report's accumulation mode links, and the links' text and tooltips. +#: hledger-web/Hledger/Web/Widget/Common.hs:141 +msgid "Show how much each balance changed in each period" +msgstr "" + +#. the label before a report's accumulation mode links, and the links' text and tooltips. +#: hledger-web/Hledger/Web/Widget/Common.hs:143 +msgid "Ending balances" +msgstr "" + +#. the label before a report's accumulation mode links, and the links' text and tooltips. +#: hledger-web/Hledger/Web/Widget/Common.hs:143 +msgid "Show each balance at the end of each period, including everything before it" msgstr "" #: hledger-web/templates/add-form.hamlet:11 diff --git a/hledger-web/Hledger/Web/App.hs b/hledger-web/Hledger/Web/App.hs index 3022f5feb8e..3ce27bec1b3 100644 --- a/hledger-web/Hledger/Web/App.hs +++ b/hledger-web/Hledger/Web/App.hs @@ -12,13 +12,14 @@ and then Application.hs completes the job. {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} module Hledger.Web.App where import Control.Applicative ((<|>)) -import Control.Monad (join, when, unless) +import Control.Monad (join, mfilter, when, unless) -- import Control.Monad.Except (runExceptT) -- now re-exported by Hledger import Data.ByteString.Base64 qualified as B64 import Data.ByteString.Char8 qualified as BC @@ -26,7 +27,7 @@ import Data.Foldable (for_) import Data.Map qualified as M import Data.Traversable (for) import Data.IORef (IORef, readIORef, writeIORef) -import Data.Maybe (fromMaybe, listToMaybe) +import Data.Maybe (catMaybes, fromMaybe, listToMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as TE @@ -156,22 +157,23 @@ instance Yesod App where let browsemode = server_mode_ opts == ServeBrowse let rspec = reportspec_ (cliopts_ opts) - ropts = _rsReportOpts rspec ropts' = (_rsReportOpts rspec) {accountlistmode_ = ALTree -- force tree mode for sidebar ,empty_ = True -- show zero items by default } rspec' = rspec{_rsQuery=q,_rsReportOpts=ropts'} - -- The balance page's period parameter, which its search form and the - -- form's clear button keep. - periodParams <- case here of - BalanceR -> maybe [] (\p -> [("period", p)]) <$> lookupGetParam "period" - _ -> pure [] + -- The parameters the search form and its clear button keep, so that + -- a search does not reset the page: a report page's period and + -- accumulation mode, and the register's mode. The mode is kept only + -- when it is not the default, as the pages' own links carry it. + let keepable :: Text -> Text -> Bool + keepable "accum" v = v == "historical" + keepable _ v = not (T.null v) + keptParams <- fmap catMaybes . for (keptParamNames here) $ \name -> + fmap (name,) . mfilter (keepable name) <$> lookupGetParam name - hideEmptyAccts <- if empty_ ropts - then return True - else (== Just "1") . lookup "hideemptyaccts" . reqCookies <$> getRequest + hideEmptyAccts <- hideEmptyAccounts let accounts = balanceReportAsHtml (JournalR, RegisterR) here hideEmptyAccts trs j qparam qopts $ @@ -353,6 +355,26 @@ checkServerSideUiEnabled = do -- permissionDenied "server-side UI is disabled due to --serve-api" sendResponseStatus status403 ("server-side UI is disabled due to --serve-api" :: Text) +-- | The report pages: those that take a period and an accumulation mode. +reportRoutes :: [Route App] +reportRoutes = [BalanceR] + +-- | The query parameters a page's own links keep. +keptParamNames :: Route App -> [Text] +keptParamNames route + | route `elem` reportRoutes = ["period", "accum"] + | route == RegisterR = ["accum"] + | otherwise = [] + +-- | Are zero-balance accounts hidden ? They are with -E at startup, or +-- with the hideemptyaccts cookie the e key sets. +hideEmptyAccounts :: Handler Bool +hideEmptyAccounts = do + App{appOpts} <- getYesod + if empty_ $ _rsReportOpts $ reportspec_ $ cliopts_ appOpts + then return True + else (== Just "1") . lookup "hideemptyaccts" . reqCookies <$> getRequest + -- | Find out if the sidebar should be visible. Show it, unless there is a -- showsidebar cookie set to "0", or a ?sidebar=0 query parameter. shouldShowSidebar :: Handler Bool diff --git a/hledger-web/Hledger/Web/Handler/BalanceR.hs b/hledger-web/Hledger/Web/Handler/BalanceR.hs index fb218500cb2..bb058e6522d 100644 --- a/hledger-web/Hledger/Web/Handler/BalanceR.hs +++ b/hledger-web/Hledger/Web/Handler/BalanceR.hs @@ -5,24 +5,19 @@ module Hledger.Web.Handler.BalanceR where -import Text.Blaze.Html5 ((!)) import Text.Blaze.Html5 qualified as H -import Text.Blaze.Html5.Attributes qualified as A -import Text.Megaparsec.Error (errorBundlePretty) import Yesod qualified import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.Commands.Balance qualified as Balance -import Hledger.Query qualified as Query import Data.Text qualified as T import Hledger.Utils.I18n (tr, trf) import Hledger.Web.Import +import Hledger.Web.ReportPage import Hledger.Web.WebOptions -import Hledger.Web.Widget.Common (balanceReportLinks) -import Hledger.Write.Html (formatRow) -import Hledger.Write.Spreadsheet (Cell, NumLines) +import Hledger.Web.Widget.Common (accumulationLinks, intervalLinks, removeDates) -- | The balance or multi-period balance view, with sidebar. @@ -31,82 +26,62 @@ getBalanceR = do checkServerSideUiEnabled VD{j, q, qopts, qparam, opts, today, trs} <- getViewData require ViewPermission - -- The period parameter is a period expression as for -p: an interval - -- ("monthly"), a date span ("2024"), or both ("monthly in 2024"). - -- An empty one is no period at all, as from a search form with nothing in it. - mperiod <- (>>= \p -> if T.null p then Nothing else Just p) <$> lookupGetParam "period" + mperiod <- lookupGetParam "period" + maccum <- lookupGetParam "accum" + hideEmpty <- hideEmptyAccounts + urlrender <- getUrlRenderParams let withFilter t = if q /= Any then trf trs "{title}, filtered" [("title", t)] else t rspecOrig = reportspec_ $ cliopts_ opts roptsOrig = _rsReportOpts rspecOrig - eperiod = case mperiod of - -- No period: keep the interval the server was started with (-M, -p ...). - Nothing -> Right (interval_ roptsOrig, nulldatespan) - Just p -> either (Left . errorBundlePretty) Right $ parsePeriodExpr today p defaultLayout $ do -- TRANSLATORS: the browser tab title of this page. setTitleI (HMsg "balance - hledger-web") - - case eperiod of - -- No report links here: this page is a dead end until the navigation - -- question (#2242) is settled, see the pull request. + case reportParams today rspecOrig qparam q qopts hideEmpty mperiod maccum of Left err -> Yesod.toWidget $ do H.h2 $ H.toHtml $ withFilter $ reportTitle roptsOrig $ tr trs "Balance report" - H.div ! A.class_ "alert alert-danger" $ do - H.toHtml $ tr trs "Could not parse the period expression:" - H.pre $ H.toHtml err - Right (ivl, spn) -> do - let -- A date: search term can carry an interval too (eg - -- date:monthly), and as on the command line it wins over the - -- period; cf reportOptsToSpec. - reportinterval = fromMaybe ivl $ intervalFromQueryOpts qopts - -- The links in the report carry the search, and the period's - -- date span as a date: term, so that a row's register link is - -- restricted the same way the report is. - spanterm = ["date:" <> showDateSpan spn | spn /= nulldatespan] - ropts = - roptsOrig { - -- -E means the opposite in hledger-ui and hledger-web: hide - -- zero items, which are shown by default. The sidebar beside - -- this report already reads the flag that way (see App.hs). - empty_ = not $ empty_ roptsOrig, - balance_base_url_ = Just "", - querystring_ = Query.words'' queryprefixes qparam ++ spanterm, - interval_ = reportinterval - } - -- The period's date span restricts the report like a date: - -- search term would; cf queryFromFlags. - dateq - | spn == nulldatespan = Any - | date2_ ropts = Date2 spn - | otherwise = Date spn - -- Unlike the journal and register pages, keep any depth limit: - -- the report reads it from the query, and it is how a balance - -- report gets summarized (--depth at startup, or depth: in the search). - rspec = - rspecOrig { - _rsQuery = simplifyQuery $ And [q, dateq], - _rsReportOpts = ropts - } + paramError trs err + Right ReportParams{rpRopts, rpRspec, rpSpan, rpInterval, rpPeriod, rpAccum} -> do + let -- The page shows balance changes unless asked for ending balances. + accum = fromMaybe PerPeriod rpAccum + ropts = rpRopts{balanceaccum_ = accum} + rspec = rpRspec{_rsReportOpts = ropts} + styles = journalCommodityStylesWith HardRounding j + mbr = styleAmounts styles $ multiBalanceReport rspec j + colspans = prDates mbr + -- What links staying on this page keep: the period as given, + -- and the mode when it is not the default. + periodParams = [("period", p) | Just p <- [rpPeriod]] + accumParams = [("accum", "historical") | accum == Historical] + qParams = [("q", qparam) | not (T.null qparam)] + -- A column heading opens this report for that column's period, + -- in place of any date terms in the search, which the column + -- narrows anyway. + headinglink spn = urlrender BalanceR $ + ("period", showDateSpanForQuery spn) : + [("q", qt) | let qt = T.unwords $ removeDates qparam, not (T.null qt)] ++ + accumParams -- The heading, and the report's rows in three parts, for the -- table's thead, tbody, and tfoot. - (title, parts) = case reportinterval of + (title, header, body, totals) = case rpInterval of NoInterval -> - let (header, body, totals) = + let (h, b, t) = Balance.balanceReportAsSpreadsheetParts oneLineNoCostFmt ropts $ - styleAmounts (journalCommodityStylesWith HardRounding j) $ - balanceReport rspec j - in ( reportTitle ropts $ tr trs "Balance report" - , ([toList header], map toList body, map toList totals)) + styleAmounts styles $ balanceReport rspec j + -- The plain page has its own name; one narrowed to a + -- period, or showing ending balances, says so like the + -- multi-period page does. + dflt | rpSpan == nulldatespan && accum == PerPeriod = tr trs "Balance report" + | otherwise = trimColon $ Balance.multiBalanceReportTitle ropts mbr + in (reportTitle ropts dflt, [toList h], map toList b, map toList t) _ -> - let mbr = styleAmounts (journalCommodityStylesWith HardRounding j) $ - multiBalanceReport rspec j - in ( maybe (trimColon $ Balance.multiBalanceReportTitle ropts mbr) id (title_ ropts) - , Balance.multiBalanceReportAsSpreadsheetParts oneLineNoCostFmt ropts mbr - ) + let (h, b, t) = Balance.multiBalanceReportAsSpreadsheetParts oneLineNoCostFmt ropts mbr + in ( reportTitle ropts $ trimColon $ Balance.multiBalanceReportTitle ropts mbr + , map (relinkDateHeaders (columnHeading ropts colspans) headinglink colspans) h, b, t) Yesod.toWidget $ H.h2 $ H.toHtml $ withFilter title - Yesod.toWidget $ balanceReportLinks BalanceR trs qparam spn reportinterval - Yesod.toWidget $ reportTable parts + Yesod.toWidget $ accumulationLinks trs BalanceR (periodParams ++ qParams) accum + Yesod.toWidget $ intervalLinks trs BalanceR accumParams qparam rpSpan rpInterval + Yesod.toWidget $ reportTable header [(Nothing, body, [])] totals -- | The heading for a report: --title if one was given, otherwise the -- given default. @@ -114,22 +89,7 @@ reportTitle :: ReportOpts -> Text -> Text reportTitle ropts dflt = fromMaybe dflt $ title_ ropts -- | Drop the trailing colon of a command line report title, which a heading --- does not want. A translation of it may end in " :" or "\uff1a" instead, +-- does not want. A translation of it may end in " :" or ":" instead, -- so drop whichever is there. trimColon :: Text -> Text trimColon = T.dropWhileEnd (`elem` (":\65306 " :: String)) - --- | A report's heading, body, and total rows as a table in the page's own --- style, scrolling sideways within the page when it is wider (see --- .report-table in hledger.css; bootstrap's .table-responsive does that --- only on a phone). -reportTable :: - ([[Cell NumLines Text]], [[Cell NumLines Text]], [[Cell NumLines Text]]) -> Html -reportTable (header, body, totals) = - H.div ! A.class_ "table-responsive report-table" $ - H.table ! A.class_ "balancereport table table-condensed" $ do - H.thead $ rows header - H.tbody $ rows body - H.tfoot $ rows totals - where - rows = traverse_ (formatRow . map (fmap H.toHtml)) diff --git a/hledger-web/Hledger/Web/Handler/RegisterR.hs b/hledger-web/Hledger/Web/Handler/RegisterR.hs index 6e69ec4baa4..07a9fcc2af8 100644 --- a/hledger-web/Hledger/Web/Handler/RegisterR.hs +++ b/hledger-web/Hledger/Web/Handler/RegisterR.hs @@ -22,7 +22,7 @@ import Hledger.Web.Import import Hledger.Web.WebOptions import Hledger.Web.Widget.AddForm (addModal) import Hledger.Web.Widget.Common - (accountQuery, mixedAmountAsHtml, + (accountQuery, accountOnlyQuery, mixedAmountAsHtml, transactionFragment, removeDates, removeInacct, replaceInacct) -- | The main journal/account register view, with accounts sidebar. @@ -31,6 +31,11 @@ getRegisterR = do checkServerSideUiEnabled VD{perms, j, q, opts, qparam, qopts, today, trs} <- getViewData require ViewPermission + -- With accum=historical the running balance is the account's balance, + -- starting from before the query's start date, rather than a total of + -- the transactions shown; a report's ending balance links here that way, + -- so that the balance ends on the figure clicked. + historical <- (== Just "historical") <$> lookupGetParam "accum" let title = case inAccount qopts of Nothing -> tr trs "all accounts" @@ -38,24 +43,47 @@ getRegisterR = do Just (a, False) -> trf trs "{account} (excluding subaccounts)" [("account", a)] header = if q /= Any then trf trs "{title}, filtered" [("title", title)] else title - let rspec = reportspec_ (cliopts_ opts) + let rspec0 = reportspec_ (cliopts_ opts) + ropts = (_rsReportOpts rspec0){balanceaccum_ = if historical then Historical else PerPeriod} + rspec = rspec0{_rsReportOpts = ropts} + -- links staying on this register keep its mode + accumParams = [("accum", "historical") | historical] + qParams t = [("q", t) | not (T.null t)] acctQuery = fromMaybe Any (inAccountQuery qopts) - acctlink acc = (RegisterR, [("q", replaceInacct qparam $ accountQuery acc)]) + acctlink acc = (RegisterR, ("q", replaceInacct qparam $ accountQuery acc) : accumParams) otherTransAccounts = map (\(acct,(name,comma)) -> (acct, (T.pack name, T.pack comma))) . undecorateLinks . elideRightDecorated 40 . decorateLinks . - addCommas . preferReal . otherTransactionAccounts q acctQuery + addCommas . preferReal . otherTransactionAccounts j q acctQuery addCommas xs = zip xs $ zip (map (T.unpack . accountSummarisedName . paccount) xs) $ tailSafe (", "<$xs) ++ [""] - items = - styleAmounts (journalCommodityStylesWith HardRounding j) $ - accountTransactionsReport rspec{_rsQuery=q} j acctQuery + styles = journalCommodityStylesWith HardRounding j + (startbal, items) = + bimap (styleAmounts styles) (styleAmounts styles) $ + accountTransactionsReportWithStart rspec{_rsQuery=q} j acctQuery balancelabel - | isJust (inAccount qopts), balanceaccum_ (_rsReportOpts rspec) == Historical = trc trs "column heading" "Historical Total" + | historical = trc trs "column heading" "Historical Total" | isJust (inAccount qopts) = trc trs "column heading" "Period Total" | otherwise = trc trs "column heading" "Total" + -- The balance column's heading switches the mode. + accumToggle = (RegisterR, qParams qparam ++ [("accum", "historical") | not historical]) + accumToggleTitle + | historical = tr trs "Show the running balance from the start of this period" + | otherwise = tr trs "Show the running balance including everything before this period" + -- In historical mode with a start date, the balance brought forward + -- from before it is the oldest row, linking to the transactions + -- before the period: those before the start date by the kind of + -- date (primary or secondary) the report took it from, as the + -- report chooses the balance's cutoff. + mstart = asum [ (,) secondary <$> queryStartDate secondary q + | secondary <- [date2_ ropts, not $ date2_ ropts] ] + broughtForwardLink (secondary, start) = + (RegisterR, [("q", T.unwords $ + maybe [] (\(acc, incl) -> [if incl then accountQuery acc else accountOnlyQuery acc]) (inAccount qopts) ++ + [(if secondary then "date2:.." else "date:..") <> showDate start] ++ + removeDates (T.unwords $ removeInacct qparam))]) transactionFrag = transactionFragment j defaultLayout $ do -- TRANSLATORS: the browser tab title of this page. @@ -63,8 +91,8 @@ getRegisterR = do $(widgetFile "register") -- cf. Hledger.Reports.AccountTransactionsReport.accountTransactionsReportItems -otherTransactionAccounts :: Query -> Query -> Transaction -> [Posting] -otherTransactionAccounts reportq thisacctq torig +otherTransactionAccounts :: Journal -> Query -> Query -> Transaction -> [Posting] +otherTransactionAccounts j reportq thisacctq torig -- no current account ? summarise all matched postings | thisacctq == None = reportps -- only postings to current account ? summarise those @@ -72,7 +100,8 @@ otherTransactionAccounts reportq thisacctq torig -- summarise matched postings to other account(s) | otherwise = otheracctps where - reportps = tpostings $ filterTransactionPostings reportq torig + -- given the account types, so that a type: term matches postings here as in the report + reportps = tpostings $ filterTransactionPostingsExtra (journalAccountType j) reportq torig (thisacctps, otheracctps) = partition (matchesPosting thisacctq) reportps otheraccts = nub $ map paccount otheracctps @@ -106,11 +135,11 @@ decorateLinks = concatMap $ \(acct, (name, comma)) -> -- | The register balance chart: its markup, carrying the per-commodity -- series as JSON in a data attribute. hledger.js draws it with flot on page -- load; see registerChartInit there. -registerChartHtml :: Text -> String -> [(CommoditySymbol, [AccountTransactionsReportItem])] -> HtmlUrl AppRoute -registerChartHtml q title percommoditytxnreports = $(hamletFile "templates/chart.hamlet") +registerChartHtml :: Text -> [(Text, Text)] -> String -> [(CommoditySymbol, [AccountTransactionsReportItem])] -> HtmlUrl AppRoute +registerChartHtml q accumParams title percommoditytxnreports = $(hamletFile "templates/chart.hamlet") where charttitle = if null title then "" else title ++ ":" - nodatelink = (RegisterR, [("q", T.unwords $ removeDates q)]) + nodatelink = (RegisterR, [("q", t) | let t = T.unwords $ removeDates q, not (T.null t)] ++ accumParams) -- One entry per commodity: its symbol, and per transaction the point flot -- plots followed by the texts the tooltip and click handler show. seriesjson = encodeToLazyText $ map commoditySeries percommoditytxnreports diff --git a/hledger-web/Hledger/Web/ReportPage.hs b/hledger-web/Hledger/Web/ReportPage.hs new file mode 100644 index 00000000000..e08831db364 --- /dev/null +++ b/hledger-web/Hledger/Web/ReportPage.hs @@ -0,0 +1,175 @@ +{-| +What hledger-web's report pages share: resolving their parameters +against the startup options, and rendering a report's cells as the +page's table. +-} + +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Hledger.Web.ReportPage ( + ReportParams(..), + ReportParamError(..), + reportParams, + paramError, + columnHeading, + relinkDateHeaders, + reportTable, +) where + +import Control.Monad (mfilter, unless) +import Data.Foldable (for_, traverse_) +import Data.Maybe (fromMaybe, listToMaybe) +import Data.Text (Text) +import Data.Text qualified as T +import Data.Time.Calendar (Day) +import Text.Blaze.Html5 ((!)) +import Text.Blaze.Html5 qualified as H +import Text.Blaze.Html5.Attributes qualified as A +import Text.Megaparsec.Error (errorBundlePretty) + +import Hledger.Utils.I18n (Translations, tr) +import Hledger +import Hledger.Cli.Anchor (dateTerm, renderPeriodHeading) +import Hledger.Query qualified as Query +import Hledger.Write.Html (Html, formatCell, nl) +import Hledger.Write.Spreadsheet (Cell(..), NumLines) + +-- | A report page's parameters, resolved. +data ReportParams = ReportParams { + rpRopts :: ReportOpts, + -- ^ The startup report options with the page's search, period, interval, + -- and zero-item setting applied, and links made relative. The + -- accumulation mode is left for the page to set. + rpRspec :: ReportSpec, + -- ^ The startup report spec with those options and the page's query. + rpSpan :: DateSpan, + -- ^ The period parameter's date span, or the unbounded span. + rpInterval :: Interval, + -- ^ The reporting interval: from a date: search term, else the period + -- parameter, else startup. + rpPeriod :: Maybe Text, + -- ^ The period parameter as given, for links that keep it. + rpAccum :: Maybe BalanceAccumulation + -- ^ The accum parameter, if one was given. +} + +-- | A parameter the page cannot use, with what was given. +data ReportParamError = BadPeriod String | BadAccum Text + +-- | Resolve a report page's parameters: today's date, the startup report +-- spec, the search parameter and its parsed query and options, whether +-- zero items are hidden, and the period and accum parameters. +-- +-- The period parameter is a period expression as for -p: an interval +-- ("monthly"), a date span ("2024"), or both ("monthly in 2024"). An +-- empty one is no period at all, as from a search form with nothing in +-- it; then the startup interval applies. A date: search term can carry +-- an interval too (date:monthly), and as on the command line it wins +-- over the period; cf reportOptsToSpec. The period's date span restricts +-- the report like a date: search term would, and the report's links +-- carry it, so that a row's register is restricted the same way. +reportParams :: + Day -> ReportSpec -> Text -> Query -> [QueryOpt] -> Bool -> Maybe Text -> Maybe Text -> + Either ReportParamError ReportParams +reportParams today rspecOrig qparam q qopts hideEmpty mperiod maccum = do + let roptsOrig = _rsReportOpts rspecOrig + rpPeriod = mfilter (not . T.null) mperiod + (ivl, rpSpan) <- case rpPeriod of + Nothing -> Right (interval_ roptsOrig, nulldatespan) + Just p -> either (Left . BadPeriod . errorBundlePretty) Right $ parsePeriodExpr today p + rpAccum <- parseAccum maccum + let rpInterval = fromMaybe ivl $ intervalFromQueryOpts qopts + rpRopts = + roptsOrig { + -- -E means the opposite in hledger-ui and hledger-web: hide + -- zero items, which are shown by default, as the sidebar does. + empty_ = not hideEmpty, + balance_base_url_ = Just "", + querystring_ = Query.words'' queryprefixes qparam ++ dateTerm (date2_ roptsOrig) rpSpan, + interval_ = rpInterval + } + -- cf queryFromFlags + dateq + | rpSpan == nulldatespan = Any + | date2_ rpRopts = Date2 rpSpan + | otherwise = Date rpSpan + -- Unlike the journal and register pages, keep any depth limit: + -- the report reads it from the query, and it is how a balance + -- report gets summarized (--depth at startup, or depth: in the search). + rpRspec = + rspecOrig { + _rsQuery = simplifyQuery $ And [q, dateq], + _rsReportOpts = rpRopts + } + Right ReportParams{..} + +-- | The accum parameter: "historical" for ending balances, "change" for +-- balance changes, or none for the page's default. +parseAccum :: Maybe Text -> Either ReportParamError (Maybe BalanceAccumulation) +parseAccum = \case + Nothing -> Right Nothing + Just "" -> Right Nothing + Just "historical" -> Right $ Just Historical + Just "change" -> Right $ Just PerPeriod + Just other -> Left $ BadAccum other + +-- | Explain a parameter the page could not use. +paramError :: Translations -> ReportParamError -> Html +paramError trs = \case + BadPeriod err -> + H.div ! A.class_ "alert alert-danger" $ do + H.toHtml $ tr trs "Could not parse the period expression:" + H.pre $ H.toHtml err + BadAccum v -> + H.div ! A.class_ "alert alert-danger" $ do + H.toHtml $ tr trs "Unknown balance accumulation mode:" + H.pre $ H.toHtml v + +-- | A column's heading: its period, or for ending balances the period's +-- end date, which is what an ending balance is at. +columnHeading :: ReportOpts -> [DateSpan] -> DateSpan -> Text +columnHeading ropts colspans spn = + case balanceaccum_ ropts of + Historical -> reportPeriodName ropts{balanceaccum_ = Historical} colspans spn + _ -> renderPeriodHeading (period_titles_ ropts) spn + +-- | Give a report's heading row this page's column headings and links: +-- the cells that link (the columns' periods) get the given heading text +-- and link for their span. +relinkDateHeaders :: + (DateSpan -> Text) -> (DateSpan -> Text) -> [DateSpan] -> [Cell NumLines Text] -> [Cell NumLines Text] +relinkDateHeaders heading link = go + where + go (spn:spns) (c:cs) + | not (T.null $ cellAnchor c) = + c {cellContent = heading spn, cellAnchor = link spn, cellTitle = "Show this report for this period"} : go spns cs + go spns (c:cs) = c : go spns cs + go _ [] = [] + +-- | A report as a table in the page's own style: the heading rows, then +-- a table section per report section (a titled row, its rows, then its +-- subtotal rows), then the total rows as the table's footer. It scrolls +-- sideways within the page when it is wider (see .report-table in +-- hledger.css; bootstrap's .table-responsive does that only on a phone). +reportTable :: + [[Cell NumLines Text]] -> [(Maybe Text, [[Cell NumLines Text]], [[Cell NumLines Text]])] -> [[Cell NumLines Text]] -> Html +reportTable header sections footer = + H.div ! A.class_ "table-responsive report-table" $ + H.table ! A.class_ "balancereport table table-condensed" $ do + H.thead $ rows Nothing header + traverse_ section sections + unless (null footer) $ H.tfoot $ rows Nothing footer + where + ncols = maybe 1 length $ listToMaybe header + section (mtitle, body, subtotals) = + H.tbody $ do + for_ mtitle $ \t -> + H.tr ! A.class_ "section" $ + H.th ! A.colspan (H.toValue ncols) ! H.customAttribute "scope" "rowgroup" $ H.toHtml t + rows Nothing body + rows (Just "subtotal") subtotals + rows mcls = traverse_ $ \r -> + maybe id (\cls -> (! A.class_ cls)) mcls H.tr (traverse_ (formatCell . fmap H.toHtml) r) <> nl diff --git a/hledger-web/Hledger/Web/Test.hs b/hledger-web/Hledger/Web/Test.hs index 0ecf884fa02..ad0d18d17ff 100644 --- a/hledger-web/Hledger/Web/Test.hs +++ b/hledger-web/Hledger/Web/Test.hs @@ -59,6 +59,7 @@ import System.Environment (setEnv, unsetEnv) import System.FilePath (()) import Test.Hspec (expectationFailure, hspec) import Text.Printf (printf) +import Web.Cookie (defaultSetCookie, setCookieName, setCookieValue) import Yesod.Default.Config import Yesod.Test @@ -392,8 +393,8 @@ hledgerWebTest = do statusIs 200 bodyContains "Salden - hledger-web" bodyContains "

Saldenbericht

" - bodyContains "Bericht:" - bodyContains "title=\"Monatlichen Saldenbericht anzeigen\">Monatlich" + bodyContains "Intervall:" + bodyContains "title=\"Eine Spalte pro Monat anzeigen\">Monatlich" -- A translation is viewer-controlled text: it must be rendered as text -- wherever it lands, including inside attributes. @@ -558,6 +559,127 @@ hledgerWebTest = do statusIs 200 bodyNotContains "name=\"period\"" + yit "shows balance changes for a period, and says so" $ do + request $ do + setMethod "GET" + setUrl BalanceR + addGetParam "period" "2025-02" + statusIs 200 + bodyContains "

Balance changes in 2025-02

" + -- the report's own links are relative (the sidebar's are not) + bodyNotContains "href=\"register?q=inacct:income:salary" + bodyContains ("class=\"current\" href=\"" ++ defbaseurl defhost defport ++ "/balance?period=2025-02\" title=\"Show how much") + + yit "shows ending balances with accum=historical, asking their registers for the same" $ do + request $ do + setMethod "GET" + setUrl BalanceR + addGetParam "period" "2025-02" + addGetParam "accum" "historical" + statusIs 200 + bodyContains "

Ending balances (historical) in 2025-02

" + -- no February postings, but a balance, whose register must start from before February + bodyContains "href=\"register?q=inacct:income:salary+date:2025-02&accum=historical\"" + -- the search form and the interval links keep the mode; the mode links mark it + bodyContains "" + bodyContains "/balance?period=monthly%202025-02&accum=historical\"" + bodyContains ("class=\"current\" href=\"" ++ defbaseurl defhost defport ++ "/balance?period=2025-02&accum=historical\"") + + yit "heads ending balance columns with their end dates, linking to this report for the period" $ do + request $ do + setMethod "GET" + setUrl BalanceR + addGetParam "period" "monthly" + addGetParam "accum" "historical" + statusIs 200 + bodyContains ">2025-01-31<" + bodyContains ">2025-02-28<" + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/balance?period=2025-01&accum=historical\" title=\"Show this report for this period\"") + + yit "links a balance change column's heading to this report for the period" $ do + request $ do + setMethod "GET" + setUrl BalanceR + addGetParam "period" "monthly" + addGetParam "q" "date:2025" + statusIs 200 + bodyContains ">2025-01<" + -- the column's period replaces the search's date term + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/balance?period=2025-01\" title=\"Show this report for this period\"") + + yit "reports an accumulation mode it does not know" $ do + request $ do + setMethod "GET" + setUrl BalanceR + addGetParam "accum" "bogus" + statusIs 200 + bodyContains "Unknown balance accumulation mode" + bodyNotContains "" + + yit "keeps the accumulation mode off the journal page's search form" $ do + request $ do + setMethod "GET" + setUrl JournalR + addGetParam "accum" "historical" + statusIs 200 + bodyNotContains "name=\"accum\"" + + -- The register: a period's transactions, with a running total from zero, + -- or with accum=historical, the account's balance from before the period. + runTests "hledger-web register page" [] bj $ do + + -- The register's table has a tbody; the sidebar, which shows the same + -- accounts, has none, so these assertions look only at the register. + yit "totals the period from zero by default" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking date:2025-02" + statusIs 200 + bodyContains ">Period Total" + htmlAnyContain "#main-content tbody td.amount" "-10" + bodyNotContains ">90<" + bodyNotContains "Balance brought forward" + bodyNotContains "name=\"accum\"" + -- the balance column's heading switches to historical mode + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/register?q=inacct%3Aassets%3Abank%3Achecking%20date%3A2025-02&accum=historical\" title=\"Show the running balance including everything before this period\">Period Total") + + yit "starts from the balance brought forward with accum=historical" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking date:2025-02" + addGetParam "accum" "historical" + statusIs 200 + bodyContains ">Historical Total" + htmlAnyContain "#main-content tbody td.amount" "90" + -- the oldest row is the balance brought forward, linking to the transactions before the period + htmlAnyContain "#main-content tbody tr.broughtforward td.amount" "100" + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/register?q=inacct%3Aassets%3Abank%3Achecking%20date%3A..2025-02-01\" title=\"Show the transactions before this period\">") + -- the search form, the other-account links, and the chart's base link keep the mode + bodyContains "" + bodyContains "&accum=historical#" + bodyContains ("data-baselink=\"" ++ defbaseurl defhost defport ++ "/register?q=inacct%3Aassets%3Abank%3Achecking&accum=historical\"") + -- the heading switches back, dropping the mode + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/register?q=inacct%3Aassets%3Abank%3Achecking%20date%3A2025-02\" title=\"Show the running balance from the start of this period\">Historical Total") + + yit "cuts the balance brought forward off by the kind of date the query has" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking date2:2025-02" + addGetParam "accum" "historical" + statusIs 200 + bodyContains "date2%3A..2025-02-01\" title=\"Show the transactions before this period\">" + + yit "names the other accounts of a type: search's transactions" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "type:X date:2025-02" + statusIs 200 + bodyContains "title=\"expenses:food\">" + -- A commodity directive sets the display precision; the page must apply it, -- as the sidebar beside it and the command line report do. sj <- fmap (either error' id) . runExceptT . journalFinalise biopts "styled.journal" "" =<< @@ -593,6 +715,13 @@ hledgerWebTest = do statusIs 200 bodyContains "href=\"register?q=inacct:assets:zeroed\"" + yit "hides them when the sidebar does (the e key's cookie)" $ do + testSetCookie defaultSetCookie{setCookieName = "hideemptyaccts", setCookieValue = "1"} + get BalanceR + statusIs 200 + bodyContains "href=\"register?q=inacct:assets:kept\"" + bodyNotContains "href=\"register?q=inacct:assets:zeroed\"" + runTests "hledger-web with -E" [("empty","")] ej $ do yit "hides zero items, the opposite of the command line" $ do diff --git a/hledger-web/Hledger/Web/Widget/Common.hs b/hledger-web/Hledger/Web/Widget/Common.hs index 5a0f8b902c0..7c5a1419f3e 100644 --- a/hledger-web/Hledger/Web/Widget/Common.hs +++ b/hledger-web/Hledger/Web/Widget/Common.hs @@ -8,7 +8,9 @@ module Hledger.Web.Widget.Common ( accountQuery , accountOnlyQuery , balanceReportAsHtml - , balanceReportLinks + , linksRow + , intervalLinks + , accumulationLinks , helplink , mixedAmountAsHtml , fromFormSuccess @@ -34,8 +36,9 @@ import Text.Hamlet (hamletFile) import Text.Printf (printf) import Yesod -import Hledger.Utils.I18n (Translations, tr) +import Hledger.Utils.I18n (Translations, tr, trc) import Hledger +import Hledger.Cli.Anchor qualified as Anchor import Hledger.Cli.Utils (writeFileWithBackupIfChanged) import Hledger.Web.Settings (manualurl) import Hledger.Query qualified as Query @@ -90,39 +93,56 @@ balanceReportAsHtml (journalR, registerR) here hideEmpty trs j qparam qopts (ite where isInteresting a = not (all (mixedAmountLooksZero . bdexcludingsubs) . pdperiods $ adata a) || any isInteresting (asubs a) matchesAcctSelector acct = Just True == ((`matchesAccount` acct) <$> inAccountQuery qopts) --- | Links to the balance report page, single-period and for each --- interval, carrying the current search and date span; the report being --- shown, identified by the interval it was built with, is marked. -balanceReportLinks :: r -> Translations -> Text -> DateSpan -> Interval -> HtmlUrl r -balanceReportLinks balanceR trs qparam spn current = - $(hamletFile "templates/balance-links.hamlet") +-- | A row of links above a report: a label, then each link's label, +-- title, target, and whether it is the one being shown. +-- Each label and title is a whole phrase, not a word slotted into a +-- sentence: an adjective that fits one language's sentence does not fit +-- another's, so a translation cannot be assembled from parts. +linksRow :: Text -> [(Text, Text, (r, [(Text, Text)]), Bool)] -> HtmlUrl r +linksRow rowlabel items = $(hamletFile "templates/balance-links.hamlet") + +-- | Links to the same report page for each reporting interval, keeping +-- the search, the period's date span, and the given parameters; the +-- interval being shown is marked. +intervalLinks :: Translations -> r -> [(Text, Text)] -> Text -> DateSpan -> Interval -> HtmlUrl r +intervalLinks trs route kept qparam spn current = + -- TRANSLATORS: the label before a report's interval links. + linksRow (tr trs "Interval:") + [ (label, title, link mword, ivl == current) | (label, title, mword, ivl) <- intervals ] where - -- TRANSLATORS: the label before the balance page's report links. - reportlabel = tr trs "Report:" - -- Each link's label and title is a whole phrase, not a word slotted - -- into a sentence: an adjective that fits one language's sentence does - -- not fit another's, so a translation cannot be assembled from parts. - -- TRANSLATORS: the balance page's report links: each link's text, and its tooltip. - reports :: [(Text, Text, Maybe Text, Interval)] - reports = - [ (tr trs "Balance", tr trs "Show the balance report", Nothing, NoInterval) - , (tr trs "Yearly", tr trs "Show the yearly balance report", Just "yearly", Years 1) - , (tr trs "Quarterly", tr trs "Show the quarterly balance report", Just "quarterly", Quarters 1) - , (tr trs "Monthly", tr trs "Show the monthly balance report", Just "monthly", Months 1) - , (tr trs "Weekly", tr trs "Show the weekly balance report", Just "weekly", Weeks 1) - , (tr trs "Daily", tr trs "Show the daily balance report", Just "daily", Days 1) + -- TRANSLATORS: the interval links above a report: each link's text, and its tooltip. + intervals :: [(Text, Text, Maybe Text, Interval)] + intervals = + [ (trc trs "interval" "None", tr trs "Show one column for the whole period", Nothing, NoInterval) + , (tr trs "Yearly", tr trs "Show a column per year", Just "yearly", Years 1) + , (tr trs "Quarterly", tr trs "Show a column per quarter", Just "quarterly", Quarters 1) + , (tr trs "Monthly", tr trs "Show a column per month", Just "monthly", Months 1) + , (tr trs "Weekly", tr trs "Show a column per week", Just "weekly", Weeks 1) + , (tr trs "Daily", tr trs "Show a column per day", Just "daily", Days 1) ] -- Each link keeps the period's date span, so that changing the -- interval does not silently widen the report to the whole journal. -- "monthly 2025-01-01..2025-12-31" is a period expression like any other. - spantext = if spn == nulldatespan then "" else showDateSpan spn + spantext = if spn == nulldatespan then "" else showDateSpanForQuery spn periodparam mword = case (mword, spantext) of (Nothing, "") -> [] (Nothing, sp) -> [("period", sp)] (Just w, "") -> [("period", w)] (Just w, sp) -> [("period", w <> " " <> sp)] link mword = - (balanceR, periodparam mword ++ [("q", qparam) | not (T.null qparam)]) + (route, periodparam mword ++ [("q", qparam) | not (T.null qparam)] ++ kept) + +-- | Links to the same report page showing balance changes or ending +-- balances, keeping the given parameters; the one being shown is marked. +accumulationLinks :: Translations -> r -> [(Text, Text)] -> BalanceAccumulation -> HtmlUrl r +accumulationLinks trs route kept current = + -- TRANSLATORS: the label before a report's accumulation mode links, and the links' text and tooltips. + linksRow (tr trs "Show:") + [ (tr trs "Balance changes", tr trs "Show how much each balance changed in each period", + (route, kept), current /= Historical) + , (tr trs "Ending balances", tr trs "Show each balance at the end of each period, including everything before it", + (route, kept ++ [("accum", "historical")]), current == Historical) + ] accountQuery :: AccountName -> Text accountQuery = ("inacct:" <>) . quoteIfSpaced @@ -150,19 +170,13 @@ transactionFragment j Transaction{tindex, tsourcepos} = -- or 0 if this txn has no known file (eg a forecasted txn) tfileindex = maybe 0 (+1) $ elemIndex (sourceName $ fst tsourcepos) (journalFilePaths j) +-- | The search's terms without its date terms, each quoted if it needs to be. removeDates :: Text -> [Text] -removeDates = - map quoteIfSpaced . - filter (\term -> - not $ T.isPrefixOf "date:" term || T.isPrefixOf "date2:" term) . - Query.words'' queryprefixes +removeDates = map quoteIfSpaced . Anchor.removeDates . Query.words'' queryprefixes +-- | The search's terms without those naming an account, each quoted if it needs to be. removeInacct :: Text -> [Text] -removeInacct = - map quoteIfSpaced . - filter (\term -> - not $ T.isPrefixOf "inacct:" term || T.isPrefixOf "inacctonly:" term) . - Query.words'' queryprefixes +removeInacct = map quoteIfSpaced . Anchor.removeInacct . Query.words'' queryprefixes replaceInacct :: Text -> Text -> Text replaceInacct q acct = T.unwords $ acct : removeInacct q diff --git a/hledger-web/hledger-web.cabal b/hledger-web/hledger-web.cabal index 959a8c2b31d..8abdf5491d7 100644 --- a/hledger-web/hledger-web.cabal +++ b/hledger-web/hledger-web.cabal @@ -108,6 +108,7 @@ library Hledger.Web.Handler.UploadR Hledger.Web.Import Hledger.Web.Main + Hledger.Web.ReportPage Hledger.Web.Settings Hledger.Web.Settings.StaticFiles Hledger.Web.Test @@ -137,6 +138,7 @@ library , conduit , conduit-extra >=1.1 , containers >=0.5.9 + , cookie , data-default , directory >=1.2.3.0 , entropy diff --git a/hledger-web/hledger-web.m4.md b/hledger-web/hledger-web.m4.md index 0f10f1d61ee..579dcb98216 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -213,19 +213,34 @@ Changes which would leave the journal file(s) unparseable or invalid # BALANCE REPORTS Besides the journal and account registers, hledger-web can show the -[balance report](hledger.md#balance) at `/balance`, with each account -linked to its register. No page links to it yet: it is reachable by -entering the url, until there are more such reports and a navigation -scheme to hold them. A `period` parameter, like the command line's -`-p/--period`, selects the interval and/or the period, eg -`/balance?period=monthly` or `/balance?period=quarterly in 2025`; -column headings link to the register for that period. +[balance report](hledger.md#balance) at `/balance`. No page links to it +yet: it is reachable by entering the url, until there are more such +reports and a navigation scheme to hold them. A `period` parameter, +like the command line's `-p/--period`, selects the interval and/or the +period, eg `/balance?period=monthly` or +`/balance?period=quarterly in 2025`; the links above the report offer +the usual intervals. The search box filters the report like the other pages, including `depth:` terms, and the general report options given at startup, such as `--depth`, `-B`, or `-V`, apply. A `date:` term can set the interval too, as on the command line (`date:monthly`, or `"date:quarterly in 2025"`), and wins over the `period` parameter. +The report shows balance changes, or with an `accum=historical` +parameter, which the "Ending balances" link above it sets, each +account's balance at the end of each period. +Every figure links to the register it is derived from: the account's +transactions in that period, whose final balance is the figure. (The +two can differ when a transaction's postings have dates of their own: +the report counts such a posting in its own period, while the register +shows its whole transaction.) +An ending balance's register runs in historical mode (its own +`accum=historical`): its running balance starts from the balance +brought forward from before the period, shown as its oldest row, and +its balance column's heading switches between the two modes. +Account names link to their registers, and column headings to this +report for that period. + # RELOADING hledger-web detects changes made to the files by other means (eg if you edit diff --git a/hledger-web/package.yaml b/hledger-web/package.yaml index 6ed4bfffd2e..05690991234 100644 --- a/hledger-web/package.yaml +++ b/hledger-web/package.yaml @@ -152,6 +152,7 @@ library: - yesod-form >=1.4 && < 1.8 - hspec - yesod-test + - cookie executables: hledger-web: diff --git a/hledger-web/templates/balance-links.hamlet b/hledger-web/templates/balance-links.hamlet index 6b7fc4b6da3..669c75ce60f 100644 --- a/hledger-web/templates/balance-links.hamlet +++ b/hledger-web/templates/balance-links.hamlet @@ -1,4 +1,4 @@

- #{reportlabel} - $forall (label, title, mperiod, interval) <- reports - #{label} + #{rowlabel} + $forall (label, title, link, current) <- items + #{label} diff --git a/hledger-web/templates/default-layout.hamlet b/hledger-web/templates/default-layout.hamlet index 8f834a62b30..790f15832dd 100644 --- a/hledger-web/templates/default-layout.hamlet +++ b/hledger-web/templates/default-layout.hamlet @@ -48,11 +48,11 @@ $if elem ViewPermission perms - $forall (name, value) <- periodParams + $forall (name, value) <- keptParams

@@ -15,7 +15,7 @@ $forall (torig, tacct, split, _acct, amt, bal) <- items @@ -34,6 +34,21 @@ ^{mixedAmountAsHtml amt} + " + bodyContains "" + bodyContains "" + bodyContains "" + bodyContains "Net:" + -- figures link to registers in historical mode, restricted to the + -- section's account types; a liability's says its sign differs + bodyContains "href=\"register?q=inacct:assets:bank:checking+type:A&accum=historical\"" + bodyContains "href=\"register?q=inacct:liabilities:card+date:2025-01-01..2025-02-07+type:L&accum=historical\" title=\"Show the transactions behind this balance, which the register shows with the opposite sign\">" + -- a section total links to the section's account types, the net total to all of them + bodyContains "href=\"register?q=type:L+date:2025-01-01..2025-02-07&accum=historical\"" + bodyContains "href=\"register?q=type:AL+date:2025-01-01..2025-02-07&accum=historical\"" + -- shown as positive amounts, and without inline styles + bodyNotContains "class=\"amount negative\"" + bodyNotContains "style=\"" + + yit "serves the multi-period balance sheet, headed by end dates linking to this report" $ do + request $ do + setMethod "GET" + setUrl BalancesheetR + addGetParam "period" "monthly" + statusIs 200 + bodyContains "

Monthly Balance Sheet 2025-01-31..2025-02-28

" + bodyContains ">2025-01-31<" + bodyContains ">2025-02-28<" + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/balancesheet?period=2025-01\" title=\"Show this report for this period\"") + bodyContains "" + + yit "shows balance changes instead when asked, and says so" $ do + request $ do + setMethod "GET" + setUrl BalancesheetR + addGetParam "accum" "change" + statusIs 200 + bodyContains "

Balance Sheet 2025-01-01..2025-02-06 (Balance Changes)

" + bodyNotContains "accum=historical" + -- the search form and the interval links keep the override + bodyContains "" + bodyContains "/balancesheet?period=monthly&accum=change\"" + + yit "does not keep the balance sheet's own mode as a parameter" $ do + request $ do + setMethod "GET" + setUrl BalancesheetR + addGetParam "accum" "historical" + statusIs 200 + bodyContains "

Balance Sheet 2025-02-06

" + bodyNotContains "name=\"accum\"" + + yit "reports an accumulation mode it does not know" $ do + request $ do + setMethod "GET" + setUrl BalancesheetR + addGetParam "accum" "bogus" + statusIs 200 + bodyContains "

Balance Sheet

" + bodyContains "Unknown balance accumulation mode" + bodyNotContains "
" + + yit "serves the income statement, with changes, revenues shown positive" $ do + get IncomestatementR + statusIs 200 + bodyContains "

Income Statement 2025-01-01..2025-02-06

" + bodyContains ">Revenues" + bodyContains ">Expenses" + bodyContains "href=\"register?q=inacct:income:salary+date:2025-01-01..2025-02-07+type:R\" title=\"Show the transactions that make up this amount, which the register shows with the opposite sign\">" + bodyContains "href=\"register?q=type:RX+date:2025-01-01..2025-02-07\" title=\"Show the transactions that make up this total, which the register shows with the opposite sign\">" + bodyNotContains "accum=" + + yit "shows the income statement's ending balances when asked, and says so" $ do + request $ do + setMethod "GET" + setUrl IncomestatementR + addGetParam "accum" "historical" + statusIs 200 + bodyContains "

Income Statement 2025-02-06 (Historical Ending Balances)

" + bodyContains "" + bodyContains "href=\"register?q=inacct:income:salary+type:R&accum=historical\"" + + yit "serves the cashflow statement, with one section and no net total" $ do + get CashflowR + statusIs 200 + bodyContains "

Cashflow Statement 2025-01-01..2025-02-06

" + bodyContains ">Cash flows" + bodyNotContains "
" + + yit "serves the balance sheet with equity" $ do + get BalancesheetequityR + statusIs 200 + bodyContains "

Balance Sheet With Equity 2025-02-06

" + bodyContains ">Equity" + bodyContains "href=\"register?q=type:ALE+date:2025-01-01..2025-02-07&accum=historical\"" + + yit "links the reports to one another, marking the one shown, and to the balance report" $ do + request $ do + setMethod "GET" + setUrl IncomestatementR + addGetParam "period" "quarterly" + addGetParam "q" "inacct:assets:bank:checking expenses" + statusIs 200 + -- the links keep the period and the search minus its account term + bodyContains ("class=\"current\" href=\"" ++ defbaseurl defhost defport ++ "/incomestatement?period=quarterly&q=expenses\" title=\"Show revenues and expenses\"") + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/balancesheet?period=quarterly&q=expenses\" title=\"Show assets, liabilities, and net worth\"") + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/balance?period=quarterly&q=expenses\" title=\"Show the balance report: any accounts, by period\"") + + yit "links the balance report to the statements too" $ do + get BalanceR + statusIs 200 + bodyContains ("class=\"current\" href=\"" ++ defbaseurl defhost defport ++ "/balance\" title=\"Show the balance report: any accounts, by period\"") + bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/incomestatement\" title=\"Show revenues and expenses\"") + + yit "escapes account names and search terms in the statements" $ do + get IncomestatementR + statusIs 200 + bodyContains "title=\"Show transactions affecting this account and subaccounts\">expenses:snacks<i>x
" + bodyNotContains "snacksx" + request $ do + setMethod "GET" + setUrl IncomestatementR + addGetParam "q" "" + statusIs 200 + bodyNotContains " [(Text, Text, (r, [(Text, Text)]), Bool)] -> HtmlUrl r linksRow rowlabel items = $(hamletFile "templates/balance-links.hamlet") +-- | Links to the report pages, given as route, label, and title, +-- keeping the given parameters; the page being shown is marked. +reportLinks :: Eq r => Translations -> r -> [(Text, Text)] -> [(r, Text, Text)] -> HtmlUrl r +reportLinks trs here kept menu = + -- TRANSLATORS: the label before a page's report links. + linksRow (tr trs "Report:") [ (tr trs label, tr trs title, (route, kept), route == here) | (route, label, title) <- menu ] + -- | Links to the same report page for each reporting interval, keeping -- the search, the period's date span, and the given parameters; the -- interval being shown is marked. diff --git a/hledger-web/config/routes b/hledger-web/config/routes index ccfab6de0d0..a868e3d0c06 100644 --- a/hledger-web/config/routes +++ b/hledger-web/config/routes @@ -8,6 +8,10 @@ /journal JournalR GET /register RegisterR GET /balance BalanceR GET +/balancesheet BalancesheetR GET +/balancesheetequity BalancesheetequityR GET +/incomestatement IncomestatementR GET +/cashflow CashflowR GET /add AddR GET POST PUT /manage ManageR GET diff --git a/hledger-web/hledger-web.cabal b/hledger-web/hledger-web.cabal index 8abdf5491d7..37962925b64 100644 --- a/hledger-web/hledger-web.cabal +++ b/hledger-web/hledger-web.cabal @@ -105,6 +105,7 @@ library Hledger.Web.Handler.JournalR Hledger.Web.Handler.MiscR Hledger.Web.Handler.RegisterR + Hledger.Web.Handler.StatementsR Hledger.Web.Handler.UploadR Hledger.Web.Import Hledger.Web.Main diff --git a/hledger-web/hledger-web.m4.md b/hledger-web/hledger-web.m4.md index 579dcb98216..f10370dd903 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -210,36 +210,52 @@ you'll have to arrange to commit the changes yourself. Changes which would leave the journal file(s) unparseable or invalid (eg with failing balance assertions) are prevented. -# BALANCE REPORTS +# REPORTS Besides the journal and account registers, hledger-web can show the -[balance report](hledger.md#balance) at `/balance`. No page links to it -yet: it is reachable by entering the url, until there are more such -reports and a navigation scheme to hold them. A `period` parameter, -like the command line's `-p/--period`, selects the interval and/or the -period, eg `/balance?period=monthly` or -`/balance?period=quarterly in 2025`; the links above the report offer -the usual intervals. -The search box filters the report like the other pages, including +reports of the command line's +[balancesheet](hledger.md#balancesheet), +[balancesheetequity](hledger.md#balancesheetequity), +[incomestatement](hledger.md#incomestatement), and +[cashflow](hledger.md#cashflow) commands, at `/balancesheet`, +`/balancesheetequity`, `/incomestatement`, and `/cashflow`, and the +general [balance report](hledger.md#balance) at `/balance`. +The pages link to one another, but no other page links to them yet: +they are reachable by entering a url, until there is a navigation +scheme to hold them. + +A `period` parameter, like the command line's `-p/--period`, selects +the interval and/or the period, eg `/balancesheet?period=monthly` or +`/incomestatement?period=quarterly in 2025`; the links above each +report offer the usual intervals, and column headings link to the +report for that period. +The search box filters the reports like the other pages, including `depth:` terms, and the general report options given at startup, such as `--depth`, `-B`, or `-V`, apply. A `date:` term can set the interval too, as on the command line (`date:monthly`, or `"date:quarterly in 2025"`), and wins over the `period` parameter. -The report shows balance changes, or with an `accum=historical` -parameter, which the "Ending balances" link above it sets, each -account's balance at the end of each period. +The balance sheets show ending balances and the income statement and +cashflow statement show changes, as their commands do; an `accum` +parameter (`historical` or `change`) overrides that, and the heading +then says so, as the commands' does. The balance report shows changes, +or with `accum=historical`, which its "Ending balances" link sets, +each account's balance at the end of each period. +Like the commands, the statements show all balances as positive +amounts, so liabilities, equity, and revenues appear with the opposite +sign to the register's; the link on such a figure says so. + Every figure links to the register it is derived from: the account's -transactions in that period, whose final balance is the figure. (The -two can differ when a transaction's postings have dates of their own: -the report counts such a posting in its own period, while the register -shows its whole transaction.) +transactions in that period, whose final balance is the figure, or its +negation in the sections shown with the opposite sign. (The two can +differ when a transaction's postings have dates of their own: the +report counts such a posting in its own period, while the register +shows its whole transaction.) A section's total links to the register +of the section's account types. An ending balance's register runs in historical mode (its own `accum=historical`): its running balance starts from the balance brought forward from before the period, shown as its oldest row, and its balance column's heading switches between the two modes. -Account names link to their registers, and column headings to this -report for that period. # RELOADING diff --git a/hledger-web/static/hledger.css b/hledger-web/static/hledger.css index f2030891a91..009b1dfb8ac 100644 --- a/hledger-web/static/hledger.css +++ b/hledger-web/static/hledger.css @@ -105,9 +105,20 @@ td.date { border-top: 1px solid #687385; } -/* The row of links to the balance reports, on the journal and balance - pages; the report being shown is set in bold, like the sidebar's - current account. */ +/* A statement's sections: each section's title row, spaced from the + section above, and its subtotal rows under a hairline like the + footer's. Bootstrap separates the sections' tbodies with a rule of + its own. */ +.balancereport > tbody > tr.section > th { + padding-top: 1em; +} +.balancereport > tbody > tr.subtotal > td { + border-top: 1px solid #687385; +} + +/* The rows of links above a report: to the other reports, to the + intervals, and on the balance report to its modes; the one being shown + is set in bold, like the sidebar's current account. */ .report-links a { margin-left: 0.5em; } diff --git a/hledger-web/templates/default-layout.hamlet b/hledger-web/templates/default-layout.hamlet index 790f15832dd..f94035c3884 100644 --- a/hledger-web/templates/default-layout.hamlet +++ b/hledger-web/templates/default-layout.hamlet @@ -91,6 +91,7 @@ $if elem ViewPermission perms
  • _{HMsg "Multiple currencies in balances are displayed one above the other"}
  • _{HMsg "Click account name links to see transactions affecting that account, with running balance"}
  • _{HMsg "Click date links to see journal entries on that date"} +
  • _{HMsg "The balance sheet and income statement show liabilities, equity, and revenues as positive amounts; their registers show them negative, as hledger does"}

    _{HMsgc "help heading" "Search"} diff --git a/hledger-web/test/browser/security.spec.js b/hledger-web/test/browser/security.spec.js index 70959034c6e..840fcaf3a79 100644 --- a/hledger-web/test/browser/security.spec.js +++ b/hledger-web/test/browser/security.spec.js @@ -170,7 +170,8 @@ test.describe('the content security policy', () => { await expect(page).toHaveURL(/date(:|%3A)/); await expectNoViolations(page, violations); - for (const url of ['/manage', '/nosuchpage']) { + for (const url of ['/balance?period=monthly', '/balancesheet', '/balancesheetequity', + '/incomestatement?period=quarterly', '/cashflow', '/manage', '/nosuchpage']) { await page.goto(url); await expectNoViolations(page, violations); } From 899afd9ca449a180c330628834a0b8273a558efe Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:25:11 -0700 Subject: [PATCH 09/13] imp: web: one linking scheme for the journal, register, and sidebar 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. --- hledger-web/Hledger/Web/App.hs | 1 + hledger-web/Hledger/Web/Handler/JournalR.hs | 2 +- hledger-web/Hledger/Web/Handler/RegisterR.hs | 10 +-- hledger-web/Hledger/Web/ReportPage.hs | 2 +- hledger-web/Hledger/Web/Test.hs | 70 ++++++++++++++++++++ hledger-web/Hledger/Web/Widget/Common.hs | 19 +++++- hledger-web/hledger-web.m4.md | 3 +- hledger-web/static/hledger.css | 15 +++++ hledger-web/static/hledger.js | 23 +++---- hledger-web/templates/balance-report.hamlet | 13 +++- hledger-web/templates/default-layout.hamlet | 1 + hledger-web/templates/journal.hamlet | 6 +- hledger-web/templates/register.hamlet | 9 +-- hledger-web/test/browser/webui.spec.js | 8 +-- 14 files changed, 149 insertions(+), 33 deletions(-) diff --git a/hledger-web/Hledger/Web/App.hs b/hledger-web/Hledger/Web/App.hs index dd44db0f119..d4d261ff67d 100644 --- a/hledger-web/Hledger/Web/App.hs +++ b/hledger-web/Hledger/Web/App.hs @@ -165,6 +165,7 @@ instance Yesod App where ropts' = (_rsReportOpts rspec) {accountlistmode_ = ALTree -- force tree mode for sidebar ,empty_ = True -- show zero items by default + ,interval_ = NoInterval -- one balance per account, over the search's own span } rspec' = rspec{_rsQuery=q,_rsReportOpts=ropts'} diff --git a/hledger-web/Hledger/Web/Handler/JournalR.hs b/hledger-web/Hledger/Web/Handler/JournalR.hs index 0198f790fcf..e8d1d2c1665 100644 --- a/hledger-web/Hledger/Web/Handler/JournalR.hs +++ b/hledger-web/Hledger/Web/Handler/JournalR.hs @@ -15,7 +15,7 @@ import Hledger.Web.WebOptions import Hledger.Web.Widget.AddForm (addModal) import Hledger.Web.Widget.Common (accountQuery, mixedAmountAsHtml, - transactionFragment, replaceInacct) + transactionFragment, replaceInacct, journalDayQuery) -- | The formatted journal view, with sidebar. getJournalR :: Handler Html diff --git a/hledger-web/Hledger/Web/Handler/RegisterR.hs b/hledger-web/Hledger/Web/Handler/RegisterR.hs index a54acf982b1..d3842a807db 100644 --- a/hledger-web/Hledger/Web/Handler/RegisterR.hs +++ b/hledger-web/Hledger/Web/Handler/RegisterR.hs @@ -23,7 +23,7 @@ import Hledger.Web.WebOptions import Hledger.Web.Widget.AddForm (addModal) import Hledger.Web.Widget.Common (accountQuery, accountOnlyQuery, mixedAmountAsHtml, - transactionFragment, removeDates, removeInacct, replaceInacct) + transactionFragment, removeDates, removeInacct, replaceInacct, journalDayQuery) -- | The main journal/account register view, with accounts sidebar. getRegisterR :: Handler Html @@ -140,8 +140,10 @@ decorateLinks = concatMap $ \(acct, (name, comma)) -> -- | The register balance chart: its markup, carrying the per-commodity -- series as JSON in a data attribute. hledger.js draws it with flot on page -- load; see registerChartInit there. -registerChartHtml :: Text -> [(Text, Text)] -> String -> [(CommoditySymbol, [AccountTransactionsReportItem])] -> HtmlUrl AppRoute -registerChartHtml q accumParams title percommoditytxnreports = $(hamletFile "templates/chart.hamlet") +registerChartHtml :: + Text -> [(Text, Text)] -> (Transaction -> String) -> String -> + [(CommoditySymbol, [AccountTransactionsReportItem])] -> HtmlUrl AppRoute +registerChartHtml q accumParams transactionFrag title percommoditytxnreports = $(hamletFile "templates/chart.hamlet") where charttitle = if null title then "" else title ++ ":" nodatelink = (RegisterR, [("q", t) | let t = T.unwords $ removeDates q, not (T.null t)] ++ accumParams) @@ -155,7 +157,7 @@ registerChartHtml q accumParams title percommoditytxnreports = $(hamletFile "tem , toJSON . showZeroCommodity $ triCommodityAmount c i , toJSON . showZeroCommodity $ triCommodityBalance c i , toJSON . T.stripEnd . showTransaction $ triOrigTransaction i - , toJSON . tindex $ triOrigTransaction i + , toJSON . transactionFrag $ triOrigTransaction i ] | i <- reverse items ] ] diff --git a/hledger-web/Hledger/Web/ReportPage.hs b/hledger-web/Hledger/Web/ReportPage.hs index 601b225fc1b..bbd52c3221c 100644 --- a/hledger-web/Hledger/Web/ReportPage.hs +++ b/hledger-web/Hledger/Web/ReportPage.hs @@ -88,7 +88,7 @@ reportParams today rspecOrig qparam q qopts hideEmpty mperiod maccum = do -- zero items, which are shown by default, as the sidebar does. empty_ = not hideEmpty, balance_base_url_ = Just "", - querystring_ = Query.words'' queryprefixes qparam ++ dateTerm (date2_ roptsOrig) rpSpan, + querystring_ = filter (not . T.null) (Query.words'' queryprefixes qparam) ++ dateTerm (date2_ roptsOrig) rpSpan, interval_ = rpInterval } -- cf queryFromFlags diff --git a/hledger-web/Hledger/Web/Test.hs b/hledger-web/Hledger/Web/Test.hs index 3ca4535a581..86e0c368ca1 100644 --- a/hledger-web/Hledger/Web/Test.hs +++ b/hledger-web/Hledger/Web/Test.hs @@ -66,6 +66,7 @@ import Yesod.Test import Hledger.Web.Application ( makeAppWith ) import Hledger.Web.WebOptions -- ( WebOpts(..), defwebopts, prognameandversion ) import Hledger.Web.Import hiding (get, j) +import Hledger.Web.Widget.Common (transactionFragment) import Hledger.Cli hiding (prognameandversion) @@ -624,6 +625,72 @@ hledgerWebTest = do statusIs 200 bodyNotContains "name=\"accum\"" + -- The journal, register, and sidebar link to one another: a date to that + -- day's journal entries, an account to its register, an amount to the + -- register that derives it, an entry to itself on either page. + -- The journal, register, and sidebar link to one another: a date to that + -- day's journal entries, an account to its register, an amount to the + -- register that derives it, an entry to itself on either page. + let base = defbaseurl defhost defport + -- an entry's id (transaction-FILE-INDEX), as the pages compute it + frag desc = maybe (error' $ "no transaction " ++ desc) (transactionFragment bj) $ + find ((== T.pack desc) . tdescription) (jtxns bj) + runTests "hledger-web journal, register, and sidebar links" [] bj $ do + + yit "links a journal entry's date to that day's entries, and its accounts to their registers at the entry" $ do + get JournalR + statusIs 200 + bodyContains ("href=\"" ++ base ++ "/journal?q=date%3A2025-01-05#" ++ frag "pay" ++ "\" title=\"Show the journal entries on this date\">") + bodyContains ("href=\"" ++ base ++ "/register?q=inacct%3Aassets%3Abank%3Achecking#" ++ frag "lunch" ++ "\" title=\"assets:bank:checking\">") + + yit "narrows the journal to a day" $ do + request $ do + setMethod "GET" + setUrl JournalR + addGetParam "q" "date:2025-02-05" + statusIs 200 + bodyContains ("id=\"" ++ frag "lunch" ++ "\"") + bodyNotContains ("id=\"" ++ frag "pay" ++ "\"") + + yit "gives register rows the journal's entry ids, and links dates to the entry on its day" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking" + statusIs 200 + bodyContains ("

  • ") + bodyContains ("href=\"" ++ base ++ "/register?q=inacct%3Aexpenses%3Afood#" ++ frag "lunch" ++ "\" title=\"expenses:food\">") + -- the chart's points name the entries the same way, and its base link is the register's + bodyContains (""" ++ frag "lunch" ++ """) + bodyContains ("data-baselink=\"" ++ base ++ "/register?q=inacct%3Aassets%3Abank%3Achecking\"") + + yit "replaces the search's date terms in a date link, and keeps its other terms" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking date:2025-02 not:desc:\"x y\"" + statusIs 200 + bodyContains ("href=\"" ++ base ++ "/journal?q=date%3A2025-02-05%20%22not%3Adesc%3Ax%20y%22#" ++ frag "lunch" ++ "\"") + bodyNotContains "/journal?q=date%3A2025-02%20" + + yit "links the sidebar's amounts where their account names go" $ do + get JournalR + statusIs 200 + bodyContains ("") + -- an empty search adds no trailing term to the account links + bodyNotContains "inacct%3Aassets%20\"" + -- an unfiltered journal's total is zero, and does not link + bodyNotContains "Show the transactions that make up this total" + + yit "links the sidebar's total to the register of the search" $ do + request $ do + setMethod "GET" + setUrl JournalR + addGetParam "q" "expenses" + statusIs 200 + bodyContains ("") + -- The register: a period's transactions, with a running total from zero, -- or with accum=historical, the account's balance from before the period. runTests "hledger-web register page" [] bj $ do @@ -905,6 +972,9 @@ hledgerWebTest = do get BalanceR statusIs 200 bodyContains "href=\"register?q=inacct:assets:zeroed\"" + -- a zero sidebar amount has an empty register, and no link + bodyNotContains "inacct%3Aassets%3Azeroed\" title=\"Show the transactions that make up this balance\"" + bodyContains "inacct%3Aassets%3Akept\" title=\"Show the transactions that make up this balance\"" yit "hides them when the sidebar does (the e key's cookie)" $ do testSetCookie defaultSetCookie{setCookieName = "hideemptyaccts", setCookieValue = "1"} diff --git a/hledger-web/Hledger/Web/Widget/Common.hs b/hledger-web/Hledger/Web/Widget/Common.hs index 61c99fa54ed..b10b90167ef 100644 --- a/hledger-web/Hledger/Web/Widget/Common.hs +++ b/hledger-web/Hledger/Web/Widget/Common.hs @@ -21,6 +21,7 @@ module Hledger.Web.Widget.Common , removeDates , removeInacct , replaceInacct + , journalDayQuery ) where import Control.Monad.Except (ExceptT, mapExceptT) @@ -28,6 +29,7 @@ import Data.Foldable (find, for_) import Data.List (elemIndex) import Data.Text (Text) import Data.Text qualified as T +import Data.Time.Calendar (Day) import System.FilePath (takeFileName) import Text.Blaze ((!), textValue) import Text.Blaze.Html5 qualified as H @@ -93,6 +95,8 @@ balanceReportAsHtml (journalR, registerR) here hideEmpty trs j qparam qopts (ite isInterestingAccount acct = maybe False isInteresting $ ledgerAccount l acct where isInteresting a = not (all (mixedAmountLooksZero . bdexcludingsubs) . pdperiods $ adata a) || any isInteresting (asubs a) matchesAcctSelector acct = Just True == ((`matchesAccount` acct) <$> inAccountQuery qopts) + -- the register of everything the sidebar's search matches + totallink = (registerR, [("q", t) | let t = T.unwords $ removeInacct qparam, not (T.null t)]) -- | A row of links above a report: a label, then each link's label, -- title, target, and whether it is the one being shown. @@ -180,11 +184,22 @@ transactionFragment j Transaction{tindex, tsourcepos} = -- | The search's terms without its date terms, each quoted if it needs to be. removeDates :: Text -> [Text] -removeDates = map quoteIfSpaced . Anchor.removeDates . Query.words'' queryprefixes +removeDates = map quoteIfSpaced . Anchor.removeDates . searchTerms -- | The search's terms without those naming an account, each quoted if it needs to be. removeInacct :: Text -> [Text] -removeInacct = map quoteIfSpaced . Anchor.removeInacct . Query.words'' queryprefixes +removeInacct = map quoteIfSpaced . Anchor.removeInacct . searchTerms + +-- | The search's terms; none for an empty search. +searchTerms :: Text -> [Text] +searchTerms = filter (not . T.null) . Query.words'' queryprefixes + +-- | The search for the journal page narrowed to a day: the day's date +-- term in place of any date terms, and without any account term, which +-- the journal page does not use. +journalDayQuery :: Text -> Day -> Text +journalDayQuery qparam d = + T.unwords $ ("date:" <> showDate d) : removeDates (T.unwords $ removeInacct qparam) replaceInacct :: Text -> Text -> Text replaceInacct q acct = T.unwords $ acct : removeInacct q diff --git a/hledger-web/hledger-web.m4.md b/hledger-web/hledger-web.m4.md index f10370dd903..fa50ee7c0d1 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -149,7 +149,8 @@ hledger-web's main views are: The **sidebar** lists accounts and their balances (parent balances include subaccounts, and multiple commodities are shown one above the other). -Click an account name to see its register, or a date to see that day's journal entries. +Click an account name to see its register, or an amount to see the transactions it is +made of. In the journal and register views, click a date to see that day's journal entries. The **search form** filters both views with hledger's [query](hledger.md#queries) syntax, eg `expenses date:thismonth`; the help dialog summarises the query types. diff --git a/hledger-web/static/hledger.css b/hledger-web/static/hledger.css index 009b1dfb8ac..ba6fd19078a 100644 --- a/hledger-web/static/hledger.css +++ b/hledger-web/static/hledger.css @@ -365,6 +365,21 @@ ul { color: #a94442; } +/* A figure that links is not coloured as a link: the figure's own colour + (red for a negative amount, from the span inside) shows through. In the + sidebar it is not padded like an account link, and underlines on hover, + as the report pages' figures do. */ +.balancereport td.amount a, +#sidebar-menu .main-menu .amount a { + color: inherit; +} +#sidebar-menu .main-menu .amount a { + padding: 0; +} +#sidebar-menu .main-menu .amount a:hover { + text-decoration: underline; +} + .col-any-0 { width:0 !important; height:0 !important; diff --git a/hledger-web/static/hledger.js b/hledger-web/static/hledger.js index a0a724540f7..14812d9c961 100644 --- a/hledger-web/static/hledger.js +++ b/hledger-web/static/hledger.js @@ -101,8 +101,9 @@ document.addEventListener('DOMContentLoaded', function() { }); // The entry targeted by the url hash is marked by a :target rule in -// hledger.css, which needs no javascript. Note that register rows have -// numeric ids, so location.hash must not be passed to querySelector. +// hledger.css, which needs no javascript. A hash can be anything, eg an +// old bookmark's numeric row id, so location.hash must not be passed to +// querySelector. // The account sidebar's scroll position is preserved across page navigations // by an inline script right after the sidebar's markup in @@ -250,8 +251,8 @@ function registerChartInit() { // Each commodity is drawn as two flot series over the same points: a // stepped line for the running balance, and one clickable, hoverable point // per transaction. A point is [timestamp, balance, amount text, balance - // text, transaction text, transaction index]: flot reads the first two, - // the tooltip and click handlers the rest. + // text, transaction text, the transaction's row id]: flot reads the first + // two, the tooltip and click handlers the rest. var series = []; commodities.forEach(function(c, i) { series.push({ @@ -368,15 +369,13 @@ function registerChartSelect(ev, ranges) { // Those x values are unix timestamps (milliseconds since epoch) enclosing the data points' timestamps. // Those are generated by dayToUtcNoonTimestamp, and are UTC times representing the transaction dates. var from = new Date(ranges.xaxis.from); - var fromy = from.getUTCFullYear(); - var fromm = from.getUTCMonth() + 1; - var fromd = from.getUTCDate(); var to = new Date(ranges.xaxis.to + 1 * 24 * 60 * 60 * 1000); - var toy = to.getUTCFullYear(); - var tom = to.getUTCMonth() + 1; - var tod = to.getUTCDate(); - - var range = fromy + "/" + fromm + "/" + fromd + "-" + toy + "/" + tom + "/" + tod; + // as a date: term reads it: YYYY-MM-DD..YYYY-MM-DD, the end exclusive + var iso = function(d) { + return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + + '-' + String(d.getUTCDate()).padStart(2, '0'); + }; + var range = iso(from) + '..' + iso(to); // The base link is this register's url without its date terms; add ours. var url = new URL($('#register-chart').attr('data-baselink'), document.baseURI); var q = url.searchParams.get('q'); diff --git a/hledger-web/templates/balance-report.hamlet b/hledger-web/templates/balance-report.hamlet index 28b78e394d4..a10bcbd0609 100644 --- a/hledger-web/templates/balance-report.hamlet +++ b/hledger-web/templates/balance-report.hamlet @@ -18,8 +18,17 @@ $forall (acct, adisplay, aindent, abal) <- items #{tr trs "only"}
    _{HMsgc "column heading" "To/From Account(s)"} _{HMsgc "column heading" "Amount Out/In"} - #{balancelabel} + #{balancelabel}
    ^{mixedAmountAsHtml bal} + $if historical + $maybe start <- mstart +
    + #{show (snd start)} + + Balance brought forward + + + + $if mixedAmountLooksZero startbal + ^{mixedAmountAsHtml startbal} + $else + + ^{mixedAmountAsHtml startbal} $if elem AddPermission perms ^{addModal AddR j today} From e68229766295eff21830fa2bed9d74f141a007e5 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:55:20 -0700 Subject: [PATCH 07/13] imp: cli: a statement's account links carry the section's account type 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. --- hledger/Hledger/Cli/CompoundBalanceCommand.hs | 8 ++++++-- hledger/test/balancesheet.test | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/hledger/Hledger/Cli/CompoundBalanceCommand.hs b/hledger/Hledger/Cli/CompoundBalanceCommand.hs index 222b93c3515..866f2ab0e13 100644 --- a/hledger/Hledger/Cli/CompoundBalanceCommand.hs +++ b/hledger/Hledger/Cli/CompoundBalanceCommand.hs @@ -505,12 +505,16 @@ compoundBalanceReportAsSpreadsheetParts fmt accountLabel ropts specs cbr = unlinked = map (map (\c -> c {Spr.cellAnchor = mempty, Spr.cellTitle = mempty})) linkedWhen mterm rows = maybe (unlinked rows) (const rows) mterm + -- A section's rows link with its type term as well, so that their + -- registers exclude a subaccount of another type, as the figures do. section spec (subreporttitle, mbr, _increasestotal) = (subreporttitle, bodyrows, totalsrows) where sropts = cbcsubreportoptions spec ropts - (_, bodyrows, _) = - balanceSubReportAsSpreadsheetParts fmt sropts{no_total_ = True} allCommodities mbr sectionterm = typeTerm $ cbcsubreportquery spec + (_, bodyrows, _) = + balanceSubReportAsSpreadsheetParts fmt + sropts{no_total_ = True, querystring_ = maybeToList sectionterm ++ querystring_ sropts} + allCommodities mbr totalsrows | no_total_ ropts = [] | otherwise = diff --git a/hledger/test/balancesheet.test b/hledger/test/balancesheet.test index 2ac8f6ffc22..ad1c5bfe75c 100644 --- a/hledger/test/balancesheet.test +++ b/hledger/test/balancesheet.test @@ -397,7 +397,8 @@ $ hledger -f - bs -M -O html --base-url '' >= 0 # ** 17. A figure in a section shown with normally negative accounts says -# the register shows it with the opposite sign. +# the register shows it with the opposite sign, and links with the +# section's type term, as the section's totals do. < 2025-01-10 salary assets:bank 100 @@ -412,7 +413,7 @@ $ hledger -f - bs -M -O html --base-url '' assets:bank $ hledger -f - bs -M -O html --base-url '' -> // +> // >= 0 # ** 18. The net total links to the register of all the sections' types. From 0a8de5f839fe52d2a24963782c6cc2c5b5d35a50 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:52:13 -0700 Subject: [PATCH 08/13] imp: web: balance sheet, income statement, and cashflow pages 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. --- hledger-web/Hledger/Web/App.hs | 34 +++- hledger-web/Hledger/Web/Application.hs | 1 + hledger-web/Hledger/Web/Handler/BalanceR.hs | 8 +- hledger-web/Hledger/Web/Handler/RegisterR.hs | 7 +- .../Hledger/Web/Handler/StatementsR.hs | 127 ++++++++++++ hledger-web/Hledger/Web/ReportPage.hs | 6 +- hledger-web/Hledger/Web/Test.hs | 191 ++++++++++++++++++ hledger-web/Hledger/Web/Widget/Common.hs | 8 + hledger-web/config/routes | 4 + hledger-web/hledger-web.cabal | 1 + hledger-web/hledger-web.m4.md | 52 +++-- hledger-web/static/hledger.css | 17 +- hledger-web/templates/default-layout.hamlet | 1 + hledger-web/test/browser/security.spec.js | 3 +- 14 files changed, 429 insertions(+), 31 deletions(-) create mode 100644 hledger-web/Hledger/Web/Handler/StatementsR.hs diff --git a/hledger-web/Hledger/Web/App.hs b/hledger-web/Hledger/Web/App.hs index 3ce27bec1b3..dd44db0f119 100644 --- a/hledger-web/Hledger/Web/App.hs +++ b/hledger-web/Hledger/Web/App.hs @@ -45,9 +45,14 @@ import Yesod import Yesod.Default.Config import Yesod.Form.I18n.German (germanFormMessage) -import Hledger.Utils.I18n (Translations(..), langTagCandidates, resolveLang, tr, trc) +import Hledger.Utils.I18n (Translations(..), i18n, langTagCandidates, resolveLang, tr, trc) import Hledger import Hledger.Cli (CliOpts(..), journalReloadIfChanged) +import Hledger.Cli.Commands.Balancesheet (balancesheetSpec) +import Hledger.Cli.Commands.Balancesheetequity (balancesheetequitySpec) +import Hledger.Cli.Commands.Cashflow (cashflowSpec) +import Hledger.Cli.Commands.Incomestatement (incomestatementSpec) +import Hledger.Cli.CompoundBalanceCommand (CompoundBalanceCommandSpec(..)) import Hledger.Web.Settings (Extra(..), widgetFile) import Hledger.Web.Settings.StaticFiles import Hledger.Web.WebOptions @@ -168,7 +173,7 @@ instance Yesod App where -- accumulation mode, and the register's mode. The mode is kept only -- when it is not the default, as the pages' own links carry it. let keepable :: Text -> Text -> Bool - keepable "accum" v = v == "historical" + keepable "accum" v = Just v == keptAccum here keepable _ v = not (T.null v) keptParams <- fmap catMaybes . for (keptParamNames here) $ \name -> fmap (name,) . mfilter (keepable name) <$> lookupGetParam name @@ -355,9 +360,32 @@ checkServerSideUiEnabled = do -- permissionDenied "server-side UI is disabled due to --serve-api" sendResponseStatus status403 ("server-side UI is disabled due to --serve-api" :: Text) +-- | The report pages, in menu order, each with its link's label and +-- title, and its default accumulation mode: the statements' commands' +-- own, and balance changes for the balance report. +reportMenu :: [(Route App, Text, Text, BalanceAccumulation)] +reportMenu = + -- TRANSLATORS: the report links above each report page: each link's text, and its tooltip. + [ (BalancesheetR, i18n "Balance sheet", i18n "Show assets, liabilities, and net worth", cbcaccum balancesheetSpec) + , (BalancesheetequityR, i18n "Balance sheet with equity", i18n "Show assets, liabilities, and equity", cbcaccum balancesheetequitySpec) + , (IncomestatementR, i18n "Income statement", i18n "Show revenues and expenses", cbcaccum incomestatementSpec) + , (CashflowR, i18n "Cashflow statement", i18n "Show changes in liquid assets", cbcaccum cashflowSpec) + , (BalanceR, i18n "Balance report", i18n "Show the balance report: any accounts, by period", PerPeriod) + ] + -- | The report pages: those that take a period and an accumulation mode. reportRoutes :: [Route App] -reportRoutes = [BalanceR] +reportRoutes = [route | (route, _, _, _) <- reportMenu] + +-- | The accum parameter value a page's links keep: the mode that is +-- not the page's default. (The register totals the period by default.) +keptAccum :: Route App -> Maybe Text +keptAccum route = + case [dflt | (r, _, _, dflt) <- reportMenu, r == route] of + [Historical] -> Just "change" + [_] -> Just "historical" + _ | route == RegisterR -> Just "historical" + _ -> Nothing -- | The query parameters a page's own links keep. keptParamNames :: Route App -> [Text] diff --git a/hledger-web/Hledger/Web/Application.hs b/hledger-web/Hledger/Web/Application.hs index 75f0a8913ca..7b6166f2404 100644 --- a/hledger-web/Hledger/Web/Application.hs +++ b/hledger-web/Hledger/Web/Application.hs @@ -31,6 +31,7 @@ import Hledger.Web.Handler.UploadR import Hledger.Web.Handler.JournalR import Hledger.Web.Handler.RegisterR import Hledger.Web.Handler.BalanceR +import Hledger.Web.Handler.StatementsR import Hledger.Web.Import import Hledger.Web.WebOptions (ServerMode(..), WebOpts(server_mode_), corsPolicy) diff --git a/hledger-web/Hledger/Web/Handler/BalanceR.hs b/hledger-web/Hledger/Web/Handler/BalanceR.hs index bb058e6522d..e0feced4db1 100644 --- a/hledger-web/Hledger/Web/Handler/BalanceR.hs +++ b/hledger-web/Hledger/Web/Handler/BalanceR.hs @@ -17,7 +17,7 @@ import Hledger.Utils.I18n (tr, trf) import Hledger.Web.Import import Hledger.Web.ReportPage import Hledger.Web.WebOptions -import Hledger.Web.Widget.Common (accumulationLinks, intervalLinks, removeDates) +import Hledger.Web.Widget.Common (accumulationLinks, intervalLinks, removeDates, removeInacct, reportLinks) -- | The balance or multi-period balance view, with sidebar. @@ -54,6 +54,9 @@ getBalanceR = do periodParams = [("period", p) | Just p <- [rpPeriod]] accumParams = [("accum", "historical") | accum == Historical] qParams = [("q", qparam) | not (T.null qparam)] + -- links to the other reports keep the search, minus any account + -- term, which the reports ignore, and the period as given + menuParams = periodParams ++ [("q", qt) | let qt = T.unwords $ removeInacct qparam, not (T.null qt)] -- A column heading opens this report for that column's period, -- in place of any date terms in the search, which the column -- narrows anyway. @@ -77,8 +80,9 @@ getBalanceR = do _ -> let (h, b, t) = Balance.multiBalanceReportAsSpreadsheetParts oneLineNoCostFmt ropts mbr in ( reportTitle ropts $ trimColon $ Balance.multiBalanceReportTitle ropts mbr - , map (relinkDateHeaders (columnHeading ropts colspans) headinglink colspans) h, b, t) + , map (relinkDateHeaders trs (columnHeading ropts colspans) headinglink colspans) h, b, t) Yesod.toWidget $ H.h2 $ H.toHtml $ withFilter title + Yesod.toWidget $ reportLinks trs BalanceR menuParams [(r, l, t) | (r, l, t, _) <- reportMenu] Yesod.toWidget $ accumulationLinks trs BalanceR (periodParams ++ qParams) accum Yesod.toWidget $ intervalLinks trs BalanceR accumParams qparam rpSpan rpInterval Yesod.toWidget $ reportTable header [(Nothing, body, [])] totals diff --git a/hledger-web/Hledger/Web/Handler/RegisterR.hs b/hledger-web/Hledger/Web/Handler/RegisterR.hs index 07a9fcc2af8..a54acf982b1 100644 --- a/hledger-web/Hledger/Web/Handler/RegisterR.hs +++ b/hledger-web/Hledger/Web/Handler/RegisterR.hs @@ -51,10 +51,15 @@ getRegisterR = do qParams t = [("q", t) | not (T.null t)] acctQuery = fromMaybe Any (inAccountQuery qopts) acctlink acc = (RegisterR, ("q", replaceInacct qparam $ accountQuery acc) : accumParams) + -- In an account's register a type: term selects the postings + -- totaled, not the accounts named beside them: a liability's + -- register names the accounts it was posted against, whatever their + -- types. A register of all accounts of a type names those accounts. otherTransAccounts = map (\(acct,(name,comma)) -> (acct, (T.pack name, T.pack comma))) . undecorateLinks . elideRightDecorated 40 . decorateLinks . - addCommas . preferReal . otherTransactionAccounts j q acctQuery + addCommas . preferReal . otherTransactionAccounts j displayq acctQuery + displayq = if isJust (inAccount qopts) then filterQuery (not . queryIsType) q else q addCommas xs = zip xs $ zip (map (T.unpack . accountSummarisedName . paccount) xs) $ diff --git a/hledger-web/Hledger/Web/Handler/StatementsR.hs b/hledger-web/Hledger/Web/Handler/StatementsR.hs new file mode 100644 index 00000000000..744370e45f7 --- /dev/null +++ b/hledger-web/Hledger/Web/Handler/StatementsR.hs @@ -0,0 +1,127 @@ +-- | The financial statement pages: /balancesheet, /balancesheetequity, +-- /incomestatement, and /cashflow, showing the reports of the commands +-- of the same names. + +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Hledger.Web.Handler.StatementsR ( + getBalancesheetR, + getBalancesheetequityR, + getIncomestatementR, + getCashflowR, +) where + +import Text.Blaze.Html5 ((!)) +import Text.Blaze.Html5 qualified as H +import Text.Blaze.Html5.Attributes qualified as A +import Yesod qualified +import Data.Text qualified as T + +import Hledger +import Hledger.Cli.CliOptions +import Hledger.Cli.Commands.Balancesheet (balancesheetSpec) +import Hledger.Cli.Commands.Balancesheetequity (balancesheetequitySpec) +import Hledger.Cli.Commands.Cashflow (cashflowSpec) +import Hledger.Cli.Commands.Incomestatement (incomestatementSpec) +import Hledger.Cli.CompoundBalanceCommand +import Hledger.Utils.I18n (Translations, i18n, tr, trf) +import Hledger.Write.Spreadsheet qualified as Spr + +import Hledger.Web.Import +import Hledger.Web.ReportPage +import Hledger.Web.WebOptions +import Hledger.Web.Widget.Common (helplink, intervalLinks, removeDates, removeInacct, reportLinks) + +getBalancesheetR, getBalancesheetequityR, getIncomestatementR, getCashflowR :: Handler Html +-- TRANSLATORS: the browser tab titles of the statement pages. +getBalancesheetR = statementPage BalancesheetR (i18n "balance sheet - hledger-web") balancesheetSpec +getBalancesheetequityR = statementPage BalancesheetequityR (i18n "balance sheet with equity - hledger-web") balancesheetequitySpec +getIncomestatementR = statementPage IncomestatementR (i18n "income statement - hledger-web") incomestatementSpec +getCashflowR = statementPage CashflowR (i18n "cashflow statement - hledger-web") cashflowSpec + +-- | A statement page: the command's report for the page's search and +-- period, with sidebar. The report's own accumulation mode applies +-- (ending balances for the balance sheets, changes for the others) +-- unless an accum parameter overrides it, which the heading then says, +-- as the command's does. +statementPage :: AppRoute -> Text -> CompoundBalanceCommandSpec -> Handler Html +statementPage here tabtitle spec = do + checkServerSideUiEnabled + VD{j, q, qopts, qparam, opts, today, trs} <- getViewData + require ViewPermission + mperiod <- lookupGetParam "period" + maccum <- lookupGetParam "accum" + hideEmpty <- hideEmptyAccounts + urlrender <- getUrlRenderParams + let withFilter t = if q /= Any then trf trs "{title}, filtered" [("title", t)] else t + rspecOrig = reportspec_ $ cliopts_ opts + roptsOrig = _rsReportOpts rspecOrig + menu = [(r, l, t) | (r, l, t, _) <- reportMenu] + + defaultLayout $ do + setTitleI (HMsg tabtitle) + case reportParams today rspecOrig qparam q qopts hideEmpty mperiod maccum of + Left err -> Yesod.toWidget $ do + H.h2 $ H.toHtml $ withFilter $ effectiveTitle roptsOrig $ tr trs $ cbctitle spec NoInterval + paramError trs err + Right ReportParams{rpRopts, rpRspec, rpSpan, rpInterval, rpPeriod, rpAccum} -> do + let accum = fromMaybe (cbcaccum spec) rpAccum + override = mfilter (/= cbcaccum spec) rpAccum + ropts = rpRopts{balanceaccum_ = accum} + rspec = rpRspec{_rsReportOpts = ropts} + cbr0 = + styleAmounts (journalCommodityStylesWith HardRounding j) $ + compoundBalanceReport rspec j (cbcqueries spec) + cbr = + applySubreportTitles ropts $ + cbr0{cbrTitle = effectiveTitle ropts $ compoundBalanceReportTitle spec rpRopts override cbr0} + colspans = cbrDates cbr + -- What links staying on this page keep: the period as given, and + -- the mode when it is not the report's own. + periodParams = [("period", p) | Just p <- [rpPeriod]] + accumParams = [("accum", if accum == Historical then "historical" else "change") | isJust override] + -- Links to the other reports keep the search, minus any account + -- term, which the reports ignore, and the period as given. + menuParams = periodParams ++ [("q", qt) | let qt = T.unwords $ removeInacct qparam, not (T.null qt)] + -- A column heading opens this report for that column's period, + -- in place of any date terms in the search, which the column + -- narrows anyway. + headinglink spn = urlrender here $ + ("period", showDateSpanForQuery spn) : + [("q", qt) | let qt = T.unwords $ removeDates qparam, not (T.null qt)] ++ + accumParams + CompoundBalanceReportParts{cbrpLeadingHeaders, cbrpDataHeaders, cbrpSections, cbrpNetRows} = + compoundBalanceReportAsSpreadsheetParts oneLineNoCostFmt "account" ropts (cbcqueries spec) cbr + -- The heading row with the classes the stylesheet aligns the + -- columns by, and this page's column headings and links. + withClass c cell = cell{Spr.cellClass = Spr.Class c} + header = + map (withClass "account") cbrpLeadingHeaders ++ + relinkDateHeaders trs (columnHeading ropts colspans) headinglink colspans (map (withClass "amount") cbrpDataHeaders) + sections = [(mfilter (not . T.null) (Just title), body, subtotals) | (title, body, subtotals) <- cbrpSections] + noRows = all (\(_, r, _) -> null $ prRows r) . cbrSubreports + empty = noRows cbr + -- An empty report with zero balances hidden may have them all: look again showing them. + zerosHidden = hideEmpty && not (noRows $ compoundBalanceReport rspec{_rsReportOpts = ropts{empty_ = True}} j (cbcqueries spec)) + Yesod.toWidget $ H.h2 $ H.toHtml $ withFilter $ cbrTitle cbr + Yesod.toWidget $ reportLinks trs here menuParams menu + Yesod.toWidget $ intervalLinks trs here accumParams qparam rpSpan rpInterval + if empty + then Yesod.toWidget $ emptyNotice trs zerosHidden (q == Any && rpSpan == nulldatespan) + else Yesod.toWidget $ reportTable [header] sections cbrpNetRows + +-- | What an empty statement shows instead of a table: that its accounts +-- all have zero balances, which are hidden; or, for the whole journal, +-- that it has no accounts of the types the report shows, with a pointer +-- to how types are found; or, for a search or a period, that nothing +-- matched. +emptyNotice :: Translations -> Bool -> Bool -> HtmlUrl AppRoute +emptyNotice trs zerosHidden whole render = + H.div ! A.class_ "alert alert-info" $ + if zerosHidden then H.toHtml $ tr trs "All the accounts this report shows have zero balances, which are hidden. Press e to show them." + else if whole + then do + H.toHtml $ tr trs "No accounts of the types this report shows were found. Declare account types, or use hledger's standard top-level account names. " + helplink "account-types" (tr trs "How hledger finds account types") render + else H.toHtml $ tr trs "Nothing matches this search in this period." diff --git a/hledger-web/Hledger/Web/ReportPage.hs b/hledger-web/Hledger/Web/ReportPage.hs index e08831db364..601b225fc1b 100644 --- a/hledger-web/Hledger/Web/ReportPage.hs +++ b/hledger-web/Hledger/Web/ReportPage.hs @@ -140,12 +140,12 @@ columnHeading ropts colspans spn = -- the cells that link (the columns' periods) get the given heading text -- and link for their span. relinkDateHeaders :: - (DateSpan -> Text) -> (DateSpan -> Text) -> [DateSpan] -> [Cell NumLines Text] -> [Cell NumLines Text] -relinkDateHeaders heading link = go + Translations -> (DateSpan -> Text) -> (DateSpan -> Text) -> [DateSpan] -> [Cell NumLines Text] -> [Cell NumLines Text] +relinkDateHeaders trs heading link = go where go (spn:spns) (c:cs) | not (T.null $ cellAnchor c) = - c {cellContent = heading spn, cellAnchor = link spn, cellTitle = "Show this report for this period"} : go spns cs + c {cellContent = heading spn, cellAnchor = link spn, cellTitle = tr trs "Show this report for this period"} : go spns cs go spns (c:cs) = c : go spns cs go _ [] = [] diff --git a/hledger-web/Hledger/Web/Test.hs b/hledger-web/Hledger/Web/Test.hs index ad0d18d17ff..3ca4535a581 100644 --- a/hledger-web/Hledger/Web/Test.hs +++ b/hledger-web/Hledger/Web/Test.hs @@ -680,6 +680,197 @@ hledgerWebTest = do statusIs 200 bodyContains "title=\"expenses:food\">" + -- The financial statements: the balancesheet, balancesheetequity, + -- incomestatement, and cashflow commands' reports, with declared and + -- inferred account types, and each figure linked to its register. + tj <- fmap (either error' id) . runExceptT . journalFinalise biopts "statements.journal" "" =<< + readJournal'' (T.pack $ unlines -- PARTIAL: readJournal'' should not fail + ["account liabilities:card ; type:L" + ,"account equity:opening ; type:E" + ,"2025-01-01 opening" + ," assets:bank:checking 1000" + ," equity:opening" + ,"2025-01-05 pay" + ," assets:bank:checking 100" + ," income:salary" + ,"2025-02-05 lunch" + ," expenses:food 10" + ," liabilities:card" + ,"2025-02-06 snack" + ," expenses:snacksx 1" + ," liabilities:card"]) + runTests "hledger-web financial statements" [] tj $ do + + yit "serves the balance sheet, with ending balances, in sections" $ do + get BalancesheetR + statusIs 200 + bodyContains "

    Balance Sheet 2025-02-06

    " + bodyContains "
    Assets
    Liabilities
    - ^{mixedAmountAsHtml abal} + $if mixedAmountLooksZero abal + ^{mixedAmountAsHtml abal} + $else + + ^{mixedAmountAsHtml abal}
    - ^{mixedAmountAsHtml total} + $if mixedAmountLooksZero total + ^{mixedAmountAsHtml total} + $else + + ^{mixedAmountAsHtml total} diff --git a/hledger-web/templates/default-layout.hamlet b/hledger-web/templates/default-layout.hamlet index f94035c3884..00c5738c68b 100644 --- a/hledger-web/templates/default-layout.hamlet +++ b/hledger-web/templates/default-layout.hamlet @@ -90,6 +90,7 @@ $if elem ViewPermission perms
  • _{HMsg "Parent account balances include subaccount balances"}
  • _{HMsg "Multiple currencies in balances are displayed one above the other"}
  • _{HMsg "Click account name links to see transactions affecting that account, with running balance"} +
  • _{HMsg "Click an amount to see the transactions it is made of, in a register ending on that amount"}
  • _{HMsg "Click date links to see journal entries on that date"}
  • _{HMsg "The balance sheet and income statement show liabilities, equity, and revenues as positive amounts; their registers show them negative, as hledger does"}
    diff --git a/hledger-web/templates/journal.hamlet b/hledger-web/templates/journal.hamlet index 81c32651ac6..a8b97368fea 100644 --- a/hledger-web/templates/journal.hamlet +++ b/hledger-web/templates/journal.hamlet @@ -17,7 +17,9 @@ $if elem AddPermission perms $forall torig <- items
  • - #{show (tdate torig)} + + #{show (tdate torig)} #{textElideRight 60 (tdescription torig)} @@ -28,7 +30,7 @@ $if elem AddPermission perms   - + #{elideAccountName 40 acc} ^{mixedAmountAsHtml amt} diff --git a/hledger-web/templates/register.hamlet b/hledger-web/templates/register.hamlet index ef467948a7d..23b96b9fa8f 100644 --- a/hledger-web/templates/register.hamlet +++ b/hledger-web/templates/register.hamlet @@ -2,7 +2,7 @@ #{header}
    - ^{registerChartHtml qparam accumParams (T.unpack balancelabel) $ accountTransactionsReportByCommodity items} + ^{registerChartHtml qparam accumParams transactionFrag (T.unpack balancelabel) $ accountTransactionsReportByCommodity items} @@ -19,15 +19,16 @@ $forall (torig, tacct, split, _acct, amt, bal) <- items - + + + title=#{tr trs "Show the transactions that make up this balance"}> ^{mixedAmountAsHtml abal}
    - + #{show (tdate tacct)} #{textElideRight 30 (tdescription tacct)} $forall (Posting { paccount = acc }, (summName,comma)) <- otherTransAccounts torig - + #{summName}#{comma} $if not split || not (mixedAmountLooksZero amt) diff --git a/hledger-web/test/browser/webui.spec.js b/hledger-web/test/browser/webui.spec.js index a6a3b57da63..4ce7df5fd91 100644 --- a/hledger-web/test/browser/webui.spec.js +++ b/hledger-web/test/browser/webui.spec.js @@ -31,10 +31,10 @@ test.describe('page initialization', () => { expect(pageErrors).toEqual([]); }); - // Register rows use bare-numeric ids (id="3"), and '#3' is not a valid CSS - // selector, so any code passing location.hash to querySelector must guard - // it. A failure here leaves the page half-initialized: no date picker, no - // keyboard shortcuts, no sidebar handlers. + // A url hash can be anything, eg an old bookmark's numeric row id, and + // '#3' is not a valid CSS selector, so any code passing location.hash to + // querySelector must guard it. A failure here leaves the page + // half-initialized: no date picker, no keyboard shortcuts, no sidebar handlers. test('register url with a numeric transaction hash initializes fully', async ({ page }) => { await page.goto('/register?q=inacct:assets:bank:checking#3'); expect(pageErrors).toEqual([]); From 921a0b64a0d03772131c35f572d63979eb4f88f1 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:03:12 -0700 Subject: [PATCH 10/13] imp: web: link the reports from the sidebar 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. --- hledger-web/Hledger/Web/App.hs | 52 +++++++++++++------ hledger-web/Hledger/Web/Handler/BalanceR.hs | 2 +- .../Hledger/Web/Handler/StatementsR.hs | 2 +- hledger-web/Hledger/Web/Test.hs | 35 +++++++++++++ hledger-web/Hledger/Web/Widget/Common.hs | 9 ++-- hledger-web/hledger-web.m4.md | 14 ++--- hledger-web/templates/balance-report.hamlet | 11 +++- hledger-web/templates/default-layout.hamlet | 1 + hledger-web/test/browser/webui.spec.js | 17 ++++++ 9 files changed, 115 insertions(+), 28 deletions(-) diff --git a/hledger-web/Hledger/Web/App.hs b/hledger-web/Hledger/Web/App.hs index d4d261ff67d..03cd6d00e45 100644 --- a/hledger-web/Hledger/Web/App.hs +++ b/hledger-web/Hledger/Web/App.hs @@ -56,7 +56,7 @@ import Hledger.Cli.CompoundBalanceCommand (CompoundBalanceCommandSpec(..)) import Hledger.Web.Settings (Extra(..), widgetFile) import Hledger.Web.Settings.StaticFiles import Hledger.Web.WebOptions -import Hledger.Web.Widget.Common (balanceReportAsHtml) +import Hledger.Web.Widget.Common (balanceReportAsHtml, removeInacct) import Data.List (isPrefixOf) -- | The site argument for your application. This can be a good place to @@ -181,8 +181,15 @@ instance Yesod App where hideEmptyAccts <- hideEmptyAccounts - let accounts = - balanceReportAsHtml (JournalR, RegisterR) here hideEmptyAccts trs j qparam qopts $ + -- The sidebar's report links keep the search, minus any account term, + -- which the reports ignore, and a report page's period, as the report + -- pages' own Report row does. + let sidebarReports = reportLinkItems $ filter rmInSidebar reportMenu + sidebarParams = + [p | p@("period", _) <- keptParams] ++ + [("q", qt) | let qt = T.unwords $ removeInacct qparam, not (T.null qt)] + accounts = + balanceReportAsHtml (JournalR, RegisterR) here sidebarReports sidebarParams hideEmptyAccts trs j qparam qopts $ styleAmounts (journalCommodityStylesWith HardRounding j) $ balanceReport rspec' j @@ -361,28 +368,43 @@ checkServerSideUiEnabled = do -- permissionDenied "server-side UI is disabled due to --serve-api" sendResponseStatus status403 ("server-side UI is disabled due to --serve-api" :: Text) --- | The report pages, in menu order, each with its link's label and --- title, and its default accumulation mode: the statements' commands' --- own, and balance changes for the balance report. -reportMenu :: [(Route App, Text, Text, BalanceAccumulation)] +-- | A report page as the menus show it. +data ReportMenuItem = ReportMenuItem { + rmRoute :: Route App, + rmLabel :: Text, + rmTitle :: Text, + rmAccum :: BalanceAccumulation, + -- ^ the page's default accumulation mode: its command's own for a + -- statement, balance changes for the balance report + rmInSidebar :: Bool + -- ^ has a row in the sidebar (the others are one click away, in the + -- report pages' own Report row) +} + +-- | The report pages, in menu order. +reportMenu :: [ReportMenuItem] reportMenu = - -- TRANSLATORS: the report links above each report page: each link's text, and its tooltip. - [ (BalancesheetR, i18n "Balance sheet", i18n "Show assets, liabilities, and net worth", cbcaccum balancesheetSpec) - , (BalancesheetequityR, i18n "Balance sheet with equity", i18n "Show assets, liabilities, and equity", cbcaccum balancesheetequitySpec) - , (IncomestatementR, i18n "Income statement", i18n "Show revenues and expenses", cbcaccum incomestatementSpec) - , (CashflowR, i18n "Cashflow statement", i18n "Show changes in liquid assets", cbcaccum cashflowSpec) - , (BalanceR, i18n "Balance report", i18n "Show the balance report: any accounts, by period", PerPeriod) + -- TRANSLATORS: the report links above each report page and in the sidebar: each link's text, and its tooltip. + [ ReportMenuItem BalancesheetR (i18n "Balance sheet") (i18n "Show assets, liabilities, and net worth") (cbcaccum balancesheetSpec) True + , ReportMenuItem BalancesheetequityR (i18n "Balance sheet with equity") (i18n "Show assets, liabilities, and equity") (cbcaccum balancesheetequitySpec) False + , ReportMenuItem IncomestatementR (i18n "Income statement") (i18n "Show revenues and expenses") (cbcaccum incomestatementSpec) True + , ReportMenuItem CashflowR (i18n "Cashflow statement") (i18n "Show changes in liquid assets") (cbcaccum cashflowSpec) True + , ReportMenuItem BalanceR (i18n "Balance report") (i18n "Show the balance report: any accounts, by period") PerPeriod False ] +-- | The report pages' routes, labels, and titles, for a row of links. +reportLinkItems :: [ReportMenuItem] -> [(Route App, Text, Text)] +reportLinkItems items = [(rmRoute i, rmLabel i, rmTitle i) | i <- items] + -- | The report pages: those that take a period and an accumulation mode. reportRoutes :: [Route App] -reportRoutes = [route | (route, _, _, _) <- reportMenu] +reportRoutes = map rmRoute reportMenu -- | The accum parameter value a page's links keep: the mode that is -- not the page's default. (The register totals the period by default.) keptAccum :: Route App -> Maybe Text keptAccum route = - case [dflt | (r, _, _, dflt) <- reportMenu, r == route] of + case [rmAccum i | i <- reportMenu, rmRoute i == route] of [Historical] -> Just "change" [_] -> Just "historical" _ | route == RegisterR -> Just "historical" diff --git a/hledger-web/Hledger/Web/Handler/BalanceR.hs b/hledger-web/Hledger/Web/Handler/BalanceR.hs index e0feced4db1..d018fcb1047 100644 --- a/hledger-web/Hledger/Web/Handler/BalanceR.hs +++ b/hledger-web/Hledger/Web/Handler/BalanceR.hs @@ -82,7 +82,7 @@ getBalanceR = do in ( reportTitle ropts $ trimColon $ Balance.multiBalanceReportTitle ropts mbr , map (relinkDateHeaders trs (columnHeading ropts colspans) headinglink colspans) h, b, t) Yesod.toWidget $ H.h2 $ H.toHtml $ withFilter title - Yesod.toWidget $ reportLinks trs BalanceR menuParams [(r, l, t) | (r, l, t, _) <- reportMenu] + Yesod.toWidget $ reportLinks trs BalanceR menuParams $ reportLinkItems reportMenu Yesod.toWidget $ accumulationLinks trs BalanceR (periodParams ++ qParams) accum Yesod.toWidget $ intervalLinks trs BalanceR accumParams qparam rpSpan rpInterval Yesod.toWidget $ reportTable header [(Nothing, body, [])] totals diff --git a/hledger-web/Hledger/Web/Handler/StatementsR.hs b/hledger-web/Hledger/Web/Handler/StatementsR.hs index 744370e45f7..72a51ac9d6b 100644 --- a/hledger-web/Hledger/Web/Handler/StatementsR.hs +++ b/hledger-web/Hledger/Web/Handler/StatementsR.hs @@ -57,7 +57,7 @@ statementPage here tabtitle spec = do let withFilter t = if q /= Any then trf trs "{title}, filtered" [("title", t)] else t rspecOrig = reportspec_ $ cliopts_ opts roptsOrig = _rsReportOpts rspecOrig - menu = [(r, l, t) | (r, l, t, _) <- reportMenu] + menu = reportLinkItems reportMenu defaultLayout $ do setTitleI (HMsg tabtitle) diff --git a/hledger-web/Hledger/Web/Test.hs b/hledger-web/Hledger/Web/Test.hs index 86e0c368ca1..1a9b4f8a5a9 100644 --- a/hledger-web/Hledger/Web/Test.hs +++ b/hledger-web/Hledger/Web/Test.hs @@ -883,6 +883,41 @@ hledgerWebTest = do bodyContains ("class=\"current\" href=\"" ++ defbaseurl defhost defport ++ "/balance\" title=\"Show the balance report: any accounts, by period\"") bodyContains ("href=\"" ++ defbaseurl defhost defport ++ "/incomestatement\" title=\"Show revenues and expenses\"") + yit "lists the balance sheet, income statement, and cashflow statement in the sidebar, marking the one shown" $ do + get BalancesheetR + statusIs 200 + bodyContains ("
    Balance sheet") + bodyContains ("
    Income statement") + bodyContains ("Cashflow statement") + -- the other two reports are in the Report row, not the sidebar + bodyNotContains ("colspan=\"2\">") + bodyNotContains "/balancesheet?period=quarterly&accum" + + yit "links the sidebar's reports from the journal and register too" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking date:2025" + statusIs 200 + bodyContains ("colspan=\"2\">") + request $ do + setMethod "GET" + setUrl JournalR + addGetParam "q" "date:2025" + statusIs 200 + bodyContains ("colspan=\"2\">") + yit "escapes account names and search terms in the statements" $ do get IncomestatementR statusIs 200 diff --git a/hledger-web/Hledger/Web/Widget/Common.hs b/hledger-web/Hledger/Web/Widget/Common.hs index b10b90167ef..91252ab0d3c 100644 --- a/hledger-web/Hledger/Web/Widget/Common.hs +++ b/hledger-web/Hledger/Web/Widget/Common.hs @@ -84,9 +84,12 @@ helplink :: Text -> Text -> HtmlUrl r helplink topic label _ = H.a ! A.href u ! A.target "hledgerhelp" $ toHtml label where u = textValue $ manualurl <> if T.null topic then "" else T.cons '#' topic --- | Render a "BalanceReport" as html. -balanceReportAsHtml :: Eq r => (r, r) -> r -> Bool -> Translations -> Journal -> Text -> [QueryOpt] -> BalanceReport -> HtmlUrl r -balanceReportAsHtml (journalR, registerR) here hideEmpty trs j qparam qopts (items, total) = +-- | Render a "BalanceReport" as the sidebar: the journal and report +-- links (each report's route, label, and title, with the parameters +-- their links carry), then the accounts. The current page's row is marked. +balanceReportAsHtml :: + Eq r => (r, r) -> r -> [(r, Text, Text)] -> [(Text, Text)] -> Bool -> Translations -> Journal -> Text -> [QueryOpt] -> BalanceReport -> HtmlUrl r +balanceReportAsHtml (journalR, registerR) here reports reportParams hideEmpty trs j qparam qopts (items, total) = $(hamletFile "templates/balance-report.hamlet") where l = ledgerFromJournal Any j diff --git a/hledger-web/hledger-web.m4.md b/hledger-web/hledger-web.m4.md index fa50ee7c0d1..7d8bd3c1780 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -143,16 +143,19 @@ If this is not working see [Install > Shell completions](install.html#shell-comp hledger-web's main views are: -- the **journal view** (the home page), showing journal entries, newest first, with their postings; and +- the **journal view** (the home page), showing journal entries, newest first, with their postings; - the **register view**, showing the transactions affecting one account (and its subaccounts), - with a running balance and a balance chart. + with a running balance and a balance chart; and +- the **report views**, described [below](#reports): the balance sheet, income statement, + and cashflow statement, linked from the sidebar, and the balance sheet with equity and + the general balance report, linked from those. The **sidebar** lists accounts and their balances (parent balances include subaccounts, and multiple commodities are shown one above the other). Click an account name to see its register, or an amount to see the transactions it is made of. In the journal and register views, click a date to see that day's journal entries. -The **search form** filters both views with hledger's [query](hledger.md#queries) syntax, +The **search form** filters every view with hledger's [query](hledger.md#queries) syntax, eg `expenses date:thismonth`; the help dialog summarises the query types. The current view and search are reflected in the URL, so views can be bookmarked and shared. @@ -221,9 +224,8 @@ reports of the command line's [cashflow](hledger.md#cashflow) commands, at `/balancesheet`, `/balancesheetequity`, `/incomestatement`, and `/cashflow`, and the general [balance report](hledger.md#balance) at `/balance`. -The pages link to one another, but no other page links to them yet: -they are reachable by entering a url, until there is a navigation -scheme to hold them. +The sidebar links to the balance sheet, income statement, and cashflow +statement, and each report page links to all five. A `period` parameter, like the command line's `-p/--period`, selects the interval and/or the period, eg `/balancesheet?period=monthly` or diff --git a/hledger-web/templates/balance-report.hamlet b/hledger-web/templates/balance-report.hamlet index a10bcbd0609..21e1322ee21 100644 --- a/hledger-web/templates/balance-report.hamlet +++ b/hledger-web/templates/balance-report.hamlet @@ -4,6 +4,13 @@ title=#{tr trs "Show general journal entries, most recent first"}> #{tr trs "Journal"} +$# A report's label spans both columns: it is longer than an account name, +$# and nothing goes beside it. +$forall (route, label, title) <- reports +
    + + #{tr trs label} $forall (acct, adisplay, aindent, abal) <- items
    @@ -30,5 +37,5 @@ $forall (acct, adisplay, aindent, abal) <- items $if mixedAmountLooksZero total ^{mixedAmountAsHtml total} $else - + ^{mixedAmountAsHtml total} diff --git a/hledger-web/templates/default-layout.hamlet b/hledger-web/templates/default-layout.hamlet index 00c5738c68b..01f1e93f5cb 100644 --- a/hledger-web/templates/default-layout.hamlet +++ b/hledger-web/templates/default-layout.hamlet @@ -87,6 +87,7 @@ $if elem ViewPermission perms
    - #{tr trs "Journal"} From 8799e0802c10fd9b085eba7bb1b95b4932ea9872 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:41:13 -0700 Subject: [PATCH 13/13] imp: web: translate the report pages The report pages' text, links, and tooltips, the sidebar's report links, and the register's new links and brought-forward row now go through the translation catalogs like the rest of hledger-web, with German entries. The link titles that the CLI's report renderers put on figures and headings (Hledger.Cli.Anchor) stay English for now, as the CLI's HTML and FODS output does. AI usage: Claude Opus 5.5, ~6k output tokens. --- hledger-lib/locale/de.po | 81 +++++++++ hledger-lib/locale/hledger.pot | 239 ++++++++++++++++++++------ hledger-web/templates/journal.hamlet | 2 +- hledger-web/templates/register.hamlet | 6 +- 4 files changed, 268 insertions(+), 60 deletions(-) diff --git a/hledger-lib/locale/de.po b/hledger-lib/locale/de.po index d290ccfb850..a9edaae81a6 100644 --- a/hledger-lib/locale/de.po +++ b/hledger-lib/locale/de.po @@ -796,3 +796,84 @@ msgstr "Den laufenden Saldo ab Beginn dieses Zeitraums anzeigen" msgid "Show the running balance including everything before this period" msgstr "Den laufenden Saldo einschließlich allem vor diesem Zeitraum anzeigen" + +msgid "Balance sheet" +msgstr "Bilanz" + +msgid "Balance sheet with equity" +msgstr "Bilanz mit Eigenkapital" + +msgid "Income statement" +msgstr "Einnahmenüberschussrechnung" + +msgid "Cashflow statement" +msgstr "Kapitalflussrechnung" + +msgid "Show assets, liabilities, and net worth" +msgstr "Vermögen, Verbindlichkeiten und Reinvermögen anzeigen" + +msgid "Show assets, liabilities, and equity" +msgstr "Vermögen, Verbindlichkeiten und Eigenkapital anzeigen" + +msgid "Show revenues and expenses" +msgstr "Einnahmen und Ausgaben anzeigen" + +msgid "Show changes in liquid assets" +msgstr "Veränderungen der liquiden Mittel anzeigen" + +msgid "Show the balance report: any accounts, by period" +msgstr "Den Saldenbericht anzeigen: beliebige Konten, nach Perioden" + +msgid "balance sheet - hledger-web" +msgstr "Bilanz - hledger-web" + +msgid "balance sheet with equity - hledger-web" +msgstr "Bilanz mit Eigenkapital - hledger-web" + +msgid "income statement - hledger-web" +msgstr "Einnahmenüberschussrechnung - hledger-web" + +msgid "cashflow statement - hledger-web" +msgstr "Kapitalflussrechnung - hledger-web" + +msgid "Show this report for this period" +msgstr "Diesen Bericht für diese Periode anzeigen" + +msgid "All the accounts this report shows have zero balances, which are hidden. Press e to show them." +msgstr "Alle Konten dieses Berichts haben den Saldo null und sind ausgeblendet. Drücken Sie e, um sie anzuzeigen." + +msgid "No accounts of the types this report shows were found. Declare account types, or use hledger's standard top-level account names. " +msgstr "Es wurden keine Konten der Typen gefunden, die dieser Bericht zeigt. Deklarieren Sie Kontotypen oder verwenden Sie die Standardnamen der obersten Kontoebene von hledger. " + +msgid "How hledger finds account types" +msgstr "Wie hledger Kontotypen erkennt" + +msgid "Nothing matches this search in this period." +msgstr "In diesem Zeitraum passt nichts zu dieser Suche." + +msgid "Show the transactions that make up this balance" +msgstr "Die Buchungen anzeigen, aus denen sich dieser Saldo zusammensetzt" + +msgid "Show the transactions that make up this total" +msgstr "Die Buchungen anzeigen, aus denen sich diese Summe zusammensetzt" + +msgid "Show the journal entries on this date" +msgstr "Die Journalbuchungen dieses Tages anzeigen" + +msgid "Show this entry and the other journal entries on its date" +msgstr "Diese und die anderen Journalbuchungen dieses Tages anzeigen" + +msgid "Balance brought forward" +msgstr "Saldovortrag" + +msgid "Show the transactions before this period" +msgstr "Die Buchungen vor diesem Zeitraum anzeigen" + +msgid "Click an amount to see the transactions it is made of, in a register ending on that amount" +msgstr "Einen Betrag anklicken, um die Buchungen zu sehen, aus denen er besteht, in einem Register, das mit diesem Betrag endet" + +msgid "The balance sheet and income statement show liabilities, equity, and revenues as positive amounts; their registers show them negative, as hledger does" +msgstr "Bilanz und Einnahmenüberschussrechnung zeigen Verbindlichkeiten, Eigenkapital und Einnahmen als positive Beträge; ihre Register zeigen sie negativ, wie hledger es tut" + +msgid "The sidebar's report links show the balance sheet, income statement, and cashflow statement, each linking to the other reports" +msgstr "Die Berichtslinks der Seitenleiste zeigen Bilanz, Einnahmenüberschussrechnung und Kapitalflussrechnung, jeweils mit Links zu den anderen Berichten" diff --git a/hledger-lib/locale/hledger.pot b/hledger-lib/locale/hledger.pot index 912406724e1..5bc13df815d 100644 --- a/hledger-lib/locale/hledger.pot +++ b/hledger-lib/locale/hledger.pot @@ -263,7 +263,7 @@ msgid "Historical gain" msgstr "" #. the label before a report's accumulation mode links, and the links' text and tooltips. -#: hledger/Hledger/Cli/Commands/Balance.hs:823 hledger-web/Hledger/Web/Widget/Common.hs:141 +#: hledger/Hledger/Cli/Commands/Balance.hs:823 hledger-web/Hledger/Web/Widget/Common.hs:159 msgid "Balance changes" msgstr "" @@ -286,7 +286,7 @@ msgctxt "column heading" msgid "Commodity" msgstr "" -#: hledger/Hledger/Cli/Commands/Balance.hs:957 hledger/Hledger/Cli/Commands/Balance/Internal.hs:165 hledger-web/Hledger/Web/Handler/RegisterR.hs:69 +#: hledger/Hledger/Cli/Commands/Balance.hs:957 hledger/Hledger/Cli/Commands/Balance/Internal.hs:165 hledger-web/Hledger/Web/Handler/RegisterR.hs:74 msgctxt "column heading" msgid "Total" msgstr "" @@ -590,6 +590,57 @@ msgstr "" msgid "All accounts" msgstr "" +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:388 +msgid "Balance sheet" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:388 +msgid "Show assets, liabilities, and net worth" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:389 +msgid "Balance sheet with equity" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:389 +msgid "Show assets, liabilities, and equity" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:390 +msgid "Income statement" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:390 +msgid "Show revenues and expenses" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:391 +msgid "Cashflow statement" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:391 +msgid "Show changes in liquid assets" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#. the browser tab title of this page. +#: hledger-web/Hledger/Web/App.hs:392 hledger-web/Hledger/Web/Handler/BalanceR.hs:42 hledger-web/Hledger/Web/Handler/BalanceR.hs:77 +msgid "Balance report" +msgstr "" + +#. the report links above each report page and in the sidebar: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/App.hs:392 +msgid "Show the balance report: any accounts, by period" +msgstr "" + #: hledger-web/Hledger/Web/ReportPage.hs:124 msgid "Could not parse the period expression:" msgstr "" @@ -598,11 +649,15 @@ msgstr "" msgid "Unknown balance accumulation mode:" msgstr "" +#: hledger-web/Hledger/Web/ReportPage.hs:148 +msgid "Show this report for this period" +msgstr "" + #: hledger-web/Hledger/Web/Handler/AddR.hs:65 msgid "Transaction added." msgstr "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:33 hledger-web/Hledger/Web/Handler/JournalR.hs:30 hledger-web/Hledger/Web/Handler/RegisterR.hs:44 +#: hledger-web/Hledger/Web/Handler/BalanceR.hs:33 hledger-web/Hledger/Web/Handler/JournalR.hs:30 hledger-web/Hledger/Web/Handler/RegisterR.hs:44 hledger-web/Hledger/Web/Handler/StatementsR.hs:57 #, python-brace-format msgid "{title}, filtered" msgstr "" @@ -612,11 +667,6 @@ msgstr "" msgid "balance - hledger-web" msgstr "" -#. the browser tab title of this page. -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:42 hledger-web/Hledger/Web/Handler/BalanceR.hs:74 -msgid "Balance report" -msgstr "" - #: hledger-web/Hledger/Web/Handler/EditR.hs:44 hledger-web/Hledger/Web/Handler/UploadR.hs:57 #, python-brace-format msgid "Failed to load journal: {error}" @@ -659,29 +709,65 @@ msgstr "" msgid "{account} (excluding subaccounts)" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:67 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:72 msgctxt "column heading" msgid "Historical Total" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:68 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:73 msgctxt "column heading" msgid "Period Total" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:73 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:78 msgid "Show the running balance from the start of this period" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:74 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:79 msgid "Show the running balance including everything before this period" msgstr "" #. the browser tab title of this page. -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:90 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:95 msgid "register - hledger-web" msgstr "" +#. the browser tab titles of the statement pages. +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:38 +msgid "balance sheet - hledger-web" +msgstr "" + +#. the browser tab titles of the statement pages. +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:39 +msgid "balance sheet with equity - hledger-web" +msgstr "" + +#. the browser tab titles of the statement pages. +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:40 +msgid "income statement - hledger-web" +msgstr "" + +#. the browser tab titles of the statement pages. +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:41 +msgid "cashflow statement - hledger-web" +msgstr "" + +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:122 +msgid "All the accounts this report shows have zero balances, which are hidden. Press e to show them." +msgstr "" + +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:125 +msgid "No accounts of the types this report shows were found. Declare account types, or use hledger's standard top-level account names. " +msgstr "" + +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:126 +msgid "How hledger finds account types" +msgstr "" + +#: hledger-web/Hledger/Web/Handler/StatementsR.hs:127 +msgid "Nothing matches this search in this period." +msgstr "" + #: hledger-web/Hledger/Web/Handler/UploadR.hs:52 #, python-brace-format msgid "Encoding error: '{error}'. If your file is not UTF-8 encoded, try the 'edit form', where the transcoding should be handled by the browser." @@ -745,89 +831,94 @@ msgstr "" msgid "Postings validation failed" msgstr "" +#. the label before a page's report links. +#: hledger-web/Hledger/Web/Widget/Common.hs:120 +msgid "Report:" +msgstr "" + #. the label before a report's interval links. -#: hledger-web/Hledger/Web/Widget/Common.hs:110 +#: hledger-web/Hledger/Web/Widget/Common.hs:128 msgid "Interval:" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:116 +#: hledger-web/Hledger/Web/Widget/Common.hs:134 msgid "Show one column for the whole period" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:116 +#: hledger-web/Hledger/Web/Widget/Common.hs:134 msgctxt "interval" msgid "None" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:117 +#: hledger-web/Hledger/Web/Widget/Common.hs:135 msgid "Yearly" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:117 +#: hledger-web/Hledger/Web/Widget/Common.hs:135 msgid "Show a column per year" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:118 +#: hledger-web/Hledger/Web/Widget/Common.hs:136 msgid "Quarterly" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:118 +#: hledger-web/Hledger/Web/Widget/Common.hs:136 msgid "Show a column per quarter" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:119 +#: hledger-web/Hledger/Web/Widget/Common.hs:137 msgid "Monthly" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:119 +#: hledger-web/Hledger/Web/Widget/Common.hs:137 msgid "Show a column per month" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:120 +#: hledger-web/Hledger/Web/Widget/Common.hs:138 msgid "Weekly" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:120 +#: hledger-web/Hledger/Web/Widget/Common.hs:138 msgid "Show a column per week" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:121 +#: hledger-web/Hledger/Web/Widget/Common.hs:139 msgid "Daily" msgstr "" #. the interval links above a report: each link's text, and its tooltip. -#: hledger-web/Hledger/Web/Widget/Common.hs:121 +#: hledger-web/Hledger/Web/Widget/Common.hs:139 msgid "Show a column per day" msgstr "" #. the label before a report's accumulation mode links, and the links' text and tooltips. -#: hledger-web/Hledger/Web/Widget/Common.hs:140 +#: hledger-web/Hledger/Web/Widget/Common.hs:158 msgid "Show:" msgstr "" #. the label before a report's accumulation mode links, and the links' text and tooltips. -#: hledger-web/Hledger/Web/Widget/Common.hs:141 +#: hledger-web/Hledger/Web/Widget/Common.hs:159 msgid "Show how much each balance changed in each period" msgstr "" #. the label before a report's accumulation mode links, and the links' text and tooltips. -#: hledger-web/Hledger/Web/Widget/Common.hs:143 +#: hledger-web/Hledger/Web/Widget/Common.hs:161 msgid "Ending balances" msgstr "" #. the label before a report's accumulation mode links, and the links' text and tooltips. -#: hledger-web/Hledger/Web/Widget/Common.hs:143 +#: hledger-web/Hledger/Web/Widget/Common.hs:161 msgid "Show each balance at the end of each period, including everything before it" msgstr "" @@ -851,18 +942,26 @@ msgstr "" msgid "Journal" msgstr "" -#: hledger-web/templates/balance-report.hamlet:15 +#: hledger-web/templates/balance-report.hamlet:22 msgid "Show transactions affecting this account and subaccounts" msgstr "" -#: hledger-web/templates/balance-report.hamlet:19 +#: hledger-web/templates/balance-report.hamlet:26 msgid "Show transactions affecting this account but not subaccounts" msgstr "" -#: hledger-web/templates/balance-report.hamlet:19 +#: hledger-web/templates/balance-report.hamlet:26 msgid "only" msgstr "" +#: hledger-web/templates/balance-report.hamlet:32 +msgid "Show the transactions that make up this balance" +msgstr "" + +#: hledger-web/templates/balance-report.hamlet:40 +msgid "Show the transactions that make up this total" +msgstr "" + #: hledger-web/templates/default-layout.hamlet:49 msgctxt "search box" msgid "Search" @@ -937,103 +1036,115 @@ msgid "The sidebar shows the resulting accounts and their final balances" msgstr "" #: hledger-web/templates/default-layout.hamlet:90 -msgid "Parent account balances include subaccount balances" +msgid "The sidebar's report links show the balance sheet, income statement, and cashflow statement, each linking to the other reports" msgstr "" #: hledger-web/templates/default-layout.hamlet:91 -msgid "Multiple currencies in balances are displayed one above the other" +msgid "Parent account balances include subaccount balances" msgstr "" #: hledger-web/templates/default-layout.hamlet:92 -msgid "Click account name links to see transactions affecting that account, with running balance" +msgid "Multiple currencies in balances are displayed one above the other" msgstr "" #: hledger-web/templates/default-layout.hamlet:93 +msgid "Click account name links to see transactions affecting that account, with running balance" +msgstr "" + +#: hledger-web/templates/default-layout.hamlet:94 +msgid "Click an amount to see the transactions it is made of, in a register ending on that amount" +msgstr "" + +#: hledger-web/templates/default-layout.hamlet:95 msgid "Click date links to see journal entries on that date" msgstr "" #: hledger-web/templates/default-layout.hamlet:96 +msgid "The balance sheet and income statement show liabilities, equity, and revenues as positive amounts; their registers show them negative, as hledger does" +msgstr "" + +#: hledger-web/templates/default-layout.hamlet:99 msgctxt "help heading" msgid "Search" msgstr "" -#: hledger-web/templates/default-layout.hamlet:98 +#: hledger-web/templates/default-layout.hamlet:101 msgid "Search patterns with spaces should be enclosed in quotes." msgstr "" -#: hledger-web/templates/default-layout.hamlet:99 +#: hledger-web/templates/default-layout.hamlet:102 msgid "match account names" msgstr "" -#: hledger-web/templates/default-layout.hamlet:100 +#: hledger-web/templates/default-layout.hamlet:103 msgid "match account types" msgstr "" -#: hledger-web/templates/default-layout.hamlet:101 +#: hledger-web/templates/default-layout.hamlet:104 msgid "match dates" msgstr "" -#: hledger-web/templates/default-layout.hamlet:102 +#: hledger-web/templates/default-layout.hamlet:105 msgid "match status" msgstr "" -#: hledger-web/templates/default-layout.hamlet:103 +#: hledger-web/templates/default-layout.hamlet:106 msgid "match transaction codes" msgstr "" -#: hledger-web/templates/default-layout.hamlet:104 +#: hledger-web/templates/default-layout.hamlet:107 msgid "or" msgstr "" -#: hledger-web/templates/default-layout.hamlet:104 +#: hledger-web/templates/default-layout.hamlet:107 msgid "match any visible field (account, amount, comment, description, code), or a date if REGEX is also a period expression" msgstr "" -#: hledger-web/templates/default-layout.hamlet:105 +#: hledger-web/templates/default-layout.hamlet:108 msgid "match transaction descriptions" msgstr "" -#: hledger-web/templates/default-layout.hamlet:106 +#: hledger-web/templates/default-layout.hamlet:109 msgid "match payee part of descriptions" msgstr "" -#: hledger-web/templates/default-layout.hamlet:107 +#: hledger-web/templates/default-layout.hamlet:110 msgid "match note part of descriptions" msgstr "" -#: hledger-web/templates/default-layout.hamlet:108 +#: hledger-web/templates/default-layout.hamlet:111 msgid "match unsigned magnitudes, or with signed N, signed amounts. For single-commodity amounts only." msgstr "" -#: hledger-web/templates/default-layout.hamlet:109 +#: hledger-web/templates/default-layout.hamlet:112 msgid "match currencies/commodities. Must match the whole symbol/name. To match dollar sign, write" msgstr "" -#: hledger-web/templates/default-layout.hamlet:110 +#: hledger-web/templates/default-layout.hamlet:113 msgid "match tags, or tag and value" msgstr "" -#: hledger-web/templates/default-layout.hamlet:111 +#: hledger-web/templates/default-layout.hamlet:114 msgid "match postings' realness/virtualness" msgstr "" -#: hledger-web/templates/default-layout.hamlet:112 +#: hledger-web/templates/default-layout.hamlet:115 msgid "prepend not: to negate a search term" msgstr "" -#: hledger-web/templates/default-layout.hamlet:113 +#: hledger-web/templates/default-layout.hamlet:116 msgid "match with a boolean query (and, or, not, (..))" msgstr "" -#: hledger-web/templates/default-layout.hamlet:114 +#: hledger-web/templates/default-layout.hamlet:117 msgid "match transactions where any posting matches" msgstr "" -#: hledger-web/templates/default-layout.hamlet:115 +#: hledger-web/templates/default-layout.hamlet:118 msgid "match transactions where all postings match" msgstr "" -#: hledger-web/templates/default-layout.hamlet:116 +#: hledger-web/templates/default-layout.hamlet:119 msgid "clip account names at this depth" msgstr "" @@ -1077,6 +1188,10 @@ msgctxt "column heading" msgid "Amount" msgstr "" +#: hledger-web/templates/journal.hamlet:21 +msgid "Show the journal entries on this date" +msgstr "" + #: hledger-web/templates/manage.hamlet:2 msgid "Your journal's files" msgstr "" @@ -1108,6 +1223,18 @@ msgctxt "column heading" msgid "Amount Out/In" msgstr "" +#: hledger-web/templates/register.hamlet:25 +msgid "Show this entry and the other journal entries on its date" +msgstr "" + +#: hledger-web/templates/register.hamlet:44 +msgid "Balance brought forward" +msgstr "" + +#: hledger-web/templates/register.hamlet:51 +msgid "Show the transactions before this period" +msgstr "" + #: hledger-web/templates/upload-form.hamlet:2 msgid "Upload to file" msgstr "" diff --git a/hledger-web/templates/journal.hamlet b/hledger-web/templates/journal.hamlet index a8b97368fea..645cb71595a 100644 --- a/hledger-web/templates/journal.hamlet +++ b/hledger-web/templates/journal.hamlet @@ -18,7 +18,7 @@ $if elem AddPermission perms
    + title=_{HMsg "Show the journal entries on this date"}> #{show (tdate torig)} #{textElideRight 60 (tdescription torig)} diff --git a/hledger-web/templates/register.hamlet b/hledger-web/templates/register.hamlet index 23b96b9fa8f..3ec993d0052 100644 --- a/hledger-web/templates/register.hamlet +++ b/hledger-web/templates/register.hamlet @@ -22,7 +22,7 @@
    + title=_{HMsg "Show this entry and the other journal entries on its date"}> #{show (tdate tacct)} #{textElideRight 30 (tdescription tacct)} @@ -41,14 +41,14 @@ #{show (snd start)} - Balance brought forward + _{HMsg "Balance brought forward"} $if mixedAmountLooksZero startbal ^{mixedAmountAsHtml startbal} $else - + ^{mixedAmountAsHtml startbal} $if elem AddPermission perms