diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index fc30a73b5..b123ddeb6 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -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. + -- remains at the top. + sortByAccountDeclaration rows = sortedrows + where + (unbudgetedrow,rows') = partition ((=="").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 = ( "" diff --git a/hledger-lib/hledger_journal.m4.md b/hledger-lib/hledger_journal.m4.md index f0f0cd7f7..413a48ad5 100644 --- a/hledger-lib/hledger_journal.m4.md +++ b/hledger-lib/hledger_journal.m4.md @@ -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 diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index bf8f6d4d9..2bf0af1c8 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -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 diff --git a/tests/budget/sorting.test b/tests/budget/sorting.test new file mode 100644 index 000000000..7486d5eac --- /dev/null +++ b/tests/budget/sorting.test @@ -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 +==================++======================================================================================================== + || 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 +==================++======================================================================================================== + || 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 +# ==============================++============================================================================== +# :expenses:cab || 0 0 $15 +# :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 +# ==================++===================================================================================== +# || 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 +==================++======================================================================================================== + || 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 +==============++======================================================================================================== + || 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 +# ==============================++============================================================================== +# :expenses:cab || 0 0 $15 +# :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 +# ==================++===================================================================================== +# || 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] +