{-# LANGUAGE NamedFieldPuns      #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Hledger.Reports.BudgetReport (
  BudgetGoal,
  BudgetTotal,
  BudgetAverage,
  BudgetCell,
  BudgetReportRow,
  BudgetReport,
  budgetReport,
  budgetReportAsTable,
  budgetReportAsText,
  budgetReportAsCsv,
  -- * Helpers
  combineBudgetAndActual,
  -- * Tests
  tests_BudgetReport
)
where

import Control.Applicative ((<|>))
import Data.Decimal (roundTo)
import Data.Default (def)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.List (find, partition, transpose)
import Data.List.Extra (nubSort)
import Data.Maybe (fromMaybe)
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
--import System.Console.CmdArgs.Explicit as C
--import Lucid as L
import Text.Tabular.AsciiWide as Tab

import Hledger.Data
import Hledger.Utils
import Hledger.Read.CsvReader (CSV)
import Hledger.Reports.ReportOptions
import Hledger.Reports.ReportTypes
import Hledger.Reports.MultiBalanceReport


type BudgetGoal    = Change
type BudgetTotal   = Total
type BudgetAverage = Average

-- | A budget report tracks expected and actual changes per account and subperiod.
type BudgetCell = (Maybe Change, Maybe BudgetGoal)
type BudgetReportRow = PeriodicReportRow DisplayName BudgetCell
type BudgetReport    = PeriodicReport    DisplayName BudgetCell

type BudgetDisplayCell = ((Text, Int), Maybe ((Text, Int), Maybe (Text, Int)))

-- | Calculate per-account, per-period budget (balance change) goals
-- from all periodic transactions, calculate actual balance changes 
-- from the regular transactions, and compare these to get a 'BudgetReport'.
-- Unbudgeted accounts may be hidden or renamed (see journalWithBudgetAccountNames).
budgetReport :: ReportSpec -> BalancingOpts -> DateSpan -> Journal -> BudgetReport
budgetReport :: ReportSpec -> BalancingOpts -> DateSpan -> Journal -> BudgetReport
budgetReport ReportSpec
rspec BalancingOpts
bopts DateSpan
reportspan Journal
j = String -> BudgetReport -> BudgetReport
forall a. Show a => String -> a -> a
dbg4 String
"sortedbudgetreport" BudgetReport
budgetreport
  where
    -- Budget report demands ALTree mode to ensure subaccounts and subaccount budgets are properly handled
    -- and that reports with and without --empty make sense when compared side by side
    ropts :: ReportOpts
ropts = (ReportSpec -> ReportOpts
rsOpts ReportSpec
rspec){ accountlistmode_ :: AccountListMode
accountlistmode_ = AccountListMode
ALTree }
    showunbudgeted :: Bool
showunbudgeted = ReportOpts -> Bool
empty_ ReportOpts
ropts
    budgetedaccts :: Set AccountName
budgetedaccts =
      String -> Set AccountName -> Set AccountName
forall a. Show a => String -> a -> a
dbg3 String
"budgetedacctsinperiod" (Set AccountName -> Set AccountName)
-> Set AccountName -> Set AccountName
forall a b. (a -> b) -> a -> b
$
      [AccountName] -> Set AccountName
forall a. Ord a => [a] -> Set a
S.fromList ([AccountName] -> Set AccountName)
-> [AccountName] -> Set AccountName
forall a b. (a -> b) -> a -> b
$
      [AccountName] -> [AccountName]
expandAccountNames ([AccountName] -> [AccountName]) -> [AccountName] -> [AccountName]
forall a b. (a -> b) -> a -> b
$
      [Posting] -> [AccountName]
accountNamesFromPostings ([Posting] -> [AccountName]) -> [Posting] -> [AccountName]
forall a b. (a -> b) -> a -> b
$
      (Transaction -> [Posting]) -> [Transaction] -> [Posting]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Transaction -> [Posting]
tpostings ([Transaction] -> [Posting]) -> [Transaction] -> [Posting]
forall a b. (a -> b) -> a -> b
$
      (PeriodicTransaction -> [Transaction])
