2018-04-03 15:07:13 +03:00
|
|
|
{- |
|
|
|
|
-}
|
|
|
|
|
|
|
|
{-# LANGUAGE CPP #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2019-05-10 01:39:43 +03:00
|
|
|
{-# LANGUAGE RecordWildCards #-}
|
2018-04-03 15:07:13 +03:00
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
|
|
|
module Hledger.Reports.BudgetReport
|
|
|
|
where
|
|
|
|
|
|
|
|
import Data.Decimal
|
|
|
|
import Data.List
|
|
|
|
import Data.Maybe
|
|
|
|
#if !(MIN_VERSION_base(4,11,0))
|
|
|
|
import Data.Monoid ((<>))
|
|
|
|
#endif
|
|
|
|
import Data.Ord
|
|
|
|
import Data.Time.Calendar
|
2018-04-24 23:42:12 +03:00
|
|
|
import Safe
|
2018-04-03 15:07:13 +03:00
|
|
|
--import Data.List
|
|
|
|
--import Data.Maybe
|
|
|
|
import qualified Data.Map as Map
|
|
|
|
import Data.Map (Map)
|
|
|
|
import qualified Data.Text as T
|
|
|
|
--import qualified Data.Text.Lazy as TL
|
|
|
|
--import System.Console.CmdArgs.Explicit as C
|
|
|
|
--import Lucid as L
|
|
|
|
import Text.Printf (printf)
|
|
|
|
import Text.Tabular as T
|
|
|
|
--import Text.Tabular.AsciiWide
|
|
|
|
|
|
|
|
import Hledger.Data
|
|
|
|
--import Hledger.Query
|
|
|
|
import Hledger.Utils
|
|
|
|
--import Hledger.Read (mamountp')
|
|
|
|
import Hledger.Reports.ReportOptions
|
|
|
|
import Hledger.Reports.ReportTypes
|
2018-09-24 21:01:52 +03:00
|
|
|
import Hledger.Reports.BalanceReport (sortAccountItemsLike)
|
2019-06-14 21:45:25 +03:00
|
|
|
import Hledger.Reports.MultiBalanceReport
|
2018-04-03 15:07:13 +03:00
|
|
|
|
|
|
|
|
2018-09-24 21:01:52 +03:00
|
|
|
-- for reference:
|
|
|
|
--
|
2018-04-03 15:07:13 +03:00
|
|
|
--type MultiBalanceReportRow = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
|
|
|
|
--type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)
|
2018-09-24 21:01:52 +03:00
|
|
|
--
|
2018-04-03 15:07:13 +03:00
|
|
|
--type PeriodicReportRow a =
|
|
|
|
-- ( AccountName -- ^ A full account name.
|
|
|
|
-- , [a] -- ^ The data value for each subperiod.
|
|
|
|
-- , a -- ^ The total of this row's values.
|
|
|
|
-- , a -- ^ The average of this row's values.
|
|
|
|
-- )
|
|
|
|
|
|
|
|
type BudgetGoal = Change
|
|
|
|
type BudgetTotal = Total
|
|
|
|
type BudgetAverage = Average
|
|
|
|
|
|
|
|
-- | A budget report tracks expected and actual changes per account and subperiod.
|
2018-09-24 21:01:52 +03:00
|
|
|
type BudgetCell = (Maybe Change, Maybe BudgetGoal)
|
|
|
|
type BudgetReport = PeriodicReport BudgetCell
|
|
|
|
type BudgetReportRow = PeriodicReportRow BudgetCell
|
2018-04-03 15:07:13 +03:00
|
|
|
|
2018-04-24 00:18:13 +03:00
|
|
|
-- | Calculate budget goals from all periodic transactions,
|
|
|
|
-- actual balance changes from the regular transactions,
|
|
|
|
-- and compare these to get a 'BudgetReport'.
|
|
|
|
-- Unbudgeted accounts may be hidden or renamed (see budgetRollup).
|
2019-01-25 22:08:13 +03:00
|
|
|
budgetReport :: ReportOpts -> Bool -> DateSpan -> Day -> Journal -> BudgetReport
|
|
|
|
budgetReport ropts' assrt reportspan d j =
|
2018-04-03 15:07:13 +03:00
|
|
|
let
|
2019-01-16 03:32:35 +03:00
|
|
|
-- Budget report demands ALTree mode to ensure subaccounts and subaccount budgets are properly handled
|
2019-01-25 22:08:13 +03:00
|
|
|
-- and that reports with and without --empty make sense when compared side by side
|
2019-01-16 03:32:35 +03:00
|
|
|
ropts = ropts' { accountlistmode_ = ALTree }
|
2019-01-25 22:08:13 +03:00
|
|
|
showunbudgeted = empty_ ropts
|
2019-07-15 13:28:52 +03:00
|
|
|
q = queryFromOpts d ropts
|
|
|
|
budgetedaccts =
|
2018-04-24 00:18:13 +03:00
|
|
|
dbg2 "budgetedacctsinperiod" $
|
2019-07-15 13:28:52 +03:00
|
|
|
nub $
|
2019-01-16 03:32:35 +03:00
|
|
|
concatMap expandAccountName $
|
2019-07-15 13:28:52 +03:00
|
|
|
accountNamesFromPostings $
|
|
|
|
concatMap tpostings $
|
|
|
|
concatMap (flip runPeriodicTransaction reportspan) $
|
2018-04-24 00:18:13 +03:00
|
|
|
jperiodictxns j
|
2019-09-12 00:48:33 +03:00
|
|
|
actualj = dbg1With (("actualj"++).show.jtxns) $ budgetRollUp budgetedaccts showunbudgeted j
|
|
|
|
budgetj = dbg1With (("budgetj"++).show.jtxns) $ budgetJournal assrt ropts reportspan j
|
2018-04-25 09:00:05 +03:00
|
|
|
actualreport@(MultiBalanceReport (actualspans, _, _)) = dbg1 "actualreport" $ multiBalanceReport ropts q actualj
|
2019-01-16 03:32:35 +03:00
|
|
|
budgetgoalreport@(MultiBalanceReport (_, budgetgoalitems, budgetgoaltotals)) = dbg1 "budgetgoalreport" $ multiBalanceReport (ropts{empty_=True}) q budgetj
|
2018-04-25 09:00:05 +03:00
|
|
|
budgetgoalreport'
|
|
|
|
-- If no interval is specified:
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 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.
|
2018-04-25 09:00:05 +03:00
|
|
|
| interval_ ropts == NoInterval = MultiBalanceReport (actualspans, budgetgoalitems, budgetgoaltotals)
|
2019-07-15 13:28:52 +03:00
|
|
|
| otherwise = budgetgoalreport
|
2018-09-24 21:01:52 +03:00
|
|
|
budgetreport = combineBudgetAndActual budgetgoalreport' actualreport
|
|
|
|
sortedbudgetreport = sortBudgetReport ropts j budgetreport
|
2018-04-03 15:07:13 +03:00
|
|
|
in
|
2018-09-24 21:01:52 +03:00
|
|
|
dbg1 "sortedbudgetreport" sortedbudgetreport
|
|
|
|
|
|
|
|
-- | Sort a budget report's rows according to options.
|
|
|
|
sortBudgetReport :: ReportOpts -> Journal -> BudgetReport -> BudgetReport
|
|
|
|
sortBudgetReport ropts j (PeriodicReport (ps, rows, trow)) = PeriodicReport (ps, sortedrows, trow)
|
|
|
|
where
|
2019-07-15 13:28:52 +03:00
|
|
|
sortedrows
|
2018-09-24 21:01:52 +03:00
|
|
|
| sort_amount_ ropts && tree_ ropts = sortTreeBURByActualAmount rows
|
|
|
|
| sort_amount_ ropts = sortFlatBURByActualAmount rows
|
|
|
|
| otherwise = sortByAccountDeclaration rows
|
|
|
|
|
|
|
|
-- Sort a tree-mode budget report's rows by total actual amount at each level.
|
2019-07-15 13:28:52 +03:00
|
|
|
sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
|
2018-09-24 21:01:52 +03:00
|
|
|
sortTreeBURByActualAmount rows = sortedrows
|
|
|
|
where
|
|
|
|
anamesandrows = [(first6 r, r) | r <- rows]
|
|
|
|
anames = map fst anamesandrows
|
|
|
|
atotals = [(a,tot) | (a,_,_,_,(tot,_),_) <- rows]
|
|
|
|
accounttree = accountTree "root" anames
|
|
|
|
accounttreewithbals = mapAccounts setibalance accounttree
|
|
|
|
where
|
|
|
|
setibalance a = a{aibalance=
|
2019-07-15 13:28:52 +03:00
|
|
|
fromMaybe 0 $ -- when there's no actual amount, assume 0; will mess up with negative amounts ? TODO
|
|
|
|
fromMaybe (error "sortTreeByAmount 1") $ -- should not happen, but it's ugly; TODO
|
2018-09-24 21:01:52 +03:00
|
|
|
lookup (aname a) atotals
|
|
|
|
}
|
|
|
|
sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals
|
|
|
|
sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree
|
2019-07-15 13:28:52 +03:00
|
|
|
sortedrows = sortAccountItemsLike sortedanames anamesandrows
|
2018-09-24 21:01:52 +03:00
|
|
|
|
|
|
|
-- Sort a flat-mode budget report's rows by total actual amount.
|
2019-07-15 13:28:52 +03:00
|
|
|
sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
|
2018-09-24 21:01:52 +03:00
|
|
|
sortFlatBURByActualAmount = sortBy (maybeflip $ comparing (fst . fifth6))
|
|
|
|
where
|
|
|
|
maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip
|
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- Sort the report rows by account declaration order then account name.
|
2018-09-24 21:01:52 +03:00
|
|
|
-- <unbudgeted> remains at the top.
|
|
|
|
sortByAccountDeclaration rows = sortedrows
|
|
|
|
where
|
|
|
|
(unbudgetedrow,rows') = partition ((=="<unbudgeted>").first6) rows
|
|
|
|
anamesandrows = [(first6 r, r) | r <- rows']
|
|
|
|
anames = map fst anamesandrows
|
|
|
|
sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames
|
2019-07-15 13:28:52 +03:00
|
|
|
sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows
|
2018-04-03 15:07:13 +03:00
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- | Use all periodic transactions in the journal to generate
|
2018-04-22 20:09:06 +03:00
|
|
|
-- budget transactions in the specified report period.
|
|
|
|
-- Budget transactions are similar to forecast transactions except
|
|
|
|
-- their purpose is to set goal amounts (of change) per account and period.
|
2018-04-03 15:07:13 +03:00
|
|
|
budgetJournal :: Bool -> ReportOpts -> DateSpan -> Journal -> Journal
|
2018-04-22 20:09:06 +03:00
|
|
|
budgetJournal assrt _ropts reportspan j =
|
2018-04-03 15:07:13 +03:00
|
|
|
either error' id $ journalBalanceTransactions assrt j{ jtxns = budgetts }
|
|
|
|
where
|
|
|
|
budgetspan = dbg2 "budgetspan" $ reportspan
|
|
|
|
budgetts =
|
|
|
|
dbg1 "budgetts" $
|
|
|
|
[makeBudgetTxn t
|
|
|
|
| pt <- jperiodictxns j
|
|
|
|
, t <- runPeriodicTransaction pt budgetspan
|
|
|
|
]
|
|
|
|
makeBudgetTxn t = txnTieKnot $ t { tdescription = T.pack "Budget transaction" }
|
|
|
|
|
2018-04-24 23:42:12 +03:00
|
|
|
-- | Adjust a journal's account names for budget reporting, in two ways:
|
2018-04-24 00:18:13 +03:00
|
|
|
--
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 1. accounts with no budget goal anywhere in their ancestry are moved
|
2018-04-24 23:42:12 +03:00
|
|
|
-- under the "unbudgeted" top level account.
|
2018-04-24 00:18:13 +03:00
|
|
|
--
|
2018-04-24 23:42:12 +03:00
|
|
|
-- 2. subaccounts with no budget goal are merged with their closest parent account
|
2019-07-15 13:28:52 +03:00
|
|
|
-- with a budget goal, so that only budgeted accounts are shown.
|
2019-01-25 22:08:13 +03:00
|
|
|
-- This can be disabled by --empty.
|
2018-04-24 00:18:13 +03:00
|
|
|
--
|
|
|
|
budgetRollUp :: [AccountName] -> Bool -> Journal -> Journal
|
|
|
|
budgetRollUp budgetedaccts showunbudgeted j = j { jtxns = remapTxn <$> jtxns j }
|
|
|
|
where
|
|
|
|
remapTxn = mapPostings (map remapPosting)
|
|
|
|
where
|
2018-04-03 15:07:13 +03:00
|
|
|
mapPostings f t = txnTieKnot $ t { tpostings = f $ tpostings t }
|
2019-02-21 07:07:40 +03:00
|
|
|
remapPosting p = p { paccount = remapAccount $ paccount p, poriginal = Just . fromMaybe p $ poriginal p }
|
2018-04-24 00:18:13 +03:00
|
|
|
where
|
2018-04-24 23:42:12 +03:00
|
|
|
remapAccount a
|
|
|
|
| hasbudget = a
|
2019-07-15 13:28:52 +03:00
|
|
|
| hasbudgetedparent = if showunbudgeted then a else budgetedparent
|
2018-04-24 23:42:12 +03:00
|
|
|
| otherwise = if showunbudgeted then u <> acctsep <> a else u
|
2018-04-24 00:18:13 +03:00
|
|
|
where
|
2018-04-24 23:42:12 +03:00
|
|
|
hasbudget = a `elem` budgetedaccts
|
|
|
|
hasbudgetedparent = not $ T.null budgetedparent
|
|
|
|
budgetedparent = headDef "" $ filter (`elem` budgetedaccts) $ parentAccountNames a
|
|
|
|
u = unbudgetedAccountName
|
2018-04-03 15:07:13 +03:00
|
|
|
|
|
|
|
-- | 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 :: MultiBalanceReport -> MultiBalanceReport -> BudgetReport
|
|
|
|
combineBudgetAndActual
|
|
|
|
(MultiBalanceReport (budgetperiods, budgetrows, (budgettots, budgetgrandtot, budgetgrandavg)))
|
|
|
|
(MultiBalanceReport (actualperiods, actualrows, (actualtots, actualgrandtot, actualgrandavg))) =
|
|
|
|
let
|
|
|
|
periods = nub $ sort $ filter (/= nulldatespan) $ budgetperiods ++ actualperiods
|
|
|
|
|
|
|
|
-- first, combine any corresponding budget goals with actual changes
|
|
|
|
rows1 =
|
|
|
|
[ (acct, treeacct, treeindent, amtandgoals, totamtandgoal, avgamtandgoal)
|
|
|
|
| (acct, treeacct, treeindent, actualamts, actualtot, actualavg) <- actualrows
|
|
|
|
, let mbudgetgoals = Map.lookup acct budgetGoalsByAcct :: Maybe ([BudgetGoal], BudgetTotal, BudgetAverage)
|
|
|
|
, let budgetmamts = maybe (replicate (length periods) Nothing) (map Just . first3) mbudgetgoals :: [Maybe BudgetGoal]
|
|
|
|
, let mbudgettot = maybe Nothing (Just . second3) mbudgetgoals :: Maybe BudgetTotal
|
|
|
|
, let mbudgetavg = maybe Nothing (Just . third3) mbudgetgoals :: Maybe BudgetAverage
|
|
|
|
, let acctBudgetByPeriod = Map.fromList [ (p,budgetamt) | (p, Just budgetamt) <- zip budgetperiods budgetmamts ] :: Map DateSpan BudgetGoal
|
|
|
|
, let acctActualByPeriod = Map.fromList [ (p,actualamt) | (p, Just actualamt) <- zip actualperiods (map Just actualamts) ] :: Map DateSpan Change
|
|
|
|
, let amtandgoals = [ (Map.lookup p acctActualByPeriod, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [(Maybe Change, Maybe BudgetGoal)]
|
|
|
|
, let totamtandgoal = (Just actualtot, mbudgettot)
|
|
|
|
, let avgamtandgoal = (Just actualavg, mbudgetavg)
|
|
|
|
]
|
|
|
|
where
|
|
|
|
budgetGoalsByAcct :: Map AccountName ([BudgetGoal], BudgetTotal, BudgetAverage) =
|
|
|
|
Map.fromList [ (acct, (amts, tot, avg)) | (acct, _, _, amts, tot, avg) <- budgetrows ]
|
|
|
|
|
|
|
|
-- next, make rows for budget goals with no actual changes
|
|
|
|
rows2 =
|
|
|
|
[ (acct, treeacct, treeindent, amtandgoals, totamtandgoal, avgamtandgoal)
|
|
|
|
| (acct, treeacct, treeindent, budgetgoals, budgettot, budgetavg) <- budgetrows
|
|
|
|
, not $ acct `elem` acctsdone
|
|
|
|
, let acctBudgetByPeriod = Map.fromList $ zip budgetperiods budgetgoals :: Map DateSpan BudgetGoal
|
|
|
|
, let amtandgoals = [ (Nothing, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [(Maybe Change, Maybe BudgetGoal)]
|
|
|
|
, let totamtandgoal = (Nothing, Just budgettot)
|
|
|
|
, let avgamtandgoal = (Nothing, Just budgetavg)
|
|
|
|
]
|
|
|
|
where
|
|
|
|
acctsdone = map first6 rows1
|
|
|
|
|
|
|
|
-- combine and re-sort rows
|
journal: a new account sorting mechanism, and a bunch of sorting fixes
A bunch of account sorting changes that got intermingled.
First, account codes have been dropped. They can still be parsed and
will be ignored, for now. I don't know if anyone used them.
Instead, account display order is now controlled by the order of account
directives, if any. From the mail list:
I'd like to drop account codes, introduced in hledger 1.9 to control
the display order of accounts. In my experience,
- they are tedious to maintain
- they duplicate/compete with the natural tendency to arrange account
directives to match your mental chart of accounts
- they duplicate/compete with the tree structure created by account
names
and it gets worse if you think about using them more extensively,
eg to classify accounts by type.
Instead, I plan to just let the position (parse order) of account
directives determine the display order of those declared accounts.
Undeclared accounts will be displayed after declared accounts,
sorted alphabetically as usual.
Second, the various account sorting modes have been implemented more
widely and more correctly. All sorting modes (alphabetically, by account
declaration, by amount) should now work correctly in almost all commands
and modes (non-tabular and tabular balance reports, tree and flat modes,
the accounts command). Sorting bugs have been fixed, eg #875.
Only the budget report (balance --budget) does not yet support sorting.
Comprehensive functional tests for sorting in the accounts and balance
commands have been added. If you are confused by some sorting behaviour,
studying these tests is recommended, as sorting gets tricky.
2018-09-23 10:45:07 +03:00
|
|
|
-- TODO: use MBR code
|
2018-04-03 15:07:13 +03:00
|
|
|
-- TODO: respect --sort-amount
|
2018-04-24 00:18:13 +03:00
|
|
|
-- TODO: add --sort-budget to sort by budget goal amount
|
2018-04-03 15:07:13 +03:00
|
|
|
rows :: [PeriodicReportRow (Maybe Change, Maybe BudgetGoal)] =
|
|
|
|
sortBy (comparing first6) $ rows1 ++ rows2
|
journal: a new account sorting mechanism, and a bunch of sorting fixes
A bunch of account sorting changes that got intermingled.
First, account codes have been dropped. They can still be parsed and
will be ignored, for now. I don't know if anyone used them.
Instead, account display order is now controlled by the order of account
directives, if any. From the mail list:
I'd like to drop account codes, introduced in hledger 1.9 to control
the display order of accounts. In my experience,
- they are tedious to maintain
- they duplicate/compete with the natural tendency to arrange account
directives to match your mental chart of accounts
- they duplicate/compete with the tree structure created by account
names
and it gets worse if you think about using them more extensively,
eg to classify accounts by type.
Instead, I plan to just let the position (parse order) of account
directives determine the display order of those declared accounts.
Undeclared accounts will be displayed after declared accounts,
sorted alphabetically as usual.
Second, the various account sorting modes have been implemented more
widely and more correctly. All sorting modes (alphabetically, by account
declaration, by amount) should now work correctly in almost all commands
and modes (non-tabular and tabular balance reports, tree and flat modes,
the accounts command). Sorting bugs have been fixed, eg #875.
Only the budget report (balance --budget) does not yet support sorting.
Comprehensive functional tests for sorting in the accounts and balance
commands have been added. If you are confused by some sorting behaviour,
studying these tests is recommended, as sorting gets tricky.
2018-09-23 10:45:07 +03:00
|
|
|
|
2018-04-03 15:07:13 +03:00
|
|
|
-- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells
|
|
|
|
totalrow =
|
|
|
|
( ""
|
|
|
|
, ""
|
|
|
|
, 0
|
|
|
|
, [ (Map.lookup p totActualByPeriod, Map.lookup p totBudgetByPeriod) | p <- periods ] :: [(Maybe Total, Maybe BudgetTotal)]
|
|
|
|
, ( Just actualgrandtot, Just budgetgrandtot ) :: (Maybe Total, Maybe BudgetTotal)
|
|
|
|
, ( Just actualgrandavg, Just budgetgrandavg ) :: (Maybe Total, Maybe BudgetTotal)
|
|
|
|
)
|
|
|
|
where
|
|
|
|
totBudgetByPeriod = Map.fromList $ zip budgetperiods budgettots :: Map DateSpan BudgetTotal
|
|
|
|
totActualByPeriod = Map.fromList $ zip actualperiods actualtots :: Map DateSpan Change
|
|
|
|
|
|
|
|
in
|
|
|
|
PeriodicReport
|
|
|
|
( periods
|
|
|
|
, rows
|
|
|
|
, totalrow
|
|
|
|
)
|
|
|
|
|
|
|
|
-- | Figure out the overall period of a BudgetReport.
|
|
|
|
budgetReportSpan :: BudgetReport -> DateSpan
|
|
|
|
budgetReportSpan (PeriodicReport ([], _, _)) = DateSpan Nothing Nothing
|
|
|
|
budgetReportSpan (PeriodicReport (spans, _, _)) = DateSpan (spanStart $ head spans) (spanEnd $ last spans)
|
|
|
|
|
|
|
|
-- | Render a budget report as plain text suitable for console output.
|
|
|
|
budgetReportAsText :: ReportOpts -> BudgetReport -> String
|
2019-05-10 01:39:43 +03:00
|
|
|
budgetReportAsText ropts@ReportOpts{..} budgetr@(PeriodicReport ( _, rows, _)) =
|
2019-07-15 13:28:52 +03:00
|
|
|
title ++ "\n\n" ++
|
2019-01-24 23:54:23 +03:00
|
|
|
tableAsText ropts showcell (maybetranspose $ budgetReportAsTable ropts budgetr)
|
2018-04-03 15:07:13 +03:00
|
|
|
where
|
2019-05-24 13:49:01 +03:00
|
|
|
multiperiod = interval_ /= NoInterval
|
2019-05-10 01:39:43 +03:00
|
|
|
title = printf "Budget performance in %s%s:"
|
|
|
|
(showDateSpan $ budgetReportSpan budgetr)
|
2019-05-23 10:36:16 +03:00
|
|
|
(case value_ of
|
2019-05-24 06:52:21 +03:00
|
|
|
Just (AtCost _mc) -> ", valued at cost"
|
2019-05-23 10:36:16 +03:00
|
|
|
Just (AtEnd _mc) -> ", valued at period ends"
|
|
|
|
Just (AtNow _mc) -> ", current value"
|
2019-05-24 13:49:01 +03:00
|
|
|
-- XXX duplicates the above
|
|
|
|
Just (AtDefault _mc) | multiperiod -> ", valued at period ends"
|
|
|
|
Just (AtDefault _mc) -> ", current value"
|
2019-05-23 10:36:16 +03:00
|
|
|
Just (AtDate d _mc) -> ", valued at "++showDate d
|
|
|
|
Nothing -> "")
|
2019-01-17 03:26:34 +03:00
|
|
|
actualwidth =
|
2019-09-12 01:11:23 +03:00
|
|
|
maximum' [ maybe 0 (length . showMixedAmountOneLineWithoutPrice) amt
|
2019-01-17 03:26:34 +03:00
|
|
|
| (_, _, _, amtandgoals, _, _) <- rows
|
|
|
|
, (amt, _) <- amtandgoals ]
|
|
|
|
budgetwidth =
|
2019-09-12 01:11:23 +03:00
|
|
|
maximum' [ maybe 0 (length . showMixedAmountOneLineWithoutPrice) goal
|
2019-01-17 03:26:34 +03:00
|
|
|
| (_, _, _, amtandgoals, _, _) <- rows
|
|
|
|
, (_, goal) <- amtandgoals ]
|
2018-04-23 18:18:51 +03:00
|
|
|
-- XXX lay out actual, percentage and/or goal in the single table cell for now, should probably use separate cells
|
2018-04-03 15:07:13 +03:00
|
|
|
showcell :: (Maybe Change, Maybe BudgetGoal) -> String
|
|
|
|
showcell (mactual, mbudget) = actualstr ++ " " ++ budgetstr
|
|
|
|
where
|
|
|
|
percentwidth = 4
|
2018-04-23 18:18:51 +03:00
|
|
|
actual = fromMaybe 0 mactual
|
|
|
|
actualstr = printf ("%"++show actualwidth++"s") (showamt actual)
|
|
|
|
budgetstr = case mbudget of
|
|
|
|
Nothing -> replicate (percentwidth + 7 + budgetwidth) ' '
|
|
|
|
Just budget ->
|
|
|
|
case percentage actual budget of
|
2018-04-03 15:07:13 +03:00
|
|
|
Just pct ->
|
|
|
|
printf ("[%"++show percentwidth++"s%% of %"++show budgetwidth++"s]")
|
|
|
|
(show $ roundTo 0 pct) (showbudgetamt budget)
|
|
|
|
Nothing ->
|
|
|
|
printf ("["++replicate (percentwidth+5) ' '++"%"++show budgetwidth++"s]")
|
|
|
|
(showbudgetamt budget)
|
|
|
|
|
2018-04-23 18:18:51 +03:00
|
|
|
-- | Calculate the percentage of actual change to budget goal to show, if any.
|
2019-05-23 23:27:37 +03:00
|
|
|
-- If valuing at cost, both amounts are converted to cost before comparing.
|
2018-04-23 18:18:51 +03:00
|
|
|
-- 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 actual budget =
|
2019-05-23 23:27:37 +03:00
|
|
|
case (maybecost $ normaliseMixedAmount actual, maybecost $ normaliseMixedAmount budget) of
|
2019-07-15 13:28:52 +03:00
|
|
|
(Mixed [a], Mixed [b]) | (acommodity a == acommodity b || isZeroAmount a) && not (isZeroAmount b)
|
2018-04-23 18:18:51 +03:00
|
|
|
-> Just $ 100 * aquantity a / aquantity b
|
2019-05-23 23:27:37 +03:00
|
|
|
_ -> -- trace (pshow $ (maybecost actual, maybecost budget)) -- debug missing percentage
|
|
|
|
Nothing
|
2018-04-23 18:18:51 +03:00
|
|
|
where
|
2019-05-23 23:27:37 +03:00
|
|
|
maybecost = if valuationTypeIsCost ropts then costOfMixedAmount else id
|
2018-04-03 15:07:13 +03:00
|
|
|
showamt :: MixedAmount -> String
|
2019-05-10 01:39:43 +03:00
|
|
|
showamt | color_ = cshowMixedAmountOneLineWithoutPrice
|
|
|
|
| otherwise = showMixedAmountOneLineWithoutPrice
|
2018-04-03 15:07:13 +03:00
|
|
|
|
|
|
|
-- don't show the budget amount in color, it messes up alignment
|
|
|
|
showbudgetamt = showMixedAmountOneLineWithoutPrice
|
|
|
|
|
2019-05-10 01:39:43 +03:00
|
|
|
maybetranspose | transpose_ = \(Table rh ch vals) -> Table ch rh (transpose vals)
|
|
|
|
| otherwise = id
|
2019-01-24 23:54:23 +03:00
|
|
|
|
2018-04-03 15:07:13 +03:00
|
|
|
-- | Build a 'Table' from a multi-column balance report.
|
|
|
|
budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount)
|
2019-07-15 13:28:52 +03:00
|
|
|
budgetReportAsTable
|
|
|
|
ropts
|
2018-04-03 15:07:13 +03:00
|
|
|
(PeriodicReport
|
|
|
|
( periods
|
|
|
|
, rows
|
|
|
|
, (_, _, _, coltots, grandtot, grandavg)
|
|
|
|
)) =
|
2019-07-15 13:28:52 +03:00
|
|
|
addtotalrow $
|
2018-04-03 15:07:13 +03:00
|
|
|
Table
|
|
|
|
(T.Group NoLine $ map Header accts)
|
|
|
|
(T.Group NoLine $ map Header colheadings)
|
|
|
|
(map rowvals rows)
|
|
|
|
where
|
|
|
|
colheadings = map showDateSpanMonthAbbrev periods
|
|
|
|
++ (if row_total_ ropts then [" Total"] else [])
|
|
|
|
++ (if average_ ropts then ["Average"] else [])
|
|
|
|
accts = map renderacct rows
|
|
|
|
renderacct (a,a',i,_,_,_)
|
|
|
|
| tree_ ropts = replicate ((i-1)*2) ' ' ++ T.unpack a'
|
|
|
|
| otherwise = T.unpack $ maybeAccountNameDrop ropts a
|
|
|
|
rowvals (_,_,_,as,rowtot,rowavg) = as
|
|
|
|
++ (if row_total_ ropts then [rowtot] else [])
|
|
|
|
++ (if average_ ropts then [rowavg] else [])
|
|
|
|
addtotalrow | no_total_ ropts = id
|
|
|
|
| otherwise = (+----+ (row "" $
|
|
|
|
coltots
|
|
|
|
++ (if row_total_ ropts && not (null coltots) then [grandtot] else [])
|
|
|
|
++ (if average_ ropts && not (null coltots) then [grandavg] else [])
|
|
|
|
))
|
|
|
|
|
|
|
|
-- XXX here for now
|
2019-07-15 13:28:52 +03:00
|
|
|
-- TODO: does not work for flat-by-default reports with --flat not specified explicitly
|
2018-04-03 15:07:13 +03:00
|
|
|
-- | Drop leading components of accounts names as specified by --drop, but only in --flat mode.
|
|
|
|
maybeAccountNameDrop :: ReportOpts -> AccountName -> AccountName
|
2018-05-06 02:12:27 +03:00
|
|
|
maybeAccountNameDrop opts a | flat_ opts = accountNameDrop (drop_ opts) a
|
|
|
|
| otherwise = a
|
2018-09-04 22:23:07 +03:00
|
|
|
|
|
|
|
-- tests
|
|
|
|
|
2018-09-06 23:08:26 +03:00
|
|
|
tests_BudgetReport = tests "BudgetReport" [
|
2018-09-04 22:23:07 +03:00
|
|
|
]
|