lib: journal: allow descriptions/comments in periodic transactions
Also period expressions relative to today's date can now be used, for what it's worth.
This commit is contained in:
parent
dfcafc2cdf
commit
e3507ad944
@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-|
|
{-|
|
||||||
@ -17,6 +18,9 @@ module Hledger.Data.AutoTransaction
|
|||||||
, mtvaluequery
|
, mtvaluequery
|
||||||
, jdatespan
|
, jdatespan
|
||||||
, periodTransactionInterval
|
, periodTransactionInterval
|
||||||
|
|
||||||
|
-- * Misc
|
||||||
|
, checkPeriodicTransactionStartDate
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -29,8 +33,8 @@ 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.Posting (post)
|
||||||
import Hledger.Data.Transaction
|
import Hledger.Data.Transaction
|
||||||
import Hledger.Utils.Parse
|
|
||||||
import Hledger.Utils.UTF8IOCompat (error')
|
import Hledger.Utils.UTF8IOCompat (error')
|
||||||
import Hledger.Query
|
import Hledger.Query
|
||||||
-- import Hledger.Utils.Debug
|
-- import Hledger.Utils.Debug
|
||||||
@ -139,134 +143,189 @@ renderPostingCommentDates p = p { pcomment = 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 <> "]"]
|
||||||
|
|
||||||
|
-- doctest helper, too much hassle to define in the comment
|
||||||
|
-- XXX duplicates some logic in periodictransactionp
|
||||||
|
_ptgen str = do
|
||||||
|
let
|
||||||
|
t = T.pack str
|
||||||
|
(i,s) = parsePeriodExpr' nulldate t
|
||||||
|
case checkPeriodicTransactionStartDate i s t of
|
||||||
|
Just e -> error' e
|
||||||
|
Nothing ->
|
||||||
|
mapM_ (putStr . show) $
|
||||||
|
runPeriodicTransaction
|
||||||
|
nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] }
|
||||||
|
nulldatespan
|
||||||
|
|
||||||
-- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan'
|
-- | Generate transactions from 'PeriodicTransaction' within a 'DateSpan'
|
||||||
--
|
--
|
||||||
-- Note that new transactions require 'txnTieKnot' post-processing.
|
-- Note that new transactions require 'txnTieKnot' post-processing.
|
||||||
--
|
--
|
||||||
-- >>> let gen str = mapM_ (putStr . show) $ runPeriodicTransaction (PeriodicTransaction str ["hi" `post` usd 1]) nulldatespan
|
-- >>> _ptgen "monthly from 2017/1 to 2017/4"
|
||||||
-- >>> gen "monthly from 2017/1 to 2017/4"
|
-- 2017/01/01
|
||||||
-- 2017/01/01 Forecast transaction (monthly from 2017/1 to 2017/4)
|
-- ; recur: monthly from 2017/1 to 2017/4
|
||||||
-- hi $1.00
|
-- a $1.00
|
||||||
-- <BLANKLINE>
|
-- <BLANKLINE>
|
||||||
-- 2017/02/01 Forecast transaction (monthly from 2017/1 to 2017/4)
|
-- 2017/02/01
|
||||||
-- hi $1.00
|
-- ; recur: monthly from 2017/1 to 2017/4
|
||||||
|
-- a $1.00
|
||||||
-- <BLANKLINE>
|
-- <BLANKLINE>
|
||||||
-- 2017/03/01 Forecast transaction (monthly from 2017/1 to 2017/4)
|
-- 2017/03/01
|
||||||
-- hi $1.00
|
-- ; recur: monthly from 2017/1 to 2017/4
|
||||||
|
-- a $1.00
|
||||||
-- <BLANKLINE>
|
-- <BLANKLINE>
|
||||||
-- >>> gen "monthly from 2017/1 to 2017/5"
|
|
||||||
-- 2017/01/01 Forecast transaction (monthly from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/02/01 Forecast transaction (monthly from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/03/01 Forecast transaction (monthly from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/04/01 Forecast transaction (monthly from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- >>> gen "every 2nd day of month from 2017/02 to 2017/04"
|
|
||||||
-- 2017/01/02 Forecast transaction (every 2nd day of month from 2017/02 to 2017/04)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/02/02 Forecast transaction (every 2nd day of month from 2017/02 to 2017/04)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/03/02 Forecast transaction (every 2nd day of month from 2017/02 to 2017/04)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- >>> gen "every 30th day of month from 2017/1 to 2017/5"
|
|
||||||
-- 2016/12/30 Forecast transaction (every 30th day of month from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/01/30 Forecast transaction (every 30th day of month from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/02/28 Forecast transaction (every 30th day of month from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/03/30 Forecast transaction (every 30th day of month from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/04/30 Forecast transaction (every 30th day of month from 2017/1 to 2017/5)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- >>> gen "every 2nd Thursday of month from 2017/1 to 2017/4"
|
|
||||||
-- 2016/12/08 Forecast transaction (every 2nd Thursday of month from 2017/1 to 2017/4)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/01/12 Forecast transaction (every 2nd Thursday of month from 2017/1 to 2017/4)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/02/09 Forecast transaction (every 2nd Thursday of month from 2017/1 to 2017/4)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/03/09 Forecast transaction (every 2nd Thursday of month from 2017/1 to 2017/4)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- >>> gen "every nov 29th from 2017 to 2019"
|
|
||||||
-- 2016/11/29 Forecast transaction (every nov 29th from 2017 to 2019)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2017/11/29 Forecast transaction (every nov 29th from 2017 to 2019)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- 2018/11/29 Forecast transaction (every nov 29th from 2017 to 2019)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- >>> gen "2017/1"
|
|
||||||
-- 2017/01/01 Forecast transaction (2017/1)
|
|
||||||
-- hi $1.00
|
|
||||||
-- <BLANKLINE>
|
|
||||||
-- >>> gen ""
|
|
||||||
-- ... Failed to parse ...
|
|
||||||
-- >>> gen "weekly from 2017"
|
|
||||||
-- *** Exception: Unable to generate transactions according to "weekly from 2017" as 2017-01-01 is not a first day of the week
|
|
||||||
-- >>> gen "monthly from 2017/5/4"
|
|
||||||
-- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" as 2017-05-04 is not a first day of the month
|
|
||||||
-- >>> gen "every quarter from 2017/1/2"
|
|
||||||
-- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" as 2017-01-02 is not a first day of the quarter
|
|
||||||
-- >>> gen "yearly from 2017/1/14"
|
|
||||||
-- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" as 2017-01-14 is not a first day of the year
|
|
||||||
--
|
--
|
||||||
-- >>> let reportperiod="daily from 2018/01/03" in runPeriodicTransaction (PeriodicTransaction reportperiod [post "a" (usd 1)]) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03"))
|
-- >>> _ptgen "monthly from 2017/1 to 2017/5"
|
||||||
|
-- 2017/01/01
|
||||||
|
-- ; recur: monthly from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/01
|
||||||
|
-- ; recur: monthly from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/01
|
||||||
|
-- ; recur: monthly from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/04/01
|
||||||
|
-- ; recur: monthly from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "every 2nd day of month from 2017/02 to 2017/04"
|
||||||
|
-- 2017/01/02
|
||||||
|
-- ; recur: every 2nd day of month from 2017/02 to 2017/04
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/02
|
||||||
|
-- ; recur: every 2nd day of month from 2017/02 to 2017/04
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/02
|
||||||
|
-- ; recur: every 2nd day of month from 2017/02 to 2017/04
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "every 30th day of month from 2017/1 to 2017/5"
|
||||||
|
-- 2016/12/30
|
||||||
|
-- ; recur: every 30th day of month from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/01/30
|
||||||
|
-- ; recur: every 30th day of month from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/28
|
||||||
|
-- ; recur: every 30th day of month from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/30
|
||||||
|
-- ; recur: every 30th day of month from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/04/30
|
||||||
|
-- ; recur: every 30th day of month from 2017/1 to 2017/5
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "every 2nd Thursday of month from 2017/1 to 2017/4"
|
||||||
|
-- 2016/12/08
|
||||||
|
-- ; recur: every 2nd Thursday of month from 2017/1 to 2017/4
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/01/12
|
||||||
|
-- ; recur: every 2nd Thursday of month from 2017/1 to 2017/4
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/09
|
||||||
|
-- ; recur: every 2nd Thursday of month from 2017/1 to 2017/4
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/09
|
||||||
|
-- ; recur: every 2nd Thursday of month from 2017/1 to 2017/4
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "every nov 29th from 2017 to 2019"
|
||||||
|
-- 2016/11/29
|
||||||
|
-- ; recur: every nov 29th from 2017 to 2019
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/11/29
|
||||||
|
-- ; recur: every nov 29th from 2017 to 2019
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2018/11/29
|
||||||
|
-- ; recur: every nov 29th from 2017 to 2019
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "2017/1"
|
||||||
|
-- 2017/01/01
|
||||||
|
-- ; recur: 2017/1
|
||||||
|
-- a $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
--
|
||||||
|
-- >>> _ptgen ""
|
||||||
|
-- *** Exception: failed to parse...
|
||||||
|
-- ...
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "weekly from 2017"
|
||||||
|
-- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the week
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "monthly from 2017/5/4"
|
||||||
|
-- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "every quarter from 2017/1/2"
|
||||||
|
-- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter
|
||||||
|
--
|
||||||
|
-- >>> _ptgen "yearly from 2017/1/14"
|
||||||
|
-- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year
|
||||||
|
--
|
||||||
|
-- >>> let reportperiod="daily from 2018/01/03" in let (i,s) = parsePeriodExpr' nulldate reportperiod in runPeriodicTransaction (nullperiodictransaction{ptperiodexpr=reportperiod, ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1]}) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03"))
|
||||||
-- []
|
-- []
|
||||||
|
--
|
||||||
runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]
|
runPeriodicTransaction :: PeriodicTransaction -> DateSpan -> [Transaction]
|
||||||
runPeriodicTransaction pt requestedspan =
|
runPeriodicTransaction PeriodicTransaction{..} requestedspan =
|
||||||
[ t{tdate=d} | (DateSpan (Just d) _) <- ptinterval `splitSpan` fillspan ]
|
[ t{tdate=d} | (DateSpan (Just d) _) <- ptinterval `splitSpan` spantofill ]
|
||||||
where
|
where
|
||||||
descr = T.pack $ "Forecast transaction (" ++ T.unpack periodexpr ++ ")"
|
spantofill = spanIntervalIntersect ptinterval ptspan requestedspan
|
||||||
t = nulltransaction { tpostings = ptpostings pt, tdescription = descr }
|
t = nulltransaction{
|
||||||
periodexpr = ptperiodicexpr pt
|
tstatus = ptstatus
|
||||||
currentdateerr = error' $ "Current date cannot be referenced in " ++ show (T.unpack periodexpr)
|
,tcode = ptcode
|
||||||
(ptinterval, ptspan) =
|
,tdescription = ptdescription
|
||||||
case parsePeriodExpr currentdateerr periodexpr of
|
,tcomment = (if T.null ptcomment then "\n" else ptcomment) <> "recur: " <> ptperiodexpr
|
||||||
Left e -> error' $ "Failed to parse " ++ show (T.unpack periodexpr) ++ ": " ++ showDateParseError e
|
,ttags = ("recur", ptperiodexpr) : pttags
|
||||||
Right x -> checkPeriodTransactionStartDate periodexpr x
|
,tpostings = ptpostings
|
||||||
fillspan = spanIntervalIntersect ptinterval ptspan requestedspan
|
}
|
||||||
|
|
||||||
checkPeriodTransactionStartDate :: T.Text -> (Interval, DateSpan) -> (Interval, DateSpan)
|
-- | Check that this date span begins at a boundary of this interval,
|
||||||
checkPeriodTransactionStartDate periodexpr (i,s) =
|
-- or return an explanatory error message including the provided period expression
|
||||||
|
-- (from which the span and interval are derived).
|
||||||
|
checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String
|
||||||
|
checkPeriodicTransactionStartDate i s periodexpr =
|
||||||
case (i, spanStart s) of
|
case (i, spanStart s) of
|
||||||
(Weeks _, Just d) -> checkStart d "week"
|
(Weeks _, Just d) -> checkStart d "week"
|
||||||
(Months _, Just d) -> checkStart d "month"
|
(Months _, Just d) -> checkStart d "month"
|
||||||
(Quarters _, Just d) -> checkStart d "quarter"
|
(Quarters _, Just d) -> checkStart d "quarter"
|
||||||
(Years _, Just d) -> checkStart d "year"
|
(Years _, Just d) -> checkStart d "year"
|
||||||
_ -> (i,s)
|
_ -> Nothing
|
||||||
where
|
where
|
||||||
checkStart d x =
|
checkStart d x =
|
||||||
let firstDate = fixSmartDate d ("","this",x)
|
let firstDate = fixSmartDate d ("","this",x)
|
||||||
in
|
in
|
||||||
if d == firstDate then (i,s)
|
if d == firstDate
|
||||||
else error' $ "Unable to generate transactions according to "++(show periodexpr)++" as "++(show d)++" is not a first day of the "++x
|
then Nothing
|
||||||
|
else Just $
|
||||||
|
"Unable to generate transactions according to "++show (T.unpack periodexpr)
|
||||||
|
++" because "++show d++" is not a first day of the "++x
|
||||||
|
|
||||||
-- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?
|
-- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?
|
||||||
periodTransactionInterval :: PeriodicTransaction -> Maybe Interval
|
periodTransactionInterval :: PeriodicTransaction -> Maybe Interval
|
||||||
periodTransactionInterval pt =
|
periodTransactionInterval pt =
|
||||||
let
|
let
|
||||||
expr = ptperiodicexpr pt
|
expr = ptperiodexpr pt
|
||||||
err = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)
|
err = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)
|
||||||
in
|
in
|
||||||
case parsePeriodExpr err expr of
|
case parsePeriodExpr err expr of
|
||||||
|
|||||||
@ -69,7 +69,7 @@ instance Show ModifierTransaction where
|
|||||||
show t = "= " ++ T.unpack (mtvalueexpr t) ++ "\n" ++ unlines (map show (mtpostings t))
|
show t = "= " ++ T.unpack (mtvalueexpr t) ++ "\n" ++ unlines (map show (mtpostings t))
|
||||||
|
|
||||||
instance Show PeriodicTransaction where
|
instance Show PeriodicTransaction where
|
||||||
show t = "~ " ++ T.unpack (ptperiodicexpr t) ++ "\n" ++ unlines (map show (ptpostings t))
|
show t = "~ " ++ T.unpack (ptperiodexpr t) ++ "\n" ++ unlines (map show (ptpostings t))
|
||||||
|
|
||||||
sourceFilePath :: GenericSourcePos -> FilePath
|
sourceFilePath :: GenericSourcePos -> FilePath
|
||||||
sourceFilePath = \case
|
sourceFilePath = \case
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE CPP, DeriveDataTypeable, StandaloneDeriving, DeriveGeneric, TypeSynonymInstances, FlexibleInstances #-}
|
{-# LANGUAGE CPP, DeriveDataTypeable, StandaloneDeriving, DeriveGeneric, TypeSynonymInstances, FlexibleInstances, OverloadedStrings #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
Most data types are defined here to avoid import cycles.
|
Most data types are defined here to avoid import cycles.
|
||||||
@ -229,7 +229,10 @@ data GenericSourcePos = GenericSourcePos FilePath Int Int -- ^ file path, 1-b
|
|||||||
|
|
||||||
instance NFData GenericSourcePos
|
instance NFData GenericSourcePos
|
||||||
|
|
||||||
{-# ANN Transaction "HLint: ignore" #-}
|
--{-# ANN Transaction "HLint: ignore" #-}
|
||||||
|
-- Ambiguous type variable ‘p0’ arising from an annotation
|
||||||
|
-- prevents the constraint ‘(Data p0)’ from being solved.
|
||||||
|
-- Probable fix: use a type annotation to specify what ‘p0’ should be.
|
||||||
data Transaction = Transaction {
|
data Transaction = Transaction {
|
||||||
tindex :: Integer, -- ^ this transaction's 1-based position in the input stream, or 0 when not available
|
tindex :: Integer, -- ^ this transaction's 1-based position in the input stream, or 0 when not available
|
||||||
tsourcepos :: GenericSourcePos,
|
tsourcepos :: GenericSourcePos,
|
||||||
@ -253,11 +256,32 @@ data ModifierTransaction = ModifierTransaction {
|
|||||||
|
|
||||||
instance NFData ModifierTransaction
|
instance NFData ModifierTransaction
|
||||||
|
|
||||||
|
-- ^ A periodic transaction rule, describing a transaction that recurs.
|
||||||
data PeriodicTransaction = PeriodicTransaction {
|
data PeriodicTransaction = PeriodicTransaction {
|
||||||
ptperiodicexpr :: Text,
|
ptperiodexpr :: Text, -- ^ the period expression as written
|
||||||
|
ptinterval :: Interval, -- ^ the interval at which this transaction recurs
|
||||||
|
ptspan :: DateSpan, -- ^ the (possibly unbounded) period during which this transaction recurs. Contains a whole number of intervals.
|
||||||
|
--
|
||||||
|
ptstatus :: Status, -- ^ some of Transaction's fields
|
||||||
|
ptcode :: Text,
|
||||||
|
ptdescription :: Text,
|
||||||
|
ptcomment :: Text,
|
||||||
|
pttags :: [Tag],
|
||||||
ptpostings :: [Posting]
|
ptpostings :: [Posting]
|
||||||
} deriving (Eq,Typeable,Data,Generic)
|
} deriving (Eq,Typeable,Data,Generic)
|
||||||
|
|
||||||
|
nullperiodictransaction = PeriodicTransaction{
|
||||||
|
ptperiodexpr = ""
|
||||||
|
,ptinterval = def
|
||||||
|
,ptspan = def
|
||||||
|
,ptstatus = Unmarked
|
||||||
|
,ptcode = ""
|
||||||
|
,ptdescription = ""
|
||||||
|
,ptcomment = ""
|
||||||
|
,pttags = []
|
||||||
|
,ptpostings = []
|
||||||
|
}
|
||||||
|
|
||||||
instance NFData PeriodicTransaction
|
instance NFData PeriodicTransaction
|
||||||
|
|
||||||
data TimeclockCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data,Generic)
|
data TimeclockCode = SetBalance | SetRequiredHours | In | Out | FinalOut deriving (Eq,Ord,Typeable,Data,Generic)
|
||||||
|
|||||||
@ -457,14 +457,45 @@ modifiertransactionp = do
|
|||||||
return $ ModifierTransaction valueexpr postings
|
return $ ModifierTransaction valueexpr postings
|
||||||
|
|
||||||
-- | Parse a periodic transaction
|
-- | Parse a periodic transaction
|
||||||
periodictransactionp :: JournalParser m PeriodicTransaction
|
periodictransactionp :: MonadIO m => JournalParser m PeriodicTransaction
|
||||||
periodictransactionp = do
|
periodictransactionp = do
|
||||||
char '~' <?> "periodic transaction"
|
char '~' <?> "periodic transaction"
|
||||||
lift (skipMany spacenonewline)
|
lift $ skipMany spacenonewline
|
||||||
periodexpr <- lift $ T.strip <$> descriptionp
|
-- XXX periodexprp in Hledger.Data.Dates is a SimpleTextParser, which we can't call directly here.
|
||||||
_ <- lift followingcommentp
|
-- Instead, read until two or more spaces and reparse that. More use of two spaces is not ideal.
|
||||||
postings <- postingsp Nothing
|
pos <- getPosition
|
||||||
return $ PeriodicTransaction periodexpr postings
|
periodtxt <- lift singlespacedtextp
|
||||||
|
d <- liftIO getCurrentDay
|
||||||
|
(interval, span) <-
|
||||||
|
case parsePeriodExpr d periodtxt of
|
||||||
|
Right (i,s) -> return (i,s)
|
||||||
|
Left e ->
|
||||||
|
-- Show an informative error. XXX a bit unidiomatic, check for megaparsec helpers
|
||||||
|
fail $ -- XXX
|
||||||
|
showGenericSourcePos (genericSourcePos pos) ++ ":\n" ++
|
||||||
|
(unlines $ drop 1 $ lines $ parseErrorPretty e) ++
|
||||||
|
"while parsing a period expression in: "++T.unpack periodtxt++"\n" ++
|
||||||
|
"2+ spaces are needed between period expression and any description/comment."
|
||||||
|
-- In periodic transactions, the period expression has an additional constraint:
|
||||||
|
case checkPeriodicTransactionStartDate interval span periodtxt of
|
||||||
|
Just e -> fail e -- XXX
|
||||||
|
Nothing -> do
|
||||||
|
status <- lift statusp
|
||||||
|
code <- lift codep
|
||||||
|
description <- lift $ T.strip <$> descriptionp
|
||||||
|
(comment, tags) <- lift transactioncommentp
|
||||||
|
postings <- postingsp (Just $ first3 $ toGregorian d)
|
||||||
|
return $ nullperiodictransaction{
|
||||||
|
ptperiodexpr=periodtxt
|
||||||
|
,ptinterval=interval
|
||||||
|
,ptspan=span
|
||||||
|
,ptstatus=status
|
||||||
|
,ptcode=code
|
||||||
|
,ptdescription=description
|
||||||
|
,ptcomment=comment
|
||||||
|
,pttags=tags
|
||||||
|
,ptpostings=postings
|
||||||
|
}
|
||||||
|
|
||||||
-- | Parse a (possibly unbalanced) transaction.
|
-- | Parse a (possibly unbalanced) transaction.
|
||||||
transactionp :: JournalParser m Transaction
|
transactionp :: JournalParser m Transaction
|
||||||
|
|||||||
@ -916,37 +916,59 @@ Prior to hledger 1.0, legacy `account` and `end` spellings were also supported.
|
|||||||
## Periodic transactions
|
## Periodic transactions
|
||||||
|
|
||||||
Periodic transaction rules describe transactions that recur.
|
Periodic transaction rules describe transactions that recur.
|
||||||
They allow you to generate future transactions for forecast reports, without writing them out in the journal (with `--forecast`).
|
They allow you to generate future transactions for forecast reports (with `--forecast`),
|
||||||
They can also be used to define budget goals (with `balance --budget`).
|
without having to write them out explicitly in the journal.
|
||||||
|
Secondly, they also can be used to define budget goals (with `--budget`).
|
||||||
|
|
||||||
A periodic transaction rule looks like a regular journal entry,
|
A periodic transaction rule looks like a regular journal entry,
|
||||||
except the first line is a tilde (`~`) followed by a [period expression](manual.html#period-expressions)
|
with the date replaced by a tilde (`~`) followed by a [period expression](manual.html#period-expressions)
|
||||||
(mnemonic: `~` looks like a repeating sine wave):
|
(mnemonic: `~` looks like a repeating sine wave):
|
||||||
```journal
|
```journal
|
||||||
~ weekly
|
~ monthly
|
||||||
assets:bank:checking $400 ; paycheck
|
expenses:rent $2000
|
||||||
|
assets:bank:checking
|
||||||
|
```
|
||||||
|
There is an additional constraint on the period expression:
|
||||||
|
the start date must fall on a natural boundary of the interval.
|
||||||
|
Eg `monthly from 2018/1/1` is valid, but `monthly from 2018/1/15` is not.
|
||||||
|
|
||||||
|
Also, if you write a transaction description or same-line comment,
|
||||||
|
it must be separated from the period expression by **two or more spaces**. Eg:
|
||||||
|
```journal
|
||||||
|
; 2 or more spaces
|
||||||
|
; ||
|
||||||
|
; vv
|
||||||
|
~ every 2 weeks from 2018/6 to 2018/9 paycheck
|
||||||
|
assets:bank:checking $1500
|
||||||
income:acme inc
|
income:acme inc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generating forecasts with periodic transactions
|
||||||
|
|
||||||
With the `--forecast` flag,
|
With the `--forecast` flag,
|
||||||
each periodic transaction rule generates future transactions recurring at the specified interval,
|
each periodic transaction rule generates future transactions recurring at the specified interval,
|
||||||
beginning the day after the latest recorded journal transaction (or today, if there are no transactions),
|
beginning the day after the latest recorded journal transaction (or today, if there are no transactions),
|
||||||
and ending 6 months from today (or at the report end date, if specified).
|
and ending 6 months from today (or at the report end date, if specified).
|
||||||
|
The generated transactions will appear in all reports.
|
||||||
|
They will have a "recur:" transaction tag added, with the generating period expression as its value.
|
||||||
|
|
||||||
Such generated transactions can be useful for forecasting balances in the future,
|
This can be useful for forecasting balances into the future,
|
||||||
and experimenting with different scenarios, without having to write a lot of journal entries.
|
and experimenting with different scenarios, without having to write a lot of journal entries.
|
||||||
They can also help with data entry, by copying the output of `print --forecast`.
|
It can also help with data entry (describe most of your transactions with periodic rules,
|
||||||
|
and every so often copy the output of `print --forecast` to the journal).
|
||||||
|
|
||||||
|
You can generate one-time transactions too;
|
||||||
|
just write a period expression specifying a date with no report interval.
|
||||||
|
You could use this to forecast an estimated transaction, that is automatically deactivated
|
||||||
|
once the actual transaction (or any other transaction on or after that date) is recorded.
|
||||||
|
|
||||||
|
### Setting budget goals with periodic transactions
|
||||||
|
|
||||||
With the `--budget` flag, currently supported by the balance command,
|
With the `--budget` flag, currently supported by the balance command,
|
||||||
each periodic transaction rule declares recurring budget goals for the specified accounts.
|
each periodic transaction rule declares recurring budget goals for the specified accounts.
|
||||||
Eg the example above declares the goal of receiving $400 from `income:acme inc`
|
Eg the first example above declares a goal of spending $2000 on rent
|
||||||
(and also, the goal of depositing $400 into `assets:bank:checking`) every week.
|
(and also, a goal of depositing $2000 into checking) every month.
|
||||||
Goals and actual performance can then be displayed in [budget reports](/manual.html#budget-report).
|
Goals and actual performance can then be compared in [budget reports](/manual.html#budget-report).
|
||||||
|
|
||||||
Periodic transaction rules can generate one-off transactions too;
|
|
||||||
just write a period expression with no report interval.
|
|
||||||
In some cases this could be more useful than an ordinary explicit transaction
|
|
||||||
(eg forecast an estimated amount until the actual transaction is recorded, automatically deactivating the forecasted one).
|
|
||||||
|
|
||||||
For more details, see:
|
For more details, see:
|
||||||
[balance: Budget report](manual.html#budget-report)
|
[balance: Budget report](manual.html#budget-report)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user