budget: periodic transactions support
This commit is contained in:
		
							parent
							
								
									2d9259ab3a
								
							
						
					
					
						commit
						3a632acea0
					
				| @ -40,11 +40,21 @@ journalBalanceTransactions' opts j = do | |||||||
| 
 | 
 | ||||||
| withJournalDo' :: CliOpts -> (CliOpts -> Journal -> IO ()) -> IO () | withJournalDo' :: CliOpts -> (CliOpts -> Journal -> IO ()) -> IO () | ||||||
| withJournalDo' opts = withJournalDo opts . wrapper where | withJournalDo' opts = withJournalDo opts . wrapper where | ||||||
|     wrapper f opts' j = f opts' =<< journalBalanceTransactions' opts' j{ jtxns = ts' } where |     wrapper f opts' j = do | ||||||
|         -- use original transactions as input for journalBalanceTransactions to re-infer balances/prices |         -- use original transactions as input for journalBalanceTransactions to re-infer balances/prices | ||||||
|         modifier = originalTransaction . foldr (flip (.) . fmap txnTieKnot . runModifierTransaction Any) id mtxns |         let modifier = originalTransaction . foldr (flip (.) . runModifierTransaction') id mtxns | ||||||
|         mtxns = jmodifiertxns j |             runModifierTransaction' = fmap txnTieKnot . runModifierTransaction Any | ||||||
|         ts' = map modifier $ jtxns j |             mtxns = jmodifiertxns j | ||||||
|  |             dates = jdatespan j | ||||||
|  |             ts' = map modifier $ jtxns j | ||||||
|  |             ts'' = [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'' } | ||||||
|  |         f opts' j' | ||||||
| 
 | 
 | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
|  | |||||||
| @ -9,9 +9,11 @@ module Hledger.Data.AutoTransaction | |||||||
|     ( |     ( | ||||||
|     -- * Transaction processors |     -- * Transaction processors | ||||||
|       runModifierTransaction |       runModifierTransaction | ||||||
|  |     , runPeriodicTransaction | ||||||
| 
 | 
 | ||||||
|     -- * Accessors |     -- * Accessors | ||||||
|     , mtvaluequery |     , mtvaluequery | ||||||
|  |     , jdatespan | ||||||
|     ) |     ) | ||||||
| where | where | ||||||
| 
 | 
 | ||||||
| @ -22,12 +24,15 @@ import qualified Data.Text as T | |||||||
| import Hledger.Data.Types | import Hledger.Data.Types | ||||||
| import Hledger.Data.Dates | import Hledger.Data.Dates | ||||||
| import Hledger.Data.Amount | import Hledger.Data.Amount | ||||||
|  | import Hledger.Data.Transaction | ||||||
|  | import Hledger.Utils.Parse | ||||||
|  | import Hledger.Utils.UTF8IOCompat (error') | ||||||
| import Hledger.Query | import Hledger.Query | ||||||
| 
 | 
 | ||||||
| -- $setup | -- $setup | ||||||
| -- >>> :set -XOverloadedStrings | -- >>> :set -XOverloadedStrings | ||||||
| -- >>> import Hledger.Data.Posting | -- >>> import Hledger.Data.Posting | ||||||
| -- >>> import Hledger.Data.Transaction | -- >>> import Hledger.Data.Journal | ||||||
| 
 | 
 | ||||||
| -- | Builds a 'Transaction' transformer based on 'ModifierTransaction'. | -- | Builds a 'Transaction' transformer based on 'ModifierTransaction'. | ||||||
| -- | -- | ||||||
| @ -76,6 +81,29 @@ runModifierTransaction q mt = modifier where | |||||||
| mtvaluequery :: ModifierTransaction -> (Day -> Query) | mtvaluequery :: ModifierTransaction -> (Day -> Query) | ||||||
| mtvaluequery mt = fst . flip parseQuery (mtvalueexpr mt) | mtvaluequery mt = fst . flip parseQuery (mtvalueexpr mt) | ||||||
| 
 | 
 | ||||||
|  | -- | 'DateSpan' of all dates mentioned in 'Journal' | ||||||
|  | -- | ||||||
|  | -- >>> jdatespan nulljournal | ||||||
|  | -- DateSpan - | ||||||
|  | -- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01"}] } | ||||||
|  | -- DateSpan 2016/01/01 | ||||||
|  | -- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01", tpostings=[nullposting{pdate=Just $ read "2016-02-01"}]}] } | ||||||
|  | -- DateSpan 2016/01/01-2016/02/01 | ||||||
|  | jdatespan :: Journal -> DateSpan | ||||||
|  | jdatespan j | ||||||
|  |         | null dates = nulldatespan | ||||||
|  |         | otherwise = DateSpan (Just $ minimum dates) (Just $ 1 `addDays` maximum dates) | ||||||
|  |     where | ||||||
|  |         dates = concatMap tdates $ jtxns j | ||||||
|  | 
 | ||||||
|  | -- | 'DateSpan' of all dates mentioned in 'Transaction' | ||||||
|  | -- | ||||||
|  | -- >>> tdates nulltransaction | ||||||
|  | -- [0000-01-01] | ||||||
|  | tdates :: Transaction -> [Day] | ||||||
|  | tdates t = tdate t : concatMap pdates (tpostings t) ++ maybeToList (tdate2 t) where | ||||||
|  |     pdates p = catMaybes [pdate p, pdate2 p] | ||||||
|  | 
 | ||||||
| postingScale :: Posting -> Maybe Quantity | postingScale :: Posting -> Maybe Quantity | ||||||
| postingScale p = | postingScale p = | ||||||
|     case amounts $ pamount p of |     case amounts $ pamount p of | ||||||
| @ -101,3 +129,28 @@ renderPostingCommentDates p = p { pcomment = comment' } | |||||||
|         comment' |         comment' | ||||||
|             | T.null datesComment = pcomment p |             | T.null datesComment = pcomment p | ||||||
|             | otherwise = T.intercalate "\n" $ filter (not . T.null) [T.strip $ pcomment p, "[" <> datesComment <> "]"] |             | otherwise = T.intercalate "\n" $ filter (not . T.null) [T.strip $ pcomment p, "[" <> datesComment <> "]"] | ||||||
|  | 
 | ||||||
|  | -- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan' | ||||||
|  | -- | ||||||
|  | -- Note that new transactions require 'txnTieKnot' post-processing. | ||||||
|  | -- | ||||||
|  | -- >>> mapM_ (putStr . show) $ runPeriodicTransaction (PeriodicTransaction "monthly from 2017/1 to 2017/4" ["hi" `post` usd 1]) nulldatespan | ||||||
|  | -- 2017/01/01 | ||||||
|  | --     hi         $1.00 | ||||||
|  | -- <BLANKLINE> | ||||||
|  | -- 2017/02/01 | ||||||
|  | --     hi         $1.00 | ||||||
|  | -- <BLANKLINE> | ||||||
|  | -- 2017/03/01 | ||||||
|  | --     hi         $1.00 | ||||||
|  | -- <BLANKLINE> | ||||||
|  | runPeriodicTransaction :: PeriodicTransaction -> (DateSpan -> [Transaction]) | ||||||
|  | runPeriodicTransaction pt = generate where | ||||||
|  |     base = nulltransaction { tpostings = ptpostings pt } | ||||||
|  |     periodExpr = ptperiodicexpr pt | ||||||
|  |     errCurrent = error' $ "Current date cannot be referenced in " ++ show (T.unpack periodExpr) | ||||||
|  |     (interval, effectspan) = | ||||||
|  |         case parsePeriodExpr errCurrent periodExpr of | ||||||
|  |             Left e -> error' $ "Failed to parse " ++ show (T.unpack periodExpr) ++ ": " ++ showDateParseError e | ||||||
|  |             Right x -> x | ||||||
|  |     generate jspan = [base {tdate=date} | span <- interval `splitSpan` spanIntersect effectspan jspan, let Just date = spanStart span] | ||||||
|  | |||||||
| @ -113,3 +113,52 @@ runghc ../../bin/hledger-budget.hs reg -f - | |||||||
|                                 assets:bank                  $0.40             0 |                                 assets:bank                  $0.40             0 | ||||||
| >>>2 | >>>2 | ||||||
| >>>=0 | >>>=0 | ||||||
|  | 
 | ||||||
|  | # Periodical transactions within journal being applied with inverted sign in amounts | ||||||
|  | runghc ../../bin/hledger-budget.hs bal -f - --no-total -DH expenses | ||||||
|  | <<< | ||||||
|  | ~ daily from 2016/12/31 | ||||||
|  |     expenses:food  $8 | ||||||
|  |     assets | ||||||
|  | 
 | ||||||
|  | ~ weekly | ||||||
|  |     expenses:leisure  $20 | ||||||
|  |     expenses:grocery  $50 | ||||||
|  |     expenses:housing  $250 | ||||||
|  |     expenses:fee  $10 | ||||||
|  |     assets | ||||||
|  | 
 | ||||||
|  | = ^assets:bank$ date:2017/1 amt:<0 | ||||||
|  |     assets:bank  *0.008 | ||||||
|  |     expenses:fee  *-0.008  ; cash withdraw fee | ||||||
|  | 
 | ||||||
|  | 2016/12/31 | ||||||
|  |     expenses:housing  $600 | ||||||
|  |     assets:cash | ||||||
|  | 
 | ||||||
|  | 2017/1/1 | ||||||
|  |     expenses:food  $20 | ||||||
|  |     expenses:leisure  $15 | ||||||
|  |     expenses:grocery  $30 | ||||||
|  |     assets:cash | ||||||
|  | 
 | ||||||
|  | 2017/1/2 | ||||||
|  |     assets:cash  $200.00 | ||||||
|  |     assets:bank | ||||||
|  | 
 | ||||||
|  | 2017/1/4 | ||||||
|  |     assets:cash  $100.00 | ||||||
|  |     assets:bank | ||||||
|  | >>> | ||||||
|  | Ending balances (historical) in 2016/12/26-2017/01/04: | ||||||
|  | 
 | ||||||
|  |                   ||  2016/12/26  2016/12/27  2016/12/28  2016/12/29  2016/12/30  2016/12/31  2017/01/01  2017/01/02  2017/01/03  2017/01/04  | ||||||
|  | ==================++========================================================================================================================= | ||||||
|  |  expenses:fee     ||        $-10        $-10        $-10        $-10        $-10        $-10        $-10     $-18.40     $-18.40     $-17.60  | ||||||
|  |  expenses:food    ||           0           0           0           0           0         $-8       $4.00      $-4.00     $-12.00     $-20.00  | ||||||
|  |  expenses:grocery ||        $-50        $-50        $-50        $-50        $-50        $-50     $-20.00     $-70.00     $-70.00     $-70.00  | ||||||
|  |  expenses:housing ||       $-250       $-250       $-250       $-250       $-250     $350.00     $350.00     $100.00     $100.00     $100.00  | ||||||
|  |  expenses:leisure ||        $-20        $-20        $-20        $-20        $-20        $-20      $-5.00     $-25.00     $-25.00     $-25.00  | ||||||
|  | 
 | ||||||
|  | >>>2 | ||||||
|  | >>>=0 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user