bal/bs/bse/cf/is: basic HTML output

This commit is contained in:
Simon Michael 2018-01-14 18:09:50 -08:00
parent 73ba78f092
commit 02516ef987
7 changed files with 138 additions and 32 deletions

View File

@ -161,7 +161,7 @@ reportflags = [
-- | Common output-related flags: --output-file, --output-format... -- | Common output-related flags: --output-file, --output-format...
outputflags = [outputFormatFlag, outputFileFlag] outputflags = [outputFormatFlag, outputFileFlag]
outputFormatFlag = flagReq ["output-format","O"] (\s opts -> Right $ setopt "output-format" s opts) "FMT" "select the output format. Supported formats:\ntxt, csv." outputFormatFlag = flagReq ["output-format","O"] (\s opts -> Right $ setopt "output-format" s opts) "FMT" "select the output format. Supported formats:\ntxt, csv, html."
outputFileFlag = flagReq ["output-file","o"] (\s opts -> Right $ setopt "output-file" s opts) "FILE" "write output to FILE. A file extension matching one of the above formats selects that format." outputFileFlag = flagReq ["output-file","o"] (\s opts -> Right $ setopt "output-file" s opts) "FILE" "write output to FILE. A file extension matching one of the above formats selects that format."
argsFlag :: FlagHelp -> Arg RawOpts argsFlag :: FlagHelp -> Arg RawOpts
@ -524,6 +524,7 @@ defaultOutputFormat = "txt"
outputFormats = outputFormats =
[defaultOutputFormat] ++ [defaultOutputFormat] ++
["csv" ["csv"
,"html"
] ]
-- | Get the output format from the --output-format option, -- | Get the output format from the --output-format option,

View File

@ -233,6 +233,8 @@ Currently, empty cells show 0.
-} -}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Hledger.Cli.Commands.Balance ( module Hledger.Cli.Commands.Balance (
balancemode balancemode
@ -241,6 +243,8 @@ module Hledger.Cli.Commands.Balance (
,balanceReportItemAsText ,balanceReportItemAsText
,multiBalanceReportAsText ,multiBalanceReportAsText
,multiBalanceReportAsCsv ,multiBalanceReportAsCsv
,multiBalanceReportAsHtml
,multiBalanceReportHtmlRows
,renderBalanceReportTable ,renderBalanceReportTable
,balanceReportAsTable ,balanceReportAsTable
,tests_Hledger_Cli_Commands_Balance ,tests_Hledger_Cli_Commands_Balance
@ -249,10 +253,11 @@ module Hledger.Cli.Commands.Balance (
import Data.List (intercalate, nub) import Data.List (intercalate, nub)
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map as Map
-- import Data.Monoid
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import Data.Decimal (roundTo) import Data.Decimal (roundTo)
import Lucid as L
import Text.CSV import Text.CSV
import Test.HUnit import Test.HUnit
import Text.Printf (printf) import Text.Printf (printf)
@ -320,8 +325,9 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
in singleBalanceReport ropts' (queryFromOpts d ropts) j in singleBalanceReport ropts' (queryFromOpts d ropts) j
| otherwise = balanceReport ropts (queryFromOpts d ropts) j | otherwise = balanceReport ropts (queryFromOpts d ropts) j
render = case format of render = case format of
"csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r "csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r
_ -> balanceReportAsText "html" -> \_ _ -> error' "Sorry, HTML output is not yet implemented for this kind of report." -- TODO
_ -> balanceReportAsText
writeOutput opts $ render ropts report writeOutput opts $ render ropts report
_ | boolopt "budget" rawopts -> do _ | boolopt "budget" rawopts -> do
@ -332,14 +338,16 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
render = case format of render = case format of
-- XXX: implement csv rendering -- XXX: implement csv rendering
"csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts "csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts
"html" -> const $ error' "Sorry, HTML output is not yet implemented for this kind of report." -- TODO
_ -> multiBalanceReportWithBudgetAsText ropts budgetReport _ -> multiBalanceReportWithBudgetAsText ropts budgetReport
writeOutput opts $ render report writeOutput opts $ render report
| otherwise -> do | otherwise -> do
let report = multiBalanceReport ropts (queryFromOpts d ropts) j let report = multiBalanceReport ropts (queryFromOpts d ropts) j
render = case format of render = case format of
"csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts "csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts
_ -> multiBalanceReportAsText ropts "html" -> (++ "\n") . TL.unpack . L.renderText . multiBalanceReportAsHtml ropts
_ -> multiBalanceReportAsText ropts
writeOutput opts $ render report writeOutput opts $ render report
-- | Re-map account names to closet parent with periodic transaction from budget. -- | Re-map account names to closet parent with periodic transaction from budget.
@ -503,17 +511,19 @@ renderComponent1 opts (acctname, depth, total) (FormatField ljust min max field)
TotalField -> fitStringMulti min max True False $ ((intercalate ", " . map strip . lines) (showamt total)) TotalField -> fitStringMulti min max True False $ ((intercalate ", " . map strip . lines) (showamt total))
where where
showamt | color_ opts = cshowMixedAmountWithoutPrice showamt | color_ opts = cshowMixedAmountWithoutPrice
| otherwise = showMixedAmountWithoutPrice | otherwise = showMixedAmountWithoutPrice
_ -> "" _ -> ""
-- multi-column balance reports -- multi-column balance reports
-- | Render a multi-column balance report as CSV. -- | Render a multi-column balance report as CSV.
-- The CSV will always include the initial headings row,
-- and will include the final totals row unless --no-total is set.
multiBalanceReportAsCsv :: ReportOpts -> MultiBalanceReport -> CSV multiBalanceReportAsCsv :: ReportOpts -> MultiBalanceReport -> CSV
multiBalanceReportAsCsv opts (MultiBalanceReport (colspans, items, (coltotals,tot,avg))) = multiBalanceReportAsCsv opts (MultiBalanceReport (colspans, items, (coltotals,tot,avg))) =
("account" : map showDateSpan colspans ("Account" : map showDateSpan colspans
++ (if row_total_ opts then ["total"] else []) ++ (if row_total_ opts then ["Total"] else [])
++ (if average_ opts then ["average"] else []) ++ (if average_ opts then ["Average"] else [])
) : ) :
[T.unpack a : [T.unpack a :
map showMixedAmountOneLineWithoutPrice map showMixedAmountOneLineWithoutPrice
@ -524,13 +534,49 @@ multiBalanceReportAsCsv opts (MultiBalanceReport (colspans, items, (coltotals,to
++ ++
if no_total_ opts if no_total_ opts
then [] then []
else [["totals"] else [["Total:"]
++ map showMixedAmountOneLineWithoutPrice ( ++ map showMixedAmountOneLineWithoutPrice (
coltotals coltotals
++ (if row_total_ opts then [tot] else []) ++ (if row_total_ opts then [tot] else [])
++ (if average_ opts then [avg] else []) ++ (if average_ opts then [avg] else [])
)] )]
-- | Render a multi-column balance report as HTML.
multiBalanceReportAsHtml :: ReportOpts -> MultiBalanceReport -> Html ()
multiBalanceReportAsHtml ropts mbr =
let
(headingsrow,bodyrows,mtotalsrow) = multiBalanceReportHtmlRows ropts mbr
in
table_ $ mconcat $
[headingsrow]
++ bodyrows
++ maybe [] (:[]) mtotalsrow
-- | Render the HTML table rows for a MultiBalanceReport.
-- Returns the heading row, 0 or more body rows, and the totals row if enabled.
multiBalanceReportHtmlRows :: ReportOpts -> MultiBalanceReport -> (Html (), [Html ()], Maybe (Html ()))
multiBalanceReportHtmlRows ropts mbr =
let
headingsrow:rest = multiBalanceReportAsCsv ropts mbr
(bodyrows, mtotalsrow) | no_total_ ropts = (rest, Nothing)
| otherwise = (init rest, Just $ last rest)
in
(thRow headingsrow
,map multiBalanceReportHtmlBodyRow bodyrows
,thRow <$> mtotalsrow
)
-- | Render one MultiBalanceReport data row as a HTML table row.
multiBalanceReportHtmlBodyRow :: [String] -> Html ()
multiBalanceReportHtmlBodyRow [] = mempty -- shouldn't happen
multiBalanceReportHtmlBodyRow (acct:amts) =
tr_ $ mconcat $
td_ (toHtml acct) :
[td_ [style_ "text-align:right"] (toHtml amt) | amt <- amts]
thRow :: [String] -> Html ()
thRow = tr_ . mconcat . map (th_ . toHtml)
-- | Render a multi-column balance report as plain text suitable for console output. -- | Render a multi-column balance report as plain text suitable for console output.
multiBalanceReportAsText :: ReportOpts -> MultiBalanceReport -> String multiBalanceReportAsText :: ReportOpts -> MultiBalanceReport -> String
multiBalanceReportAsText opts r = multiBalanceReportAsText opts r =
@ -559,16 +605,16 @@ multiBalanceReportWithBudgetAsText opts budget r =
CumulativeChange -> "Ending balances (cumulative)" CumulativeChange -> "Ending balances (cumulative)"
HistoricalBalance -> "Ending balances (historical)" HistoricalBalance -> "Ending balances (historical)"
showcell (real, Nothing) = showamt real showcell (real, Nothing) = showamt real
showcell (real, Just budget) = showcell (real, Just budget) =
case percentage real budget of case percentage real budget of
Just pct -> printf "%s [%s%% of %s]" (showamt real) (show $ roundTo 0 pct) (showamt budget) Just pct -> printf "%s [%s%% of %s]" (showamt real) (show $ roundTo 0 pct) (showamt budget)
Nothing -> printf "%s [%s]" (showamt real) (showamt budget) Nothing -> printf "%s [%s]" (showamt real) (showamt budget)
percentage real budget = percentage real budget =
-- percentage of budget consumed is always computed in the cost basis -- percentage of budget consumed is always computed in the cost basis
case (toCost real, toCost budget) of case (toCost real, toCost budget) of
(Mixed [a1], Mixed [a2]) (Mixed [a1], Mixed [a2])
| isReallyZeroAmount a1 -> Just 0 -- if there are no postings, we consumed 0% of budget | isReallyZeroAmount a1 -> Just 0 -- if there are no postings, we consumed 0% of budget
| acommodity a1 == acommodity a2 && aquantity a2 /= 0 -> | acommodity a1 == acommodity a2 && aquantity a2 /= 0 ->
Just $ 100 * aquantity a1 / aquantity a2 Just $ 100 * aquantity a1 / aquantity a2
_ -> Nothing _ -> Nothing
where where
@ -582,11 +628,11 @@ multiBalanceReportWithBudgetAsText opts budget r =
-- Both of these are satisfied by construction of budget report and process of rolling up -- Both of these are satisfied by construction of budget report and process of rolling up
-- account names. -- account names.
combine (Table l t d) (Table l' t' d') = Table l t combinedRows combine (Table l t d) (Table l' t' d') = Table l t combinedRows
where where
-- For all accounts that are present in the budget, zip real amounts with budget amounts -- For all accounts that are present in the budget, zip real amounts with budget amounts
combinedRows = [ combineRow row budgetRow combinedRows = [ combineRow row budgetRow
| (acct, row) <- zip (headerContents l) d | (acct, row) <- zip (headerContents l) d
, let budgetRow = , let budgetRow =
if acct == "" then [] -- "" is totals row if acct == "" then [] -- "" is totals row
else fromMaybe [] $ Map.lookup acct budgetAccts else fromMaybe [] $ Map.lookup acct budgetAccts
] ]
@ -594,26 +640,26 @@ multiBalanceReportWithBudgetAsText opts budget r =
-- Headers for budget row will always be a sublist of headers of row -- Headers for budget row will always be a sublist of headers of row
combineRow r br = combineRow r br =
let reportRow = zip (headerContents t) r let reportRow = zip (headerContents t) r
budgetRow = Map.fromList $ zip (headerContents t') br budgetRow = Map.fromList $ zip (headerContents t') br
findBudgetVal hdr = Map.lookup hdr budgetRow findBudgetVal hdr = Map.lookup hdr budgetRow
in map (\(hdr, val) -> (val, findBudgetVal hdr)) reportRow in map (\(hdr, val) -> (val, findBudgetVal hdr)) reportRow
budgetAccts = Map.fromList $ zip (headerContents l') d' budgetAccts = Map.fromList $ zip (headerContents l') d'
-- | Given a table representing a multi-column balance report (for example, -- | Given a table representing a multi-column balance report (for example,
-- made using 'balanceReportAsTable'), render it in a format suitable for -- made using 'balanceReportAsTable'), render it in a format suitable for
-- console output. -- console output.
renderBalanceReportTable :: ReportOpts -> Table String String MixedAmount -> String renderBalanceReportTable :: ReportOpts -> Table String String MixedAmount -> String
renderBalanceReportTable ropts = renderBalanceReportTable ropts =
renderBalanceReportTable' ropts showamt renderBalanceReportTable' ropts showamt
where where
showamt | color_ ropts = cshowMixedAmountOneLineWithoutPrice showamt | color_ ropts = cshowMixedAmountOneLineWithoutPrice
| otherwise = showMixedAmountOneLineWithoutPrice | otherwise = showMixedAmountOneLineWithoutPrice
renderBalanceReportTable' :: ReportOpts -> (a -> String) -> Table String String a -> String renderBalanceReportTable' :: ReportOpts -> (a -> String) -> Table String String a -> String
renderBalanceReportTable' (ReportOpts { pretty_tables_ = pretty}) showCell = renderBalanceReportTable' (ReportOpts { pretty_tables_ = pretty}) showCell =
unlines unlines
. addtrailingblank . addtrailingblank
. trimborder . trimborder
. lines . lines
. render pretty id id showCell . render pretty id id showCell
. align . align

View File

@ -59,8 +59,9 @@ printEntries opts@CliOpts{reportopts_=ropts} j = do
let q = queryFromOpts d ropts let q = queryFromOpts d ropts
fmt = outputFormatFromOpts opts fmt = outputFormatFromOpts opts
(render, ropts') = case fmt of (render, ropts') = case fmt of
"csv" -> ((++"\n") . printCSV . entriesReportAsCsv, ropts{accountlistmode_=ALFlat}) "csv" -> ((++"\n") . printCSV . entriesReportAsCsv, ropts{accountlistmode_=ALFlat})
_ -> (entriesReportAsText opts, ropts) "html" -> (const $ error' "Sorry, HTML output is not yet implemented for this kind of report.", ropts{accountlistmode_=ALFlat}) -- TODO
_ -> (entriesReportAsText opts, ropts)
writeOutput opts $ render $ entriesReport ropts' q j writeOutput opts $ render $ entriesReport ropts' q j
entriesReportAsText :: CliOpts -> EntriesReport -> String entriesReportAsText :: CliOpts -> EntriesReport -> String

View File

@ -61,8 +61,9 @@ register :: CliOpts -> Journal -> IO ()
register opts@CliOpts{reportopts_=ropts} j = do register opts@CliOpts{reportopts_=ropts} j = do
d <- getCurrentDay d <- getCurrentDay
let fmt = outputFormatFromOpts opts let fmt = outputFormatFromOpts opts
render | fmt=="csv" = const ((++"\n") . printCSV . postingsReportAsCsv) render | fmt=="csv" = const ((++"\n") . printCSV . postingsReportAsCsv)
| otherwise = postingsReportAsText | fmt=="html" = const $ error' "Sorry, HTML output is not yet implemented for this kind of report." -- TODO
| otherwise = postingsReportAsText
writeOutput opts $ render opts $ postingsReport ropts (queryFromOpts d ropts) j writeOutput opts $ render opts $ postingsReport ropts (queryFromOpts d ropts) j
postingsReportAsCsv :: PostingsReport -> CSV postingsReportAsCsv :: PostingsReport -> CSV

View File

@ -15,9 +15,12 @@ module Hledger.Cli.CompoundBalanceCommand (
import Data.List (intercalate, foldl') import Data.List (intercalate, foldl')
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Monoid (Sum(..), (<>)) import Data.Monoid (Sum(..), (<>))
import qualified Data.Text
import qualified Data.Text.Lazy as TL
import Data.Tuple.HT (uncurry3) import Data.Tuple.HT (uncurry3)
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import Text.CSV import Text.CSV
import Lucid as L
import Text.Tabular as T import Text.Tabular as T
import Hledger import Hledger
@ -181,8 +184,9 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{command_=cmd,
-- render appropriately -- render appropriately
writeOutput opts $ writeOutput opts $
case format of case format of
"csv" -> printCSV (compoundBalanceReportAsCsv ropts cbr) ++ "\n" "csv" -> printCSV (compoundBalanceReportAsCsv ropts cbr) ++ "\n"
_ -> compoundBalanceReportAsText ropts' cbr "html" -> (++ "\n") $ TL.unpack $ L.renderText $ compoundBalanceReportAsHtml ropts cbr
_ -> compoundBalanceReportAsText ropts' cbr
-- | Run one subreport for a compound balance command in single-column mode. -- | Run one subreport for a compound balance command in single-column mode.
-- Currently this returns the plain text rendering of the subreport, and its total. -- Currently this returns the plain text rendering of the subreport, and its total.
@ -347,4 +351,55 @@ compoundBalanceReportAsCsv ropts (title, colspans, subreports, (coltotals, grand
++ (if row_total_ ropts then [grandtotal] else []) ++ (if row_total_ ropts then [grandtotal] else [])
++ (if average_ ropts then [grandavg] else []) ++ (if average_ ropts then [grandavg] else [])
) )
]) ])
-- | Render a compound balance report as HTML.
compoundBalanceReportAsHtml :: ReportOpts -> CompoundBalanceReport -> Html ()
compoundBalanceReportAsHtml ropts cbr =
let
(title, colspans, subreports, (coltotals, grandtotal, grandavg)) = cbr
colspanattr = colspan_ $ Data.Text.pack $ show $ length colspans + 1
leftattr = style_ "text-align:left"
blankrow = tr_ $ td_ [colspanattr] $ toHtmlRaw ("&nbsp;"::String)
titlerows =
[tr_ $ th_ [colspanattr, leftattr] $ toHtml title]
++ [thRow $
"" :
map showDateSpan colspans
++ (if row_total_ ropts then ["Total"] else [])
++ (if average_ ropts then ["Average"] else [])
]
-- Make rows for a subreport: its title row, not the headings row,
-- the data rows, any totals row, and a blank row for whitespace.
subreportrows :: (String, MultiBalanceReport) -> [Html ()]
subreportrows (subreporttitle, mbr) =
let
(_,bodyrows,mtotalsrow) = multiBalanceReportHtmlRows ropts mbr
in
[tr_ $ th_ [colspanattr, leftattr] $ toHtml subreporttitle]
++ bodyrows
++ maybe [] (:[]) mtotalsrow
++ [blankrow]
totalrows | no_total_ ropts || length subreports == 1 = []
| otherwise =
[thRow $
"Grand Total:" :
map showMixedAmountOneLineWithoutPrice (
coltotals
++ (if row_total_ ropts then [grandtotal] else [])
++ (if average_ ropts then [grandavg] else [])
)
]
in
table_ $ mconcat $
titlerows
++ [blankrow]
++ concatMap subreportrows subreports
++ totalrows
thRow :: [String] -> Html ()
thRow = tr_ . mconcat . map (th_ . toHtml)

View File

@ -2,7 +2,7 @@
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
-- --
-- hash: b3929eebc33ec5bd2e240d49c003aba48ddc1d4f0e9a09ca2e8161d531bd9fb3 -- hash: cb1407ac28a973e8fc74c9e78c06c2c29715873a64eb4586417cf02d12bfa60f
name: hledger name: hledger
version: 1.5.99 version: 1.5.99
@ -97,6 +97,7 @@ library
, haskeline >=0.6 , haskeline >=0.6
, here , here
, hledger-lib >=1.5.99 && <1.6 , hledger-lib >=1.5.99 && <1.6
, lucid
, megaparsec >=5.0 , megaparsec >=5.0
, mtl , mtl
, mtl-compat , mtl-compat

View File

@ -138,6 +138,7 @@ library:
- hashable >=1.2.4 - hashable >=1.2.4
- haskeline >=0.6 - haskeline >=0.6
- HUnit - HUnit
- lucid
- mtl - mtl
- mtl-compat - mtl-compat
- old-time - old-time