mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-26 20:02:27 +03:00
budget: refactor; show budget goals even with no or zero actual
This makes budget reports more intuitive. It is a temporary hack which can misorder columns in some cases (if actual and budget activity occur in a different range of columns). We should redo this in a more principled way.
This commit is contained in:
parent
2b34ff1184
commit
2a87ea56ff
@ -251,7 +251,7 @@ module Hledger.Cli.Commands.Balance (
|
||||
) where
|
||||
|
||||
import Data.Decimal
|
||||
import Data.List (intercalate, nub)
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Text as T
|
||||
@ -329,7 +329,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
||||
|
||||
_ | boolopt "budget" rawopts -> do
|
||||
-- multi column budget report
|
||||
reportspan <- dbg1 "reportspan" <$> reportSpan j ropts
|
||||
reportspan <- reportSpan j ropts
|
||||
let budget = budgetJournal opts reportspan j
|
||||
j' = budgetRollUp opts budget j
|
||||
report = dbg1 "report" $ multiBalanceReport ropts (queryFromOpts d ropts) j'
|
||||
@ -655,7 +655,7 @@ type ActualAmountsReport = MultiBalanceReport
|
||||
type BudgetAmountsReport = MultiBalanceReport
|
||||
type ActualAmountsTable = Table String String MixedAmount
|
||||
type BudgetAmountsTable = Table String String MixedAmount
|
||||
type ActualAndBudgetAmountsTable = Table String String (MixedAmount, Maybe MixedAmount)
|
||||
type ActualAndBudgetAmountsTable = Table String String (Maybe MixedAmount, Maybe MixedAmount)
|
||||
type Percentage = Decimal
|
||||
|
||||
-- | Given two multi-column balance reports, the first representing a budget
|
||||
@ -674,28 +674,29 @@ multiBalanceReportWithBudgetAsText opts budgetr actualr =
|
||||
HistoricalBalance -> "Ending balances (historical)"
|
||||
|
||||
actualandbudgetamts :: ActualAndBudgetAmountsTable
|
||||
actualandbudgetamts = combine (balanceReportAsTable opts actualr) (balanceReportAsTable opts budgetr)
|
||||
actualandbudgetamts = combineTables (balanceReportAsTable opts actualr) (balanceReportAsTable opts budgetr)
|
||||
|
||||
showcell :: (ActualAmount, Maybe BudgetAmount) -> String
|
||||
showcell (actual, mbudget) =
|
||||
case (actual, mbudget) of
|
||||
(actual, Nothing) ->
|
||||
printf ("%"++show actualwidth++"s " ++ replicate (percentwidth + 7 + budgetwidth) ' ') (showamt actual)
|
||||
(actual, Just budget) ->
|
||||
case percentage actual budget of
|
||||
Just pct ->
|
||||
printf ("%"++show actualwidth++"s [%"++show percentwidth++"s%% of %"++show budgetwidth++"s]")
|
||||
(showamt actual) (show $ roundTo 0 pct) (showamt budget)
|
||||
Nothing ->
|
||||
printf ("%"++show actualwidth++"s ["++replicate (percentwidth+5) ' '++"%"++show budgetwidth++"s]")
|
||||
(showamt actual) (showamt budget)
|
||||
showcell :: (Maybe ActualAmount, Maybe BudgetAmount) -> String
|
||||
showcell (mactual, mbudget) = actualstr ++ " " ++ budgetstr
|
||||
where
|
||||
actualwidth = 7
|
||||
percentwidth = 4
|
||||
budgetwidth = 5
|
||||
actualstr = printf ("%"++show actualwidth++"s") (maybe "" showamt mactual)
|
||||
budgetstr = case (mactual, mbudget) of
|
||||
(_, Nothing) -> replicate (percentwidth + 7 + budgetwidth) ' '
|
||||
(mactual, Just budget) ->
|
||||
case percentage mactual budget of
|
||||
Just pct ->
|
||||
printf ("[%"++show percentwidth++"s%% of %"++show budgetwidth++"s]")
|
||||
(show $ roundTo 0 pct) (showamt budget)
|
||||
Nothing ->
|
||||
printf ("["++replicate (percentwidth+5) ' '++"%"++show budgetwidth++"s]")
|
||||
(showamt budget)
|
||||
|
||||
percentage :: ActualAmount -> BudgetAmount -> Maybe Percentage
|
||||
percentage actual budget =
|
||||
percentage :: Maybe ActualAmount -> BudgetAmount -> Maybe Percentage
|
||||
percentage Nothing _ = Nothing
|
||||
percentage (Just actual) budget =
|
||||
-- percentage of budget consumed is always computed in the cost basis
|
||||
case (toCost actual, toCost budget) of
|
||||
(Mixed [a1], Mixed [a2])
|
||||
@ -711,28 +712,37 @@ multiBalanceReportWithBudgetAsText opts budgetr actualr =
|
||||
| otherwise = showMixedAmountOneLineWithoutPrice
|
||||
|
||||
-- Combine a table of actual amounts and a table of budgeted amounts into
|
||||
-- a single table of (actualamount, Maybe budgetamount) tuples.
|
||||
-- The budget table's row/column titles should be a subset of the actual table's.
|
||||
-- (This is satisfied by the construction of the budget report and the
|
||||
-- process of rolling up account names.)
|
||||
combine :: ActualAmountsTable -> BudgetAmountsTable -> ActualAndBudgetAmountsTable
|
||||
combine (Table l t d) (Table l' t' d') = Table l t combinedRows
|
||||
-- a single table of (Maybe actualamount, Maybe budgetamount) tuples.
|
||||
-- The actual and budget table need not have the same account rows or date columns.
|
||||
-- Every row and column from either table will appear in the combined table.
|
||||
-- TODO better to combine the reports, not these tables which are just rendering helpers
|
||||
combineTables :: ActualAmountsTable -> BudgetAmountsTable -> ActualAndBudgetAmountsTable
|
||||
combineTables (Table aaccthdrs adatehdrs arows) (Table baccthdrs bdatehdrs brows) =
|
||||
addtotalrow $ Table caccthdrs cdatehdrs crows
|
||||
where
|
||||
-- For all accounts that are present in the budget, zip actual amounts with budget amounts
|
||||
combinedRows = [ combineRow row budgetRow
|
||||
| (acct, row) <- zip (headerContents l) d
|
||||
, let budgetRow =
|
||||
if acct == "" then [] -- "" is totals row
|
||||
else fromMaybe [] $ Map.lookup acct budgetAccts
|
||||
]
|
||||
-- Budget could cover smaller interval of time than the whole journal.
|
||||
-- Headers for budget row will always be a sublist of headers of row
|
||||
combineRow r br =
|
||||
let reportRow = zip (headerContents t) r
|
||||
budgetRow = Map.fromList $ zip (headerContents t') br
|
||||
findBudgetVal hdr = Map.lookup hdr budgetRow
|
||||
in map (\(hdr, val) -> (val, findBudgetVal hdr)) reportRow
|
||||
budgetAccts = Map.fromList $ zip (headerContents l') d'
|
||||
[aaccts, adates, baccts, bdates] = map headerContents [aaccthdrs, adatehdrs, baccthdrs, bdatehdrs]
|
||||
-- combined account names
|
||||
-- TODO Can't sort these or things will fall apart.
|
||||
caccts = dbg2 "caccts" $ init $ (dbg2 "aaccts" $ filter (not . null) aaccts) `union` (dbg2 "baccts" baccts)
|
||||
caccthdrs = T.Group NoLine $ map Header $ caccts
|
||||
-- Actual column dates and budget column dates could be different.
|
||||
-- TODO Can't easily combine these preserving correct order, will go wrong on monthly reports probably.
|
||||
cdates = dbg2 "cdates" $ sort $ (dbg2 "adates" adates) `union` (dbg2 "bdates" bdates)
|
||||
cdatehdrs = T.Group NoLine $ map Header cdates
|
||||
-- corresponding rows of combined actual and/or budget amounts
|
||||
crows = [ combineRow (actualRow a) (budgetRow a) | a <- caccts ]
|
||||
-- totals row
|
||||
addtotalrow | no_total_ opts = id
|
||||
| otherwise = (+----+ (row "" $ combineRow (actualRow "") (budgetRow "")))
|
||||
-- helpers
|
||||
combineRow arow brow =
|
||||
dbg1 "row" $ [(actualAmt d, budgetAmt d) | d <- cdates]
|
||||
where
|
||||
actualAmt date = Map.lookup date $ Map.fromList $ zip adates arow
|
||||
budgetAmt date = Map.lookup date $ Map.fromList $ zip bdates brow
|
||||
|
||||
actualRow acct = fromMaybe [] $ Map.lookup acct $ Map.fromList $ zip aaccts arows
|
||||
budgetRow acct = fromMaybe [] $ Map.lookup acct $ Map.fromList $ zip baccts brows
|
||||
|
||||
-- | Given a table representing a multi-column balance report (for example,
|
||||
-- made using 'balanceReportAsTable'), render it in a format suitable for
|
||||
|
@ -41,7 +41,7 @@ Balance changes in 2016/12/01-2016/12/03:
|
||||
expenses:food || $10 [ 100% of $10] $9 [ 90% of $10] $11 [ 110% of $10]
|
||||
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15]
|
||||
-----------------------++------------------------------------------------------------------------------
|
||||
|| 0 0 0
|
||||
|| 0 [ 0% of 0] 0 [ 0% of 0] 0 [ 0% of 0]
|
||||
|
||||
# 2. --show-unbudgeted
|
||||
$ hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget --show-unbudgeted
|
||||
@ -55,7 +55,7 @@ Balance changes in 2016/12/01-2016/12/03:
|
||||
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15]
|
||||
expenses:movies || 0 0 $25
|
||||
------------------++------------------------------------------------------------------------------
|
||||
|| 0 0 0
|
||||
|| 0 [ 0% of 0] 0 [ 0% of 0] 0 [ 0% of 0]
|
||||
|
||||
# 3. Test that budget works with mix of commodities
|
||||
<
|
||||
@ -102,7 +102,8 @@ Balance changes in 2016/12/01-2016/12/03:
|
||||
expenses:food || £10 [ 150% of $10] 20 CAD [ 210% of $10] $11 [ 110% of $10]
|
||||
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15]
|
||||
-----------------------++---------------------------------------------------------------------------------------
|
||||
|| $-15, £10 $-21.0, 20 CAD 0
|
||||
|| $-15, £10 [ 0% of 0] $-21.0, 20 CAD [ 0% of 0] 0 [ 0% of 0]
|
||||
# TODO zero totals ^
|
||||
|
||||
<
|
||||
~ daily
|
||||
@ -144,7 +145,8 @@ Balance changes in 2018/01/01-2018/01/03:
|
||||
b || 1 0 1
|
||||
c || 1 0 1
|
||||
---++------------------------------------------------------------------------------
|
||||
|| 3 0 3
|
||||
|| 3 [ 30% of 10] 0 [ 0% of 10] 3 [ 30% of 10]
|
||||
# TODO misleading totals ? ^
|
||||
|
||||
# 6. And with -W it selects the weekly budget, defined by all weekly periodic transactions.
|
||||
$ hledger -f- bal --budget -W
|
||||
@ -156,7 +158,7 @@ Balance changes in 2018/01/01w01:
|
||||
b || 2 [ 2% of 100]
|
||||
c || 2 [ 0% of 1000]
|
||||
---++--------------------------
|
||||
|| 6
|
||||
|| 6 [ 1% of 1100]
|
||||
|
||||
# 7. A bounded two day budget. The end date is exclusive as usual.
|
||||
<
|
||||
@ -187,7 +189,7 @@ Balance changes in 2018/01/01-2018/01/04:
|
||||
<unbudgeted>:b || 1 1 1 1
|
||||
a || 1 1 [ 100% of 1] 1 [ 100% of 1] 1
|
||||
----------------++--------------------------------------------------------------------------------------------------------
|
||||
|| 2 2 2 2
|
||||
|| 2 2 [ 200% of 1] 2 [ 200% of 1] 2
|
||||
|
||||
# 8. Multiple bounded budgets.
|
||||
<
|
||||
@ -216,7 +218,7 @@ Balance changes in 2018/01/01-2018/01/04:
|
||||
===++========================================================================================================
|
||||
a || 1 [ 100% of 1] 1 [ 100% of 1] 1 [ 10% of 10] 1 [ 10% of 10]
|
||||
---++--------------------------------------------------------------------------------------------------------
|
||||
|| 1 1 1 1
|
||||
|| 1 [ 100% of 1] 1 [ 100% of 1] 1 [ 10% of 10] 1 [ 10% of 10]
|
||||
|
||||
# 9. A "from A to B" budget should not be included in a report beginning on B.
|
||||
$ hledger -f- bal --budget -D -b 2018/1/3
|
||||
@ -226,5 +228,37 @@ Balance changes in 2018/01/03-2018/01/04:
|
||||
===++====================================================
|
||||
a || 1 [ 10% of 10] 1 [ 10% of 10]
|
||||
---++----------------------------------------------------
|
||||
|| 1 1
|
||||
|| 1 [ 10% of 10] 1 [ 10% of 10]
|
||||
|
||||
<
|
||||
~ daily
|
||||
(a) 1
|
||||
|
||||
2018/1/2
|
||||
(a) 2
|
||||
|
||||
2018/1/2
|
||||
(a) -2
|
||||
|
||||
# 10. accounts with non-zero budget should be shown by default
|
||||
# even if there are no actual transactions in the period,
|
||||
# or if the actual amount is zero.
|
||||
# $ hledger -f- bal --budget -D date:2018/1/1-2018/1/3
|
||||
# Balance changes in 2018/01/01-2018/01/02:
|
||||
|
||||
# || 2018/01/01 2018/01/02
|
||||
# ===++====================================================
|
||||
# a || [ 1] [ 1]
|
||||
# ---++----------------------------------------------------
|
||||
# || [ 1] [ 1]
|
||||
|
||||
# 11. With -E, zeroes are shown
|
||||
$ hledger -f- bal --budget -D date:2018/1/1-2018/1/3 -E
|
||||
Balance changes in 2018/01/01-2018/01/02:
|
||||
|
||||
|| 2018/01/01 2018/01/02
|
||||
===++====================================================
|
||||
a || 0 [ 0% of 1] 0 [ 0% of 1]
|
||||
---++----------------------------------------------------
|
||||
|| 0 [ 0% of 1] 0 [ 0% of 1]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user