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-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 apply the rule for each matched posting within a transaction , if there's more than one
- 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
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 = []
}
}
type PostingExpr = ( AccountName , AmountExpr )
data AmountExpr = AmountLiteral String | AmountMultiplier Quantity deriving ( Show )
addPostingExprsFromOpts :: RawOpts -> [ PostingExpr ]
2016-09-03 17:51:36 +03:00
addPostingExprsFromOpts = map ( either parseerror id . runParser ( postingexprp <* eof ) " " ) . map ( stripquotes . T . pack ) . listofstringopt " add-posting "
2014-02-06 06:59:27 +04:00
postingexprp = do
a <- accountnamep
2016-09-03 17:51:36 +03:00
spacenonewline >> some spacenonewline
2014-02-06 06:59:27 +04:00
aex <- amountexprp
many spacenonewline
return ( a , aex )
amountexprp =
choice [
AmountMultiplier <$> ( do char '*'
many spacenonewline
2014-11-03 09:01:32 +03:00
( q , _ , _ , _ ) <- numberp
2014-02-06 06:59:27 +04:00
return q )
, AmountLiteral <$> many anyChar
]
amountExprRenderer :: Query -> AmountExpr -> ( Transaction -> MixedAmount )
amountExprRenderer q aex =
2014-09-11 00:07:53 +04:00
case aex of
2016-03-03 04:05:56 +03:00
AmountLiteral s -> const ( mamountp' s )
2014-02-06 06:59:27 +04:00
AmountMultiplier n -> ( ` divideMixedAmount ` ( 1 / n ) ) . ( ` firstAmountMatching ` q )
where
firstAmountMatching :: Transaction -> Query -> MixedAmount
firstAmountMatching t q = pamount $ head $ filter ( q ` matchesPosting ` ) $ tpostings t
2014-02-06 01:56:49 +04:00
rewriteTransaction :: Transaction -> [ ( AccountName , Transaction -> MixedAmount ) ] -> Transaction
rewriteTransaction t addps = t { tpostings = tpostings t ++ map ( uncurry ( generatePosting t ) ) addps }
2014-02-06 06:59:27 +04:00
where
generatePosting :: Transaction -> AccountName -> ( Transaction -> MixedAmount ) -> Posting
generatePosting t acct amtfn = nullposting { paccount = accountNameWithoutPostingType acct
, ptype = accountNamePostingType acct
, pamount = amtfn t
, ptransaction = Just t
}
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
addps = [ ( a , amountExprRenderer q aex ) | ( a , aex ) <- addPostingExprsFromOpts rawopts ]
withJournalDo opts $ \ opts j @ Journal { jtxns = ts } -> do
-- rewrite matched transactions
let j' = j { jtxns = map ( \ t -> if q ` matchesTransaction ` t then rewriteTransaction t addps else t ) ts }
-- run the print command, showing all transactions
print' opts { reportopts_ = ropts { query_ = " " } } j'