bs/cf/is: support --output-file and --output-format=txt|csv
The CSV output should be reasonably ok for dragging into a spreadsheet and reformatting.
This commit is contained in:
		
							parent
							
								
									71b206dfc5
								
							
						
					
					
						commit
						8851ebc29f
					
				| @ -36,7 +36,7 @@ import Hledger.Reports.BalanceReport | |||||||
| -- | -- | ||||||
| -- 1. a list of each column's period (date span) | -- 1. a list of each column's period (date span) | ||||||
| -- | -- | ||||||
| -- 2. a list of row items, each containing: | -- 2. a list of rows, each containing: | ||||||
| -- | -- | ||||||
| --   * the full account name | --   * the full account name | ||||||
| -- | -- | ||||||
| @ -44,7 +44,7 @@ import Hledger.Reports.BalanceReport | |||||||
| -- | -- | ||||||
| --   * the account's depth | --   * the account's depth | ||||||
| -- | -- | ||||||
| --   * the amounts to show in each column | --   * a list of amounts, one for each column | ||||||
| -- | -- | ||||||
| --   * the total of the row's amounts | --   * the total of the row's amounts | ||||||
| -- | -- | ||||||
|  | |||||||
| @ -240,6 +240,7 @@ module Hledger.Cli.Balance ( | |||||||
|  ,balanceReportAsText |  ,balanceReportAsText | ||||||
|  ,balanceReportItemAsText |  ,balanceReportItemAsText | ||||||
|  ,multiBalanceReportAsText |  ,multiBalanceReportAsText | ||||||
|  |  ,multiBalanceReportAsCsv | ||||||
|  ,renderBalanceReportTable |  ,renderBalanceReportTable | ||||||
|  ,balanceReportAsTable |  ,balanceReportAsTable | ||||||
|  ,tests_Hledger_Cli_Balance |  ,tests_Hledger_Cli_Balance | ||||||
|  | |||||||
| @ -12,20 +12,21 @@ module Hledger.Cli.CompoundBalanceCommand ( | |||||||
|  ,compoundBalanceCommand |  ,compoundBalanceCommand | ||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import Control.Monad (unless) | import Data.List (intercalate, foldl') | ||||||
| import Data.List (intercalate, foldl', isPrefixOf) |  | ||||||
| import Data.Maybe (fromMaybe) | import Data.Maybe (fromMaybe) | ||||||
| import Data.Monoid (Sum(..), (<>)) | import Data.Monoid (Sum(..), (<>)) | ||||||
| import System.Console.CmdArgs.Explicit as C | import System.Console.CmdArgs.Explicit as C | ||||||
|  | import Text.CSV | ||||||
| import Text.Tabular as T | import Text.Tabular as T | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.Balance | import Hledger.Cli.Balance | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
|  | import Hledger.Cli.Utils (writeOutput) | ||||||
| 
 | 
 | ||||||
| -- | Description of a compound balance report command,  | -- | Description of a compound balance report command,  | ||||||
| -- from which we generate the command's cmdargs mode and IO action. | -- from which we generate the command's cmdargs mode and IO action. | ||||||
| -- A compound balance report shows one or more sections/subreports,  | -- A compound balance report command shows one or more sections/subreports,  | ||||||
| -- each with its own title and subtotals row, in a certain order,  | -- each with its own title and subtotals row, in a certain order,  | ||||||
| -- plus a grand totals row if there's more than one section. | -- plus a grand totals row if there's more than one section. | ||||||
| -- Examples are the balancesheet, cashflow and incomestatement commands. | -- Examples are the balancesheet, cashflow and incomestatement commands. | ||||||
| @ -64,6 +65,8 @@ compoundBalanceCommandMode CompoundBalanceCommandSpec{..} = (defCommandMode $ cb | |||||||
|      ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "don't squash boring parent accounts (in tree mode)" |      ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "don't squash boring parent accounts (in tree mode)" | ||||||
|      ,flagReq  ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format (in simple reports)" |      ,flagReq  ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format (in simple reports)" | ||||||
|      ,flagNone ["pretty-tables"] (\opts -> setboolopt "pretty-tables" opts) "use unicode when displaying tables" |      ,flagNone ["pretty-tables"] (\opts -> setboolopt "pretty-tables" opts) "use unicode when displaying tables" | ||||||
|  |      ,outputFormatFlag | ||||||
|  |      ,outputFileFlag | ||||||
|      ] |      ] | ||||||
|     ,groupHidden = [] |     ,groupHidden = [] | ||||||
|     ,groupNamed = [generalflagsgroup1] |     ,groupNamed = [generalflagsgroup1] | ||||||
| @ -76,7 +79,7 @@ compoundBalanceCommandMode CompoundBalanceCommandSpec{..} = (defCommandMode $ cb | |||||||
| 
 | 
 | ||||||
| -- | Generate a runnable command from a compound balance command specification. | -- | Generate a runnable command from a compound balance command specification. | ||||||
| compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> IO ()) | compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> IO ()) | ||||||
| compoundBalanceCommand CompoundBalanceCommandSpec{..} CliOpts{command_=cmd, reportopts_=ropts, rawopts_=rawopts} j = do | compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{command_=cmd, reportopts_=ropts, rawopts_=rawopts} j = do | ||||||
|     d <- getCurrentDay |     d <- getCurrentDay | ||||||
|     let |     let | ||||||
|       -- use the default balance type for this report, unless the user overrides   |       -- use the default balance type for this report, unless the user overrides   | ||||||
| @ -109,68 +112,70 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} CliOpts{command_=cmd, repo | |||||||
|         | otherwise |         | otherwise | ||||||
|             = ropts{balancetype_=balancetype} |             = ropts{balancetype_=balancetype} | ||||||
|       userq = queryFromOpts d ropts' |       userq = queryFromOpts d ropts' | ||||||
|  |       format = outputFormatFromOpts opts | ||||||
| 
 | 
 | ||||||
|     case interval_ ropts' of |     case interval_ ropts' of | ||||||
| 
 | 
 | ||||||
|       -- single-column report |       -- single-column report | ||||||
|  |       -- TODO refactor, support output format like multi column  | ||||||
|       NoInterval -> do |       NoInterval -> do | ||||||
|         let |         let | ||||||
|           -- concatenate the rendering and sum the totals from each subreport |           -- concatenate the rendering and sum the totals from each subreport | ||||||
|           (subreportstr, total) =  |           (subreportstr, total) =  | ||||||
|             foldMap (uncurry (compoundBalanceCommandSingleColumnReport ropts' userq j)) cbcqueries |             foldMap (uncurry (compoundBalanceCommandSingleColumnReport ropts' userq j)) cbcqueries | ||||||
|         putStrLn $ title ++ "\n" | 
 | ||||||
|         mapM_ putStrLn subreportstr |         writeOutput opts $ unlines $ | ||||||
|         unless (no_total_ ropts' || cmd=="cashflow") . mapM_ putStrLn $ |           [title ++ "\n"] ++ | ||||||
|           [ "Total:" |           subreportstr ++ | ||||||
|           , "--------------------" |           if (no_total_ ropts' || cmd=="cashflow") | ||||||
|           , padLeftWide 20 $ showamt (getSum total) |            then [] | ||||||
|           , "" |            else | ||||||
|           ] |              [ "Total:" | ||||||
|         where |              , "--------------------" | ||||||
|           showamt | color_ ropts' = cshowMixedAmountWithoutPrice |              , padLeftWide 20 $ showamt (getSum total) | ||||||
|                   | otherwise    = showMixedAmountWithoutPrice |              , "" | ||||||
|            |              ] | ||||||
|  |              where | ||||||
|  |                showamt | color_ ropts' = cshowMixedAmountWithoutPrice | ||||||
|  |                        | otherwise    = showMixedAmountWithoutPrice | ||||||
|  | 
 | ||||||
|       -- multi-column report |       -- multi-column report | ||||||
|       _ -> do |       _ -> do | ||||||
|         let |         let | ||||||
|           -- list the tables, list the totals rows, and sum the totals from each subreport |           -- make a CompoundBalanceReport | ||||||
|           (subreporttables, subreporttotals, Sum overalltotal) =  |           namedsubreports =  | ||||||
|             foldMap (uncurry (compoundBalanceCommandMultiColumnReport ropts' userq j)) cbcqueries |             map (\(subreporttitle, subreportq) ->  | ||||||
|           overalltable = case subreporttables of |                   (subreporttitle, compoundBalanceSubreport ropts' userq j subreportq))  | ||||||
|             t1:ts -> foldl' concatTables t1 ts |                 cbcqueries | ||||||
|             []    -> T.empty |           subtotalrows = [coltotals | MultiBalanceReport (_,_,(coltotals,_,_)) <- map snd namedsubreports] | ||||||
|           overalltable' |           overalltotals = case subtotalrows of | ||||||
|             | no_total_ ropts' || length cbcqueries == 1 = |             [] -> ([], nullmixedamt, nullmixedamt) | ||||||
|                 overalltable |             rs -> | ||||||
|             | otherwise = |               -- Sum the subtotals in each column. | ||||||
|                 overalltable |               -- A subreport might be empty and have no subtotals, count those as zeros (#588). | ||||||
|                 +====+ |               -- Short subtotals rows are also implicitly padded with zeros, though that is not expected to happen.   | ||||||
|                 row "Total" overalltotals' |               let | ||||||
|               where |                 numcols = maximum $ map length rs  -- depends on non-null ts | ||||||
|                 overalltotals = case subreporttotals of |                 zeros = replicate numcols nullmixedamt | ||||||
|                   [] -> [] |                 rs' = [take numcols $ as ++ repeat nullmixedamt | as <- rs] | ||||||
|                   ts -> |                 coltotals = foldl' (zipWith (+)) zeros rs' | ||||||
|                     -- Sum the subtotals in each column. |                 grandtotal = sum coltotals | ||||||
|                     -- A subreport might be empty and have no subtotals, count those as zeros (#588). |                 grandavg | null coltotals = nullmixedamt | ||||||
|                     -- Short subtotals rows are also implicitly padded with zeros, though that is not expected to happen.   |                          | otherwise      = grandtotal `divideMixedAmount` fromIntegral (length coltotals) | ||||||
|                     let |               in  | ||||||
|                       numcols = maximum $ map length ts |                 (coltotals, grandtotal, grandavg) | ||||||
|                       zeros = replicate numcols nullmixedamt |           cbr = | ||||||
|                       ts' = [take numcols $ as ++ repeat nullmixedamt | as <- ts] |             (title | ||||||
|                     in foldl' (zipWith (+)) zeros ts' |             ,namedsubreports | ||||||
|                 -- add values for the total/average columns if enabled |             ,overalltotals  | ||||||
|                 overalltotals' |             ) | ||||||
|                   | null overalltotals = [] |         -- render appropriately | ||||||
|                   | otherwise = |         writeOutput opts $ | ||||||
|                       overalltotals |           case format of | ||||||
|                       ++ (if row_total_ ropts' then [overalltotal]   else []) |             "csv" -> printCSV (compoundBalanceReportAsCsv ropts cbr) ++ "\n" | ||||||
|                       ++ (if average_ ropts'   then [overallaverage] else []) |             _     -> compoundBalanceReportAsText ropts' cbr | ||||||
|                       where |  | ||||||
|                         overallaverage =  |  | ||||||
|                           overalltotal `divideMixedAmount` fromIntegral (length overalltotals) -- depends on non-null overalltotals |  | ||||||
|         putStrLn $ title ++ "\n" |  | ||||||
|         putStr $ renderBalanceReportTable ropts' overalltable' |  | ||||||
| 
 | 
 | ||||||
|  | -- | Render a multi-column balance report as plain text suitable for console output. | ||||||
| -- Add the second table below the first, discarding its column headings. | -- Add the second table below the first, discarding its column headings. | ||||||
| concatTables (Table hLeft hTop dat) (Table hLeft' _ dat') = | concatTables (Table hLeft hTop dat) (Table hLeft' _ dat') = | ||||||
|     Table (T.Group DoubleLine [hLeft, hLeft']) hTop (dat ++ dat') |     Table (T.Group DoubleLine [hLeft, hLeft']) hTop (dat ++ dat') | ||||||
| @ -197,37 +202,141 @@ compoundBalanceCommandSingleColumnReport ropts userq j subreporttitle subreportq | |||||||
|       | otherwise                                                       = balanceReport       ropts q j |       | otherwise                                                       = balanceReport       ropts q j | ||||||
|     subreportstr = intercalate "\n" [subreporttitle <> ":", balanceReportAsText ropts r] |     subreportstr = intercalate "\n" [subreporttitle <> ":", balanceReportAsText ropts r] | ||||||
| 
 | 
 | ||||||
|  | -- | A compound balance report has: | ||||||
|  | -- | ||||||
|  | -- * an overall title | ||||||
|  | -- | ||||||
|  | -- * one or more named multi balance reports, with the same column headings | ||||||
|  | -- | ||||||
|  | -- * a list of overall totals for each column, and their grand total and average | ||||||
|  | -- | ||||||
|  | -- It is used in compound balance report commands like balancesheet,  | ||||||
|  | -- cashflow and incomestatement. | ||||||
|  | type CompoundBalanceReport =  | ||||||
|  |   ( String | ||||||
|  |   , [(String, MultiBalanceReport)] | ||||||
|  |   , ([MixedAmount], MixedAmount, MixedAmount) | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
| -- | Run one subreport for a compound balance command in multi-column mode. | -- | Run one subreport for a compound balance command in multi-column mode. | ||||||
| -- Currently this returns a table of rendered balance amounts, including the | -- This returns a MultiBalanceReport. | ||||||
| -- totals row; the totals row again, as mixedamounts; and the grand total. | compoundBalanceSubreport :: ReportOpts -> Query -> Journal -> (Journal -> Query) -> MultiBalanceReport | ||||||
| -- The first two are wrapped in a list and the third in a Sum, for easy | compoundBalanceSubreport ropts userq j subreportqfn = r' | ||||||
| -- monoidal combining. |  | ||||||
| compoundBalanceCommandMultiColumnReport |  | ||||||
|     :: ReportOpts |  | ||||||
|     -> Query |  | ||||||
|     -> Journal |  | ||||||
|     -> String |  | ||||||
|     -> (Journal -> Query) |  | ||||||
|     -> ([Table String String MixedAmount], [[MixedAmount]], Sum MixedAmount) |  | ||||||
| compoundBalanceCommandMultiColumnReport ropts userq j subreporttitle subreportqfn = |  | ||||||
|   ([tabl], [coltotals], Sum tot) |  | ||||||
|   where |   where | ||||||
|     -- disable totals row if there's just one section and --no-total |     -- force --empty to ensure same columns in all sections | ||||||
|     -- force --empty to ensure same columns in all sections, or something |     ropts' = ropts { empty_ = True } | ||||||
|     ropts' = ropts { no_total_ = singlesection && no_total_ ropts, empty_ = True } |  | ||||||
|       where |  | ||||||
|         singlesection = "Cash" `isPrefixOf` subreporttitle -- TODO temp |  | ||||||
|     -- run the report |     -- run the report | ||||||
|     q = And [subreportqfn j, userq] |     q = And [subreportqfn j, userq] | ||||||
|     MultiBalanceReport (dates, rows, (coltotals,tot,avg)) = multiBalanceReport ropts' q j |     r@(MultiBalanceReport (dates, rows, totals)) = multiBalanceReport ropts' q j | ||||||
|     -- if user didn't specify --empty, now remove the all-zero rows |     -- if user didn't specify --empty, now remove the all-zero rows | ||||||
|     r = MultiBalanceReport (dates, rows', (coltotals, tot, avg)) |     r' | empty_ ropts = r | ||||||
|       where |        | otherwise    = MultiBalanceReport (dates, rows', totals)  | ||||||
|         rows' | empty_ ropts = rows |  | ||||||
|               | otherwise    = filter (not . emptyRow) rows |  | ||||||
|           where |           where | ||||||
|             emptyRow (_,_,_,amts,_,_) = all isZeroMixedAmount amts |             rows' = filter (not . emptyRow) rows | ||||||
|     -- convert to a table for rendering |               where | ||||||
|     Table lefthdrs tophdrs cells = balanceReportAsTable ropts' r |                 emptyRow (_,_,_,amts,_,_) = all isZeroMixedAmount amts | ||||||
|     -- tweak the table layout | 
 | ||||||
|     tabl = Table (T.Group SingleLine [Header subreporttitle, lefthdrs]) tophdrs ([]:cells) | -- | Render a compound balance report as plain text suitable for console output. | ||||||
|  | {- Eg: | ||||||
|  | Balance Sheet | ||||||
|  | 
 | ||||||
|  |              ||  2017/12/31    Total  Average  | ||||||
|  | =============++=============================== | ||||||
|  |  Assets      ||                                | ||||||
|  | -------------++------------------------------- | ||||||
|  |  assets:b    ||           1        1        1  | ||||||
|  | -------------++------------------------------- | ||||||
|  |              ||           1        1        1  | ||||||
|  | =============++=============================== | ||||||
|  |  Liabilities ||                                | ||||||
|  | -------------++------------------------------- | ||||||
|  | -------------++------------------------------- | ||||||
|  |              ||                                | ||||||
|  | =============++=============================== | ||||||
|  |  Total       ||           1        1        1  | ||||||
|  | 
 | ||||||
|  | -} | ||||||
|  | compoundBalanceReportAsText :: ReportOpts -> CompoundBalanceReport -> String | ||||||
|  | compoundBalanceReportAsText ropts (title, subreports, (coltotals, grandtotal, grandavg)) = | ||||||
|  |   title ++ "\n\n" ++  | ||||||
|  |   renderBalanceReportTable ropts bigtable' | ||||||
|  |   where | ||||||
|  |     singlesubreport = length subreports == 1 | ||||||
|  |     bigtable =  | ||||||
|  |       case map (subreportAsTable ropts singlesubreport) subreports of | ||||||
|  |         []   -> T.empty | ||||||
|  |         r:rs -> foldl' concatTables r rs | ||||||
|  |     bigtable' | ||||||
|  |       | no_total_ ropts || singlesubreport =  | ||||||
|  |           bigtable | ||||||
|  |       | otherwise = | ||||||
|  |           bigtable | ||||||
|  |           +====+ | ||||||
|  |           row "Total" ( | ||||||
|  |             coltotals | ||||||
|  |             ++ (if row_total_ ropts then [grandtotal] else []) | ||||||
|  |             ++ (if average_ ropts   then [grandavg]   else []) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     -- | Convert a named multi balance report to a table suitable for | ||||||
|  |     -- concatenating with others to make a compound balance report table. | ||||||
|  |     subreportAsTable ropts singlesubreport (title, r) = t | ||||||
|  |       where | ||||||
|  |         -- unless there's only one section, always show the subtotal row | ||||||
|  |         ropts' | singlesubreport = ropts | ||||||
|  |                | otherwise       = ropts{ no_total_=False } | ||||||
|  |         -- convert to table | ||||||
|  |         Table lefthdrs tophdrs cells = balanceReportAsTable ropts' r | ||||||
|  |         -- tweak the layout | ||||||
|  |         t = Table (T.Group SingleLine [Header title, lefthdrs]) tophdrs ([]:cells) | ||||||
|  | 
 | ||||||
|  | -- | Render a compound balance report as CSV. | ||||||
|  | {- Eg:  | ||||||
|  | ghci> :main -f examples/sample.journal bs -Y -O csv -AT | ||||||
|  | "Balance Sheet","","","","","" | ||||||
|  | "Assets","","","","","" | ||||||
|  | "account","short account","indent","2008","total","average" | ||||||
|  | "assets:bank:saving","saving","3","$1","$1","$1" | ||||||
|  | "assets:cash","cash","2","$-2","$-2","$-2" | ||||||
|  | "totals","","","$-1","$-1","$-1" | ||||||
|  | "Liabilities","","","","","" | ||||||
|  | "account","short account","indent","2008","total","average" | ||||||
|  | "liabilities:debts","debts","2","$1","$1","$1" | ||||||
|  | "totals","","","$1","$1","$1" | ||||||
|  | "Total","0","0","0" | ||||||
|  | -} | ||||||
|  | compoundBalanceReportAsCsv :: ReportOpts -> CompoundBalanceReport -> CSV | ||||||
|  | compoundBalanceReportAsCsv ropts (title, subreports, (coltotals, grandtotal, grandavg)) = | ||||||
|  |   addtotals $ | ||||||
|  |   padRow title : | ||||||
|  |   concatMap (subreportAsCsv ropts singlesubreport) subreports | ||||||
|  |   where | ||||||
|  |     singlesubreport = length subreports == 1 | ||||||
|  |     subreportAsCsv ropts singlesubreport (subreporttitle, multibalreport) = | ||||||
|  |       padRow subreporttitle : | ||||||
|  |       multiBalanceReportAsCsv ropts' multibalreport | ||||||
|  |       where | ||||||
|  |         -- unless there's only one section, always show the subtotal row | ||||||
|  |         ropts' | singlesubreport = ropts | ||||||
|  |                | otherwise       = ropts{ no_total_=False } | ||||||
|  |     padRow s = take numcols $ s : repeat "" | ||||||
|  |       where | ||||||
|  |         numcols | ||||||
|  |           | null subreports = 1 | ||||||
|  |           | otherwise = | ||||||
|  |             (3 +) $ -- account name & indent columns | ||||||
|  |             (if row_total_ ropts then (1+) else id) $ | ||||||
|  |             (if average_ ropts then (1+) else id) $ | ||||||
|  |             maximum $ -- depends on non-null subreports | ||||||
|  |             map (\(MultiBalanceReport (amtcolheadings, _, _)) -> length amtcolheadings) $  | ||||||
|  |             map snd subreports | ||||||
|  |     addtotals | ||||||
|  |       | no_total_ ropts || length subreports == 1 = id | ||||||
|  |       | otherwise = (++  | ||||||
|  |           ["Total" : | ||||||
|  |            map showMixedAmountOneLineWithoutPrice ( | ||||||
|  |              coltotals | ||||||
|  |              ++ (if row_total_ ropts then [grandtotal] else []) | ||||||
|  |              ++ (if average_ ropts   then [grandavg]   else []) | ||||||
|  |              ) | ||||||
|  |           ]) | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user