{-# LANGUAGE OverloadedStrings, RecordWildCards, LambdaCase #-}
{-|

Common helpers for making multi-section balance report commands
like balancesheet, cashflow, and incomestatement.

-}

module Hledger.Cli.CompoundBalanceCommand (
  CompoundBalanceCommandSpec(..)
 ,CBCSubreportSpec(..)
 ,compoundBalanceCommandMode
 ,compoundBalanceCommand
) where

import Data.List (foldl')
import Data.Maybe
import qualified Data.Text as TS
import qualified Data.Text.Lazy as TL
import Data.Time.Calendar
import Data.Time.Format
import System.Console.CmdArgs.Explicit as C
import Hledger.Read.CsvReader (CSV, printCSV)
import Lucid as L hiding (value_)
import Text.Tabular as T

import Hledger
import Hledger.Cli.Commands.Balance
import Hledger.Cli.CliOptions
import Hledger.Cli.Utils (writeOutput)

-- | Description of a compound balance report command,
-- from which we generate the command's cmdargs mode and IO action.
-- A compound balance report command shows one or more sections/subreports,
-- each with its own title and subtotals row, in a certain order,
-- plus a grand totals row if there's more than one section.
-- Examples are the balancesheet, cashflow and incomestatement commands.
--
-- Compound balance reports do sign normalisation: they show all account balances
-- as normally positive, unlike the ordinary BalanceReport and most hledger commands
-- which show income/liability/equity balances as normally negative.
-- Each subreport specifies the normal sign of its amounts, and whether
-- it should be added to or subtracted from the grand total.
--
data CompoundBalanceCommandSpec = CompoundBalanceCommandSpec {
  CompoundBalanceCommandSpec -> CommandDoc
cbcdoc      :: CommandDoc,          -- ^ the command's name(s) and documentation
  CompoundBalanceCommandSpec -> CommandDoc
cbctitle    :: String,              -- ^ overall report title
  CompoundBalanceCommandSpec -> [CBCSubreportSpec]
cbcqueries  :: [CBCSubreportSpec],  -- ^ subreport details
  CompoundBalanceCommandSpec -> BalanceType
cbctype     :: BalanceType          -- ^ the "balance" type (change, cumulative, historical)
                                      --   this report shows (overrides command line flags)
}

-- | Description of one subreport within a compound balance report.
data CBCSubreportSpec = CBCSubreportSpec {
   CBCSubreportSpec -> CommandDoc
cbcsubreporttitle :: String
  ,CBCSubreportSpec -> Journal -> Query
cbcsubreportquery :: Journal -> Query
  ,CBCSubreportSpec -> NormalSign
cbcsubreportnormalsign :: NormalSign
  ,CBCSubreportSpec -> Bool
cbcsubreportincreasestotal :: Bool
}

-- | A compound balance report has:
--
-- * an overall title
--
-- * the period (date span) of each column
--
-- * one or more named, normal-positive multi balance reports,
--   with columns corresponding to the above, and a flag indicating
--   whether they increased or decreased the overall totals
--
-- * 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
  , [DateSpan]
  , [(String, MultiBalanceReport, Bool)]
  , ([MixedAmount], MixedAmount, MixedAmount)
  )


-- | Generate a cmdargs option-parsing mode from a compound balance command
-- specification.
compoundBalanceCommandMode :: CompoundBalanceCommandSpec -> Mode RawOpts
compoundBalanceCommandMode :: CompoundBalanceCommandSpec -> Mode RawOpts
compoundBalanceCommandMode CompoundBalanceCommandSpec{..} =
  CommandDoc
-> [Flag RawOpts]
-> [(CommandDoc, [Flag RawOpts])]
-> [Flag RawOpts]
-> ([Arg RawOpts], Maybe (Arg RawOpts))
-> Mode RawOpts
hledgerCommandMode
    CommandDoc
cbcdoc
    [[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["change"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "change")
       ("show balance change in each period" CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ BalanceType -> CommandDoc
defType BalanceType
PeriodChange)
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["cumulative"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "cumulative")
       ("show balance change accumulated across periods (in multicolumn reports)"
           CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ BalanceType -> CommandDoc
defType BalanceType
CumulativeChange
       )
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["historical","H"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "historical")
       ("show historical ending balance in each period (includes postings before report start date)"
           CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ BalanceType -> CommandDoc
defType BalanceType
HistoricalBalance
       )
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["flat"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "flat") "show accounts as a list"
    ,[CommandDoc]
-> Update RawOpts -> CommandDoc -> CommandDoc -> Flag RawOpts
forall a.
[CommandDoc] -> Update a -> CommandDoc -> CommandDoc -> Flag a
flagReq  ["drop"] (\s :: CommandDoc
s opts :: RawOpts
opts -> RawOpts -> Either CommandDoc RawOpts
forall a b. b -> Either a b
Right (RawOpts -> Either CommandDoc RawOpts)
-> RawOpts -> Either CommandDoc RawOpts
forall a b. (a -> b) -> a -> b
$ CommandDoc -> CommandDoc -> RawOpts -> RawOpts
setopt "drop" CommandDoc
s RawOpts
opts) "N" "flat mode: omit N leading account name parts"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["no-total","N"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "no-total") "omit the final total row"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["tree"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "tree") "show accounts as a tree; amounts include subaccounts (default in simple reports)"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["average","A"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "average") "show a row average column (in multicolumn reports)"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["row-total","T"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "row-total") "show a row total column (in multicolumn reports)"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["no-elide"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "no-elide") "don't squash boring parent accounts (in tree mode)"
    ,[CommandDoc]
-> Update RawOpts -> CommandDoc -> CommandDoc -> Flag RawOpts
forall a.
[CommandDoc] -> Update a -> CommandDoc -> CommandDoc -> Flag a
flagReq  ["format"] (\s :: CommandDoc
s opts :: RawOpts
opts -> RawOpts -> Either CommandDoc RawOpts
forall a b. b -> Either a b
Right (RawOpts -> Either CommandDoc RawOpts)
-> RawOpts -> Either CommandDoc RawOpts
forall a b. (a -> b) -> a -> b
$ CommandDoc -> CommandDoc -> RawOpts -> RawOpts
setopt "format" CommandDoc
s RawOpts
opts) "FORMATSTR" "use this custom line format (in simple reports)"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["pretty-tables"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "pretty-tables") "use unicode when displaying tables"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["sort-amount","S"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "sort-amount") "sort by amount instead of account code/name"
    ,[CommandDoc] -> (RawOpts -> RawOpts) -> CommandDoc -> Flag RawOpts
forall a. [CommandDoc] -> (a -> a) -> CommandDoc -> Flag a
flagNone ["percent", "%"] (CommandDoc -> RawOpts -> RawOpts
setboolopt "percent") "express values in percentage of each column's total"
    ,Flag RawOpts
outputFormatFlag
    ,Flag RawOpts
outputFileFlag
    ]
    [(CommandDoc, [Flag RawOpts])
generalflagsgroup1]
    [Flag RawOpts]
hiddenflags
    ([], Arg RawOpts -> Maybe (Arg RawOpts)
forall a. a -> Maybe a
Just (Arg RawOpts -> Maybe (Arg RawOpts))
-> Arg RawOpts -> Maybe (Arg RawOpts)
forall a b. (a -> b) -> a -> b
$ CommandDoc -> Arg RawOpts
argsFlag "[QUERY]")
 where
   defType :: BalanceType -> String
   defType :: BalanceType -> CommandDoc
defType bt :: BalanceType
bt | BalanceType
bt BalanceType -> BalanceType -> Bool
forall a. Eq a => a -> a -> Bool
== BalanceType
cbctype = " (default)"
              | Bool
otherwise    = ""

-- | Generate a runnable command from a compound balance command specification.
compoundBalanceCommand :: CompoundBalanceCommandSpec -> (CliOpts -> Journal -> IO ())
compoundBalanceCommand :: CompoundBalanceCommandSpec -> CliOpts -> Journal -> IO ()
compoundBalanceCommand CompoundBalanceCommandSpec{..} opts :: CliOpts
opts@CliOpts{reportopts_ :: CliOpts -> ReportOpts
reportopts_=ropts :: ReportOpts
ropts@ReportOpts{..}, rawopts_ :: CliOpts -> RawOpts
rawopts_=RawOpts
rawopts} j :: Journal
j = do
    Day
d <- IO Day
getCurrentDay
    let
      -- use the default balance type for this report, unless the user overrides
      mBalanceTypeOverride :: Maybe BalanceType
mBalanceTypeOverride =
        (CommandDoc -> Maybe BalanceType) -> RawOpts -> Maybe BalanceType
forall a. (CommandDoc -> Maybe a) -> RawOpts -> Maybe a
choiceopt CommandDoc -> Maybe BalanceType
parse RawOpts
rawopts where
          parse :: CommandDoc -> Maybe BalanceType
parse = \case
            "historical" -> BalanceType -> Maybe BalanceType
forall a. a -> Maybe a
Just BalanceType
HistoricalBalance
            "cumulative" -> BalanceType -> Maybe BalanceType
forall a. a -> Maybe a
Just BalanceType
CumulativeChange
            "change"     -> BalanceType -> Maybe BalanceType
forall a. a -> Maybe a
Just BalanceType
PeriodChange
            _            -> Maybe BalanceType
forall a. Maybe a
Nothing
      balancetype :: BalanceType
balancetype = BalanceType -> Maybe BalanceType -> BalanceType
forall a. a -> Maybe a -> a
fromMaybe BalanceType
cbctype Maybe BalanceType
mBalanceTypeOverride
      -- Set balance type in the report options.
      -- Also, use tree mode (by default, at least?) if --cumulative/--historical
      -- are used in single column mode, since in that situation we will be using
      -- balanceReportFromMultiBalanceReport which does not support eliding boring parents,
      -- and tree mode hides this.. or something.. XXX
      ropts' :: ReportOpts
ropts' = ReportOpts
ropts{
        balancetype_ :: BalanceType
balancetype_=BalanceType
balancetype,
        accountlistmode_ :: AccountListMode
accountlistmode_=if Bool -> Bool
not (ReportOpts -> Bool
flat_ ReportOpts
ropts) Bool -> Bool -> Bool
&& Interval
interval_Interval -> Interval -> Bool
forall a. Eq a => a -> a -> Bool
==Interval
NoInterval Bool -> Bool -> Bool
&& BalanceType
balancetype BalanceType -> [BalanceType] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [BalanceType
CumulativeChange, BalanceType
HistoricalBalance] then AccountListMode
ALTree else AccountListMode
accountlistmode_,
        no_total_ :: Bool
no_total_=if Bool
percent_ Bool -> Bool -> Bool
&& [CBCSubreportSpec] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [CBCSubreportSpec]
cbcqueries Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> 1 then Bool
True else Bool
no_total_
      }
      userq :: Query
userq = Day -> ReportOpts -> Query
queryFromOpts Day
d ReportOpts
ropts'
      format :: CommandDoc
format = CliOpts -> CommandDoc
outputFormatFromOpts CliOpts
opts

      -- make a CompoundBalanceReport.
      -- For efficiency, generate a price oracle here and reuse it with each subreport.
      priceoracle :: PriceOracle
priceoracle = Journal -> PriceOracle
journalPriceOracle Journal
j
      subreports :: [(CommandDoc, MultiBalanceReport, Bool)]
subreports =
        (CBCSubreportSpec -> (CommandDoc, MultiBalanceReport, Bool))
-> [CBCSubreportSpec] -> [(CommandDoc, MultiBalanceReport, Bool)]
forall a b. (a -> b) -> [a] -> [b]
map (\CBCSubreportSpec{..} ->
                (CommandDoc
cbcsubreporttitle
                ,NormalSign -> MultiBalanceReport -> MultiBalanceReport
mbrNormaliseSign NormalSign
cbcsubreportnormalsign (MultiBalanceReport -> MultiBalanceReport)
-> MultiBalanceReport -> MultiBalanceReport
forall a b. (a -> b) -> a -> b
$ -- <- convert normal-negative to normal-positive
                  ReportOpts
-> Query
-> Journal
-> PriceOracle
-> (Journal -> Query)
-> NormalSign
-> MultiBalanceReport
compoundBalanceSubreport ReportOpts
ropts' Query
userq Journal
j PriceOracle
priceoracle Journal -> Query
cbcsubreportquery NormalSign
cbcsubreportnormalsign
                ,Bool
cbcsubreportincreasestotal
                ))
            [CBCSubreportSpec]
cbcqueries

      subtotalrows :: [([MixedAmount], Bool)]
subtotalrows =
        [([MixedAmount]
coltotals, Bool
increasesoveralltotal)
        | (_, MultiBalanceReport (_,_,(coltotals :: [MixedAmount]
coltotals,_,_)), increasesoveralltotal :: Bool
increasesoveralltotal) <- [(CommandDoc, MultiBalanceReport, Bool)]
subreports
        ]

      -- Sum the subreport totals by column. Handle these cases:
      -- - no subreports
      -- - empty subreports, having no subtotals (#588)
      -- - subreports with a shorter subtotals row than the others
      overalltotals :: ([MixedAmount], MixedAmount, MixedAmount)
overalltotals = case [([MixedAmount], Bool)]
subtotalrows of
        [] -> ([], MixedAmount
nullmixedamt, MixedAmount
nullmixedamt)
        rs :: [([MixedAmount], Bool)]
rs ->
          let
            numcols :: Int
numcols = [Int] -> Int
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
maximum ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (([MixedAmount], Bool) -> Int) -> [([MixedAmount], Bool)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([MixedAmount] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length([MixedAmount] -> Int)
-> (([MixedAmount], Bool) -> [MixedAmount])
-> ([MixedAmount], Bool)
-> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
.([MixedAmount], Bool) -> [MixedAmount]
forall a b. (a, b) -> a
fst) [([MixedAmount], Bool)]
rs  -- partial maximum is ok, rs is non-null
            paddedsignedsubtotalrows :: [[MixedAmount]]
paddedsignedsubtotalrows =
              [(MixedAmount -> MixedAmount) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (if Bool
increasesoveralltotal then MixedAmount -> MixedAmount
forall a. a -> a
id else MixedAmount -> MixedAmount
forall a. Num a => a -> a
negate) ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$  -- maybe flip the signs
               Int -> [MixedAmount] -> [MixedAmount]
forall a. Int -> [a] -> [a]
take Int
numcols ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ [MixedAmount]
as [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a. [a] -> [a] -> [a]
++ MixedAmount -> [MixedAmount]
forall a. a -> [a]
repeat MixedAmount
nullmixedamt              -- pad short rows with zeros
              | (as :: [MixedAmount]
as,increasesoveralltotal :: Bool
increasesoveralltotal) <- [([MixedAmount], Bool)]
rs
              ]
            coltotals :: [MixedAmount]
coltotals = ([MixedAmount] -> [MixedAmount] -> [MixedAmount])
-> [MixedAmount] -> [[MixedAmount]] -> [MixedAmount]
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' ((MixedAmount -> MixedAmount -> MixedAmount)
-> [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith MixedAmount -> MixedAmount -> MixedAmount
forall a. Num a => a -> a -> a
(+)) [MixedAmount]
zeros [[MixedAmount]]
paddedsignedsubtotalrows  -- sum the columns
              where zeros :: [MixedAmount]
zeros = Int -> MixedAmount -> [MixedAmount]
forall a. Int -> a -> [a]
replicate Int
numcols MixedAmount
nullmixedamt
            grandtotal :: MixedAmount
grandtotal = [MixedAmount] -> MixedAmount
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [MixedAmount]
coltotals
            grandavg :: MixedAmount
grandavg | [MixedAmount] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [MixedAmount]
coltotals = MixedAmount
nullmixedamt
                     | Bool
otherwise      = Int -> Quantity
forall a b. (Integral a, Num b) => a -> b
fromIntegral ([MixedAmount] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [MixedAmount]
coltotals) Quantity -> MixedAmount -> MixedAmount
`divideMixedAmount` MixedAmount
grandtotal
          in
            ([MixedAmount]
coltotals, MixedAmount
grandtotal, MixedAmount
grandavg)

      colspans :: [DateSpan]
colspans =
        case [(CommandDoc, MultiBalanceReport, Bool)]
subreports of
          (_, MultiBalanceReport (ds :: [DateSpan]
ds,_,_), _):_ -> [DateSpan]
ds
          [] -> []

      title :: CommandDoc
title =
        CommandDoc
cbctitle
        CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ " "
        CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ CommandDoc
titledatestr
        CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ CommandDoc
-> (CommandDoc -> CommandDoc) -> Maybe CommandDoc -> CommandDoc
forall b a. b -> (a -> b) -> Maybe a -> b
maybe "" (' 'Char -> CommandDoc -> CommandDoc
forall a. a -> [a] -> [a]
:) Maybe CommandDoc
mtitleclarification
        CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ CommandDoc
valuationdesc
        where

          -- XXX #1078 the title of ending balance reports
          -- (HistoricalBalance) should mention the end date(s) shown as
          -- column heading(s) (not the date span of the transactions).
          -- Also the dates should not be simplified (it should show
          -- "2008/01/01-2008/12/31", not "2008").
          titledatestr :: CommandDoc
titledatestr
            | BalanceType
balancetype BalanceType -> BalanceType -> Bool
forall a. Eq a => a -> a -> Bool
== BalanceType
HistoricalBalance = [Day] -> CommandDoc
showEndDates [Day]
enddates
            | Bool
otherwise                        = DateSpan -> CommandDoc
showDateSpan DateSpan
requestedspan 
            where
              enddates :: [Day]
enddates = (Day -> Day) -> [Day] -> [Day]
forall a b. (a -> b) -> [a] -> [b]
map (Integer -> Day -> Day
addDays (-1)) ([Day] -> [Day]) -> [Day] -> [Day]
forall a b. (a -> b) -> a -> b
$ [Maybe Day] -> [Day]
forall a. [Maybe a] -> [a]
catMaybes ([Maybe Day] -> [Day]) -> [Maybe Day] -> [Day]
forall a b. (a -> b) -> a -> b
$ (DateSpan -> Maybe Day) -> [DateSpan] -> [Maybe Day]
forall a b. (a -> b) -> [a] -> [b]
map DateSpan -> Maybe Day
spanEnd [DateSpan]
colspans  -- these spans will always have a definite end date
              requestedspan :: DateSpan
requestedspan = Bool -> Query -> DateSpan
queryDateSpan Bool
date2_ Query
userq DateSpan -> DateSpan -> DateSpan
`spanDefaultsFrom` Bool -> Journal -> DateSpan
journalDateSpan Bool
date2_ Journal
j

          -- when user overrides, add an indication to the report title
          mtitleclarification :: Maybe CommandDoc
mtitleclarification = ((BalanceType -> CommandDoc)
 -> Maybe BalanceType -> Maybe CommandDoc)
-> Maybe BalanceType
-> (BalanceType -> CommandDoc)
-> Maybe CommandDoc
forall a b c. (a -> b -> c) -> b -> a -> c
flip (BalanceType -> CommandDoc)
-> Maybe BalanceType -> Maybe CommandDoc
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Maybe BalanceType
mBalanceTypeOverride ((BalanceType -> CommandDoc) -> Maybe CommandDoc)
-> (BalanceType -> CommandDoc) -> Maybe CommandDoc
forall a b. (a -> b) -> a -> b
$ \t :: BalanceType
t ->
            case BalanceType
t of
              PeriodChange      -> "(Balance Changes)"
              CumulativeChange  -> "(Cumulative Ending Balances)"
              HistoricalBalance -> "(Historical Ending Balances)"

          valuationdesc :: CommandDoc
valuationdesc = case Maybe ValuationType
value_ of
            Just (AtCost _mc :: Maybe CommoditySymbol
_mc)   -> ", valued at cost"
            Just (AtEnd _mc :: Maybe CommoditySymbol
_mc)    -> ", valued at period ends"
            Just (AtNow _mc :: Maybe CommoditySymbol
_mc)    -> ", current value"
            Just (AtDefault _mc :: Maybe CommoditySymbol
_mc) | Bool
multiperiod   -> ", valued at period ends"
            Just (AtDefault _mc :: Maybe CommoditySymbol
_mc)    -> ", current value"
            Just (AtDate d :: Day
d _mc :: Maybe CommoditySymbol
_mc) -> ", valued at "CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++Day -> CommandDoc
showDate Day
d
            Nothing             -> ""
            where
              multiperiod :: Bool
multiperiod = Interval
interval_ Interval -> Interval -> Bool
forall a. Eq a => a -> a -> Bool
/= Interval
NoInterval
      cbr :: (CommandDoc, [DateSpan], [(CommandDoc, MultiBalanceReport, Bool)],
 ([MixedAmount], MixedAmount, MixedAmount))
cbr =
        (CommandDoc
title
        ,[DateSpan]
colspans
        ,[(CommandDoc, MultiBalanceReport, Bool)]
subreports
        ,([MixedAmount], MixedAmount, MixedAmount)
overalltotals
        )

    -- render appropriately
    CliOpts -> CommandDoc -> IO ()
writeOutput CliOpts
opts (CommandDoc -> IO ()) -> CommandDoc -> IO ()
forall a b. (a -> b) -> a -> b
$
      case CommandDoc
format of
        "csv"  -> CSV -> CommandDoc
printCSV (ReportOpts
-> (CommandDoc, [DateSpan],
    [(CommandDoc, MultiBalanceReport, Bool)],
    ([MixedAmount], MixedAmount, MixedAmount))
-> CSV
compoundBalanceReportAsCsv ReportOpts
ropts (CommandDoc, [DateSpan], [(CommandDoc, MultiBalanceReport, Bool)],
 ([MixedAmount], MixedAmount, MixedAmount))
cbr) CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ "\n"
        "html" -> (CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ "\n") (CommandDoc -> CommandDoc) -> CommandDoc -> CommandDoc
forall a b. (a -> b) -> a -> b
$ Text -> CommandDoc
TL.unpack (Text -> CommandDoc) -> Text -> CommandDoc
forall a b. (a -> b) -> a -> b
$ Html () -> Text
forall a. Html a -> Text
L.renderText (Html () -> Text) -> Html () -> Text
forall a b. (a -> b) -> a -> b
$ ReportOpts
-> (CommandDoc, [DateSpan],
    [(CommandDoc, MultiBalanceReport, Bool)],
    ([MixedAmount], MixedAmount, MixedAmount))
-> Html ()
compoundBalanceReportAsHtml ReportOpts
ropts (CommandDoc, [DateSpan], [(CommandDoc, MultiBalanceReport, Bool)],
 ([MixedAmount], MixedAmount, MixedAmount))
cbr
        _      -> ReportOpts
-> (CommandDoc, [DateSpan],
    [(CommandDoc, MultiBalanceReport, Bool)],
    ([MixedAmount], MixedAmount, MixedAmount))
-> CommandDoc
compoundBalanceReportAsText ReportOpts
ropts' (CommandDoc, [DateSpan], [(CommandDoc, MultiBalanceReport, Bool)],
 ([MixedAmount], MixedAmount, MixedAmount))
cbr

-- | Summarise one or more (inclusive) end dates, in a way that's
-- visually different from showDateSpan, suggesting discrete end dates
-- rather than a continuous span.
showEndDates :: [Day] -> String
showEndDates :: [Day] -> CommandDoc
showEndDates es :: [Day]
es = case [Day]
es of
  -- cf showPeriod
  (e :: Day
e:_:_) -> Day -> CommandDoc
showdate Day
e CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ ",," CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ Day -> CommandDoc
showdate ([Day] -> Day
forall a. [a] -> a
last [Day]
es)
  [e :: Day
e]     -> Day -> CommandDoc
showdate Day
e
  []      -> ""
  where
    showdate :: Day -> CommandDoc
showdate = TimeLocale -> CommandDoc -> Day -> CommandDoc
forall t.
FormatTime t =>
TimeLocale -> CommandDoc -> t -> CommandDoc
formatTime TimeLocale
defaultTimeLocale "%0C%y/%m/%d" 

-- | Run one subreport for a compound balance command in multi-column mode.
-- This returns a MultiBalanceReport.
compoundBalanceSubreport :: ReportOpts -> Query -> Journal -> PriceOracle -> (Journal -> Query) -> NormalSign -> MultiBalanceReport
compoundBalanceSubreport :: ReportOpts
-> Query
-> Journal
-> PriceOracle
-> (Journal -> Query)
-> NormalSign
-> MultiBalanceReport
compoundBalanceSubreport ropts :: ReportOpts
ropts@ReportOpts{..} userq :: Query
userq j :: Journal
j priceoracle :: PriceOracle
priceoracle subreportqfn :: Journal -> Query
subreportqfn subreportnormalsign :: NormalSign
subreportnormalsign = MultiBalanceReport
r'
  where
    -- force --empty to ensure same columns in all sections
    ropts' :: ReportOpts
ropts' = ReportOpts
ropts { empty_ :: Bool
empty_=Bool
True, normalbalance_ :: Maybe NormalSign
normalbalance_=NormalSign -> Maybe NormalSign
forall a. a -> Maybe a
Just NormalSign
subreportnormalsign }
    -- run the report
    q :: Query
q = [Query] -> Query
And [Journal -> Query
subreportqfn Journal
j, Query
userq]
    r :: MultiBalanceReport
r@(MultiBalanceReport (dates :: [DateSpan]
dates, rows :: [MultiBalanceReportRow]
rows, totals :: ([MixedAmount], MixedAmount, MixedAmount)
totals)) = ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith ReportOpts
ropts' Query
q Journal
j PriceOracle
priceoracle
    -- if user didn't specify --empty, now remove the all-zero rows, unless they have non-zero subaccounts
    -- in this report
    r' :: MultiBalanceReport
r' | Bool
empty_    = MultiBalanceReport
r
       | Bool
otherwise = ([DateSpan], [MultiBalanceReportRow],
 ([MixedAmount], MixedAmount, MixedAmount))
-> MultiBalanceReport
MultiBalanceReport ([DateSpan]
dates, [MultiBalanceReportRow]
rows', ([MixedAmount], MixedAmount, MixedAmount)
totals)
          where
            nonzeroaccounts :: [CommoditySymbol]
nonzeroaccounts =
              CommandDoc -> [CommoditySymbol] -> [CommoditySymbol]
forall a. Show a => CommandDoc -> a -> a
dbg1 "nonzeroaccounts" ([CommoditySymbol] -> [CommoditySymbol])
-> [CommoditySymbol] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$
              [Maybe CommoditySymbol] -> [CommoditySymbol]
forall a. [Maybe a] -> [a]
catMaybes ([Maybe CommoditySymbol] -> [CommoditySymbol])
-> [Maybe CommoditySymbol] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$ (MultiBalanceReportRow -> Maybe CommoditySymbol)
-> [MultiBalanceReportRow] -> [Maybe CommoditySymbol]
forall a b. (a -> b) -> [a] -> [b]
map (\(act :: CommoditySymbol
act,_,_,amts :: [MixedAmount]
amts,_,_) ->
                            if Bool -> Bool
not ((MixedAmount -> Bool) -> [MixedAmount] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all MixedAmount -> Bool
isZeroMixedAmount [MixedAmount]
amts) then CommoditySymbol -> Maybe CommoditySymbol
forall a. a -> Maybe a
Just CommoditySymbol
act else Maybe CommoditySymbol
forall a. Maybe a
Nothing) [MultiBalanceReportRow]
rows
            rows' :: [MultiBalanceReportRow]
rows' = (MultiBalanceReportRow -> Bool)
-> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool)
-> (MultiBalanceReportRow -> Bool) -> MultiBalanceReportRow -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. MultiBalanceReportRow -> Bool
forall (t :: * -> *) b c e f.
Foldable t =>
(CommoditySymbol, b, c, t MixedAmount, e, f) -> Bool
emptyRow) [MultiBalanceReportRow]
rows
              where
                emptyRow :: (CommoditySymbol, b, c, t MixedAmount, e, f) -> Bool
emptyRow (act :: CommoditySymbol
act,_,_,amts :: t MixedAmount
amts,_,_) =
                  (MixedAmount -> Bool) -> t MixedAmount -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all MixedAmount -> Bool
isZeroMixedAmount t MixedAmount
amts Bool -> Bool -> Bool
&& (CommoditySymbol -> Bool) -> [CommoditySymbol] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (Bool -> Bool
not (Bool -> Bool)
-> (CommoditySymbol -> Bool) -> CommoditySymbol -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (CommoditySymbol
act CommoditySymbol -> CommoditySymbol -> Bool
`isAccountNamePrefixOf`)) [CommoditySymbol]
nonzeroaccounts

-- | 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 :: ReportOpts
-> (CommandDoc, [DateSpan],
    [(CommandDoc, MultiBalanceReport, Bool)],
    ([MixedAmount], MixedAmount, MixedAmount))
-> CommandDoc
compoundBalanceReportAsText ropts :: ReportOpts
ropts (title :: CommandDoc
title, _colspans :: [DateSpan]
_colspans, subreports :: [(CommandDoc, MultiBalanceReport, Bool)]
subreports, (coltotals :: [MixedAmount]
coltotals, grandtotal :: MixedAmount
grandtotal, grandavg :: MixedAmount
grandavg)) =
  CommandDoc
title CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++ "\n\n" CommandDoc -> CommandDoc -> CommandDoc
forall a. [a] -> [a] -> [a]
++
  ReportOpts -> Table CommandDoc CommandDoc MixedAmount -> CommandDoc
balanceReportTableAsText ReportOpts
ropts Table CommandDoc CommandDoc MixedAmount
bigtable'
  where
    singlesubreport :: Bool
singlesubreport = [(CommandDoc, MultiBalanceReport, Bool)] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(CommandDoc, MultiBalanceReport, Bool)]
subreports Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== 1
    bigtable :: Table CommandDoc CommandDoc MixedAmount
bigtable =
      case ((CommandDoc, MultiBalanceReport, Bool)
 -> Table CommandDoc CommandDoc MixedAmount)
-> [(CommandDoc, MultiBalanceReport, Bool)]
-> [Table CommandDoc CommandDoc MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (ReportOpts
-> Bool
-> (CommandDoc, MultiBalanceReport, Bool)
-> Table CommandDoc CommandDoc MixedAmount
forall c.
ReportOpts
-> Bool
-> (CommandDoc, MultiBalanceReport, c)
-> Table CommandDoc CommandDoc MixedAmount
subreportAsTable ReportOpts
ropts Bool
singlesubreport) [(CommandDoc, MultiBalanceReport, Bool)]
subreports of
        []   -> Table CommandDoc CommandDoc MixedAmount
forall rh ch a. Table rh ch a
T.empty
        r :: Table CommandDoc CommandDoc MixedAmount
r:rs :: [Table CommandDoc CommandDoc MixedAmount]
rs -> (Table CommandDoc CommandDoc MixedAmount
 -> Table CommandDoc CommandDoc MixedAmount
 -> Table CommandDoc CommandDoc MixedAmount)
-> Table CommandDoc CommandDoc MixedAmount
-> [Table CommandDoc CommandDoc MixedAmount]
-> Table CommandDoc CommandDoc MixedAmount
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' Table CommandDoc CommandDoc MixedAmount
-> Table CommandDoc CommandDoc MixedAmount
-> Table CommandDoc CommandDoc MixedAmount
forall rh ch a ch. Table rh ch a -> Table rh ch a -> Table rh ch a
concatTables Table CommandDoc CommandDoc MixedAmount
r [Table CommandDoc CommandDoc MixedAmount]
rs
    bigtable' :: Table CommandDoc CommandDoc MixedAmount
bigtable'
      | ReportOpts -> Bool
no_total_ ReportOpts
ropts Bool -> Bool -> Bool
|| Bool
singlesubreport =
          Table CommandDoc CommandDoc MixedAmount
bigtable
      | Bool
otherwise =
          Table CommandDoc CommandDoc MixedAmount
bigtable
          Table CommandDoc CommandDoc MixedAmount
-> SemiTable CommandDoc MixedAmount
-> Table CommandDoc CommandDoc MixedAmount
forall rh ch a. Table rh ch a -> SemiTable rh a -> Table rh ch a
+====+
          CommandDoc -> [MixedAmount] -> SemiTable CommandDoc MixedAmount
forall rh a. rh -> [a] -> SemiTable rh a
row "Net:" (
            [MixedAmount]
coltotals
            [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then [MixedAmount
grandtotal] else [])
            [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
average_ ReportOpts
ropts   then [MixedAmount
grandavg]   else [])
            )

    -- | Convert a named multi balance report to a table suitable for
    -- concatenating with others to make a compound balance report table.
    subreportAsTable :: ReportOpts
-> Bool
-> (CommandDoc, MultiBalanceReport, c)
-> Table CommandDoc CommandDoc MixedAmount
subreportAsTable ropts :: ReportOpts
ropts singlesubreport :: Bool
singlesubreport (title :: CommandDoc
title, r :: MultiBalanceReport
r, _) = Table CommandDoc CommandDoc MixedAmount
t
      where
        -- unless there's only one section, always show the subtotal row
        ropts' :: ReportOpts
ropts' | Bool
singlesubreport = ReportOpts
ropts
               | Bool
otherwise       = ReportOpts
ropts{ no_total_ :: Bool
no_total_=Bool
False }
        -- convert to table
        Table lefthdrs :: Header CommandDoc
lefthdrs tophdrs :: Header CommandDoc
tophdrs cells :: [[MixedAmount]]
cells = ReportOpts
-> MultiBalanceReport -> Table CommandDoc CommandDoc MixedAmount
balanceReportAsTable ReportOpts
ropts' MultiBalanceReport
r
        -- tweak the layout
        t :: Table CommandDoc CommandDoc MixedAmount
t = Header CommandDoc
-> Header CommandDoc
-> [[MixedAmount]]
-> Table CommandDoc CommandDoc MixedAmount
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table (Properties -> [Header CommandDoc] -> Header CommandDoc
forall h. Properties -> [Header h] -> Header h
T.Group Properties
SingleLine [CommandDoc -> Header CommandDoc
forall h. h -> Header h
Header CommandDoc
title, Header CommandDoc
lefthdrs]) Header CommandDoc
tophdrs ([][MixedAmount] -> [[MixedAmount]] -> [[MixedAmount]]
forall a. a -> [a] -> [a]
:[[MixedAmount]]
cells)

-- | Add the second table below the first, discarding its column headings.
concatTables :: Table rh ch a -> Table rh ch a -> Table rh ch a
concatTables (Table hLeft :: Header rh
hLeft hTop :: Header ch
hTop dat :: [[a]]
dat) (Table hLeft' :: Header rh
hLeft' _ dat' :: [[a]]
dat') =
    Header rh -> Header ch -> [[a]] -> Table rh ch a
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table (Properties -> [Header rh] -> Header rh
forall h. Properties -> [Header h] -> Header h
T.Group Properties
DoubleLine [Header rh
hLeft, Header rh
hLeft']) Header ch
hTop ([[a]]
dat [[a]] -> [[a]] -> [[a]]
forall a. [a] -> [a] -> [a]
++ [[a]]
dat')

-- | Render a compound balance report as CSV.
-- Subreports' CSV is concatenated, with the headings rows replaced by a
-- subreport title row, and an overall title row, one headings row, and an
-- optional overall totals row is added.
compoundBalanceReportAsCsv :: ReportOpts -> CompoundBalanceReport -> CSV
compoundBalanceReportAsCsv :: ReportOpts
-> (CommandDoc, [DateSpan],
    [(CommandDoc, MultiBalanceReport, Bool)],
    ([MixedAmount], MixedAmount, MixedAmount))
-> CSV
compoundBalanceReportAsCsv ropts :: ReportOpts
ropts (title :: CommandDoc
title, colspans :: [DateSpan]
colspans, subreports :: [(CommandDoc, MultiBalanceReport, Bool)]
subreports, (coltotals :: [MixedAmount]
coltotals, grandtotal :: MixedAmount
grandtotal, grandavg :: MixedAmount
grandavg)) =
  CSV -> CSV
addtotals (CSV -> CSV) -> CSV -> CSV
forall a b. (a -> b) -> a -> b
$
  CommandDoc -> [CommandDoc]
forall a. IsString a => a -> [a]
padRow CommandDoc
title [CommandDoc] -> CSV -> CSV
forall a. a -> [a] -> [a]
:
  ("Account" CommandDoc -> [CommandDoc] -> [CommandDoc]
forall a. a -> [a] -> [a]
:
   (DateSpan -> CommandDoc) -> [DateSpan] -> [CommandDoc]
forall a b. (a -> b) -> [a] -> [b]
map DateSpan -> CommandDoc
showDateSpanMonthAbbrev [DateSpan]
colspans
   [CommandDoc] -> [CommandDoc] -> [CommandDoc]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then ["Total"] else [])
   [CommandDoc] -> [CommandDoc] -> [CommandDoc]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
average_ ReportOpts
ropts then ["Average"] else [])
   ) [CommandDoc] -> CSV -> CSV
forall a. a -> [a] -> [a]
:
  ((CommandDoc, MultiBalanceReport, Bool) -> CSV)
-> [(CommandDoc, MultiBalanceReport, Bool)] -> CSV
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (ReportOpts -> Bool -> (CommandDoc, MultiBalanceReport, Bool) -> CSV
forall c.
ReportOpts -> Bool -> (CommandDoc, MultiBalanceReport, c) -> CSV
subreportAsCsv ReportOpts
ropts Bool
singlesubreport) [(CommandDoc, MultiBalanceReport, Bool)]
subreports
  where
    singlesubreport :: Bool
singlesubreport = [(CommandDoc, MultiBalanceReport, Bool)] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(CommandDoc, MultiBalanceReport, Bool)]
subreports Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== 1
    -- | Add a subreport title row and drop the heading row.
    subreportAsCsv :: ReportOpts -> Bool -> (CommandDoc, MultiBalanceReport, c) -> CSV
subreportAsCsv ropts :: ReportOpts
ropts singlesubreport :: Bool
singlesubreport (subreporttitle :: CommandDoc
subreporttitle, multibalreport :: MultiBalanceReport
multibalreport, _) =
      CommandDoc -> [CommandDoc]
forall a. IsString a => a -> [a]
padRow CommandDoc
subreporttitle [CommandDoc] -> CSV -> CSV
forall a. a -> [a] -> [a]
:
      CSV -> CSV
forall a. [a] -> [a]
tail (ReportOpts -> MultiBalanceReport -> CSV
multiBalanceReportAsCsv ReportOpts
ropts' MultiBalanceReport
multibalreport)
      where
        -- unless there's only one section, always show the subtotal row
        ropts' :: ReportOpts
ropts' | Bool
singlesubreport = ReportOpts
ropts
               | Bool
otherwise       = ReportOpts
ropts{ no_total_ :: Bool
no_total_=Bool
False }
    padRow :: a -> [a]
padRow s :: a
s = Int -> [a] -> [a]
forall a. Int -> [a] -> [a]
take Int
numcols ([a] -> [a]) -> [a] -> [a]
forall a b. (a -> b) -> a -> b
$ a
s a -> [a] -> [a]
forall a. a -> [a] -> [a]
: a -> [a]
forall a. a -> [a]
repeat ""
      where
        numcols :: Int
numcols
          | [(CommandDoc, MultiBalanceReport, Bool)] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(CommandDoc, MultiBalanceReport, Bool)]
subreports = 1
          | Bool
otherwise =
            (3 Int -> Int -> Int
forall a. Num a => a -> a -> a
+) (Int -> Int) -> Int -> Int
forall a b. (a -> b) -> a -> b
$ -- account name & indent columns
            (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then (1Int -> Int -> Int
forall a. Num a => a -> a -> a
+) else Int -> Int
forall a. a -> a
id) (Int -> Int) -> Int -> Int
forall a b. (a -> b) -> a -> b
$
            (if ReportOpts -> Bool
average_ ReportOpts
ropts then (1Int -> Int -> Int
forall a. Num a => a -> a -> a
+) else Int -> Int
forall a. a -> a
id) (Int -> Int) -> Int -> Int
forall a b. (a -> b) -> a -> b
$
            [Int] -> Int
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
maximum ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ -- depends on non-null subreports
            (MultiBalanceReport -> Int) -> [MultiBalanceReport] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map (\(MultiBalanceReport (amtcolheadings :: [DateSpan]
amtcolheadings, _, _)) -> [DateSpan] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [DateSpan]
amtcolheadings) ([MultiBalanceReport] -> [Int]) -> [MultiBalanceReport] -> [Int]
forall a b. (a -> b) -> a -> b
$
            ((CommandDoc, MultiBalanceReport, Bool) -> MultiBalanceReport)
-> [(CommandDoc, MultiBalanceReport, Bool)] -> [MultiBalanceReport]
forall a b. (a -> b) -> [a] -> [b]
map (CommandDoc, MultiBalanceReport, Bool) -> MultiBalanceReport
forall a b c. (a, b, c) -> b
second3 [(CommandDoc, MultiBalanceReport, Bool)]
subreports
    addtotals :: CSV -> CSV
addtotals
      | ReportOpts -> Bool
no_total_ ReportOpts
ropts Bool -> Bool -> Bool
|| [(CommandDoc, MultiBalanceReport, Bool)] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(CommandDoc, MultiBalanceReport, Bool)]
subreports Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== 1 = CSV -> CSV
forall a. a -> a
id
      | Bool
otherwise = (CSV -> CSV -> CSV
forall a. [a] -> [a] -> [a]
++
          ["Net:" CommandDoc -> [CommandDoc] -> [CommandDoc]
forall a. a -> [a] -> [a]
:
           (MixedAmount -> CommandDoc) -> [MixedAmount] -> [CommandDoc]
forall a b. (a -> b) -> [a] -> [b]
map MixedAmount -> CommandDoc
showMixedAmountOneLineWithoutPrice (
             [MixedAmount]
coltotals
             [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then [MixedAmount
grandtotal] else [])
             [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
average_ ReportOpts
ropts   then [MixedAmount
grandavg]   else [])
             )
          ])

-- | Render a compound balance report as HTML.
compoundBalanceReportAsHtml :: ReportOpts -> CompoundBalanceReport -> Html ()
compoundBalanceReportAsHtml :: ReportOpts
-> (CommandDoc, [DateSpan],
    [(CommandDoc, MultiBalanceReport, Bool)],
    ([MixedAmount], MixedAmount, MixedAmount))
-> Html ()
compoundBalanceReportAsHtml ropts :: ReportOpts
ropts cbr :: (CommandDoc, [DateSpan], [(CommandDoc, MultiBalanceReport, Bool)],
 ([MixedAmount], MixedAmount, MixedAmount))
cbr =
  let
    (title :: CommandDoc
title, colspans :: [DateSpan]
colspans, subreports :: [(CommandDoc, MultiBalanceReport, Bool)]
subreports, (coltotals :: [MixedAmount]
coltotals, grandtotal :: MixedAmount
grandtotal, grandavg :: MixedAmount
grandavg)) = (CommandDoc, [DateSpan], [(CommandDoc, MultiBalanceReport, Bool)],
 ([MixedAmount], MixedAmount, MixedAmount))
cbr
    colspanattr :: Attribute
colspanattr = CommoditySymbol -> Attribute
colspan_ (CommoditySymbol -> Attribute) -> CommoditySymbol -> Attribute
forall a b. (a -> b) -> a -> b
$ CommandDoc -> CommoditySymbol
TS.pack (CommandDoc -> CommoditySymbol) -> CommandDoc -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ Int -> CommandDoc
forall a. Show a => a -> CommandDoc
show (Int -> CommandDoc) -> Int -> CommandDoc
forall a b. (a -> b) -> a -> b
$
      1 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ [DateSpan] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [DateSpan]
colspans Int -> Int -> Int
forall a. Num a => a -> a -> a
+ (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then 1 else 0) Int -> Int -> Int
forall a. Num a => a -> a -> a
+ (if ReportOpts -> Bool
average_ ReportOpts
ropts then 1 else 0)
    leftattr :: Attribute
leftattr = CommoditySymbol -> Attribute
forall arg result. TermRaw arg result => arg -> result
style_ "text-align:left"
    blankrow :: Html ()
blankrow = Html () -> Html ()
forall arg result. Term arg result => arg -> result
tr_ (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ [Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
td_ [Attribute
colspanattr] (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtmlRaw ("&nbsp;"::String)

    titlerows :: [Html ()]
titlerows =
         [Html () -> Html ()
forall arg result. Term arg result => arg -> result
tr_ (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ [Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ [Attribute
colspanattr, Attribute
leftattr] (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ Html () -> Html ()
forall arg result. Term arg result => arg -> result
h2_ (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtml CommandDoc
title]
      [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ [[CommandDoc] -> Html ()
thRow ([CommandDoc] -> Html ()) -> [CommandDoc] -> Html ()
forall a b. (a -> b) -> a -> b
$
          "" CommandDoc -> [CommandDoc] -> [CommandDoc]
forall a. a -> [a] -> [a]
:
          (DateSpan -> CommandDoc) -> [DateSpan] -> [CommandDoc]
forall a b. (a -> b) -> [a] -> [b]
map DateSpan -> CommandDoc
showDateSpanMonthAbbrev [DateSpan]
colspans
          [CommandDoc] -> [CommandDoc] -> [CommandDoc]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then ["Total"] else [])
          [CommandDoc] -> [CommandDoc] -> [CommandDoc]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
average_ ReportOpts
ropts then ["Average"] else [])
          ]

    thRow :: [String] -> Html ()
    thRow :: [CommandDoc] -> Html ()
thRow = Html () -> Html ()
forall arg result. Term arg result => arg -> result
tr_ (Html () -> Html ())
-> ([CommandDoc] -> Html ()) -> [CommandDoc] -> Html ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Html ()] -> Html ()
forall a. Monoid a => [a] -> a
mconcat ([Html ()] -> Html ())
-> ([CommandDoc] -> [Html ()]) -> [CommandDoc] -> Html ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (CommandDoc -> Html ()) -> [CommandDoc] -> [Html ()]
forall a b. (a -> b) -> [a] -> [b]
map (Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ (Html () -> Html ())
-> (CommandDoc -> Html ()) -> CommandDoc -> Html ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtml)

    -- 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, Bool) -> [Html ()]
    subreportrows :: (CommandDoc, MultiBalanceReport, Bool) -> [Html ()]
subreportrows (subreporttitle :: CommandDoc
subreporttitle, mbr :: MultiBalanceReport
mbr, _increasestotal :: Bool
_increasestotal) =
      let
        (_,bodyrows :: [Html ()]
bodyrows,mtotalsrow :: Maybe (Html ())
mtotalsrow) = ReportOpts
-> MultiBalanceReport -> (Html (), [Html ()], Maybe (Html ()))
multiBalanceReportHtmlRows ReportOpts
ropts MultiBalanceReport
mbr
      in
           [Html () -> Html ()
forall arg result. Term arg result => arg -> result
tr_ (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ [Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ [Attribute
colspanattr, Attribute
leftattr] (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtml CommandDoc
subreporttitle]
        [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ [Html ()]
bodyrows
        [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ [Html ()] -> (Html () -> [Html ()]) -> Maybe (Html ()) -> [Html ()]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [] (Html () -> [Html ()] -> [Html ()]
forall a. a -> [a] -> [a]
:[]) Maybe (Html ())
mtotalsrow
        [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ [Html ()
blankrow]

    totalrows :: [Html ()]
totalrows | ReportOpts -> Bool
no_total_ ReportOpts
ropts Bool -> Bool -> Bool
|| [(CommandDoc, MultiBalanceReport, Bool)] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(CommandDoc, MultiBalanceReport, Bool)]
subreports Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== 1 = []
              | Bool
otherwise =
                  let defstyle :: Attribute
defstyle = CommoditySymbol -> Attribute
forall arg result. TermRaw arg result => arg -> result
style_ "text-align:right"
                  in
                    [Html () -> Html ()
forall arg result. Term arg result => arg -> result
tr_ (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ [Html ()] -> Html ()
forall a. Monoid a => [a] -> a
mconcat ([Html ()] -> Html ()) -> [Html ()] -> Html ()
forall a b. (a -> b) -> a -> b
$
                         [Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ [CommoditySymbol -> Attribute
class_ "", CommoditySymbol -> Attribute
forall arg result. TermRaw arg result => arg -> result
style_ "text-align:left"] "Net:"
                       Html () -> [Html ()] -> [Html ()]
forall a. a -> [a] -> [a]
: [[Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ [CommoditySymbol -> Attribute
class_ "amount coltotal", Attribute
defstyle] (CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtml (CommandDoc -> Html ()) -> CommandDoc -> Html ()
forall a b. (a -> b) -> a -> b
$ MixedAmount -> CommandDoc
showMixedAmountOneLineWithoutPrice MixedAmount
a) | MixedAmount
a <- [MixedAmount]
coltotals]
                      [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
row_total_ ReportOpts
ropts then [[Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ [CommoditySymbol -> Attribute
class_ "amount coltotal", Attribute
defstyle] (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtml (CommandDoc -> Html ()) -> CommandDoc -> Html ()
forall a b. (a -> b) -> a -> b
$ MixedAmount -> CommandDoc
showMixedAmountOneLineWithoutPrice (MixedAmount -> CommandDoc) -> MixedAmount -> CommandDoc
forall a b. (a -> b) -> a -> b
$ MixedAmount
grandtotal] else [])
                      [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ (if ReportOpts -> Bool
average_ ReportOpts
ropts   then [[Attribute] -> Html () -> Html ()
forall arg result. Term arg result => arg -> result
th_ [CommoditySymbol -> Attribute
class_ "amount colaverage", Attribute
defstyle] (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ CommandDoc -> Html ()
forall a (m :: * -> *). (ToHtml a, Monad m) => a -> HtmlT m ()
toHtml (CommandDoc -> Html ()) -> CommandDoc -> Html ()
forall a b. (a -> b) -> a -> b
$ MixedAmount -> CommandDoc
showMixedAmountOneLineWithoutPrice (MixedAmount -> CommandDoc) -> MixedAmount -> CommandDoc
forall a b. (a -> b) -> a -> b
$ MixedAmount
grandavg] else [])
                    ]

  in do
    CommoditySymbol -> Html ()
forall arg result. TermRaw arg result => arg -> result
style_ ([CommoditySymbol] -> CommoditySymbol
TS.unlines [""
      ,"td { padding:0 0.5em; }"
      ,"td:nth-child(1) { white-space:nowrap; }"
      ,"tr:nth-child(even) td { background-color:#eee; }"
      ])
    [Attribute] -> Html ()
forall (m :: * -> *). Applicative m => [Attribute] -> HtmlT m ()
link_ [CommoditySymbol -> Attribute
rel_ "stylesheet", CommoditySymbol -> Attribute
href_ "hledger.css"]
    Html () -> Html ()
forall arg result. Term arg result => arg -> result
table_ (Html () -> Html ()) -> Html () -> Html ()
forall a b. (a -> b) -> a -> b
$ [Html ()] -> Html ()
forall a. Monoid a => [a] -> a
mconcat ([Html ()] -> Html ()) -> [Html ()] -> Html ()
forall a b. (a -> b) -> a -> b
$
         [Html ()]
titlerows
      [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ [Html ()
blankrow]
      [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ ((CommandDoc, MultiBalanceReport, Bool) -> [Html ()])
-> [(CommandDoc, MultiBalanceReport, Bool)] -> [Html ()]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (CommandDoc, MultiBalanceReport, Bool) -> [Html ()]
subreportrows [(CommandDoc, MultiBalanceReport, Bool)]
subreports
      [Html ()] -> [Html ()] -> [Html ()]
forall a. [a] -> [a] -> [a]
++ [Html ()]
totalrows