mirror of
https://github.com/simonmichael/hledger.git
synced 2025-01-01 06:41:55 +03:00
182 lines
5.2 KiB
Haskell
182 lines
5.2 KiB
Haskell
{-|
|
|
|
|
A ledger-compatible @balance@ command. Here's how it should work:
|
|
|
|
A sample account tree (as in the sample.ledger file):
|
|
|
|
@
|
|
assets
|
|
cash
|
|
checking
|
|
saving
|
|
expenses
|
|
food
|
|
supplies
|
|
income
|
|
gifts
|
|
salary
|
|
liabilities
|
|
debts
|
|
@
|
|
|
|
The balance command shows top-level accounts by default:
|
|
|
|
@
|
|
\> ledger balance
|
|
$-1 assets
|
|
$2 expenses
|
|
$-2 income
|
|
$1 liabilities
|
|
@
|
|
|
|
With -s (--showsubs), also show the subaccounts:
|
|
|
|
@
|
|
$-1 assets
|
|
$-2 cash
|
|
$1 saving
|
|
$2 expenses
|
|
$1 food
|
|
$1 supplies
|
|
$-2 income
|
|
$-1 gifts
|
|
$-1 salary
|
|
$1 liabilities:debts
|
|
@
|
|
|
|
- @checking@ is not shown because it has a zero balance and no interesting
|
|
subaccounts.
|
|
|
|
- @liabilities@ is displayed only as a prefix because it has no transactions
|
|
of its own and only one subaccount.
|
|
|
|
With an account pattern, show only the accounts with matching names:
|
|
|
|
@
|
|
\> ledger balance o
|
|
$1 expenses:food
|
|
$-2 income
|
|
--------------------
|
|
$-1
|
|
@
|
|
|
|
- The o matched @food@ and @income@, so they are shown.
|
|
|
|
- Parents of matched accounts are also shown for context (@expenses@).
|
|
|
|
- This time the grand total is also shown, because it is not zero.
|
|
|
|
Again, -s adds the subaccounts:
|
|
|
|
@
|
|
\> ledger -s balance o
|
|
$1 expenses:food
|
|
$-2 income
|
|
$-1 gifts
|
|
$-1 salary
|
|
--------------------
|
|
$-1
|
|
@
|
|
|
|
- @food@ has no subaccounts. @income@ has two, so they are shown.
|
|
|
|
- We do not add the subaccounts of parents included for context (@expenses@).
|
|
|
|
Here are some rules for account balance display, as seen above:
|
|
|
|
- grand total is omitted if it is 0
|
|
|
|
- leaf accounts and branches with 0 balance or 0 transactions are omitted
|
|
|
|
- inner accounts with 0 transactions and 1 subaccount are displayed inline
|
|
|
|
- in a filtered report, matched accounts are displayed with their parents
|
|
inline (a consequence of the above)
|
|
|
|
- in a showsubs report, all subaccounts of matched accounts are displayed
|
|
|
|
-}
|
|
|
|
module BalanceCommand
|
|
where
|
|
import Ledger.Utils
|
|
import Ledger.Types
|
|
import Ledger.Amount
|
|
import Ledger.AccountName
|
|
import Ledger.Ledger
|
|
import Options
|
|
import Utils
|
|
|
|
|
|
-- | Print a balance report.
|
|
printbalance :: [Opt] -> [String] -> Ledger -> IO ()
|
|
printbalance opts args l = putStr $ balancereport opts args l
|
|
|
|
balancereport :: [Opt] -> [String] -> Ledger -> String
|
|
balancereport opts args l = showLedgerAccountBalances l depth
|
|
where
|
|
showsubs = (ShowSubs `elem` opts)
|
|
pats = parseAccountDescriptionArgs args
|
|
-- when there is no -s or pattern args, show with depth 1
|
|
depth = case (pats, showsubs) of
|
|
(([],[]), False) -> 1
|
|
otherwise -> 9999
|
|
|
|
-- | Generate balance report output for a ledger, to the specified depth.
|
|
showLedgerAccountBalances :: Ledger -> Int -> String
|
|
showLedgerAccountBalances l maxdepth =
|
|
concatMap (showAccountTree l maxdepth) acctbranches
|
|
++
|
|
if isZeroAmount total
|
|
then ""
|
|
else printf "--------------------\n%20s\n" $ showAmountRounded total
|
|
where
|
|
acctbranches = branches $ ledgerAccountTree maxdepth l
|
|
filteredacctbranches = branches $ ledgerFilteredAccountTree maxdepth (acctpat l) l
|
|
total = sum $ map (abalance . root) filteredacctbranches
|
|
|
|
-- | Get the string representation of a tree of accounts.
|
|
-- The ledger from which the accounts come is required so that
|
|
-- we can check for boring accounts.
|
|
showAccountTree :: Ledger -> Int -> Tree Account -> String
|
|
showAccountTree l maxdepth = showAccountTree' l maxdepth 0 ""
|
|
|
|
showAccountTree' :: Ledger -> Int -> Int -> String -> Tree Account -> String
|
|
showAccountTree' l maxdepth indentlevel prefix t
|
|
-- merge boring inner account names with the next line
|
|
| isBoringInnerAccount l maxdepth acct = subsindented 0 (fullname++":")
|
|
-- ditto with unmatched parent accounts when filtering by account
|
|
| filtering && doesnotmatch = subsindented 0 (fullname++":")
|
|
-- otherwise show this account's name & balance
|
|
| otherwise = bal ++ " " ++ indent ++ prefix ++ leafname ++ "\n" ++ (subsindented 1 "")
|
|
where
|
|
acct = root t
|
|
subs = branches t
|
|
subsindented i p = concatMap (showAccountTree' l maxdepth (indentlevel+i) p) subs
|
|
bal = printf "%20s" $ show $ abalance $ acct
|
|
indent = replicate (indentlevel * 2) ' '
|
|
fullname = aname acct
|
|
leafname = accountLeafName fullname
|
|
filtering = filteredaccountnames l /= (accountnames l)
|
|
doesnotmatch = not (containsRegex (acctpat l) leafname)
|
|
|
|
-- | Is this account a boring inner account in this ledger ?
|
|
-- Boring inner accounts have no transactions, one subaccount,
|
|
-- and depth less than the maximum display depth.
|
|
-- Also, they are unmatched parent accounts when account matching is in effect.
|
|
isBoringInnerAccount :: Ledger -> Int -> Account -> Bool
|
|
isBoringInnerAccount l maxdepth a
|
|
| name == "top" = False
|
|
| depth < maxdepth && numtxns == 0 && numsubs == 1 = True
|
|
| otherwise = False
|
|
where
|
|
name = aname a
|
|
depth = accountNameLevel name
|
|
numtxns = length $ atransactions a
|
|
-- how many (filter-matching) subaccounts has this account ?
|
|
numsubs = length $ subAccountNamesFrom (filteredaccountnames l) name
|
|
|
|
-- | Is the named account a boring inner account in this ledger ?
|
|
isBoringInnerAccountName :: Ledger -> Int -> AccountName -> Bool
|
|
isBoringInnerAccountName l maxdepth = isBoringInnerAccount l maxdepth . ledgerAccount l
|