hledger/bin/hledger-rewrite.hs
Mykola Orliuk 8954944ee6 Apply ModifierTransaction's from journal during hledger-rewrite. (#477)
* rewrite: rewrite every posting

Also start using ModifierTransaction

* rewrite: use journal parser for postings

* rewrite: use ModifierTransactions from Journal

See simonmichael/hledger#99
2017-01-13 12:41:16 -08:00

111 lines
4.4 KiB
Haskell
Executable File

#!/usr/bin/env stack
{- stack runghc --verbosity info
--package hledger-lib
--package hledger
--package megaparsec
--package text
-}
{-# LANGUAGE OverloadedStrings, LambdaCase #-}
{-
hledger-rewrite [PATTERNS] --add-posting "ACCT AMTEXPR" ...
A start at a generic rewriter of journal entries.
Reads the default journal and prints the entries, like print,
but adds the specified postings to any entries matching PATTERNS.
Examples:
hledger-rewrite.hs ^income --add-posting '(liabilities:tax) *.33' --add-posting '(reserve:gifts) $100'
hledger-rewrite.hs expenses:gifts --add-posting '(reserve:gifts) *-1"'
Note the single quotes to protect the dollar sign from bash, and the two spaces between account and amount.
See the command-line help for more details.
Currently does not work when invoked via hledger, run it directly instead.
Related: https://github.com/simonmichael/hledger/issues/99
TODO:
- should allow regex matching and interpolating matched name in replacement
- should apply all matching rules to a transaction, not just one
- should be possible to use this on unbalanced entries, eg while editing one
-}
import Data.Monoid
import qualified Data.Text as T
-- hledger lib, cli and cmdargs utils
import Hledger.Cli
-- more utils for parsing
-- #if !MIN_VERSION_base(4,8,0)
-- import Control.Applicative.Compat ((<*))
-- #endif
import Text.Megaparsec
import Text.Megaparsec.Text
cmdmode :: Mode RawOpts
cmdmode = (defCommandMode ["hledger-rewrite"]) {
modeArgs = ([], Just $ argsFlag "[PATTERNS] --add-posting \"ACCT AMTEXPR\" ...")
,modeHelp = "print all journal entries, with custom postings added to the matched ones"
,modeGroupFlags = Group {
groupNamed = [("Input", inputflags)
,("Reporting", reportflags)
,("Misc", helpflags)
]
,groupUnnamed = [flagReq ["add-posting"] (\s opts -> Right $ setopt "add-posting" s opts) "'ACCT AMTEXPR'"
"add a posting to ACCT, which may be parenthesised. AMTEXPR is either a literal amount, or *N which means the transaction's first matched amount multiplied by N (a decimal number). Two spaces separate ACCT and AMTEXPR."]
,groupHidden = []
}
}
postingp' :: T.Text -> IO Posting
postingp' t = runErroringJournalParser (postingp Nothing <* eof) t' >>= \case
Left err -> fail err
Right p -> return p
where t' = " " <> t <> "\n" -- inject space and newline for proper parsing
modifierTransactionFromOpts :: RawOpts -> IO ModifierTransaction
modifierTransactionFromOpts opts = do
postings <- mapM (postingp' . stripquotes . T.pack) $ listofstringopt "add-posting" opts
return
ModifierTransaction { mtvalueexpr = T.empty, mtpostings = postings }
post' :: AccountName -> Amount -> Posting
post' acct amt = (accountNameWithoutPostingType acct `post` amt) { ptype = accountNamePostingType acct }
-- mtvaluequery :: ModifierTransaction -> Day -> Query
mtvaluequery mod = fst . flip parseQuery (mtvalueexpr mod)
postingScale :: Posting -> Maybe Quantity
postingScale p =
case amounts $ pamount p of
[a] | acommodity a == "*" -> Just $ aquantity a
_ -> Nothing
runModifierPosting :: Posting -> (Posting -> Posting)
runModifierPosting p' =
case postingScale p' of
Nothing -> \p -> p' { ptransaction = ptransaction p }
Just n -> \p -> p' { pamount = pamount p `divideMixedAmount` (1/n), ptransaction = ptransaction p }
runModifierTransaction :: Query -> ModifierTransaction -> (Transaction -> Transaction)
runModifierTransaction q mod = modifier where
q' = simplifyQuery $ And [q, mtvaluequery mod (error "query cannot depend on current time")]
mods = map runModifierPosting $ mtpostings mod
generatePostings ps = [mod p | p <- ps, q' `matchesPosting` p, mod <- mods]
modifier t@Transaction{ tpostings = ps } = t { tpostings = ps ++ generatePostings ps }
main = do
opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} <- getCliOpts cmdmode
d <- getCurrentDay
let q = queryFromOpts d ropts
mod <- modifierTransactionFromOpts rawopts
withJournalDo opts $ \opts j@Journal{jtxns=ts} -> do
-- create re-writer
let mods = jmodifiertxns j ++ [mod]
modifier = foldr (.) id $ map (runModifierTransaction q) mods
-- rewrite matched transactions
let j' = j{jtxns=map modifier ts}
-- run the print command, showing all transactions
print' opts{reportopts_=ropts{query_=""}} j'