hledger/bin/hledger-budget.hs
2017-01-20 13:42:17 -08:00

122 lines
4.6 KiB
Haskell
Executable File

#!/usr/bin/env stack
{- stack runghc --verbosity info
--package hledger-lib
--package hledger
--package text
-}
{-# LANGUAGE OverloadedStrings #-}
{-
hledger-budget REPORT-COMMAND [--no-offset] [--no-buckets] [OPTIONS...]
Perform some subset of reports available in core hledger but process automated
and periodic transactions. Also simplify tree of accounts to ease view of
"budget buckets".
This addon tries to simulate behavior of "ledger --budget".
-}
import Control.Arrow (first)
import Data.Maybe
import Data.List
import System.Console.CmdArgs
import Hledger.Cli
import Hledger.Cli.Main (mainmode)
import Hledger.Data.AutoTransaction
budgetFlags :: [Flag RawOpts]
budgetFlags =
[ flagNone ["no-buckets"] (setboolopt "no-buckets") "show all accounts besides mentioned in periodic transactions"
, flagNone ["no-offset"] (setboolopt "no-offset") "do not add up periodic transactions"
]
actions :: [(Mode RawOpts, CliOpts -> IO ())]
actions = first injectBudgetFlags <$>
[ (manmode, man)
, (infomode, info')
, (balancemode, flip withJournalDo' balance)
, (balancesheetmode, flip withJournalDo' balancesheet)
, (cashflowmode, flip withJournalDo' cashflow)
, (incomestatementmode, flip withJournalDo' incomestatement)
, (registermode, flip withJournalDo' register)
, (printmode, flip withJournalDo' print')
]
injectBudgetFlags :: Mode RawOpts -> Mode RawOpts
injectBudgetFlags = injectFlags "\nBudgeting" budgetFlags
-- maybe lenses will help...
injectFlags :: String -> [Flag RawOpts] -> Mode RawOpts -> Mode RawOpts
injectFlags section flags mode0 = mode' where
mode' = mode0 { modeGroupFlags = groupFlags' }
groupFlags0 = modeGroupFlags mode0
groupFlags' = groupFlags0 { groupNamed = namedFlags' }
namedFlags0 = groupNamed groupFlags0
namedFlags' =
case ((section ==) . fst) `partition` namedFlags0 of
([g], gs) -> (fst g, snd g ++ flags) : gs
_ -> (section, flags) : namedFlags0
cmdmode :: Mode RawOpts
cmdmode = (mainmode [])
{ modeNames = ["hledger-budget"]
, modeGroupModes = Group
{ groupUnnamed = map fst actions
, groupNamed = []
, groupHidden = []
}
}
journalBalanceTransactions' :: CliOpts -> Journal -> IO Journal
journalBalanceTransactions' opts j = do
let assrt = not $ ignore_assertions_ opts
either error' return $ journalBalanceTransactions assrt j
withJournalDo' :: CliOpts -> (CliOpts -> Journal -> IO ()) -> IO ()
withJournalDo' opts = withJournalDo opts . wrapper where
wrapper f opts' j = do
-- use original transactions as input for journalBalanceTransactions to re-infer balances/prices
let modifier = originalTransaction . foldr (flip (.) . runModifierTransaction') id mtxns
runModifierTransaction' = fmap txnTieKnot . runModifierTransaction Any
mtxns = jmodifiertxns j
dates = jdatespan j
ts' = map modifier $ jtxns j
ts'' | boolopt "no-offset" $ rawopts_ opts' = ts'
| otherwise= [makeBudget t | pt <- jperiodictxns j, t <- runPeriodicTransaction pt dates] ++ ts'
makeBudget t = txnTieKnot $ t
{ tdescription = "Budget transaction"
, tpostings = map makeBudgetPosting $ tpostings t
}
makeBudgetPosting p = p { pamount = negate $ pamount p }
j' <- journalBalanceTransactions' opts' j{ jtxns = ts'' }
-- re-map account names into buckets from periodic transaction
let buckets = budgetBuckets j
remapAccount "" = "<unbucketed>"
remapAccount an
| an `elem` buckets = an
| otherwise = remapAccount (parentAccountName an)
remapPosting p = p { paccount = remapAccount $ paccount p, porigin = Just . fromMaybe p $ porigin p }
remapTxn = mapPostings (map remapPosting)
let j'' | boolopt "no-buckets" $ rawopts_ opts' = j'
| null buckets = j'
| otherwise = j' { jtxns = remapTxn <$> jtxns j' }
-- finally feed to real command
f opts' j''
budgetBuckets :: Journal -> [AccountName]
budgetBuckets = nub . map paccount . concatMap ptpostings . jperiodictxns
mapPostings :: ([Posting] -> [Posting]) -> (Transaction -> Transaction)
mapPostings f t = txnTieKnot $ t { tpostings = f $ tpostings t }
main :: IO ()
main = do
rawopts <- fmap decodeRawOpts . processArgs $ cmdmode
opts <- rawOptsToCliOpts rawopts
case find (\e -> command_ opts `elem` modeNames (fst e)) actions of
Just (amode, _) | "h" `elem` map fst (rawopts_ opts) -> print amode
Just (_, action) -> action opts
Nothing -> print cmdmode