-> [PeriodicTransaction] -> [Transaction]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (PeriodicTransaction -> DateSpan -> [Transaction]
`runPeriodicTransaction` DateSpan
reportspan) ([PeriodicTransaction] -> [Transaction])
-> [PeriodicTransaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$
      Journal -> [PeriodicTransaction]
jperiodictxns Journal
j
    actualj :: Journal
actualj = Set AccountName -> Bool -> Journal -> Journal
journalWithBudgetAccountNames Set AccountName
budgetedaccts Bool
showunbudgeted Journal
j
    budgetj :: Journal
budgetj = BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal
journalAddBudgetGoalTransactions BalancingOpts
bopts ReportOpts
ropts DateSpan
reportspan Journal
j
    actualreport :: PeriodicReport DisplayName MixedAmount
actualreport@(PeriodicReport [DateSpan]
actualspans [PeriodicReportRow DisplayName MixedAmount]
_ PeriodicReportRow () MixedAmount
_) =
        String
-> PeriodicReport DisplayName MixedAmount
-> PeriodicReport DisplayName MixedAmount
forall a. Show a => String -> a -> a
dbg5 String
"actualreport" (PeriodicReport DisplayName MixedAmount
 -> PeriodicReport DisplayName MixedAmount)
-> PeriodicReport DisplayName MixedAmount
-> PeriodicReport DisplayName MixedAmount
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Journal -> PeriodicReport DisplayName MixedAmount
multiBalanceReport ReportSpec
rspec{rsOpts :: ReportOpts
rsOpts=ReportOpts
ropts{empty_ :: Bool
empty_=Bool
True}} Journal
actualj
    budgetgoalreport :: PeriodicReport DisplayName MixedAmount
budgetgoalreport@(PeriodicReport [DateSpan]
_ [PeriodicReportRow DisplayName MixedAmount]
budgetgoalitems PeriodicReportRow () MixedAmount
budgetgoaltotals) =
        String
-> PeriodicReport DisplayName MixedAmount
-> PeriodicReport DisplayName MixedAmount
forall a. Show a => String -> a -> a
dbg5 String
"budgetgoalreport" (PeriodicReport DisplayName MixedAmount
 -> PeriodicReport DisplayName MixedAmount)
-> PeriodicReport DisplayName MixedAmount
-> PeriodicReport DisplayName MixedAmount
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Journal -> PeriodicReport DisplayName MixedAmount
multiBalanceReport ReportSpec
rspec{rsOpts :: ReportOpts
rsOpts=ReportOpts
ropts{empty_ :: Bool
empty_=Bool
True}} Journal
budgetj
    budgetgoalreport' :: PeriodicReport DisplayName MixedAmount
budgetgoalreport'
      -- If no interval is specified:
      -- budgetgoalreport's span might be shorter actualreport's due to periodic txns;
      -- it should be safe to replace it with the latter, so they combine well.
      | ReportOpts -> Interval
interval_ ReportOpts
ropts Interval -> Interval -> Bool
forall a. Eq a => a -> a -> Bool
== Interval
NoInterval = [DateSpan]
-> [PeriodicReportRow DisplayName MixedAmount]
-> PeriodicReportRow () MixedAmount
-> PeriodicReport DisplayName MixedAmount
forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
actualspans [PeriodicReportRow DisplayName MixedAmount]
budgetgoalitems PeriodicReportRow () MixedAmount
budgetgoaltotals
      | Bool
otherwise = PeriodicReport DisplayName MixedAmount
budgetgoalreport
    budgetreport :: BudgetReport
budgetreport = ReportOpts
-> Journal
-> PeriodicReport DisplayName MixedAmount
-> PeriodicReport DisplayName MixedAmount
-> BudgetReport
combineBudgetAndActual ReportOpts
ropts Journal
j PeriodicReport DisplayName MixedAmount
budgetgoalreport' PeriodicReport DisplayName MixedAmount
actualreport

-- | Use all periodic transactions in the journal to generate
-- budget goal transactions in the specified date span.
-- Budget goal transactions are similar to forecast transactions except
-- their purpose and effect is to define balance change goals, per account and period,
-- for BudgetReport.
journalAddBudgetGoalTransactions :: BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal
journalAddBudgetGoalTransactions :: BalancingOpts -> ReportOpts -> DateSpan -> Journal -> Journal
journalAddBudgetGoalTransactions BalancingOpts
bopts ReportOpts
_ropts DateSpan
reportspan Journal
j =
  (String -> Journal)
-> (Journal -> Journal) -> Either String Journal -> Journal
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either String -> Journal
forall a. String -> a
error' Journal -> Journal
forall a. a -> a
id (Either String Journal -> Journal)
-> Either String Journal -> Journal
forall a b. (a -> b) -> a -> b
$ BalancingOpts -> Journal -> Either String Journal
journalBalanceTransactions BalancingOpts
bopts Journal
j{ jtxns :: [Transaction]
jtxns = [Transaction]
budgetts }  -- PARTIAL:
  where
    budgetspan :: DateSpan
budgetspan = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg3 String
"budget span" (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ DateSpan
reportspan
    budgetts :: [Transaction]
budgetts =
      String -> [Transaction] -> [Transaction]
forall a. Show a => String -> a -> a
dbg5 String
"budget goal txns" ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$
      [Transaction -> Transaction
makeBudgetTxn Transaction
t
      | PeriodicTransaction
pt <- Journal -> [PeriodicTransaction]
jperiodictxns Journal
j
      , Transaction
t <- PeriodicTransaction -> DateSpan -> [Transaction]
runPeriodicTransaction PeriodicTransaction
pt DateSpan
budgetspan
      ]
    makeBudgetTxn :: Transaction -> Transaction
makeBudgetTxn Transaction
t = Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$ Transaction
t { tdescription :: AccountName
tdescription = String -> AccountName
T.pack String
"Budget transaction" }

-- | Adjust a journal's account names for budget reporting, in two ways:
--
-- 1. accounts with no budget goal anywhere in their ancestry are moved
--    under the "unbudgeted" top level account.
--
-- 2. subaccounts with no budget goal are merged with their closest parent account
--    with a budget goal, so that only budgeted accounts are shown.
--    This can be disabled by -E/--empty.
--
journalWithBudgetAccountNames :: S.Set AccountName -> Bool -> Journal -> Journal
journalWithBudgetAccountNames :: Set AccountName -> Bool -> Journal -> Journal
journalWithBudgetAccountNames Set AccountName
budgetedaccts Bool
showunbudgeted Journal
j =
  (Journal -> String) -> Journal -> Journal
forall a. Show a => (a -> String) -> a -> a
dbg5With ((String
"budget account names: "String -> String -> String
forall a. [a] -> [a] -> [a]
++)(String -> String) -> (Journal -> String) -> Journal -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
.[AccountName] -> String
forall a. Show a => a -> String
pshow([AccountName] -> String)
-> (Journal -> [AccountName]) -> Journal -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Journal -> [AccountName]
journalAccountNamesUsed) (Journal -> Journal) -> Journal -> Journal
forall a b. (a -> b) -> a -> b
$
  Journal
j { jtxns :: [Transaction]
jtxns = Transaction -> Transaction
remapTxn (Transaction -> Transaction) -> [Transaction] -> [Transaction]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Journal -> [Transaction]
jtxns Journal
j }
  where
    remapTxn :: Transaction -> Transaction
remapTxn = Transaction -> Transaction
txnTieKnot (Transaction -> Transaction)
-> (Transaction -> Transaction) -> Transaction -> Transaction
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings Posting -> Posting
remapPosting
    remapPosting :: Posting -> Posting
remapPosting Posting
p = Posting
p { paccount :: AccountName
paccount = AccountName -> AccountName
remapAccount (AccountName -> AccountName) -> AccountName -> AccountName
forall a b. (a -> b) -> a -> b
$ Posting -> AccountName
paccount Posting
p, poriginal :: Maybe Posting
poriginal = Posting -> Maybe Posting
poriginal Posting
p Maybe Posting -> Maybe Posting -> Maybe Posting
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> Posting -> Maybe Posting
forall a. a -> Maybe a
Just Posting
p }
    remapAccount :: AccountName -> AccountName
remapAccount AccountName
a
      | AccountName
a AccountName -> Set AccountName -> Bool
forall a. Ord a => a -> Set a -> Bool
`S.member` Set AccountName
budgetedaccts = AccountName
a
      | Just AccountName
p <- Maybe AccountName
budgetedparent   = if Bool
showunbudgeted then AccountName
a else AccountName
p
      | Bool
otherwise                  = if Bool
showunbudgeted then AccountName
u AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
acctsep AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
a else AccountName
u
      where
        budgetedparent :: Maybe AccountName
budgetedparent = (AccountName -> Bool) -> [AccountName] -> Maybe AccountName
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (AccountName -> Set AccountName -> Bool
forall a. Ord a => a -> Set a -> Bool
`S.member` Set AccountName
budgetedaccts) ([AccountName] -> Maybe AccountName)
-> [AccountName] -> Maybe AccountName
forall a b. (a -> b) -> a -> b
$ AccountName -> [AccountName]
parentAccountNames AccountName
a
        u :: AccountName
u = AccountName
unbudgetedAccountName

-- | Combine a per-account-and-subperiod report of budget goals, and one
-- of actual change amounts, into a budget performance report.
-- The two reports should have the same report interval, but need not
-- have exactly the same account rows or date columns.
-- (Cells in the combined budget report can be missing a budget goal,
-- an actual amount, or both.) The combined report will include:
--
-- - consecutive subperiods at the same interval as the two reports,
--   spanning the period of both reports
--
-- - all accounts mentioned in either report, sorted by account code or
--   account name or amount as appropriate.
--
combineBudgetAndActual :: ReportOpts -> Journal -> MultiBalanceReport -> MultiBalanceReport -> BudgetReport
combineBudgetAndActual :: ReportOpts
-> Journal
-> PeriodicReport DisplayName MixedAmount
-> PeriodicReport DisplayName MixedAmount
-> BudgetReport
combineBudgetAndActual ReportOpts
ropts Journal
j
      (PeriodicReport [DateSpan]
budgetperiods [PeriodicReportRow DisplayName MixedAmount]
budgetrows (PeriodicReportRow ()
_ [MixedAmount]
budgettots MixedAmount
budgetgrandtot MixedAmount
budgetgrandavg))
      (PeriodicReport [DateSpan]
actualperiods [PeriodicReportRow DisplayName MixedAmount]
actualrows (PeriodicReportRow ()
_ [MixedAmount]
actualtots MixedAmount
actualgrandtot MixedAmount
actualgrandavg)) =
    [DateSpan]
-> [PeriodicReportRow DisplayName BudgetCell]
-> PeriodicReportRow () BudgetCell
-> BudgetReport
forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
periods [PeriodicReportRow DisplayName BudgetCell]
sortedrows PeriodicReportRow () BudgetCell
totalrow
  where
    periods :: [DateSpan]
periods = [DateSpan] -> [DateSpan]
forall a. Ord a => [a] -> [a]
nubSort ([DateSpan] -> [DateSpan])
-> ([DateSpan] -> [DateSpan]) -> [DateSpan] -> [DateSpan]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (DateSpan -> Bool) -> [DateSpan] -> [DateSpan]
forall a. (a -> Bool) -> [a] -> [a]
filter (DateSpan -> DateSpan -> Bool
forall a. Eq a => a -> a -> Bool
/= DateSpan
nulldatespan) ([DateSpan] -> [DateSpan]) -> [DateSpan] -> [DateSpan]
forall a b. (a -> b) -> a -> b
$ [DateSpan]
budgetperiods [DateSpan] -> [DateSpan] -> [DateSpan]
forall a. [a] -> [a] -> [a]
++ [DateSpan]
actualperiods

    -- first, combine any corresponding budget goals with actual changes
    rows1 :: [PeriodicReportRow DisplayName BudgetCell]
rows1 =
      [ DisplayName
-> [BudgetCell]
-> BudgetCell
-> BudgetCell
-> PeriodicReportRow DisplayName BudgetCell
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow DisplayName
acct [BudgetCell]
amtandgoals BudgetCell
totamtandgoal BudgetCell
avgamtandgoal
      | PeriodicReportRow DisplayName
acct [MixedAmount]
actualamts MixedAmount
actualtot MixedAmount
actualavg <- [PeriodicReportRow DisplayName MixedAmount]
actualrows
      , let mbudgetgoals :: Maybe ([MixedAmount], MixedAmount, MixedAmount)
mbudgetgoals       = AccountName
-> HashMap AccountName ([MixedAmount], MixedAmount, MixedAmount)
-> Maybe ([MixedAmount], MixedAmount, MixedAmount)
forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HM.lookup (DisplayName -> AccountName
displayFull DisplayName
acct) HashMap AccountName ([MixedAmount], MixedAmount, MixedAmount)
budgetGoalsByAcct :: Maybe ([BudgetGoal], BudgetTotal, BudgetAverage)
      , let budgetmamts :: [Maybe MixedAmount]
budgetmamts        = [Maybe MixedAmount]
-> (([MixedAmount], MixedAmount, MixedAmount)
    -> [Maybe MixedAmount])
-> Maybe ([MixedAmount], MixedAmount, MixedAmount)
-> [Maybe MixedAmount]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (Maybe MixedAmount
forall a. Maybe a
Nothing Maybe MixedAmount -> [DateSpan] -> [Maybe MixedAmount]
forall (f :: * -> *) a b. Functor f => a -> f b -> f a
<$ [DateSpan]
periods) ((MixedAmount -> Maybe MixedAmount)
-> [MixedAmount] -> [Maybe MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just ([MixedAmount] -> [Maybe MixedAmount])
-> (([MixedAmount], MixedAmount, MixedAmount) -> [MixedAmount])
-> ([MixedAmount], MixedAmount, MixedAmount)
-> [Maybe MixedAmount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([MixedAmount], MixedAmount, MixedAmount) -> [MixedAmount]
forall a b c. (a, b, c) -> a
first3) Maybe ([MixedAmount], MixedAmount, MixedAmount)
mbudgetgoals :: [Maybe BudgetGoal]
      , let mbudgettot :: Maybe MixedAmount
mbudgettot         = ([MixedAmount], MixedAmount, MixedAmount) -> MixedAmount
forall a b c. (a, b, c) -> b
second3 (([MixedAmount], MixedAmount, MixedAmount) -> MixedAmount)
-> Maybe ([MixedAmount], MixedAmount, MixedAmount)
-> Maybe MixedAmount
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe ([MixedAmount], MixedAmount, MixedAmount)
mbudgetgoals :: Maybe BudgetTotal
      , let mbudgetavg :: Maybe MixedAmount
mbudgetavg         = ([MixedAmount], MixedAmount, MixedAmount) -> MixedAmount
forall a b c. (a, b, c) -> c
third3 (([MixedAmount], MixedAmount, MixedAmount) -> MixedAmount)
-> Maybe ([MixedAmount], MixedAmount, MixedAmount)
-> Maybe MixedAmount
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe ([MixedAmount], MixedAmount, MixedAmount)
mbudgetgoals  :: Maybe BudgetAverage
      , let acctBudgetByPeriod :: Map DateSpan MixedAmount
acctBudgetByPeriod = [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [ (DateSpan
p,MixedAmount
budgetamt) | (DateSpan
p, Just MixedAmount
budgetamt) <- [DateSpan]
-> [Maybe MixedAmount] -> [(DateSpan, Maybe MixedAmount)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [Maybe MixedAmount]
budgetmamts ] :: Map DateSpan BudgetGoal
      , let acctActualByPeriod :: Map DateSpan MixedAmount
acctActualByPeriod = [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [ (DateSpan
p,MixedAmount
actualamt) | (DateSpan
p, Just MixedAmount
actualamt) <- [DateSpan]
-> [Maybe MixedAmount] -> [(DateSpan, Maybe MixedAmount)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
actualperiods ((MixedAmount -> Maybe MixedAmount)
-> [MixedAmount] -> [Maybe MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just [MixedAmount]
actualamts) ] :: Map DateSpan Change
      , let amtandgoals :: [BudgetCell]
amtandgoals        = [ (DateSpan -> Map DateSpan MixedAmount -> Maybe MixedAmount
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan MixedAmount
acctActualByPeriod, DateSpan -> Map DateSpan MixedAmount -> Maybe MixedAmount
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan MixedAmount
acctBudgetByPeriod) | DateSpan
p <- [DateSpan]
periods ] :: [BudgetCell]
      , let totamtandgoal :: BudgetCell
totamtandgoal      = (MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
actualtot, Maybe MixedAmount
mbudgettot)
      , let avgamtandgoal :: BudgetCell
avgamtandgoal      = (MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
actualavg, Maybe MixedAmount
mbudgetavg)
      ]
      where
        HashMap AccountName ([MixedAmount], MixedAmount, MixedAmount)
budgetGoalsByAcct :: HashMap AccountName ([BudgetGoal], BudgetTotal, BudgetAverage) =
          [(AccountName, ([MixedAmount], MixedAmount, MixedAmount))]
-> HashMap AccountName ([MixedAmount], MixedAmount, MixedAmount)
forall k v. (Eq k, Hashable k) => [(k, v)] -> HashMap k v
HM.fromList [ (DisplayName -> AccountName
displayFull DisplayName
acct, ([MixedAmount]
amts, MixedAmount
tot, MixedAmount
avg))
                         | PeriodicReportRow DisplayName
acct [MixedAmount]
amts MixedAmount
tot MixedAmount
avg <- [PeriodicReportRow DisplayName MixedAmount]
budgetrows ]

    -- next, make rows for budget goals with no actual changes
    rows2 :: [PeriodicReportRow DisplayName BudgetCell]
rows2 =
      [ DisplayName
-> [BudgetCell]
-> BudgetCell
-> BudgetCell
-> PeriodicReportRow DisplayName BudgetCell
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow DisplayName
acct [BudgetCell]
amtandgoals BudgetCell
forall a. (Maybe a, Maybe MixedAmount)
totamtandgoal BudgetCell
forall a. (Maybe a, Maybe MixedAmount)
avgamtandgoal
      | PeriodicReportRow DisplayName
acct [MixedAmount]
budgetgoals MixedAmount
budgettot MixedAmount
budgetavg <- [PeriodicReportRow DisplayName MixedAmount]
budgetrows
      , DisplayName -> AccountName
displayFull DisplayName
acct AccountName -> [AccountName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` (PeriodicReportRow DisplayName BudgetCell -> AccountName)
-> [PeriodicReportRow DisplayName BudgetCell] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName BudgetCell -> AccountName
forall a. PeriodicReportRow DisplayName a -> AccountName
prrFullName [PeriodicReportRow DisplayName BudgetCell]
rows1
      , let acctBudgetByPeriod :: Map DateSpan MixedAmount
acctBudgetByPeriod = [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount)
-> [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> [MixedAmount] -> [(DateSpan, MixedAmount)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [MixedAmount]
budgetgoals :: Map DateSpan BudgetGoal
      , let amtandgoals :: [BudgetCell]
amtandgoals        = [ (Maybe MixedAmount
forall a. Maybe a
Nothing, DateSpan -> Map DateSpan MixedAmount -> Maybe MixedAmount
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan MixedAmount
acctBudgetByPeriod) | DateSpan
p <- [DateSpan]
periods ] :: [BudgetCell]
      , let totamtandgoal :: (Maybe a, Maybe MixedAmount)
totamtandgoal      = (Maybe a
forall a. Maybe a
Nothing, MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
budgettot)
      , let avgamtandgoal :: (Maybe a, Maybe MixedAmount)
avgamtandgoal      = (Maybe a
forall a. Maybe a
Nothing, MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
budgetavg)
      ]

    -- combine and re-sort rows
    -- TODO: add --sort-budget to sort by budget goal amount
    [PeriodicReportRow DisplayName BudgetCell]
sortedrows :: [BudgetReportRow] = [AccountName]
-> [PeriodicReportRow DisplayName BudgetCell]
-> [PeriodicReportRow DisplayName BudgetCell]
forall b.
[AccountName]
-> [PeriodicReportRow DisplayName b]
-> [PeriodicReportRow DisplayName b]
sortRowsLike ([PeriodicReportRow DisplayName BudgetCell] -> [AccountName]
forall b.
[PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
-> [AccountName]
mbrsorted [PeriodicReportRow DisplayName BudgetCell]
unbudgetedrows [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ [PeriodicReportRow DisplayName BudgetCell] -> [AccountName]
forall b.
[PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
-> [AccountName]
mbrsorted [PeriodicReportRow DisplayName BudgetCell]
rows') [PeriodicReportRow DisplayName BudgetCell]
rows
      where
        ([PeriodicReportRow DisplayName BudgetCell]
unbudgetedrows, [PeriodicReportRow DisplayName BudgetCell]
rows') = (PeriodicReportRow DisplayName BudgetCell -> Bool)
-> [PeriodicReportRow DisplayName BudgetCell]
-> ([PeriodicReportRow DisplayName BudgetCell],
    [PeriodicReportRow DisplayName BudgetCell])
forall a. (a -> Bool) -> [a] -> ([a], [a])
partition ((AccountName -> AccountName -> Bool
forall a. Eq a => a -> a -> Bool
==AccountName
unbudgetedAccountName) (AccountName -> Bool)
-> (PeriodicReportRow DisplayName BudgetCell -> AccountName)
-> PeriodicReportRow DisplayName BudgetCell
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodicReportRow DisplayName BudgetCell -> AccountName
forall a. PeriodicReportRow DisplayName a -> AccountName
prrFullName) [PeriodicReportRow DisplayName BudgetCell]
rows
        mbrsorted :: [PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
-> [AccountName]
mbrsorted = (PeriodicReportRow DisplayName MixedAmount -> AccountName)
-> [PeriodicReportRow DisplayName MixedAmount] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName MixedAmount -> AccountName
forall a. PeriodicReportRow DisplayName a -> AccountName
prrFullName ([PeriodicReportRow DisplayName MixedAmount] -> [AccountName])
-> ([PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
    -> [PeriodicReportRow DisplayName MixedAmount])
-> [PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
-> [AccountName]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ReportOpts
-> Journal
-> [PeriodicReportRow DisplayName MixedAmount]
-> [PeriodicReportRow DisplayName MixedAmount]
sortRows ReportOpts
ropts Journal
j ([PeriodicReportRow DisplayName MixedAmount]
 -> [PeriodicReportRow DisplayName MixedAmount])
-> ([PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
    -> [PeriodicReportRow DisplayName MixedAmount])
-> [PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
-> [PeriodicReportRow DisplayName MixedAmount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (PeriodicReportRow DisplayName (Maybe MixedAmount, b)
 -> PeriodicReportRow DisplayName MixedAmount)
-> [PeriodicReportRow DisplayName (Maybe MixedAmount, b)]
-> [PeriodicReportRow DisplayName MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (((Maybe MixedAmount, b) -> MixedAmount)
-> PeriodicReportRow DisplayName (Maybe MixedAmount, b)
-> PeriodicReportRow DisplayName MixedAmount
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (((Maybe MixedAmount, b) -> MixedAmount)
 -> PeriodicReportRow DisplayName (Maybe MixedAmount, b)
 -> PeriodicReportRow DisplayName MixedAmount)
-> ((Maybe MixedAmount, b) -> MixedAmount)
-> PeriodicReportRow DisplayName (Maybe MixedAmount, b)
-> PeriodicReportRow DisplayName MixedAmount
forall a b. (a -> b) -> a -> b
$ MixedAmount -> Maybe MixedAmount -> MixedAmount
forall a. a -> Maybe a -> a
fromMaybe MixedAmount
nullmixedamt (Maybe MixedAmount -> MixedAmount)
-> ((Maybe MixedAmount, b) -> Maybe MixedAmount)
-> (Maybe MixedAmount, b)
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Maybe MixedAmount, b) -> Maybe MixedAmount
forall a b. (a, b) -> a
fst)
        rows :: [PeriodicReportRow DisplayName BudgetCell]
rows = [PeriodicReportRow DisplayName BudgetCell]
rows1 [PeriodicReportRow DisplayName BudgetCell]
-> [PeriodicReportRow DisplayName BudgetCell]
-> [PeriodicReportRow DisplayName BudgetCell]
forall a. [a] -> [a] -> [a]
++ [PeriodicReportRow DisplayName BudgetCell]
rows2

    -- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells
    totalrow :: PeriodicReportRow () BudgetCell
totalrow = ()
-> [BudgetCell]
-> BudgetCell
-> BudgetCell
-> PeriodicReportRow () BudgetCell
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow ()
        [ (DateSpan -> Map DateSpan MixedAmount -> Maybe MixedAmount
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan MixedAmount
totActualByPeriod, DateSpan -> Map DateSpan MixedAmount -> Maybe MixedAmount
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup DateSpan
p Map DateSpan MixedAmount
totBudgetByPeriod) | DateSpan
p <- [DateSpan]
periods ]
        ( MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
actualgrandtot, MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
budgetgrandtot )
        ( MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
actualgrandavg, MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
budgetgrandavg )
      where
        totBudgetByPeriod :: Map DateSpan MixedAmount
totBudgetByPeriod = [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount)
-> [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> [MixedAmount] -> [(DateSpan, MixedAmount)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
budgetperiods [MixedAmount]
budgettots :: Map DateSpan BudgetTotal
        totActualByPeriod :: Map DateSpan MixedAmount
totActualByPeriod = [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount)
-> [(DateSpan, MixedAmount)] -> Map DateSpan MixedAmount
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> [MixedAmount] -> [(DateSpan, MixedAmount)]
forall a b. [a] -> [b] -> [(a, b)]
zip [DateSpan]
actualperiods [MixedAmount]
actualtots :: Map DateSpan Change

-- | Render a budget report as plain text suitable for console output.
budgetReportAsText :: ReportOpts -> BudgetReport -> TL.Text
budgetReportAsText :: ReportOpts -> BudgetReport -> Text
budgetReportAsText ropts :: ReportOpts
ropts@ReportOpts{Bool
Int
[AccountName]
[Status]
Maybe Int
Maybe NormalSign
Maybe DateSpan
Maybe ValuationType
Interval
Period
StringFormat
Costing
AccountListMode
BalanceType
ReportType
transpose_ :: ReportOpts -> Bool
forecast_ :: ReportOpts -> Maybe DateSpan
color_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
invert_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
pretty_tables_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
drop_ :: ReportOpts -> Int
balancetype_ :: ReportOpts -> BalanceType
reporttype_ :: ReportOpts -> ReportType
txn_dates_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
average_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [AccountName]
format_ :: ReportOpts -> StringFormat
real_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
date2_ :: ReportOpts -> Bool
depth_ :: ReportOpts -> Maybe Int
infer_value_ :: ReportOpts -> Bool
value_ :: ReportOpts -> Maybe ValuationType
cost_ :: ReportOpts -> Costing
statuses_ :: ReportOpts -> [Status]
period_ :: ReportOpts -> Period
transpose_ :: Bool
forecast_ :: Maybe DateSpan
color_ :: Bool
normalbalance_ :: Maybe NormalSign
invert_ :: Bool
percent_ :: Bool
sort_amount_ :: Bool
pretty_tables_ :: Bool
show_costs_ :: Bool
no_total_ :: Bool
row_total_ :: Bool
drop_ :: Int
accountlistmode_ :: AccountListMode
balancetype_ :: BalanceType
reporttype_ :: ReportType
txn_dates_ :: Bool
related_ :: Bool
average_ :: Bool
querystring_ :: [AccountName]
format_ :: StringFormat
real_ :: Bool
no_elide_ :: Bool
empty_ :: Bool
date2_ :: Bool
depth_ :: Maybe Int
infer_value_ :: Bool
value_ :: Maybe ValuationType
cost_ :: Costing
statuses_ :: [Status]
interval_ :: Interval
period_ :: Period
interval_ :: ReportOpts -> Interval
empty_ :: ReportOpts -> Bool
accountlistmode_ :: ReportOpts -> AccountListMode
..} BudgetReport
budgetr = Builder -> Text
TB.toLazyText (Builder -> Text) -> Builder -> Text
forall a b. (a -> b) -> a -> b
$
    AccountName -> Builder
TB.fromText AccountName
title Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> AccountName -> Builder
TB.fromText AccountName
"\n\n" Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<>
      TableOpts
-> (AccountName -> Cell)
-> (AccountName -> Cell)
-> (((Int, Int, Int), BudgetDisplayCell) -> Cell)
-> Table
     AccountName AccountName ((Int, Int, Int), BudgetDisplayCell)
-> Builder
forall rh ch a.
TableOpts
-> (rh -> Cell)
-> (ch -> Cell)
-> (a -> Cell)
-> Table rh ch a
-> Builder
renderTableB TableOpts
forall a. Default a => a
def{tableBorders :: Bool
tableBorders=Bool
False,prettyTable :: Bool
prettyTable=Bool
pretty_tables_}
        (Align -> AccountName -> Cell
textCell Align
TopLeft) (Align -> AccountName -> Cell
textCell Align
TopRight) (((Int, Int, Int) -> BudgetDisplayCell -> Cell)
-> ((Int, Int, Int), BudgetDisplayCell) -> Cell
forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry (Int, Int, Int) -> BudgetDisplayCell -> Cell
showcell) Table AccountName AccountName ((Int, Int, Int), BudgetDisplayCell)
displayTableWithWidths
  where
    title :: AccountName
title = AccountName
"Budget performance in " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> DateSpan -> AccountName
showDateSpan (BudgetReport -> DateSpan
forall a b. PeriodicReport a b -> DateSpan
periodicReportSpan BudgetReport
budgetr)
           AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> (case Costing
cost_ of
                 Costing
Cost   -> AccountName
", converted to cost"
                 Costing
NoCost -> AccountName
"")
           AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> (case Maybe ValuationType
value_ of
                 Just (AtThen Maybe AccountName
_mc)   -> AccountName
", valued at posting date"
                 Just (AtEnd Maybe AccountName
_mc)    -> AccountName
", valued at period ends"
                 Just (AtNow Maybe AccountName
_mc)    -> AccountName
", current value"
                 Just (AtDate Day
d Maybe AccountName
_mc) -> AccountName
", valued at " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> Day -> AccountName
showDate Day
d
                 Maybe ValuationType
Nothing             -> AccountName
"")
           AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
":"

    displayTableWithWidths :: Table Text Text ((Int, Int, Int), BudgetDisplayCell)
    displayTableWithWidths :: Table AccountName AccountName ((Int, Int, Int), BudgetDisplayCell)
displayTableWithWidths = Header AccountName
-> Header AccountName
-> [[((Int, Int, Int), BudgetDisplayCell)]]
-> Table
     AccountName AccountName ((Int, Int, Int), BudgetDisplayCell)
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table Header AccountName
rh Header AccountName
ch ([[((Int, Int, Int), BudgetDisplayCell)]]
 -> Table
      AccountName AccountName ((Int, Int, Int), BudgetDisplayCell))
-> [[((Int, Int, Int), BudgetDisplayCell)]]
-> Table
     AccountName AccountName ((Int, Int, Int), BudgetDisplayCell)
forall a b. (a -> b) -> a -> b
$ ([BudgetDisplayCell] -> [((Int, Int, Int), BudgetDisplayCell)])
-> [[BudgetDisplayCell]]
-> [[((Int, Int, Int), BudgetDisplayCell)]]
forall a b. (a -> b) -> [a] -> [b]
map (((Int, Int, Int)
 -> BudgetDisplayCell -> ((Int, Int, Int), BudgetDisplayCell))
-> [(Int, Int, Int)]
-> [BudgetDisplayCell]
-> [((Int, Int, Int), BudgetDisplayCell)]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (,) [(Int, Int, Int)]
widths) [[BudgetDisplayCell]]
displaycells
    Table Header AccountName
rh Header AccountName
ch [[BudgetDisplayCell]]
displaycells = case ReportOpts
-> BudgetReport -> Table AccountName AccountName BudgetCell
budgetReportAsTable ReportOpts
ropts BudgetReport
budgetr of
        Table Header AccountName
rh' Header AccountName
ch' [[BudgetCell]]
vals -> Table AccountName AccountName BudgetDisplayCell
-> Table AccountName AccountName BudgetDisplayCell
forall rh a. Table rh rh a -> Table rh rh a
maybetranspose (Table AccountName AccountName BudgetDisplayCell
 -> Table AccountName AccountName BudgetDisplayCell)
-> ([[BudgetDisplayCell]]
    -> Table AccountName AccountName BudgetDisplayCell)
-> [[BudgetDisplayCell]]
-> Table AccountName AccountName BudgetDisplayCell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Header AccountName
-> Header AccountName
-> [[BudgetDisplayCell]]
-> Table AccountName AccountName BudgetDisplayCell
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table Header AccountName
rh' Header AccountName
ch' ([[BudgetDisplayCell]]
 -> Table AccountName AccountName BudgetDisplayCell)
-> [[BudgetDisplayCell]]
-> Table AccountName AccountName BudgetDisplayCell
forall a b. (a -> b) -> a -> b
$ ([BudgetCell] -> [BudgetDisplayCell])
-> [[BudgetCell]] -> [[BudgetDisplayCell]]
forall a b. (a -> b) -> [a] -> [b]
map ((BudgetCell -> BudgetDisplayCell)
-> [BudgetCell] -> [BudgetDisplayCell]
forall a b. (a -> b) -> [a] -> [b]
map BudgetCell -> BudgetDisplayCell
forall (f :: * -> *).
Functor f =>
(Maybe MixedAmount, f MixedAmount)
-> ((AccountName, Int),
    f ((AccountName, Int), Maybe (AccountName, Int)))
displayCell) [[BudgetCell]]
vals

    displayCell :: (Maybe MixedAmount, f MixedAmount)
-> ((AccountName, Int),
    f ((AccountName, Int), Maybe (AccountName, Int)))
displayCell (Maybe MixedAmount
actual, f MixedAmount
budget) = (MixedAmount -> (AccountName, Int)
showamt MixedAmount
actual', MixedAmount -> ((AccountName, Int), Maybe (AccountName, Int))
budgetAndPerc (MixedAmount -> ((AccountName, Int), Maybe (AccountName, Int)))
-> f MixedAmount
-> f ((AccountName, Int), Maybe (AccountName, Int))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> f MixedAmount
budget)
      where
        actual' :: MixedAmount
actual' = MixedAmount -> Maybe MixedAmount -> MixedAmount
forall a. a -> Maybe a -> a
fromMaybe MixedAmount
nullmixedamt Maybe MixedAmount
actual
        budgetAndPerc :: MixedAmount -> ((AccountName, Int), Maybe (AccountName, Int))
budgetAndPerc MixedAmount
b = (MixedAmount -> (AccountName, Int)
showamt MixedAmount
b, DecimalRaw Integer -> (AccountName, Int)
forall i.
(Integral i, Show i) =>
DecimalRaw i -> (AccountName, Int)
showper (DecimalRaw Integer -> (AccountName, Int))
-> Maybe (DecimalRaw Integer) -> Maybe (AccountName, Int)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> MixedAmount -> MixedAmount -> Maybe (DecimalRaw Integer)
percentage MixedAmount
actual' MixedAmount
b)
        showamt :: MixedAmount -> (AccountName, Int)
showamt = (\(WideBuilder Builder
b Int
w) -> (Text -> AccountName
TL.toStrict (Text -> AccountName) -> Text -> AccountName
forall a b. (a -> b) -> a -> b
$ Builder -> Text
TB.toLazyText Builder
b, Int
w)) (WideBuilder -> (AccountName, Int))
-> (MixedAmount -> WideBuilder)
-> MixedAmount
-> (AccountName, Int)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountDisplayOpts -> MixedAmount -> WideBuilder
showMixedAmountB AmountDisplayOpts
oneLine{displayColour :: Bool
displayColour=Bool
color_, displayMaxWidth :: Maybe Int
displayMaxWidth=Int -> Maybe Int
forall a. a -> Maybe a
Just Int
32}
        showper :: DecimalRaw i -> (AccountName, Int)
showper DecimalRaw i
p = let str :: AccountName
str = String -> AccountName
T.pack (DecimalRaw i -> String
forall a. Show a => a -> String
show (DecimalRaw i -> String) -> DecimalRaw i -> String
forall a b. (a -> b) -> a -> b
$ Word8 -> DecimalRaw i -> DecimalRaw i
forall i. Integral i => Word8 -> DecimalRaw i -> DecimalRaw i
roundTo Word8
0 DecimalRaw i
p) in (AccountName
str, AccountName -> Int
T.length AccountName
str)
    cellWidth :: ((a, a), Maybe ((a, b), Maybe (a, c))) -> (a, b, c)
cellWidth ((a
_,a
wa), Maybe ((a, b), Maybe (a, c))
Nothing)                    = (a
wa,  b
0,  c
0)
    cellWidth ((a
_,a
wa), Just ((a
_,b
wb), Maybe (a, c)
Nothing))     = (a
wa, b
wb,  c
0)
    cellWidth ((a
_,a
wa), Just ((a
_,b
wb), Just (a
_,c
wp))) = (a
wa, b
wb, c
wp)

    widths :: [(Int, Int, Int)]
widths = [Int] -> [Int] -> [Int] -> [(Int, Int, Int)]
forall a b c. [a] -> [b] -> [c] -> [(a, b, c)]
zip3 [Int]
actualwidths [Int]
budgetwidths [Int]
percentwidths
    actualwidths :: [Int]
actualwidths  = ([BudgetDisplayCell] -> Int) -> [[BudgetDisplayCell]] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int)
-> ([BudgetDisplayCell] -> [Int]) -> [BudgetDisplayCell] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (BudgetDisplayCell -> Int) -> [BudgetDisplayCell] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ((Int, Int, Int) -> Int
forall a b c. (a, b, c) -> a
first3  ((Int, Int, Int) -> Int)
-> (BudgetDisplayCell -> (Int, Int, Int))
-> BudgetDisplayCell
-> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BudgetDisplayCell -> (Int, Int, Int)
forall b c a a a a.
(Num b, Num c) =>
((a, a), Maybe ((a, b), Maybe (a, c))) -> (a, b, c)
cellWidth)) [[BudgetDisplayCell]]
cols
    budgetwidths :: [Int]
budgetwidths  = ([BudgetDisplayCell] -> Int) -> [[BudgetDisplayCell]] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int)
-> ([BudgetDisplayCell] -> [Int]) -> [BudgetDisplayCell] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (BudgetDisplayCell -> Int) -> [BudgetDisplayCell] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ((Int, Int, Int) -> Int
forall a b c. (a, b, c) -> b
second3 ((Int, Int, Int) -> Int)
-> (BudgetDisplayCell -> (Int, Int, Int))
-> BudgetDisplayCell
-> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BudgetDisplayCell -> (Int, Int, Int)
forall b c a a a a.
(Num b, Num c) =>
((a, a), Maybe ((a, b), Maybe (a, c))) -> (a, b, c)
cellWidth)) [[BudgetDisplayCell]]
cols
    percentwidths :: [Int]
percentwidths = ([BudgetDisplayCell] -> Int) -> [[BudgetDisplayCell]] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int)
-> ([BudgetDisplayCell] -> [Int]) -> [BudgetDisplayCell] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (BudgetDisplayCell -> Int) -> [BudgetDisplayCell] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ((Int, Int, Int) -> Int
forall a b c. (a, b, c) -> c
third3  ((Int, Int, Int) -> Int)
-> (BudgetDisplayCell -> (Int, Int, Int))
-> BudgetDisplayCell
-> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BudgetDisplayCell -> (Int, Int, Int)
forall b c a a a a.
(Num b, Num c) =>
((a, a), Maybe ((a, b), Maybe (a, c))) -> (a, b, c)
cellWidth)) [[BudgetDisplayCell]]
cols
    cols :: [[BudgetDisplayCell]]
cols = [[BudgetDisplayCell]] -> [[BudgetDisplayCell]]
forall a. [[a]] -> [[a]]
transpose [[BudgetDisplayCell]]
displaycells

    -- XXX lay out actual, percentage and/or goal in the single table cell for now, should probably use separate cells
    showcell :: (Int, Int, Int) -> BudgetDisplayCell -> Cell
    showcell :: (Int, Int, Int) -> BudgetDisplayCell -> Cell
showcell (Int
actualwidth, Int
budgetwidth, Int
percentwidth) ((AccountName
actual,Int
wa), Maybe ((AccountName, Int), Maybe (AccountName, Int))
mbudget) =
        Align -> [WideBuilder] -> Cell
Cell Align
TopRight [Builder -> Int -> WideBuilder
WideBuilder ( AccountName -> Builder
TB.fromText (Int -> AccountName -> AccountName
T.replicate (Int
actualwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
wa) AccountName
" ")
                                   Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> AccountName -> Builder
TB.fromText AccountName
actual
                                   Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
budgetstr
                                   ) (Int
actualwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
totalbudgetwidth)]
      where
        totalpercentwidth :: Int
totalpercentwidth = if Int
percentwidth Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0 then Int
0 else Int
percentwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
5
        totalbudgetwidth :: Int
totalbudgetwidth  = if Int
budgetwidth Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0 then Int
0 else Int
budgetwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
totalpercentwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
3
        budgetstr :: Builder
budgetstr = AccountName -> Builder
TB.fromText (AccountName -> Builder) -> AccountName -> Builder
forall a b. (a -> b) -> a -> b
$ case Maybe ((AccountName, Int), Maybe (AccountName, Int))
mbudget of
          Maybe ((AccountName, Int), Maybe (AccountName, Int))
Nothing                             -> Int -> AccountName -> AccountName
T.replicate Int
totalbudgetwidth AccountName
" "
          Just ((AccountName
budget, Int
wb), Maybe (AccountName, Int)
Nothing)        -> AccountName
" [" AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> Int -> AccountName -> AccountName
T.replicate Int
totalpercentwidth AccountName
" " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> Int -> AccountName -> AccountName
T.replicate (Int
budgetwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
wb) AccountName
" " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
budget AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
"]"
          Just ((AccountName
budget, Int
wb), Just (AccountName
pct, Int
wp)) -> AccountName
" [" AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> Int -> AccountName -> AccountName
T.replicate (Int
percentwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
wp) AccountName
" " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
pct AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
"% of " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> Int -> AccountName -> AccountName
T.replicate (Int
budgetwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
wb) AccountName
" " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
budget AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> AccountName
"]"

    -- | Calculate the percentage of actual change to budget goal to show, if any.
    -- If valuing at cost, both amounts are converted to cost before comparing.
    -- A percentage will not be shown if:
    -- - actual or goal are not the same, single, commodity
    -- - the goal is zero
    percentage :: Change -> BudgetGoal -> Maybe Percentage
    percentage :: MixedAmount -> MixedAmount -> Maybe (DecimalRaw Integer)
percentage MixedAmount
actual MixedAmount
budget =
      case (MixedAmount -> [Amount]
costedAmounts MixedAmount
actual, MixedAmount -> [Amount]
costedAmounts MixedAmount
budget) of
        ([Amount
a], [Amount
b]) | (Amount -> AccountName
acommodity Amount
a AccountName -> AccountName -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> AccountName
acommodity Amount
b Bool -> Bool -> Bool
|| Amount -> Bool
amountLooksZero Amount
a) Bool -> Bool -> Bool
&& Bool -> Bool
not (Amount -> Bool
amountLooksZero Amount
b)
            -> DecimalRaw Integer -> Maybe (DecimalRaw Integer)
forall a. a -> Maybe a
Just (DecimalRaw Integer -> Maybe (DecimalRaw Integer))
-> DecimalRaw Integer -> Maybe (DecimalRaw Integer)
forall a b. (a -> b) -> a -> b
$ DecimalRaw Integer
100 DecimalRaw Integer -> DecimalRaw Integer -> DecimalRaw Integer
forall a. Num a => a -> a -> a
* Amount -> DecimalRaw Integer
aquantity Amount
a DecimalRaw Integer -> DecimalRaw Integer -> DecimalRaw Integer
forall a. Fractional a => a -> a -> a
/ Amount -> DecimalRaw Integer
aquantity Amount
b
        ([Amount], [Amount])
_   -> -- trace (pshow $ (maybecost actual, maybecost budget))  -- debug missing percentage
               Maybe (DecimalRaw Integer)
forall a. Maybe a
Nothing
      where
        costedAmounts :: MixedAmount -> [Amount]
costedAmounts = case Costing
cost_ of
            Costing
Cost   -> MixedAmount -> [Amount]
amounts (MixedAmount -> [Amount])
-> (MixedAmount -> MixedAmount) -> MixedAmount -> [Amount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. MixedAmount -> MixedAmount
mixedAmountCost
            Costing
NoCost -> MixedAmount -> [Amount]
amounts

    maybetranspose :: Table rh rh a -> Table rh rh a
maybetranspose | Bool
transpose_ = \(Table Header rh
rh Header rh
ch [[a]]
vals) -> Header rh -> Header rh -> [[a]] -> Table rh rh a
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table Header rh
ch Header rh
rh ([[a]] -> [[a]]
forall a. [[a]] -> [[a]]
transpose [[a]]
vals)
                   | Bool
otherwise  = Table rh rh a -> Table rh rh a
forall a. a -> a
id

-- | Build a 'Table' from a multi-column balance report.
budgetReportAsTable :: ReportOpts -> BudgetReport -> Table Text Text (Maybe MixedAmount, Maybe MixedAmount)
budgetReportAsTable :: ReportOpts
-> BudgetReport -> Table AccountName AccountName BudgetCell
budgetReportAsTable
  ropts :: ReportOpts
ropts@ReportOpts{BalanceType
balancetype_ :: BalanceType
balancetype_ :: ReportOpts -> BalanceType
balancetype_}
  (PeriodicReport [DateSpan]
spans [PeriodicReportRow DisplayName BudgetCell]
rows (PeriodicReportRow ()
_ [BudgetCell]
coltots BudgetCell
grandtot BudgetCell
grandavg)) =
    Table AccountName AccountName BudgetCell
-> Table AccountName AccountName BudgetCell
forall ch.
Table AccountName ch BudgetCell -> Table AccountName ch BudgetCell
addtotalrow (Table AccountName AccountName BudgetCell
 -> Table AccountName AccountName BudgetCell)
-> Table AccountName AccountName BudgetCell
-> Table AccountName AccountName BudgetCell
forall a b. (a -> b) -> a -> b
$
    Header AccountName
-> Header AccountName
-> [[BudgetCell]]
-> Table AccountName AccountName BudgetCell
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table
      (Properties -> [Header AccountName] -> Header AccountName
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
NoLine ([Header AccountName] -> Header AccountName)
-> [Header AccountName] -> Header AccountName
forall a b. (a -> b) -> a -> b
$ (AccountName -> Header AccountName)
-> [AccountName] -> [Header AccountName]
forall a b. (a -> b) -> [a] -> [b]
map AccountName -> Header AccountName
forall h. h -> Header h
Header [AccountName]
accts)
      (Properties -> [Header AccountName] -> Header AccountName
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
NoLine ([Header AccountName] -> Header AccountName)
-> [Header AccountName] -> Header AccountName
forall a b. (a -> b) -> a -> b
$ (AccountName -> Header AccountName)
-> [AccountName] -> [Header AccountName]
forall a b. (a -> b) -> [a] -> [b]
map AccountName -> Header AccountName
forall h. h -> Header h
Header [AccountName]
colheadings)
      ((PeriodicReportRow DisplayName BudgetCell -> [BudgetCell])
-> [PeriodicReportRow DisplayName BudgetCell] -> [[BudgetCell]]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName BudgetCell -> [BudgetCell]
forall a a. PeriodicReportRow a a -> [a]
rowvals [PeriodicReportRow DisplayName BudgetCell]
rows)
  where
    colheadings :: [AccountName]
colheadings = (DateSpan -> AccountName) -> [DateSpan] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map (BalanceType -> [DateSpan] -> DateSpan -> AccountName
reportPeriodName BalanceType
balancetype_ [DateSpan]
spans) [DateSpan]
spans
                  [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ [AccountName
"  Total" | ReportOpts -> Bool
row_total_ ReportOpts
ropts]
                  [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ [AccountName
"Average" | ReportOpts -> Bool
average_ ReportOpts
ropts]

    accts :: [AccountName]
accts = (PeriodicReportRow DisplayName BudgetCell -> AccountName)
-> [PeriodicReportRow DisplayName BudgetCell] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName BudgetCell -> AccountName
forall a. PeriodicReportRow DisplayName a -> AccountName
renderacct [PeriodicReportRow DisplayName BudgetCell]
rows
    -- FIXME. Have to check explicitly for which to render here, since
    -- budgetReport sets accountlistmode to ALTree. Find a principled way to do
    -- this.
    renderacct :: PeriodicReportRow DisplayName a -> AccountName
renderacct PeriodicReportRow DisplayName a
row = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
        AccountListMode
ALTree -> Int -> AccountName -> AccountName
T.replicate ((PeriodicReportRow DisplayName a -> Int
forall a. PeriodicReportRow DisplayName a -> Int
prrDepth PeriodicReportRow DisplayName a
row Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)Int -> Int -> Int
forall a. Num a => a -> a -> a
*Int
2) AccountName
" " AccountName -> AccountName -> AccountName
forall a. Semigroup a => a -> a -> a
<> PeriodicReportRow DisplayName a -> AccountName
forall a. PeriodicReportRow DisplayName a -> AccountName
prrDisplayName PeriodicReportRow DisplayName a
row
        AccountListMode
ALFlat -> Int -> AccountName -> AccountName
accountNameDrop (ReportOpts -> Int
drop_ ReportOpts
ropts) (AccountName -> AccountName) -> AccountName -> AccountName
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow DisplayName a -> AccountName
forall a. PeriodicReportRow DisplayName a -> AccountName
prrFullName PeriodicReportRow DisplayName a
row
    rowvals :: PeriodicReportRow a a -> [a]
rowvals (PeriodicReportRow a
_ [a]
as a
rowtot a
rowavg) =
        [a]
as [a] -> [a] -> [a]
forall a. [a] -> [a] -> [a]
++ [a
rowtot | ReportOpts -> Bool
row_total_ ReportOpts
ropts] [a] -> [a] -> [a]
forall a. [a] -> [a] -> [a]
++ [a
rowavg | ReportOpts -> Bool
average_ ReportOpts
ropts]
    addtotalrow :: Table AccountName ch BudgetCell -> Table AccountName ch BudgetCell
addtotalrow
      | ReportOpts -> Bool
no_total_ ReportOpts
ropts = Table AccountName ch BudgetCell -> Table AccountName ch BudgetCell
forall a. a -> a
id
      | Bool
otherwise = (Table AccountName ch BudgetCell
-> SemiTable AccountName BudgetCell
-> Table AccountName ch BudgetCell
forall rh ch a. Table rh ch a -> SemiTable rh a -> Table rh ch a
+----+ (AccountName -> [BudgetCell] -> SemiTable AccountName BudgetCell
forall rh a. rh -> [a] -> SemiTable rh a
row AccountName
"" ([BudgetCell] -> SemiTable AccountName BudgetCell)
-> [BudgetCell] -> SemiTable AccountName BudgetCell
forall a b. (a -> b) -> a -> b
$
                       [BudgetCell]
coltots [BudgetCell] -> [BudgetCell] -> [BudgetCell]
forall a. [a] -> [a] -> [a]
++ [BudgetCell
grandtot | ReportOpts -> Bool
row_total_ ReportOpts
ropts Bool -> Bool -> Bool
&& Bool -> Bool
not ([BudgetCell] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [BudgetCell]
coltots)]
                               [BudgetCell] -> [BudgetCell] -> [BudgetCell]
forall a. [a] -> [a] -> [a]
++ [BudgetCell
grandavg | ReportOpts -> Bool
average_ ReportOpts
ropts Bool -> Bool -> Bool
&& Bool -> Bool
not ([BudgetCell] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [BudgetCell]
coltots)]
                    ))

-- XXX generalise this with multiBalanceReportAsCsv ?
-- | Render a budget report as CSV. Like multiBalanceReportAsCsv,
-- but includes alternating actual and budget amount columns.
budgetReportAsCsv :: ReportOpts -> BudgetReport -> CSV
budgetReportAsCsv :: ReportOpts -> BudgetReport -> CSV
budgetReportAsCsv
  ReportOpts{Bool
average_ :: Bool
average_ :: ReportOpts -> Bool
average_, Bool
row_total_ :: Bool
row_total_ :: ReportOpts -> Bool
row_total_, Bool
no_total_ :: Bool
no_total_ :: ReportOpts -> Bool
no_total_, Bool
transpose_ :: Bool
transpose_ :: ReportOpts -> Bool
transpose_}
  (PeriodicReport [DateSpan]
colspans [PeriodicReportRow DisplayName BudgetCell]
items (PeriodicReportRow ()
_ [BudgetCell]
abtotals (Maybe MixedAmount
magrandtot,Maybe MixedAmount
mbgrandtot) (Maybe MixedAmount
magrandavg,Maybe MixedAmount
mbgrandavg)))
  = (if Bool
transpose_ then CSV -> CSV
forall a. [[a]] -> [[a]]
transpose else CSV -> CSV
forall a. a -> a
id) (CSV -> CSV) -> CSV -> CSV
forall a b. (a -> b) -> a -> b
$

  -- heading row
  (AccountName
"Account" AccountName -> [AccountName] -> [AccountName]
forall a. a -> [a] -> [a]
:
   (DateSpan -> [AccountName]) -> [DateSpan] -> [AccountName]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\DateSpan
span -> [DateSpan -> AccountName
showDateSpan DateSpan
span, AccountName
"budget"]) [DateSpan]
colspans
   [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ CSV -> [AccountName]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[AccountName
"Total"  ,AccountName
"budget"] | Bool
row_total_]
   [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ CSV -> [AccountName]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[AccountName
"Average",AccountName
"budget"] | Bool
average_]
  ) [AccountName] -> CSV -> CSV
forall a. a -> [a] -> [a]
:

  -- account rows
  [DisplayName -> AccountName
displayFull DisplayName
a AccountName -> [AccountName] -> [AccountName]
forall a. a -> [a] -> [a]
:
   (Maybe MixedAmount -> AccountName)
-> [Maybe MixedAmount] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map Maybe MixedAmount -> AccountName
showmamt ([BudgetCell] -> [Maybe MixedAmount]
forall a. [(a, a)] -> [a]
flattentuples [BudgetCell]
abamts)
   [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ CSV -> [AccountName]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
mactualrowtot, Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
mbudgetrowtot] | Bool
row_total_]
   [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ CSV -> [AccountName]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
mactualrowavg, Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
mbudgetrowavg] | Bool
average_]
  | PeriodicReportRow DisplayName
a [BudgetCell]
abamts (Maybe MixedAmount
mactualrowtot,Maybe MixedAmount
mbudgetrowtot) (Maybe MixedAmount
mactualrowavg,Maybe MixedAmount
mbudgetrowavg) <- [PeriodicReportRow DisplayName BudgetCell]
items
  ]

  -- totals row
  CSV -> CSV -> CSV
forall a. [a] -> [a] -> [a]
++ [CSV] -> CSV
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [
    [
    AccountName
"Total:" AccountName -> [AccountName] -> [AccountName]
forall a. a -> [a] -> [a]
:
    (Maybe MixedAmount -> AccountName)
-> [Maybe MixedAmount] -> [AccountName]
forall a b. (a -> b) -> [a] -> [b]
map Maybe MixedAmount -> AccountName
showmamt ([BudgetCell] -> [Maybe MixedAmount]
forall a. [(a, a)] -> [a]
flattentuples [BudgetCell]
abtotals)
    [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ CSV -> [AccountName]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
magrandtot,Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
mbgrandtot] | Bool
row_total_]
    [AccountName] -> [AccountName] -> [AccountName]
forall a. [a] -> [a] -> [a]
++ CSV -> [AccountName]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
magrandavg,Maybe MixedAmount -> AccountName
showmamt Maybe MixedAmount
mbgrandavg] | Bool
average_]
    ]
  | Bool -> Bool
not Bool
no_total_
  ]

  where
    flattentuples :: [(a, a)] -> [a]
flattentuples [(a, a)]
abs = [[a]] -> [a]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[a
a,a
b] | (a
a,a
b) <- [(a, a)]
abs]
    showmamt :: Maybe MixedAmount -> AccountName
showmamt = AccountName
-> (MixedAmount -> AccountName) -> Maybe MixedAmount -> AccountName
forall b a. b -> (a -> b) -> Maybe a -> b
maybe AccountName
"" (WideBuilder -> AccountName
wbToText (WideBuilder -> AccountName)
-> (MixedAmount -> WideBuilder) -> MixedAmount -> AccountName
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AmountDisplayOpts -> MixedAmount -> WideBuilder
showMixedAmountB AmountDisplayOpts
oneLine)

-- tests

tests_BudgetReport :: TestTree
tests_BudgetReport = String -> [TestTree] -> TestTree
tests String
"BudgetReport" [
 ]