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..4ea23bdc1d8 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, @@ -23,12 +25,13 @@ 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) +import Data.Time.Calendar (Day, fromGregorian) import Hledger.Data import Hledger.Query @@ -99,7 +102,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. @@ -157,11 +168,15 @@ accountTransactionsReport rspec@ReportSpec{_rsReportOpts=ropts} j thisacctq = it 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 = @@ -169,7 +184,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 +242,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 +311,30 @@ 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"] + -- 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=[ + ("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..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'). -} @@ -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 diff --git a/hledger-lib/locale/de.po b/hledger-lib/locale/de.po index ace2dba68ea..a9edaae81a6 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,131 @@ 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" + +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 d2fddf89a33..5bc13df815d 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:159 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:74 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,83 @@ msgstr "" msgid "All accounts" msgstr "" -#: hledger-web/Hledger/Web/Handler/AddR.hs:65 -msgid "Transaction added." +#. 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 "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:38 hledger-web/Hledger/Web/Handler/JournalR.hs:30 hledger-web/Hledger/Web/Handler/RegisterR.hs:39 -#, python-brace-format -msgid "{title}, filtered" +#. 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 browser tab title of this page. -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:48 -msgid "balance - hledger-web" +#. 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 "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:54 hledger-web/Hledger/Web/Handler/BalanceR.hs:99 +#. 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 "" -#: hledger-web/Hledger/Web/Handler/BalanceR.hs:56 +#. 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 "" +#: hledger-web/Hledger/Web/ReportPage.hs:128 +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/StatementsR.hs:57 +#, python-brace-format +msgid "{title}, filtered" +msgstr "" + +#. the browser tab title of this page. +#: hledger-web/Hledger/Web/Handler/BalanceR.hs:39 +msgid "balance - hledger-web" +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,30 +700,74 @@ 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:72 msgctxt "column heading" msgid "Historical Total" msgstr "" -#: hledger-web/Hledger/Web/Handler/RegisterR.hs:57 +#: hledger-web/Hledger/Web/Handler/RegisterR.hs:73 msgctxt "column heading" msgid "Period Total" msgstr "" +#: 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: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:62 +#: 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." @@ -731,69 +831,95 @@ msgstr "" msgid "Postings validation failed" msgstr "" -#. the label before the balance page's report links. -#: hledger-web/Hledger/Web/Widget/Common.hs:101 +#. the label before a page's report links. +#: hledger-web/Hledger/Web/Widget/Common.hs:120 msgid "Report:" 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 label before a report's interval links. +#: hledger-web/Hledger/Web/Widget/Common.hs:128 +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 "Show the balance report" +#. the interval links above a report: each link's text, and its tooltip. +#: hledger-web/Hledger/Web/Widget/Common.hs:134 +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:109 +#. the interval links above a report: each link's text, and its tooltip. +#: 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:135 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:135 +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:136 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:136 +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:137 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:137 +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:138 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:138 +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:139 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: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: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: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: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:161 +msgid "Show each balance at the end of each period, including everything before it" msgstr "" #: hledger-web/templates/add-form.hamlet:11 @@ -816,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" @@ -902,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 "" @@ -1042,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 "" @@ -1073,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/Hledger/Web/App.hs b/hledger-web/Hledger/Web/App.hs index 3022f5feb8e..03cd6d00e45 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 @@ -44,13 +45,18 @@ 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 -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 @@ -156,25 +162,34 @@ 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 + ,interval_ = NoInterval -- one balance per account, over the search's own span } 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 [] - - hideEmptyAccts <- if empty_ ropts - then return True - else (== Just "1") . lookup "hideemptyaccts" . reqCookies <$> getRequest - - let accounts = - balanceReportAsHtml (JournalR, RegisterR) here hideEmptyAccts trs j qparam qopts $ + -- 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 = Just v == keptAccum here + keepable _ v = not (T.null v) + keptParams <- fmap catMaybes . for (keptParamNames here) $ \name -> + fmap (name,) . mfilter (keepable name) <$> lookupGetParam name + + hideEmptyAccts <- hideEmptyAccounts + + -- 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 @@ -353,6 +368,64 @@ checkServerSideUiEnabled = do -- permissionDenied "server-side UI is disabled due to --serve-api" sendResponseStatus status403 ("server-side UI is disabled due to --serve-api" :: Text) +-- | 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 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 = 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 [rmAccum i | i <- reportMenu, rmRoute i == 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] +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/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 fb218500cb2..d018fcb1047 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, removeInacct, reportLinks) -- | The balance or multi-period balance view, with sidebar. @@ -31,82 +26,66 @@ 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)] + -- 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 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 trs (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 $ 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 -- | The heading for a report: --title if one was given, otherwise the -- given default. @@ -114,22 +93,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/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 6e69ec4baa4..d3842a807db 100644 --- a/hledger-web/Hledger/Web/Handler/RegisterR.hs +++ b/hledger-web/Hledger/Web/Handler/RegisterR.hs @@ -22,8 +22,8 @@ import Hledger.Web.Import import Hledger.Web.WebOptions import Hledger.Web.Widget.AddForm (addModal) import Hledger.Web.Widget.Common - (accountQuery, mixedAmountAsHtml, - transactionFragment, removeDates, removeInacct, replaceInacct) + (accountQuery, accountOnlyQuery, mixedAmountAsHtml, + transactionFragment, removeDates, removeInacct, replaceInacct, journalDayQuery) -- | The main journal/account register view, with accounts sidebar. getRegisterR :: Handler Html @@ -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,52 @@ 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) + -- 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 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) $ 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 +96,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 +105,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 +140,13 @@ 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)] -> (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.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 @@ -121,7 +157,7 @@ registerChartHtml q title percommoditytxnreports = $(hamletFile "templates/chart , 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/Handler/StatementsR.hs b/hledger-web/Hledger/Web/Handler/StatementsR.hs new file mode 100644 index 00000000000..72a51ac9d6b --- /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 = reportLinkItems 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 new file mode 100644 index 00000000000..bbd52c3221c --- /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_ = filter (not . T.null) (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 :: + 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 = tr trs "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..115e415b7b8 100644 --- a/hledger-web/Hledger/Web/Test.hs +++ b/hledger-web/Hledger/Web/Test.hs @@ -59,12 +59,14 @@ 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 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) @@ -392,8 +394,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 +560,431 @@ 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 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" ++ "\"") + -- (the sidebar's Journal link keeps the search as typed; the date links are the ones with a fragment) + bodyNotContains "/journal?q=date%3A2025-02%20%22not%3Adesc%3Ax%20y%22#" + + 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 "keeps the search, minus its account term, on the sidebar's Journal link" $ do + request $ do + setMethod "GET" + setUrl RegisterR + addGetParam "q" "inacct:assets:bank:checking date:2025" + statusIs 200 + bodyContains ("") + get JournalR + statusIs 200 + bodyContains ("") + + 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 + + -- 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\">" + + -- 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" + bodyContains "Liabilities" + 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 "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 + 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 -> 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 @@ -89,40 +98,69 @@ 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) - --- | 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") + -- The journal, and the register of everything, for the sidebar's + -- search minus any account term; the search form's clear button is + -- the way out of a search. + journallink = (journalR, [("q", t) | let t = T.unwords $ removeInacct qparam, not (T.null t)]) + 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. +-- 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 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. +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 +188,24 @@ 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 . searchTerms +-- | 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 . 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/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 959a8c2b31d..37962925b64 100644 --- a/hledger-web/hledger-web.cabal +++ b/hledger-web/hledger-web.cabal @@ -105,9 +105,11 @@ 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 + Hledger.Web.ReportPage Hledger.Web.Settings Hledger.Web.Settings.StaticFiles Hledger.Web.Test @@ -137,6 +139,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..7d8bd3c1780 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -143,15 +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 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, +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. @@ -210,22 +214,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`, 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. -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 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 +`/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 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, 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. + # 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/static/hledger.css b/hledger-web/static/hledger.css index f2030891a91..ba6fd19078a 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; } @@ -354,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-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/balance-report.hamlet b/hledger-web/templates/balance-report.hamlet index 28b78e394d4..22b34d80c44 100644 --- a/hledger-web/templates/balance-report.hamlet +++ b/hledger-web/templates/balance-report.hamlet @@ -1,9 +1,16 @@ - #{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 #{tr trs "only"} - ^{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 8f834a62b30..01f1e93f5cb 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

$if not (T.null qparam) - +