budget: declaration and (actual) amount sorting for bal --budget

Account declaration-aware sorting is the default throughout hledger now.
This commit is contained in:
Simon Michael 2018-09-24 08:01:52 -10:00
parent f2b4fca9b0
commit 855bd54d19
4 changed files with 304 additions and 47 deletions

View File

@ -35,12 +35,15 @@ import Hledger.Utils
--import Hledger.Read (mamountp')
import Hledger.Reports.ReportOptions
import Hledger.Reports.ReportTypes
import Hledger.Reports.BalanceReport (sortAccountItemsLike)
import Hledger.Reports.MultiBalanceReports
-- for reference:
--
--type MultiBalanceReportRow = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
--type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)
--
--type PeriodicReportRow a =
-- ( AccountName -- ^ A full account name.
-- , [a] -- ^ The data value for each subperiod.
@ -53,7 +56,9 @@ type BudgetTotal = Total
type BudgetAverage = Average
-- | A budget report tracks expected and actual changes per account and subperiod.
type BudgetReport = PeriodicReport (Maybe Change, Maybe BudgetGoal)
type BudgetCell = (Maybe Change, Maybe BudgetGoal)
type BudgetReport = PeriodicReport BudgetCell
type BudgetReportRow = PeriodicReportRow BudgetCell
-- | Calculate budget goals from all periodic transactions,
-- actual balance changes from the regular transactions,
@ -79,8 +84,54 @@ budgetReport ropts assrt showunbudgeted reportspan d j =
-- it should be safe to replace it with the latter, so they combine well.
| interval_ ropts == NoInterval = MultiBalanceReport (actualspans, budgetgoalitems, budgetgoaltotals)
| otherwise = budgetgoalreport
budgetreport = combineBudgetAndActual budgetgoalreport' actualreport
sortedbudgetreport = sortBudgetReport ropts j budgetreport
in
dbg1 "budgetreport" $ combineBudgetAndActual budgetgoalreport' actualreport
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
sortedrows
| 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.
sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
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=
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
lookup (aname a) atotals
}
sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals
sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree
sortedrows = sortAccountItemsLike sortedanames anamesandrows
-- Sort a flat-mode budget report's rows by total actual amount.
sortFlatBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow]
sortFlatBURByActualAmount = sortBy (maybeflip $ comparing (fst . fifth6))
where
maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip
-- Sort the report rows by account declaration order then account name.
-- <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
sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows
-- | Use all periodic transactions in the journal to generate
-- budget transactions in the specified report period.
@ -184,41 +235,6 @@ combineBudgetAndActual
rows :: [PeriodicReportRow (Maybe Change, Maybe BudgetGoal)] =
sortBy (comparing first6) $ rows1 ++ rows2
-- -- like MultiBalanceReport
-- sortedrows
-- | sort_amount_ opts && tree_ opts = sortTreeBURByAmount items
-- | sort_amount_ opts = sortFlatBURByAmount items
-- | otherwise = sortBURByAccountDeclaration items
--
-- where
-- -- Sort the report rows, representing a tree of accounts, by row total at each level.
-- sortTreeMBRByAmount 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
-- -- should not happen, but it's ugly; TODO
-- setibalance a = a{aibalance=fromMaybe (error "sortTreeBURByAmount 1") $ lookup (aname a) atotals}
-- sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals
-- sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree
-- sortedrows = sortAccountItemsLike sortedanames anamesandrows
--
-- -- Sort the report rows, representing a flat account list, by row total.
-- sortFlatBURByAmount = sortBy (maybeflip $ comparing fifth6)
-- where
-- maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip
--
-- -- Sort the report rows by account declaration order then account name.
-- sortBURByAccountDeclaration rows = sortedrows
-- where
-- anamesandrows = [(first6 r, r) | r <- rows]
-- anames = map fst anamesandrows
-- sortedanames = sortAccountNamesByDeclaration j (tree_ opts) anames
-- sortedrows = sortAccountItemsLike sortedanames anamesandrows
-- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells
totalrow =
( ""

View File

@ -832,7 +832,8 @@ account assets:bank:checking
### Account display order
Account directives have another purpose: they set the display order of accounts in reports.
Account directives have another purpose: they set the order in which accounts are displayed,
in hledger reports, hledger-ui accounts screen, hledger-web sidebar etc.
For example, say you have these top-level accounts:
```shell
$ accounts -1
@ -867,13 +868,14 @@ misc
other
```
Ie, declared accounts first, in the order they were declared, followed by undeclared accounts in alphabetic order.
Ie, declared accounts first, in the order they were declared, followed by any undeclared accounts in alphabetic order.
This is supported in most reports organised by account (accounts/balance/bs/bse/cf/is).
It is not yet supported in budget reports (balance --budget) or hledger-web's sidebar.
Note sorting is done at each level of the account tree (within each group of sibling accounts
under the same parent).
Note that sorting is done at each level of the account tree (within each group of sibling accounts under the same parent).
This directive:
```journal
account other:zoo
```
would influence the position of `zoo` among `other`'s subaccounts, but not the position of `other` among the top-level accounts.
### Rewriting accounts

View File

@ -250,7 +250,6 @@ module Hledger.Cli.Commands.Balance (
,tests_Balance
) where
import Control.Monad (when)
import Data.List
import Data.Maybe
--import qualified Data.Map as Map
@ -313,7 +312,6 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
case (budget, interval) of
(True, _) -> do
-- single or multicolumn budget report
when (sort_amount_ ropts) $ error' "Sorry, --sort-amount is not yet supported with --budget." -- TODO
reportspan <- reportSpan j ropts
let budgetreport = dbg1 "budgetreport" $ budgetReport ropts assrt showunbudgeted reportspan d j
where

241
tests/budget/sorting.test Normal file
View File

@ -0,0 +1,241 @@
#* budget report sorting
# These tests below aren't very thorough, could use more varied amounts
# and pathological cases.
#** Default sort without account declarations
# already tested in budget.test, but for completeness:
<
~ daily from 2016/1/1
expenses:food $10
expenses:leisure $15
assets:cash
2016/12/01
expenses:food $10
assets:cash
2016/12/02
expenses:food $9
assets:cash
2016/12/03
expenses:food $11
assets:cash
2016/12/02
expenses:leisure $5
assets:cash
2016/12/03
expenses:movies $25
assets:cash
2016/12/03
expenses:cab $15
assets:cash
$ hledger -f- bal --budget -DTN
Budget performance in 2016/12/01-2016/12/03:
|| 2016/12/01 2016/12/02 2016/12/03 Total
==================++========================================================================================================
<unbudgeted> || 0 0 $40 $40
assets:cash || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25] $-75 [ 100% of $-75]
expenses:food || $10 [ 100% of $10] $9 [ 90% of $10] $11 [ 110% of $10] $30 [ 100% of $30]
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15] $5 [ 11% of $45]
#** Default sort with account declarations
<
account expenses
account expenses:leisure
~ daily from 2016/1/1
expenses:food $10
expenses:leisure $15
assets:cash
2016/12/01
expenses:food $10
assets:cash
2016/12/02
expenses:food $9
assets:cash
2016/12/03
expenses:food $11
assets:cash
2016/12/02
expenses:leisure $5
assets:cash
2016/12/03
expenses:movies $25
assets:cash
2016/12/03
expenses:cab $15
assets:cash
$ hledger -f- bal --budget -DTN
Budget performance in 2016/12/01-2016/12/03:
|| 2016/12/01 2016/12/02 2016/12/03 Total
==================++========================================================================================================
<unbudgeted> || 0 0 $40 $40
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15] $5 [ 11% of $45]
expenses:food || $10 [ 100% of $10] $9 [ 90% of $10] $11 [ 110% of $10] $30 [ 100% of $30]
assets:cash || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25] $-75 [ 100% of $-75]
# # 2. --show-unbudgeted
# $ hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget --show-unbudgeted
# Budget performance in 2016/12/01-2016/12/03:
# || 2016/12/01 2016/12/02 2016/12/03
# ==============================++==============================================================================
# <unbudgeted>:expenses:cab || 0 0 $15
# <unbudgeted>:expenses:movies || 0 0 $25
# assets:cash || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25]
# 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 [ 0]
# # 3. Test that budget works with mix of commodities
# <
# 2016/12/01
# expenses:food £10 @@ $15
# assets:cash
# 2016/12/02
# expenses:food 10 CAD @ $1
# assets:cash
# 2016/12/02
# expenses:food 10 CAD @ $1.1
# assets:cash
# 2016/12/03
# expenses:food $11
# assets:cash
# 2016/12/02
# expenses:leisure $5
# assets:cash
# 2016/12/03
# expenses:movies $25
# assets:cash
# 2016/12/03
# expenses:cab $15
# assets:cash
# ~ daily from 2016/1/1
# expenses:food $10
# expenses:leisure $15
# assets:cash
# $ hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget
# Budget performance in 2016/12/01-2016/12/03:
# || 2016/12/01 2016/12/02 2016/12/03
# ==================++=====================================================================================
# <unbudgeted> || 0 0 $40
# assets:cash || $-15 [ 60% of $-25] $-26 [ 104% of $-25] $-51 [ 204% of $-25]
# 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 [ 0] $-21, 20 CAD [ 0] 0 [ 0]
#** Sort by actual amount, flat mode.
$ hledger -f- bal --budget -DTNS
Budget performance in 2016/12/01-2016/12/03:
|| 2016/12/01 2016/12/02 2016/12/03 Total
==================++========================================================================================================
<unbudgeted> || 0 0 $40 $40
expenses:food || $10 [ 100% of $10] $9 [ 90% of $10] $11 [ 110% of $10] $30 [ 100% of $30]
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15] $5 [ 11% of $45]
assets:cash || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25] $-75 [ 100% of $-75]
#** Sort by actual amount, tree mode.
$ hledger -f- bal --budget -DTNS --tree
Budget performance in 2016/12/01-2016/12/03:
|| 2016/12/01 2016/12/02 2016/12/03 Total
==============++========================================================================================================
<unbudgeted> || 0 0 $40 $40
expenses || $10 [ 40% of $25] $14 [ 56% of $25] $11 [ 44% of $25] $35 [ 47% of $75]
food || $10 [ 100% of $10] $9 [ 90% of $10] $11 [ 110% of $10] $30 [ 100% of $30]
leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15] $5 [ 11% of $45]
assets || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25] $-75 [ 100% of $-75]
cash || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25] $-75 [ 100% of $-75]
#** other ?
# with --show-unbudgeted
# $ hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget --show-unbudgeted
# Budget performance in 2016/12/01-2016/12/03:
# || 2016/12/01 2016/12/02 2016/12/03
# ==============================++==============================================================================
# <unbudgeted>:expenses:cab || 0 0 $15
# <unbudgeted>:expenses:movies || 0 0 $25
# assets:cash || $-10 [ 40% of $-25] $-14 [ 56% of $-25] $-51 [ 204% of $-25]
# 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 [ 0]
# with multiple commodities
# <
# 2016/12/01
# expenses:food £10 @@ $15
# assets:cash
# 2016/12/02
# expenses:food 10 CAD @ $1
# assets:cash
# 2016/12/02
# expenses:food 10 CAD @ $1.1
# assets:cash
# 2016/12/03
# expenses:food $11
# assets:cash
# 2016/12/02
# expenses:leisure $5
# assets:cash
# 2016/12/03
# expenses:movies $25
# assets:cash
# 2016/12/03
# expenses:cab $15
# assets:cash
# ~ daily from 2016/1/1
# expenses:food $10
# expenses:leisure $15
# assets:cash
# $ hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget
# Budget performance in 2016/12/01-2016/12/03:
# || 2016/12/01 2016/12/02 2016/12/03
# ==================++=====================================================================================
# <unbudgeted> || 0 0 $40
# assets:cash || $-15 [ 60% of $-25] $-26 [ 104% of $-25] $-51 [ 204% of $-25]
# 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 [ 0] $-21, 20 CAD [ 0] 0 [ 0]