2016-09-07 17:39:10 +03:00
#!/ usr / bin / env stack
{- stack runghc - - verbosity info
--package hledger-lib
--package hledger
--package megaparsec
--package text
- }
2017-01-13 23:41:16 +03:00
{- # LANGUAGE OverloadedStrings, LambdaCase # -}
2017-01-08 21:24:33 +03:00
{-
2016-09-07 17:39:10 +03:00
2014-02-06 06:59:27 +04:00
hledger - rewrite [ PATTERNS ] --add-posting "ACCT AMTEXPR" ...
2014-02-06 01:02:24 +04:00
2014-02-06 06:59:27 +04:00
A start at a generic rewriter of journal entries .
2014-02-06 01:02:24 +04:00
Reads the default journal and prints the entries , like print ,
but adds the specified postings to any entries matching PATTERNS .
2014-02-06 06:59:27 +04:00
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 .
2017-01-08 18:19:25 +03:00
Currently does not work when invoked via hledger , run it directly instead .
2014-02-06 01:56:49 +04:00
2017-01-06 03:05:09 +03:00
Related : https :// github . com / simonmichael / hledger / issues / 99
TODO :
- should allow regex matching and interpolating matched name in replacement
2017-01-08 16:52:45 +03:00
- should apply all matching rules to a transaction , not just one
2017-01-06 03:05:09 +03:00
- should be possible to use this on unbalanced entries , eg while editing one
2017-01-08 21:24:33 +03:00
- }
2014-02-06 01:02:24 +04:00
2017-01-13 23:41:16 +03:00
import Data.Monoid
2016-09-03 17:51:36 +03:00
import qualified Data.Text as T
2014-02-06 06:59:27 +04:00
-- hledger lib, cli and cmdargs utils
2014-02-06 01:02:24 +04:00
import Hledger.Cli
2014-02-06 06:59:27 +04:00
-- more utils for parsing
2015-05-25 18:39:50 +03:00
-- #if !MIN_VERSION_base(4,8,0)
-- import Control.Applicative.Compat ((<*))
-- #endif
2016-09-03 17:51:36 +03:00
import Text.Megaparsec
import Text.Megaparsec.Text
2014-02-06 01:56:49 +04:00
2014-02-06 06:59:27 +04:00
cmdmode :: Mode RawOpts
cmdmode = ( defCommandMode [ " hledger-rewrite " ] ) {
modeArgs = ( [] , Just $ argsFlag " [PATTERNS] --add-posting \ " ACCT AMTEXPR \ " ... " )
2014-05-01 18:22:44 +04:00
, modeHelp = " print all journal entries, with custom postings added to the matched ones "
2014-02-06 06:59:27 +04:00
, modeGroupFlags = Group {
groupNamed = [ ( " Input " , inputflags )
, ( " Reporting " , reportflags )
, ( " Misc " , helpflags )
]
2014-09-11 00:07:53 +04:00
, groupUnnamed = [ flagReq [ " add-posting " ] ( \ s opts -> Right $ setopt " add-posting " s opts ) " 'ACCT AMTEXPR' "
2014-05-01 18:22:44 +04:00
" 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. " ]
2014-02-06 06:59:27 +04:00
, groupHidden = []
}
}
2017-01-13 23:41:16 +03:00
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 }
2014-02-06 01:56:49 +04:00
2014-02-06 06:59:27 +04:00
main = do
opts @ CliOpts { rawopts_ = rawopts , reportopts_ = ropts } <- getCliOpts cmdmode
d <- getCurrentDay
let q = queryFromOpts d ropts
2017-01-13 23:41:16 +03:00
mod <- modifierTransactionFromOpts rawopts
2014-02-06 06:59:27 +04:00
withJournalDo opts $ \ opts j @ Journal { jtxns = ts } -> do
2017-01-13 23:41:16 +03:00
-- create re-writer
let mods = jmodifiertxns j ++ [ mod ]
modifier = foldr ( . ) id $ map ( runModifierTransaction q ) mods
2014-02-06 06:59:27 +04:00
-- rewrite matched transactions
2017-01-13 23:41:16 +03:00
let j' = j { jtxns = map modifier ts }
2014-02-06 06:59:27 +04:00
-- run the print command, showing all transactions
print' opts { reportopts_ = ropts { query_ = " " } } j'