1 {-| 2 3 A ledger-compatible @balance@ command. 4 5 ledger's balance command is easy to use but not easy to describe 6 precisely. In the examples below we'll use sample.ledger, which has the 7 following account tree: 8 9 @ 10 assets 11 bank 12 checking 13 saving 14 cash 15 expenses 16 food 17 supplies 18 income 19 gifts 20 salary 21 liabilities 22 debts 23 @ 24 25 The balance command shows accounts with their aggregate balances. 26 Subaccounts are displayed indented below their parent. Each balance is the 27 sum of any transactions in that account plus any balances from 28 subaccounts: 29 30 @ 31 $ hledger -f sample.ledger balance 32 $-1 assets 33 $1 bank:saving 34 $-2 cash 35 $2 expenses 36 $1 food 37 $1 supplies 38 $-2 income 39 $-1 gifts 40 $-1 salary 41 $1 liabilities:debts 42 @ 43 44 Usually, the non-interesting accounts are elided or omitted. Above, 45 @checking@ is omitted because it has no subaccounts and a zero balance. 46 @bank@ is elided because it has only a single displayed subaccount 47 (@saving@) and it would be showing the same balance as that ($1). Ditto 48 for @liabilities@. We will return to this in a moment. 49 50 The --depth argument can be used to limit the depth of the balance report. 51 So, to see just the top level accounts: 52 53 @ 54 $ hledger -f sample.ledger balance --depth 1 55 $-1 assets 56 $2 expenses 57 $-2 income 58 $1 liabilities 59 @ 60 61 This time liabilities has no displayed subaccounts (due to --depth) and 62 is not elided. 63 64 With one or more account pattern arguments, the balance command shows 65 accounts whose name matches one of the patterns, plus their parents 66 (elided) and subaccounts. So with the pattern o we get: 67 68 @ 69 $ hledger -f sample.ledger balance o 70 $1 expenses:food 71 $-2 income 72 $-1 gifts 73 $-1 salary 74 -------------------- 75 $-1 76 @ 77 78 The o pattern matched @food@ and @income@, so they are shown. Unmatched 79 parents of matched accounts are also shown (elided) for context (@expenses@). 80 81 Also, the balance report shows the total of all displayed accounts, when 82 that is non-zero. Here, it is displayed because the accounts shown add up 83 to $-1. 84 85 Here is a more precise definition of \"interesting\" accounts in ledger's 86 balance report: 87 88 - an account which has just one interesting subaccount branch, and which 89 is not at the report's maximum depth, is interesting if the balance is 90 different from the subaccount's, and otherwise boring. 91 92 - any other account is interesting if it has a non-zero balance, or the -E 93 flag is used. 94 95 -} 96 97 module Commands.Balance 98 where 99 import Prelude hiding (putStr) 100 import Ledger.Utils 101 import Ledger.Types 102 import Ledger.Amount 103 import Ledger.AccountName 104 import Ledger.Transaction 105 import Ledger.Ledger 106 import Ledger.Parse 107 import Options 108 import Utils 109 import System.IO.UTF8 110 111 112 -- | Print a balance report. 113 balance :: [Opt] -> [String] -> Ledger -> IO () 114 balance opts args l = putStr $ showBalanceReport opts args l 115 116 -- | Generate a balance report with the specified options for this ledger. 117 showBalanceReport :: [Opt] -> [String] -> Ledger -> String 118 showBalanceReport opts args l = acctsstr ++ totalstr 119 where 120 acctsstr = unlines $ map showacct interestingaccts 121 where 122 showacct = showInterestingAccount l interestingaccts 123 interestingaccts = filter (isInteresting opts l) acctnames 124 acctnames = sort $ tail $ flatten $ treemap aname accttree 125 accttree = ledgerAccountTree (depthFromOpts opts) l 126 totalstr | NoTotal `elem` opts = "" 127 | not (Empty `elem` opts) && isZeroMixedAmount total = "" 128 | otherwise = printf "--------------------\n%s\n" $ padleft 20 $ showMixedAmount total 129 where 130 total = sum $ map abalance $ ledgerTopAccounts l 131 132 -- | Display one line of the balance report with appropriate indenting and eliding. 133 showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String 134 showInterestingAccount l interestingaccts a = concatTopPadded [amt, " ", depthspacer ++ partialname] 135 where 136 amt = padleft 20 $ showMixedAmount $ abalance $ ledgerAccount l a 137 -- the depth spacer (indent) is two spaces for each interesting parent 138 parents = parentAccountNames a 139 interestingparents = filter (`elem` interestingaccts) parents 140 depthspacer = replicate (2 * length interestingparents) ' ' 141 -- the partial name is the account's leaf name, prefixed by the 142 -- names of any boring parents immediately above 143 partialname = accountNameFromComponents $ (reverse $ map accountLeafName ps) ++ [accountLeafName a] 144 where ps = takeWhile boring parents where boring = not . (`elem` interestingparents) 145 146 -- | Is the named account considered interesting for this ledger's balance report ? 147 isInteresting :: [Opt] -> Ledger -> AccountName -> Bool 148 isInteresting opts l a 149 | numinterestingsubs==1 && not atmaxdepth = notlikesub 150 | otherwise = notzero || emptyflag 151 where 152 atmaxdepth = accountNameLevel a == depthFromOpts opts 153 emptyflag = Empty `elem` opts 154 acct = ledgerAccount l a 155 notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct 156 notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumTransactions $ atransactions acct 157 numinterestingsubs = length $ filter isInterestingTree subtrees 158 where 159 isInterestingTree t = treeany (isInteresting opts l . aname) t 160 subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a 161