module Hledger.Cli.CompoundBalanceCommand (
CompoundBalanceCommandSpec(..)
,compoundBalanceCommandMode
,compoundBalanceCommand
) where
import Data.List (intercalate, foldl')
import Data.Maybe (fromMaybe)
import Data.Monoid (Sum(..), (<>))
import Data.Tuple.HT (uncurry3)
import System.Console.CmdArgs.Explicit as C
import Text.CSV
import Text.Tabular as T
import Hledger
import Hledger.Cli.Commands.Balance
import Hledger.Cli.CliOptions
import Hledger.Cli.Utils (writeOutput)
data CompoundBalanceCommandSpec = CompoundBalanceCommandSpec {
cbcname :: String,
cbcaliases :: [String],
cbchelp :: String,
cbctitle :: String,
cbcqueries :: [(String, Journal -> Query, Maybe NormalBalance)],
cbctype :: BalanceType
}
compoundBalanceCommandMode :: CompoundBalanceCommandSpec -> Mode RawOpts
compoundBalanceCommandMode CompoundBalanceCommandSpec{..} = (defCommandMode $ cbcname : cbcaliases) {
modeHelp = cbchelp `withAliases` cbcaliases
,modeGroupFlags = C.Group {
groupUnnamed = [
flagNone ["change"] (\opts -> setboolopt "change" opts)
("show balance change in each period" ++ defType PeriodChange)
,flagNone ["cumulative"] (\opts -> setboolopt "cumulative" opts)
("show balance change accumulated across periods (in multicolumn reports)"
++ defType CumulativeChange
)
,flagNone ["historical","H"] (\opts -> setboolopt "historical" opts)
("show historical ending balance in each period (includes postings before report start date)"
++ defType HistoricalBalance
)
,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show accounts as a list"
,flagReq ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"
,flagNone ["no-total","N"] (\opts -> setboolopt "no-total" opts) "omit the final total row"
,flagNone ["tree"] (\opts -> setboolopt "tree" opts) "show accounts as a tree; amounts include subaccounts (default in simple reports)"
,flagNone ["average","A"] (\opts -> setboolopt "average" opts) "show a row average column (in multicolumn reports)"
,flagNone ["row-total","T"] (\opts -> setboolopt "row-total" opts) "show a row total column (in multicolumn reports)"
,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)"
,flagNone ["pretty-tables"] (\opts -> setboolopt "pretty-tables" opts) "use unicode when displaying tables"
,flagNone ["sort-amount","S"] (\opts -> setboolopt "sort-amount" opts) "sort by amount instead of account name"
,outputFormatFlag
,outputFileFlag
]
,groupHidden = []
,groupNamed = [generalflagsgroup1]
}
}
where
defType :: BalanceType -> String
defType bt | bt == cbctype = " (default)"
| otherwise = ""
compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> IO ())
compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{command_=cmd, reportopts_=ropts, rawopts_=rawopts} j = do
d <- getCurrentDay
let
mBalanceTypeOverride =
case reverse $ filter (`elem` ["change","cumulative","historical"]) $ map fst rawopts of
"historical":_ -> Just HistoricalBalance
"cumulative":_ -> Just CumulativeChange
"change":_ -> Just PeriodChange
_ -> Nothing
balancetype = fromMaybe cbctype mBalanceTypeOverride
title = cbctitle ++ maybe "" (' ':) mtitleclarification
where
mtitleclarification = flip fmap mBalanceTypeOverride $ \t ->
case t of
PeriodChange -> "(Balance Changes)"
CumulativeChange -> "(Cumulative Ending Balances)"
HistoricalBalance -> "(Historical Ending Balances)"
ropts'
| not (flat_ ropts) &&
interval_ ropts==NoInterval &&
balancetype `elem` [CumulativeChange, HistoricalBalance]
= ropts{balancetype_=balancetype, accountlistmode_=ALTree}
| otherwise
= ropts{balancetype_=balancetype}
userq = queryFromOpts d ropts'
format = outputFormatFromOpts opts
case interval_ ropts' of
NoInterval -> do
let
(subreportstr, total) =
foldMap (uncurry3 (compoundBalanceCommandSingleColumnReport ropts' userq j)) cbcqueries
writeOutput opts $ unlines $
[title ++ "\n"] ++
subreportstr ++
if (no_total_ ropts' || cmd=="cashflow")
then []
else
[ "Total:"
, "--------------------"
, padLeftWide 20 $ showamt (getSum total)
, ""
]
where
showamt | color_ ropts' = cshowMixedAmountWithoutPrice
| otherwise = showMixedAmountWithoutPrice
_ -> do
let
namedsubreports =
map (\(subreporttitle, subreportq, subreportnormalsign) ->
(subreporttitle, compoundBalanceSubreport ropts' userq j subreportq subreportnormalsign))
cbcqueries
subtotalrows = [coltotals | MultiBalanceReport (_,_,(coltotals,_,_)) <- map snd namedsubreports]
overalltotals = case subtotalrows of
[] -> ([], nullmixedamt, nullmixedamt)
rs ->
let
numcols = maximum $ map length rs
zeros = replicate numcols nullmixedamt
rs' = [take numcols $ as ++ repeat nullmixedamt | as <- rs]
coltotals = foldl' (zipWith (+)) zeros rs'
grandtotal = sum coltotals
grandavg | null coltotals = nullmixedamt
| otherwise = grandtotal `divideMixedAmount` fromIntegral (length coltotals)
in
(coltotals, grandtotal, grandavg)
cbr =
(title
,namedsubreports
,overalltotals
)
writeOutput opts $
case format of
"csv" -> printCSV (compoundBalanceReportAsCsv ropts cbr) ++ "\n"
_ -> compoundBalanceReportAsText ropts' cbr
compoundBalanceCommandSingleColumnReport
:: ReportOpts
-> Query
-> Journal
-> String
-> (Journal -> Query)
-> Maybe NormalBalance
-> ([String], Sum MixedAmount)
compoundBalanceCommandSingleColumnReport ropts userq j subreporttitle subreportqfn subreportnormalsign =
([subreportstr], Sum total)
where
q = And [subreportqfn j, userq]
ropts' = ropts{normalbalance_=subreportnormalsign}
r@(_,total)
| balancetype_ ropts `elem` [CumulativeChange, HistoricalBalance] = singleBalanceReport ropts' q j
| otherwise = balanceReport ropts' q j
subreportstr = intercalate "\n" [subreporttitle <> ":", balanceReportAsText ropts r]
type CompoundBalanceReport =
( String
, [(String, MultiBalanceReport)]
, ([MixedAmount], MixedAmount, MixedAmount)
)
compoundBalanceSubreport :: ReportOpts -> Query -> Journal -> (Journal -> Query) -> Maybe NormalBalance -> MultiBalanceReport
compoundBalanceSubreport ropts userq j subreportqfn subreportnormalsign = r'
where
ropts' = ropts { empty_=True, normalbalance_=subreportnormalsign }
q = And [subreportqfn j, userq]
r@(MultiBalanceReport (dates, rows, totals)) = multiBalanceReport ropts' q j
r' | empty_ ropts = r
| otherwise = MultiBalanceReport (dates, rows', totals)
where
rows' = filter (not . emptyRow) rows
where
emptyRow (_,_,_,amts,_,_) = all isZeroMixedAmount amts
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 [])
)
subreportAsTable ropts singlesubreport (title, r) = t
where
ropts' | singlesubreport = ropts
| otherwise = ropts{ no_total_=False }
Table lefthdrs tophdrs cells = balanceReportAsTable ropts' r
t = Table (T.Group SingleLine [Header title, lefthdrs]) tophdrs ([]:cells)
concatTables (Table hLeft hTop dat) (Table hLeft' _ dat') =
Table (T.Group DoubleLine [hLeft, hLeft']) hTop (dat ++ dat')
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
ropts' | singlesubreport = ropts
| otherwise = ropts{ no_total_=False }
padRow s = take numcols $ s : repeat ""
where
numcols
| null subreports = 1
| otherwise =
(3 +) $
(if row_total_ ropts then (1+) else id) $
(if average_ ropts then (1+) else id) $
maximum $
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 [])
)
])