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