* rewrite: rewrite every posting Also start using ModifierTransaction * rewrite: use journal parser for postings * rewrite: use ModifierTransactions from Journal See simonmichael/hledger#99
		
			
				
	
	
		
			111 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Haskell
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			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'
 |