|
-
+
#{show (tdate tacct)}
|
#{textElideRight 30 (tdescription tacct)}
|
$forall (Posting { paccount = acc }, (summName,comma)) <- otherTransAccounts torig
-
+
#{summName}#{comma}
|
$if not split || not (mixedAmountLooksZero amt)
^{mixedAmountAsHtml amt}
|
^{mixedAmountAsHtml bal}
+ $if historical
+ $maybe start <- mstart
+ |
+ |
+ #{show (snd start)}
+ |
+ _{HMsg "Balance brought forward"}
+ |
+ |
+ |
+ $if mixedAmountLooksZero startbal
+ ^{mixedAmountAsHtml startbal}
+ $else
+
+ ^{mixedAmountAsHtml startbal}
$if elem AddPermission perms
^{addModal AddR j today}
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);
}
diff --git a/hledger-web/test/browser/webui.spec.js b/hledger-web/test/browser/webui.spec.js
index a6a3b57da63..f6c2171178d 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([]);
@@ -276,4 +276,21 @@ test.describe('sidebar', () => {
expect(pageErrors).toEqual([]);
});
+ test('links to the financial statements, marking the one shown', async ({ page }) => {
+ // at a laptop width the sidebar is narrow; the report labels must still fit
+ await page.setViewportSize({ width: 1000, height: 800 });
+ await page.goto('/journal');
+ const clipped = await page.locator('#sidebar-menu td.top').evaluateAll(
+ tds => tds.filter(td => td.scrollWidth > td.clientWidth).map(td => td.textContent.trim()));
+ expect(clipped).toEqual([]);
+ await page.locator('#sidebar-menu a', { hasText: 'Income statement' }).click();
+ await expect(page).toHaveURL(/\/incomestatement$/);
+ await expect(page.locator('#main-content h2')).toContainText('Income Statement');
+ await expect(page.locator('#sidebar-menu tr.inacct a')).toHaveText('Income statement');
+ // from a report, the other reports are one click away in the Report row
+ await page.locator('#main-content .report-links a', { hasText: 'Balance sheet with equity' }).click();
+ await expect(page).toHaveURL(/\/balancesheetequity$/);
+ expect(pageErrors).toEqual([]);
+ });
+
});
diff --git a/hledger/Hledger/Cli/Anchor.hs b/hledger/Hledger/Cli/Anchor.hs
index af0c90b36f2..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, showDate)
-import Hledger.Data.Types (DateSpan)
-import Hledger.Reports.ReportOptions (PeriodTitles(..))
+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
-registerQueryUrl :: [Text] -> Text
-registerQueryUrl query =
+-- | 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,43 +134,103 @@ removeDates =
filter (\term_ ->
not $ Text.isPrefixOf "date:" term_ || Text.isPrefixOf "date2:" term_)
-replaceDate :: Text -> [Text] -> [Text]
-replaceDate prd query = "date:"<>prd : removeDates query
+-- | 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_)
+-- | 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 =
- let prd = renderPeriodHeading ph spn in
- (headerCell prd) {
- Spr.cellAnchor = composeAnchor base $ replaceDate prd 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
-dateQueryCell ::
+dateSpanCell ::
(Spr.Lines border) =>
- Maybe Text -> [Text] -> Text -> Text -> Spr.Cell border Text
-dateQueryCell base query acct dateTerm =
- (Spr.defaultCell dateTerm) {
- Spr.cellAnchor =
- composeAnchor base $ "inacct:"<>acct : replaceDate dateTerm 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 = dateQueryCell base query acct . showDate
+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
-dateSpanCell ::
+-- | 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 = dateQueryCell base query acct . renderPeriodHeading ph
+ 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 : 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..6af968b74f0 100644
--- a/hledger/Hledger/Cli/Commands/Balance.md
+++ b/hledger/Hledger/Cli/Commands/Balance.md
@@ -994,7 +994,13 @@ 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).
+The two can differ when a transaction's postings have dates of their own, since the report counts
+such a posting in its own period while the register shows its whole transaction.
+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/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 4606c326f1b..866f2ab0e13 100644
--- a/hledger/Hledger/Cli/CompoundBalanceCommand.hs
+++ b/hledger/Hledger/Cli/CompoundBalanceCommand.hs
@@ -12,12 +12,16 @@ module Hledger.Cli.CompoundBalanceCommand (
CompoundBalanceCommandSpec(..)
,compoundBalanceCommandMode
,compoundBalanceCommand
+ ,compoundBalanceReportTitle
+ ,applySubreportTitles
+ ,CompoundBalanceReportParts(..)
+ ,compoundBalanceReportAsSpreadsheetParts
+ ,allCommoditiesFromSubreports
) where
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 +42,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)
@@ -138,93 +143,104 @@ 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
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
+-- | 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
@@ -339,12 +355,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 +371,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,31 +391,20 @@ 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 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 -> 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 =
- Spr.headerCell accountLabel :
- case layout_ ropts of
- LayoutTidy -> map Spr.headerCell tidyColumnLabels
- LayoutBare -> [Spr.headerCell "Commodity"]
- _ -> []
- dataHeaders =
- (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"])
+ CompoundBalanceReportParts{..} = compoundBalanceReportAsSpreadsheetParts fmt accountLabel ropts specs cbr
+ dataHeaders = map Spr.cellContent cbrpDataHeaders
headerrow =
- leadingHeaders ++
- concatMap (Spr.horizontalSpan subColumns . Spr.headerCell) dataHeaders
+ cbrpLeadingHeaders ++
+ concatMap (Spr.horizontalSpan subColumns) cbrpDataHeaders
blankrow =
fmap (Spr.horizontalSpan headerrow . Spr.defaultCell) maybeBlank
@@ -405,16 +412,10 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts cbr =
case layout_ ropts of
LayoutBareWide -> void allCommodities
_ -> [()]
- allCommodities = allCommoditiesFromSubreports subreports
+ allCommodities = allCommoditiesFromSubreports $ cbrSubreports cbr
- -- Make rows for a subreport: its title row, not the headings row,
- -- the data rows, any totals row, and a blank row for whitespace.
- subreportrows ::
- (T.Text, MultiBalanceReport, Bool) -> [[Spr.Cell Spr.NumLines T.Text]]
- subreportrows (subreporttitle, mbr, _increasestotal) =
+ sectionrows (subreporttitle, bodyrows, totalsrows) =
let
- (_, bodyrows, mtotalsrows) =
- balanceSubReportAsSpreadsheetParts fmt ropts allCommodities mbr
accountCell =
(Spr.defaultCell subreporttitle) {
Spr.cellStyle = Spr.Body Spr.Total,
@@ -431,24 +432,108 @@ compoundBalanceReportAsSpreadsheet fmt accountLabel maybeBlank ropts cbr =
in
titleRows ++
bodyrows ++
- mtotalsrows ++
+ totalsrows ++
maybeToList blankrow
- totalrows =
+ 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
+ LayoutTidy -> map Spr.headerCell tidyColumnLabels
+ LayoutBare -> [Spr.headerCell "Commodity"]
+ _ -> []
+ -- 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) >>) $
+ [ (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"])
+ 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
+
+ -- 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
+ sectionterm = typeTerm $ cbcsubreportquery spec
+ (_, bodyrows, _) =
+ balanceSubReportAsSpreadsheetParts fmt
+ sropts{no_total_ = True, querystring_ = maybeToList sectionterm ++ querystring_ sropts}
+ allCommodities mbr
+ totalsrows
+ | no_total_ ropts = []
+ | otherwise =
+ linkedWhen sectionterm $
+ multiBalanceTotalsRows fmt sropts (reportLinkOpts sropts (prDates mbr)) (maybeToList sectionterm)
+ allCommodities (prDates mbr) totalRowHeadingSpreadsheet (prTotals mbr)
+
+ -- 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
+ netrows =
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
-
- in (title,
- ((1, multiBalanceReportNumHeaderColumns $ layout_ ropts),
- headerrow :| concatMap subreportrows subreports ++ totalrows))
+ linkedWhen netterm $
+ multiBalanceTotalsRows fmt ropts (reportLinkOpts ropts colspans){loNegated = netnegated}
+ (maybeToList netterm) allCommodities colspans "Net:" totalrow
-- | 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
new file mode 100644
index 00000000000..db2d473b9ba
--- /dev/null
+++ b/hledger/test/balance/base-url.test
@@ -0,0 +1,335 @@
+# * 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
+
+# ** 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..ad1c5bfe75c 100644
--- a/hledger/test/balancesheet.test
+++ b/hledger/test/balancesheet.test
@@ -376,3 +376,78 @@ 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, and links with the
+# section's type term, as the section's totals do.
+<
+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
| |