Merge branch 'adept-budgeting-and-forecasting' (#654)
Cleaned-up versions of a number of related PRs relating to budgeting, periodic transactions, automated postings and period expressions, such as: #644, #645, #646, #647, #651, #652, #653.
This commit is contained in:
commit
8ab1911345
@ -13,7 +13,6 @@ import Data.List
|
|||||||
import Data.String.Here
|
import Data.String.Here
|
||||||
import System.Console.CmdArgs
|
import System.Console.CmdArgs
|
||||||
import Hledger.Cli
|
import Hledger.Cli
|
||||||
import Hledger.Data.AutoTransaction
|
|
||||||
|
|
||||||
-- hledger-budget REPORT-COMMAND [--no-offset] [--no-buckets] [OPTIONS...]
|
-- hledger-budget REPORT-COMMAND [--no-offset] [--no-buckets] [OPTIONS...]
|
||||||
|
|
||||||
|
|||||||
16
examples/budget.journal
Normal file
16
examples/budget.journal
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
~ monthly from 2013/01
|
||||||
|
Expenses:Food 500 USD
|
||||||
|
Expenses:Health 200 USD
|
||||||
|
Expenses:Home 2545 USD
|
||||||
|
Expenses:Transport 120 USD
|
||||||
|
Expenses:Taxes 4300 USD ;; Taken from monthly average report
|
||||||
|
Income:US -10700 USD
|
||||||
|
Assets:US
|
||||||
|
|
||||||
|
~ every Dec 20th from 2014
|
||||||
|
Expenses:Food 500 USD ; Prize turkey, the biggest of the big
|
||||||
|
Assets:US
|
||||||
|
|
||||||
|
~ 2014/11/17
|
||||||
|
Assets:US
|
||||||
|
Expenses:Food 6000 USD ; Birthday, lots of guests
|
||||||
@ -22,6 +22,7 @@ module Hledger.Data (
|
|||||||
module Hledger.Data.StringFormat,
|
module Hledger.Data.StringFormat,
|
||||||
module Hledger.Data.Timeclock,
|
module Hledger.Data.Timeclock,
|
||||||
module Hledger.Data.Transaction,
|
module Hledger.Data.Transaction,
|
||||||
|
module Hledger.Data.AutoTransaction,
|
||||||
module Hledger.Data.Types,
|
module Hledger.Data.Types,
|
||||||
tests_Hledger_Data
|
tests_Hledger_Data
|
||||||
)
|
)
|
||||||
@ -42,6 +43,7 @@ import Hledger.Data.RawOptions
|
|||||||
import Hledger.Data.StringFormat
|
import Hledger.Data.StringFormat
|
||||||
import Hledger.Data.Timeclock
|
import Hledger.Data.Timeclock
|
||||||
import Hledger.Data.Transaction
|
import Hledger.Data.Transaction
|
||||||
|
import Hledger.Data.AutoTransaction
|
||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
|
|
||||||
tests_Hledger_Data :: Test
|
tests_Hledger_Data :: Test
|
||||||
|
|||||||
@ -136,7 +136,8 @@ renderPostingCommentDates p = p { pcomment = comment' }
|
|||||||
--
|
--
|
||||||
-- Note that new transactions require 'txnTieKnot' post-processing.
|
-- 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
|
-- >>> let gen str = mapM_ (putStr . show) $ runPeriodicTransaction (PeriodicTransaction str ["hi" `post` usd 1]) nulldatespan
|
||||||
|
-- >>> gen "monthly from 2017/1 to 2017/4"
|
||||||
-- 2017/01/01
|
-- 2017/01/01
|
||||||
-- hi $1.00
|
-- hi $1.00
|
||||||
-- <BLANKLINE>
|
-- <BLANKLINE>
|
||||||
@ -146,6 +147,86 @@ renderPostingCommentDates p = p { pcomment = comment' }
|
|||||||
-- 2017/03/01
|
-- 2017/03/01
|
||||||
-- hi $1.00
|
-- hi $1.00
|
||||||
-- <BLANKLINE>
|
-- <BLANKLINE>
|
||||||
|
-- >>> gen "monthly from 2017/1 to 2017/5"
|
||||||
|
-- 2017/01/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/04/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- >>> gen "every 2nd day of month from 2017/02 to 2017/04"
|
||||||
|
-- 2017/01/02
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/02
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/02
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- >>> gen "monthly from 2017/1 to 2017/4"
|
||||||
|
-- 2017/01/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/01
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- >>> gen "every 30th day of month from 2017/1 to 2017/5"
|
||||||
|
-- 2016/12/30
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/01/30
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/28
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/30
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/04/30
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- >>> gen "every 2nd Thursday of month from 2017/1 to 2017/4"
|
||||||
|
-- 2016/12/08
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/01/12
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/02/09
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/03/09
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- >>> gen "every nov 29th from 2017 to 2019"
|
||||||
|
-- 2016/11/29
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2017/11/29
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- 2018/11/29
|
||||||
|
-- hi $1.00
|
||||||
|
-- <BLANKLINE>
|
||||||
|
-- >>> 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
|
||||||
runPeriodicTransaction :: PeriodicTransaction -> (DateSpan -> [Transaction])
|
runPeriodicTransaction :: PeriodicTransaction -> (DateSpan -> [Transaction])
|
||||||
runPeriodicTransaction pt = generate where
|
runPeriodicTransaction pt = generate where
|
||||||
base = nulltransaction { tpostings = ptpostings pt }
|
base = nulltransaction { tpostings = ptpostings pt }
|
||||||
@ -154,5 +235,18 @@ runPeriodicTransaction pt = generate where
|
|||||||
(interval, effectspan) =
|
(interval, effectspan) =
|
||||||
case parsePeriodExpr errCurrent periodExpr of
|
case parsePeriodExpr errCurrent periodExpr of
|
||||||
Left e -> error' $ "Failed to parse " ++ show (T.unpack periodExpr) ++ ": " ++ showDateParseError e
|
Left e -> error' $ "Failed to parse " ++ show (T.unpack periodExpr) ++ ": " ++ showDateParseError e
|
||||||
Right x -> x
|
Right x -> checkProperStartDate x
|
||||||
generate jspan = [base {tdate=date} | span <- interval `splitSpan` spanIntersect effectspan jspan, let Just date = spanStart span]
|
generate jspan = [base {tdate=date} | span <- interval `splitSpan` spanIntersect effectspan jspan, let Just date = spanStart span]
|
||||||
|
checkProperStartDate (i,s) =
|
||||||
|
case (i,spanStart s) of
|
||||||
|
(Weeks _, Just d) -> checkStart d "week"
|
||||||
|
(Months _, Just d) -> checkStart d "month"
|
||||||
|
(Quarters _, Just d) -> checkStart d "quarter"
|
||||||
|
(Years _, Just d) -> checkStart d "year"
|
||||||
|
_ -> (i,s)
|
||||||
|
where
|
||||||
|
checkStart d x =
|
||||||
|
let firstDate = fixSmartDate d ("","this",x)
|
||||||
|
in
|
||||||
|
if d == firstDate then (i,s)
|
||||||
|
else error' $ "Unable to generate transactions according to "++(show periodExpr)++" as "++(show d)++" is not a first day of the "++x
|
||||||
|
|||||||
@ -88,6 +88,7 @@ import Data.Time.Clock
|
|||||||
import Data.Time.LocalTime
|
import Data.Time.LocalTime
|
||||||
import Safe (headMay, lastMay, readMay)
|
import Safe (headMay, lastMay, readMay)
|
||||||
import Text.Megaparsec.Compat
|
import Text.Megaparsec.Compat
|
||||||
|
import Text.Megaparsec.Perm
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
|
|
||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
@ -165,9 +166,15 @@ spansSpan spans = DateSpan (maybe Nothing spanStart $ headMay spans) (maybe Noth
|
|||||||
-- >>> t (Weeks 2) "2008/01/01" "2008/01/15"
|
-- >>> t (Weeks 2) "2008/01/01" "2008/01/15"
|
||||||
-- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]
|
-- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]
|
||||||
-- >>> t (DayOfMonth 2) "2008/01/01" "2008/04/01"
|
-- >>> t (DayOfMonth 2) "2008/01/01" "2008/04/01"
|
||||||
-- [DateSpan 2008/01/02-2008/02/01,DateSpan 2008/02/02-2008/03/01,DateSpan 2008/03/02-2008/04/01]
|
-- [DateSpan 2007/12/02-2008/01/01,DateSpan 2008/01/02-2008/02/01,DateSpan 2008/02/02-2008/03/01,DateSpan 2008/03/02-2008/04/01]
|
||||||
|
-- >>> t (WeekdayOfMonth 2 4) "2011/01/01" "2011/02/15"
|
||||||
|
-- [DateSpan 2010/12/09-2011/01/12,DateSpan 2011/01/13-2011/02/09,DateSpan 2011/02/10-2011/03/09]
|
||||||
-- >>> t (DayOfWeek 2) "2011/01/01" "2011/01/15"
|
-- >>> t (DayOfWeek 2) "2011/01/01" "2011/01/15"
|
||||||
-- [DateSpan 2011/01/04-2011/01/10,DateSpan 2011/01/11-2011/01/17]
|
-- [DateSpan 2010/12/28-2011/01/03,DateSpan 2011/01/04-2011/01/10,DateSpan 2011/01/11-2011/01/17]
|
||||||
|
-- >>> t (DayOfYear 11 29) "2011/10/01" "2011/10/15"
|
||||||
|
-- [DateSpan 2010/11/29-2011/11/28]
|
||||||
|
-- >>> t (DayOfYear 11 29) "2011/12/01" "2012/12/15"
|
||||||
|
-- [DateSpan 2011/11/29-2012/11/28,DateSpan 2012/11/29-2013/11/28]
|
||||||
--
|
--
|
||||||
splitSpan :: Interval -> DateSpan -> [DateSpan]
|
splitSpan :: Interval -> DateSpan -> [DateSpan]
|
||||||
splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]
|
splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]
|
||||||
@ -177,8 +184,10 @@ splitSpan (Weeks n) s = splitspan startofweek (applyN n nextweek) s
|
|||||||
splitSpan (Months n) s = splitspan startofmonth (applyN n nextmonth) s
|
splitSpan (Months n) s = splitspan startofmonth (applyN n nextmonth) s
|
||||||
splitSpan (Quarters n) s = splitspan startofquarter (applyN n nextquarter) s
|
splitSpan (Quarters n) s = splitspan startofquarter (applyN n nextquarter) s
|
||||||
splitSpan (Years n) s = splitspan startofyear (applyN n nextyear) s
|
splitSpan (Years n) s = splitspan startofyear (applyN n nextyear) s
|
||||||
splitSpan (DayOfMonth n) s = splitspan (nthdayofmonthcontaining n) (applyN (n-1) nextday . nextmonth) s
|
splitSpan (DayOfMonth n) s = splitspan (nthdayofmonthcontaining n) (nthdayofmonth n . nextmonth) s
|
||||||
|
splitSpan (WeekdayOfMonth n wd) s = splitspan (nthweekdayofmonthcontaining n wd) (advancetonthweekday n wd . nextmonth) s
|
||||||
splitSpan (DayOfWeek n) s = splitspan (nthdayofweekcontaining n) (applyN (n-1) nextday . nextweek) s
|
splitSpan (DayOfWeek n) s = splitspan (nthdayofweekcontaining n) (applyN (n-1) nextday . nextweek) s
|
||||||
|
splitSpan (DayOfYear m n) s= splitspan (nthdayofyearcontaining m n) (applyN (n-1) nextday . applyN (m-1) nextmonth . nextyear) s
|
||||||
-- splitSpan (WeekOfYear n) s = splitspan startofweek (applyN n nextweek) s
|
-- splitSpan (WeekOfYear n) s = splitspan startofweek (applyN n nextweek) s
|
||||||
-- splitSpan (MonthOfYear n) s = splitspan startofmonth (applyN n nextmonth) s
|
-- splitSpan (MonthOfYear n) s = splitspan startofmonth (applyN n nextmonth) s
|
||||||
-- splitSpan (QuarterOfYear n) s = splitspan startofquarter (applyN n nextquarter) s
|
-- splitSpan (QuarterOfYear n) s = splitspan startofquarter (applyN n nextquarter) s
|
||||||
@ -257,7 +266,7 @@ earliest (Just d1) (Just d2) = Just $ min d1 d2
|
|||||||
-- | Parse a period expression to an Interval and overall DateSpan using
|
-- | Parse a period expression to an Interval and overall DateSpan using
|
||||||
-- the provided reference date, or return a parse error.
|
-- the provided reference date, or return a parse error.
|
||||||
parsePeriodExpr :: Day -> Text -> Either (ParseError Char MPErr) (Interval, DateSpan)
|
parsePeriodExpr :: Day -> Text -> Either (ParseError Char MPErr) (Interval, DateSpan)
|
||||||
parsePeriodExpr refdate = parsewith (periodexpr refdate <* eof)
|
parsePeriodExpr refdate s = parsewith (periodexpr refdate <* eof) (T.toLower s)
|
||||||
|
|
||||||
maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan)
|
maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan)
|
||||||
maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate
|
maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate
|
||||||
@ -447,6 +456,7 @@ thismonth = startofmonth
|
|||||||
prevmonth = startofmonth . addGregorianMonthsClip (-1)
|
prevmonth = startofmonth . addGregorianMonthsClip (-1)
|
||||||
nextmonth = startofmonth . addGregorianMonthsClip 1
|
nextmonth = startofmonth . addGregorianMonthsClip 1
|
||||||
startofmonth day = fromGregorian y m 1 where (y,m,_) = toGregorian day
|
startofmonth day = fromGregorian y m 1 where (y,m,_) = toGregorian day
|
||||||
|
nthdayofmonth d day = fromGregorian y m d where (y,m,_) = toGregorian day
|
||||||
|
|
||||||
thisquarter = startofquarter
|
thisquarter = startofquarter
|
||||||
prevquarter = startofquarter . addGregorianMonthsClip (-3)
|
prevquarter = startofquarter . addGregorianMonthsClip (-3)
|
||||||
@ -461,18 +471,106 @@ prevyear = startofyear . addGregorianYearsClip (-1)
|
|||||||
nextyear = startofyear . addGregorianYearsClip 1
|
nextyear = startofyear . addGregorianYearsClip 1
|
||||||
startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day
|
startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day
|
||||||
|
|
||||||
nthdayofmonthcontaining n d | d1 >= d = d1
|
-- | For given date d find year-long interval that starts on given MM/DD of year
|
||||||
| otherwise = d2
|
-- and covers it.
|
||||||
where d1 = addDays (fromIntegral n-1) s
|
--
|
||||||
d2 = addDays (fromIntegral n-1) $ nextmonth s
|
-- Examples: lets take 2017-11-22. Year-long intervals covering it that
|
||||||
|
-- starts before Nov 22 will start in 2017. However
|
||||||
|
-- intervals that start after Nov 23rd should start in 2016:
|
||||||
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||||
|
-- >>> nthdayofyearcontaining 11 21 wed22nd
|
||||||
|
-- 2017-11-21
|
||||||
|
-- >>> nthdayofyearcontaining 11 22 wed22nd
|
||||||
|
-- 2017-11-22
|
||||||
|
-- >>> nthdayofyearcontaining 11 23 wed22nd
|
||||||
|
-- 2016-11-23
|
||||||
|
-- >>> nthdayofyearcontaining 12 02 wed22nd
|
||||||
|
-- 2016-12-02
|
||||||
|
-- >>> nthdayofyearcontaining 12 31 wed22nd
|
||||||
|
-- 2016-12-31
|
||||||
|
-- >>> nthdayofyearcontaining 1 1 wed22nd
|
||||||
|
-- 2017-01-01
|
||||||
|
nthdayofyearcontaining m n d | mmddOfSameYear <= d = mmddOfSameYear
|
||||||
|
| otherwise = mmddOfPrevYear
|
||||||
|
where mmddOfSameYear = addDays (fromIntegral n-1) $ applyN (m-1) nextmonth s
|
||||||
|
mmddOfPrevYear = addDays (fromIntegral n-1) $ applyN (m-1) nextmonth $ prevyear s
|
||||||
|
s = startofyear d
|
||||||
|
|
||||||
|
-- | For given date d find month-long interval that starts on nth day of month
|
||||||
|
-- and covers it.
|
||||||
|
--
|
||||||
|
-- Examples: lets take 2017-11-22. Month-long intervals covering it that
|
||||||
|
-- start on 1st-22nd of month will start in Nov. However
|
||||||
|
-- intervals that start on 23rd-30th of month should start in Oct:
|
||||||
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||||
|
-- >>> nthdayofmonthcontaining 1 wed22nd
|
||||||
|
-- 2017-11-01
|
||||||
|
-- >>> nthdayofmonthcontaining 12 wed22nd
|
||||||
|
-- 2017-11-12
|
||||||
|
-- >>> nthdayofmonthcontaining 22 wed22nd
|
||||||
|
-- 2017-11-22
|
||||||
|
-- >>> nthdayofmonthcontaining 23 wed22nd
|
||||||
|
-- 2017-10-23
|
||||||
|
-- >>> nthdayofmonthcontaining 30 wed22nd
|
||||||
|
-- 2017-10-30
|
||||||
|
nthdayofmonthcontaining n d | nthOfSameMonth <= d = nthOfSameMonth
|
||||||
|
| otherwise = nthOfPrevMonth
|
||||||
|
where nthOfSameMonth = nthdayofmonth n s
|
||||||
|
nthOfPrevMonth = nthdayofmonth n $ prevmonth s
|
||||||
s = startofmonth d
|
s = startofmonth d
|
||||||
|
|
||||||
nthdayofweekcontaining n d | d1 >= d = d1
|
-- | For given date d find week-long interval that starts on nth day of week
|
||||||
| otherwise = d2
|
-- and covers it.
|
||||||
where d1 = addDays (fromIntegral n-1) s
|
--
|
||||||
d2 = addDays (fromIntegral n-1) $ nextweek s
|
-- Examples: 2017-11-22 is Wed. Week-long intervals that cover it and
|
||||||
|
-- start on Mon, Tue or Wed will start in the same week. However
|
||||||
|
-- intervals that start on Thu or Fri should start in prev week:
|
||||||
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||||
|
-- >>> nthdayofweekcontaining 1 wed22nd
|
||||||
|
-- 2017-11-20
|
||||||
|
-- >>> nthdayofweekcontaining 2 wed22nd
|
||||||
|
-- 2017-11-21
|
||||||
|
-- >>> nthdayofweekcontaining 3 wed22nd
|
||||||
|
-- 2017-11-22
|
||||||
|
-- >>> nthdayofweekcontaining 4 wed22nd
|
||||||
|
-- 2017-11-16
|
||||||
|
-- >>> nthdayofweekcontaining 5 wed22nd
|
||||||
|
-- 2017-11-17
|
||||||
|
nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek
|
||||||
|
| otherwise = nthOfPrevWeek
|
||||||
|
where nthOfSameWeek = addDays (fromIntegral n-1) s
|
||||||
|
nthOfPrevWeek = addDays (fromIntegral n-1) $ prevweek s
|
||||||
s = startofweek d
|
s = startofweek d
|
||||||
|
|
||||||
|
-- | For given date d find month-long interval that starts on nth weekday of month
|
||||||
|
-- and covers it.
|
||||||
|
--
|
||||||
|
-- Examples: 2017-11-22 is 3rd Wed of Nov. Month-long intervals that cover it and
|
||||||
|
-- start on 1st-4th Wed will start in Nov. However
|
||||||
|
-- intervals that start on 4th Thu or Fri or later should start in Oct:
|
||||||
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
||||||
|
-- >>> nthweekdayofmonthcontaining 1 3 wed22nd
|
||||||
|
-- 2017-11-01
|
||||||
|
-- >>> nthweekdayofmonthcontaining 3 2 wed22nd
|
||||||
|
-- 2017-11-21
|
||||||
|
-- >>> nthweekdayofmonthcontaining 4 3 wed22nd
|
||||||
|
-- 2017-11-22
|
||||||
|
-- >>> nthweekdayofmonthcontaining 4 4 wed22nd
|
||||||
|
-- 2017-10-26
|
||||||
|
-- >>> nthweekdayofmonthcontaining 4 5 wed22nd
|
||||||
|
-- 2017-10-27
|
||||||
|
nthweekdayofmonthcontaining n wd d | nthWeekdaySameMonth <= d = nthWeekdaySameMonth
|
||||||
|
| otherwise = nthWeekdayPrevMonth
|
||||||
|
where nthWeekdaySameMonth = advancetonthweekday n wd $ startofmonth d
|
||||||
|
nthWeekdayPrevMonth = advancetonthweekday n wd $ prevmonth d
|
||||||
|
|
||||||
|
-- | Advance to nth weekday wd after given start day s
|
||||||
|
advancetonthweekday n wd s = addWeeks (n-1) . firstMatch (>=s) . iterate (addWeeks 1) $ firstweekday s
|
||||||
|
where
|
||||||
|
addWeeks k = addDays (7 * fromIntegral k)
|
||||||
|
firstMatch p = head . dropWhile (not . p)
|
||||||
|
firstweekday = addDays (fromIntegral wd-1) . startofweek
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- parsing
|
-- parsing
|
||||||
|
|
||||||
@ -633,17 +731,11 @@ md = do
|
|||||||
months = ["january","february","march","april","may","june",
|
months = ["january","february","march","april","may","june",
|
||||||
"july","august","september","october","november","december"]
|
"july","august","september","october","november","december"]
|
||||||
monthabbrevs = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]
|
monthabbrevs = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]
|
||||||
-- weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
||||||
-- weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
||||||
|
|
||||||
#if MIN_VERSION_megaparsec(6,0,0)
|
monthIndex t = maybe 0 (+1) $ t `elemIndex` months
|
||||||
lc = T.toLower
|
monIndex t = maybe 0 (+1) $ t `elemIndex` monthabbrevs
|
||||||
#else
|
|
||||||
lc = lowercase
|
|
||||||
#endif
|
|
||||||
|
|
||||||
monthIndex t = maybe 0 (+1) $ lc t `elemIndex` months
|
|
||||||
monIndex t = maybe 0 (+1) $ lc t `elemIndex` monthabbrevs
|
|
||||||
|
|
||||||
month :: SimpleTextParser SmartDate
|
month :: SimpleTextParser SmartDate
|
||||||
month = do
|
month = do
|
||||||
@ -657,6 +749,12 @@ mon = do
|
|||||||
let i = monIndex m
|
let i = monIndex m
|
||||||
return ("",show i,"")
|
return ("",show i,"")
|
||||||
|
|
||||||
|
weekday :: SimpleTextParser Int
|
||||||
|
weekday = do
|
||||||
|
wday <- choice . map string' $ weekdays ++ weekdayabbrevs
|
||||||
|
let i = head . catMaybes $ [wday `elemIndex` weekdays, wday `elemIndex` weekdayabbrevs]
|
||||||
|
return (i+1)
|
||||||
|
|
||||||
today,yesterday,tomorrow :: SimpleTextParser SmartDate
|
today,yesterday,tomorrow :: SimpleTextParser SmartDate
|
||||||
today = string "today" >> return ("","","today")
|
today = string "today" >> return ("","","today")
|
||||||
yesterday = string "yesterday" >> return ("","","yesterday")
|
yesterday = string "yesterday" >> return ("","","yesterday")
|
||||||
@ -683,17 +781,43 @@ lastthisnextthing = do
|
|||||||
return ("", T.unpack r, T.unpack p)
|
return ("", T.unpack r, T.unpack p)
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- >>> let p = parsewith (periodexpr (parsedate "2008/11/26")) :: T.Text -> Either (ParseError Char MPErr) (Interval, DateSpan)
|
-- >>> let p s = parsewith (periodexpr (parsedate "2008/11/26") <* eof) (T.toLower s) :: Either (ParseError Char MPErr) (Interval, DateSpan)
|
||||||
-- >>> p "from aug to oct"
|
-- >>> p "from Aug to Oct"
|
||||||
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
||||||
-- >>> p "aug to oct"
|
-- >>> p "aug to oct"
|
||||||
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
||||||
-- >>> p "every 3 days in aug"
|
-- >>> p "every 3 days in Aug"
|
||||||
-- Right (Days 3,DateSpan 2008/08)
|
-- Right (Days 3,DateSpan 2008/08)
|
||||||
-- >>> p "daily from aug"
|
-- >>> p "daily from aug"
|
||||||
-- Right (Days 1,DateSpan 2008/08/01-)
|
-- Right (Days 1,DateSpan 2008/08/01-)
|
||||||
-- >>> p "every week to 2009"
|
-- >>> p "every week to 2009"
|
||||||
-- Right (Weeks 1,DateSpan -2008/12/31)
|
-- Right (Weeks 1,DateSpan -2008/12/31)
|
||||||
|
-- >>> p "every 2nd day of month"
|
||||||
|
-- Right (DayOfMonth 2,DateSpan -)
|
||||||
|
-- >>> p "every 2nd day"
|
||||||
|
-- Right (DayOfMonth 2,DateSpan -)
|
||||||
|
-- >>> p "every 2nd day 2009-"
|
||||||
|
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
||||||
|
-- >>> p "every 29th Nov"
|
||||||
|
-- Right (DayOfYear 11 29,DateSpan -)
|
||||||
|
-- >>> p "every 29th nov -2009"
|
||||||
|
-- Right (DayOfYear 11 29,DateSpan -2008/12/31)
|
||||||
|
-- >>> p "every nov 29th"
|
||||||
|
-- Right (DayOfYear 11 29,DateSpan -)
|
||||||
|
-- >>> p "every Nov 29th 2009-"
|
||||||
|
-- Right (DayOfYear 11 29,DateSpan 2009/01/01-)
|
||||||
|
-- >>> p "every 11/29 from 2009"
|
||||||
|
-- Right (DayOfYear 11 29,DateSpan 2009/01/01-)
|
||||||
|
-- >>> p "every 2nd Thursday of month to 2009"
|
||||||
|
-- Right (WeekdayOfMonth 2 4,DateSpan -2008/12/31)
|
||||||
|
-- >>> p "every 1st monday of month to 2009"
|
||||||
|
-- Right (WeekdayOfMonth 1 1,DateSpan -2008/12/31)
|
||||||
|
-- >>> p "every tue"
|
||||||
|
-- Right (DayOfWeek 2,DateSpan -)
|
||||||
|
-- >>> p "every 2nd day of week"
|
||||||
|
-- Right (DayOfWeek 2,DateSpan -)
|
||||||
|
-- >>> p "every 2nd day 2009-"
|
||||||
|
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
||||||
periodexpr :: Day -> SimpleTextParser (Interval, DateSpan)
|
periodexpr :: Day -> SimpleTextParser (Interval, DateSpan)
|
||||||
periodexpr rdate = choice $ map try [
|
periodexpr rdate = choice $ map try [
|
||||||
intervalanddateperiodexpr rdate,
|
intervalanddateperiodexpr rdate,
|
||||||
@ -736,31 +860,53 @@ reportinginterval = choice' [
|
|||||||
return $ Months 2,
|
return $ Months 2,
|
||||||
do string "every"
|
do string "every"
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
n <- fmap read $ some digitChar
|
n <- nth
|
||||||
thsuffix
|
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
string "day"
|
string "day"
|
||||||
many spacenonewline
|
of_ "week"
|
||||||
string "of"
|
|
||||||
many spacenonewline
|
|
||||||
string "week"
|
|
||||||
return $ DayOfWeek n,
|
return $ DayOfWeek n,
|
||||||
do string "every"
|
do string "every"
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
n <- fmap read $ some digitChar
|
n <- weekday
|
||||||
thsuffix
|
return $ DayOfWeek n,
|
||||||
|
do string "every"
|
||||||
|
many spacenonewline
|
||||||
|
n <- nth
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
string "day"
|
string "day"
|
||||||
optional $ do
|
optOf_ "month"
|
||||||
|
return $ DayOfMonth n,
|
||||||
|
do string "every"
|
||||||
|
many spacenonewline
|
||||||
|
let mnth = choice' [month, mon] >>= \(_,m,_) -> return (read m)
|
||||||
|
d_o_y <- makePermParser $ DayOfYear <$$> (mnth <* many spacenonewline) <||> (nth <* many spacenonewline)
|
||||||
|
optOf_ "year"
|
||||||
|
return d_o_y,
|
||||||
|
do string "every"
|
||||||
|
many spacenonewline
|
||||||
|
("",m,d) <- md
|
||||||
|
optOf_ "year"
|
||||||
|
return $ DayOfYear (read m) (read d),
|
||||||
|
do string "every"
|
||||||
|
many spacenonewline
|
||||||
|
n <- nth
|
||||||
|
many spacenonewline
|
||||||
|
wd <- weekday
|
||||||
|
optOf_ "month"
|
||||||
|
return $ WeekdayOfMonth n wd
|
||||||
|
]
|
||||||
|
where
|
||||||
|
of_ period = do
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
string "of"
|
string "of"
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
string "month"
|
string period
|
||||||
return $ DayOfMonth n
|
|
||||||
]
|
|
||||||
where
|
|
||||||
|
|
||||||
thsuffix = choice' $ map string ["st","nd","rd","th"]
|
optOf_ period = optional $ try $ of_ period
|
||||||
|
|
||||||
|
nth = do n <- some digitChar
|
||||||
|
choice' $ map string ["st","nd","rd","th"]
|
||||||
|
return $ read n
|
||||||
|
|
||||||
-- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".
|
-- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".
|
||||||
tryinterval :: String -> String -> (Int -> Interval) -> SimpleTextParser Interval
|
tryinterval :: String -> String -> (Int -> Interval) -> SimpleTextParser Interval
|
||||||
|
|||||||
@ -89,7 +89,9 @@ data Interval =
|
|||||||
| Quarters Int
|
| Quarters Int
|
||||||
| Years Int
|
| Years Int
|
||||||
| DayOfMonth Int
|
| DayOfMonth Int
|
||||||
|
| WeekdayOfMonth Int Int
|
||||||
| DayOfWeek Int
|
| DayOfWeek Int
|
||||||
|
| DayOfYear Int Int -- Month, Day
|
||||||
-- WeekOfYear Int
|
-- WeekOfYear Int
|
||||||
-- MonthOfYear Int
|
-- MonthOfYear Int
|
||||||
-- QuarterOfYear Int
|
-- QuarterOfYear Int
|
||||||
|
|||||||
@ -104,6 +104,8 @@ data ReportOpts = ReportOpts {
|
|||||||
-- eg in the income section of an income statement, this helps --sort-amount know
|
-- eg in the income section of an income statement, this helps --sort-amount know
|
||||||
-- how to sort negative numbers.
|
-- how to sort negative numbers.
|
||||||
,color_ :: Bool
|
,color_ :: Bool
|
||||||
|
,forecast_ :: Bool
|
||||||
|
,auto_ :: Bool
|
||||||
} deriving (Show, Data, Typeable)
|
} deriving (Show, Data, Typeable)
|
||||||
|
|
||||||
instance Default ReportOpts where def = defreportopts
|
instance Default ReportOpts where def = defreportopts
|
||||||
@ -134,6 +136,8 @@ defreportopts = ReportOpts
|
|||||||
def
|
def
|
||||||
def
|
def
|
||||||
def
|
def
|
||||||
|
def
|
||||||
|
def
|
||||||
|
|
||||||
rawOptsToReportOpts :: RawOpts -> IO ReportOpts
|
rawOptsToReportOpts :: RawOpts -> IO ReportOpts
|
||||||
rawOptsToReportOpts rawopts = checkReportOpts <$> do
|
rawOptsToReportOpts rawopts = checkReportOpts <$> do
|
||||||
@ -164,6 +168,8 @@ rawOptsToReportOpts rawopts = checkReportOpts <$> do
|
|||||||
,sort_amount_ = boolopt "sort-amount" rawopts'
|
,sort_amount_ = boolopt "sort-amount" rawopts'
|
||||||
,pretty_tables_ = boolopt "pretty-tables" rawopts'
|
,pretty_tables_ = boolopt "pretty-tables" rawopts'
|
||||||
,color_ = color
|
,color_ = color
|
||||||
|
,forecast_ = boolopt "forecast" rawopts'
|
||||||
|
,auto_ = boolopt "auto" rawopts'
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Do extra validation of raw option values, raising an error if there's a problem.
|
-- | Do extra validation of raw option values, raising an error if there's a problem.
|
||||||
|
|||||||
@ -155,6 +155,8 @@ reportflags = [
|
|||||||
,flagNone ["empty","E"] (setboolopt "empty") "show items with zero amount, normally hidden"
|
,flagNone ["empty","E"] (setboolopt "empty") "show items with zero amount, normally hidden"
|
||||||
,flagNone ["cost","B"] (setboolopt "cost") "convert amounts to their cost at transaction time (using the transaction price, if any)"
|
,flagNone ["cost","B"] (setboolopt "cost") "convert amounts to their cost at transaction time (using the transaction price, if any)"
|
||||||
,flagNone ["value","V"] (setboolopt "value") "convert amounts to their market value on the report end date (using the most recent applicable market price, if any)"
|
,flagNone ["value","V"] (setboolopt "value") "convert amounts to their market value on the report end date (using the most recent applicable market price, if any)"
|
||||||
|
,flagNone ["forecast"] (setboolopt "forecast") "generate forecast transactions"
|
||||||
|
,flagNone ["auto"] (setboolopt "auto") "generate automated postings"
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | Common output-related flags: --output-file, --output-format...
|
-- | Common output-related flags: --output-file, --output-format...
|
||||||
|
|||||||
@ -246,11 +246,13 @@ module Hledger.Cli.Commands.Balance (
|
|||||||
,tests_Hledger_Cli_Commands_Balance
|
,tests_Hledger_Cli_Commands_Balance
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate, nub)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
import qualified Data.Map as Map
|
||||||
-- import Data.Monoid
|
-- import Data.Monoid
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import System.Console.CmdArgs.Explicit as C
|
import System.Console.CmdArgs.Explicit as C
|
||||||
|
import Data.Decimal (roundTo)
|
||||||
import Text.CSV
|
import Text.CSV
|
||||||
import Test.HUnit
|
import Test.HUnit
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
@ -283,6 +285,8 @@ balancemode = (defCommandMode $ ["balance"] ++ aliases) { -- also accept but don
|
|||||||
,flagReq ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format (in simple reports)"
|
,flagReq ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format (in simple reports)"
|
||||||
,flagNone ["pretty-tables"] (\opts -> setboolopt "pretty-tables" opts) "use unicode when displaying tables"
|
,flagNone ["pretty-tables"] (\opts -> setboolopt "pretty-tables" opts) "use unicode when displaying tables"
|
||||||
,flagNone ["sort-amount","S"] (\opts -> setboolopt "sort-amount" opts) "sort by amount instead of account name"
|
,flagNone ["sort-amount","S"] (\opts -> setboolopt "sort-amount" opts) "sort by amount instead of account name"
|
||||||
|
,flagNone ["budget"] (setboolopt "budget") "compute budget from periodic transactions and compare real balances to it"
|
||||||
|
,flagNone ["show-unbudgeted"] (setboolopt "show-unbudgeted") "show full names of accounts not mentioned in budget"
|
||||||
]
|
]
|
||||||
++ outputflags
|
++ outputflags
|
||||||
,groupHidden = []
|
,groupHidden = []
|
||||||
@ -293,7 +297,7 @@ balancemode = (defCommandMode $ ["balance"] ++ aliases) { -- also accept but don
|
|||||||
|
|
||||||
-- | The balance command, prints a balance report.
|
-- | The balance command, prints a balance report.
|
||||||
balance :: CliOpts -> Journal -> IO ()
|
balance :: CliOpts -> Journal -> IO ()
|
||||||
balance opts@CliOpts{reportopts_=ropts} j = do
|
balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
||||||
d <- getCurrentDay
|
d <- getCurrentDay
|
||||||
case lineFormatFromOpts ropts of
|
case lineFormatFromOpts ropts of
|
||||||
Left err -> error' $ unlines [err]
|
Left err -> error' $ unlines [err]
|
||||||
@ -319,12 +323,58 @@ balance opts@CliOpts{reportopts_=ropts} j = do
|
|||||||
"csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r
|
"csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r
|
||||||
_ -> balanceReportAsText
|
_ -> balanceReportAsText
|
||||||
writeOutput opts $ render ropts report
|
writeOutput opts $ render ropts report
|
||||||
_ -> do
|
|
||||||
|
_ | boolopt "budget" rawopts -> do
|
||||||
|
let budget = budgetJournal opts j
|
||||||
|
j' = budgetRollUp opts budget j
|
||||||
|
report = multiBalanceReport ropts (queryFromOpts d ropts) j'
|
||||||
|
budgetReport = multiBalanceReport ropts (queryFromOpts d ropts) budget
|
||||||
|
render = case format of
|
||||||
|
-- XXX: implement csv rendering
|
||||||
|
"csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts
|
||||||
|
_ -> multiBalanceReportWithBudgetAsText ropts budgetReport
|
||||||
|
writeOutput opts $ render report
|
||||||
|
|
||||||
|
| otherwise -> do
|
||||||
let report = multiBalanceReport ropts (queryFromOpts d ropts) j
|
let report = multiBalanceReport ropts (queryFromOpts d ropts) j
|
||||||
render = case format of
|
render = case format of
|
||||||
"csv" -> \ropts r -> (++ "\n") $ printCSV $ multiBalanceReportAsCsv ropts r
|
"csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts
|
||||||
_ -> multiBalanceReportAsText
|
_ -> multiBalanceReportAsText ropts
|
||||||
writeOutput opts $ render ropts report
|
writeOutput opts $ render report
|
||||||
|
|
||||||
|
-- | Re-map account names to closet parent with periodic transaction from budget.
|
||||||
|
-- Accounts that dont have suitable parent are either remapped to "<unbudgeted>:topAccount"
|
||||||
|
-- or left as-is if --show-unbudgeted is provided
|
||||||
|
budgetRollUp :: CliOpts -> Journal -> Journal -> Journal
|
||||||
|
budgetRollUp CliOpts{rawopts_=rawopts} budget j = j { jtxns = remapTxn <$> jtxns j }
|
||||||
|
where
|
||||||
|
budgetAccounts = nub $ concatMap (map paccount . ptpostings) $ jperiodictxns budget
|
||||||
|
remapAccount origAcctName = remapAccount' origAcctName
|
||||||
|
where
|
||||||
|
remapAccount' acctName
|
||||||
|
| acctName `elem` budgetAccounts = acctName
|
||||||
|
| otherwise =
|
||||||
|
case parentAccountName acctName of
|
||||||
|
"" | boolopt "show-unbudgeted" rawopts -> origAcctName
|
||||||
|
| otherwise -> T.append (T.pack "<unbudgeted>:") acctName
|
||||||
|
parent -> remapAccount' parent
|
||||||
|
remapPosting p = p { paccount = remapAccount $ paccount p, porigin = Just . fromMaybe p $ porigin p }
|
||||||
|
remapTxn = mapPostings (map remapPosting)
|
||||||
|
mapPostings f t = txnTieKnot $ t { tpostings = f $ tpostings t }
|
||||||
|
|
||||||
|
-- | Generate journal of all periodic transactions in the given journal for the
|
||||||
|
-- entireity of its history or reporting period, whatever is smaller
|
||||||
|
budgetJournal :: CliOpts -> Journal -> Journal
|
||||||
|
budgetJournal opts j = journalBalanceTransactions' opts j { jtxns = budget }
|
||||||
|
where
|
||||||
|
dates = spanIntersect (jdatespan j) (periodAsDateSpan $ period_ $ reportopts_ opts)
|
||||||
|
budget = [makeBudget t | pt <- jperiodictxns j, t <- runPeriodicTransaction pt dates]
|
||||||
|
makeBudget t = txnTieKnot $ t { tdescription = T.pack "Budget transaction" }
|
||||||
|
journalBalanceTransactions' opts j =
|
||||||
|
let assrt = not . ignore_assertions_ $ inputopts_ opts
|
||||||
|
in
|
||||||
|
either error' id $ journalBalanceTransactions assrt j
|
||||||
|
|
||||||
|
|
||||||
-- single-column balance reports
|
-- single-column balance reports
|
||||||
|
|
||||||
@ -494,16 +544,73 @@ multiBalanceReportAsText opts r =
|
|||||||
CumulativeChange -> "Ending balances (cumulative)"
|
CumulativeChange -> "Ending balances (cumulative)"
|
||||||
HistoricalBalance -> "Ending balances (historical)"
|
HistoricalBalance -> "Ending balances (historical)"
|
||||||
|
|
||||||
|
-- | Render two multi-column balance reports as plain text suitable for console output.
|
||||||
|
-- They are assumed to have same number of columns, one of them representing
|
||||||
|
-- a budget
|
||||||
|
multiBalanceReportWithBudgetAsText :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport -> String
|
||||||
|
multiBalanceReportWithBudgetAsText opts budget r =
|
||||||
|
printf "%s in %s:\n\n" typeStr (showDateSpan $ multiBalanceReportSpan r)
|
||||||
|
++ renderBalanceReportTable' opts showcell tabl
|
||||||
|
where
|
||||||
|
tabl = combine (balanceReportAsTable opts r) (balanceReportAsTable opts budget)
|
||||||
|
typeStr :: String
|
||||||
|
typeStr = case balancetype_ opts of
|
||||||
|
PeriodChange -> "Balance changes"
|
||||||
|
CumulativeChange -> "Ending balances (cumulative)"
|
||||||
|
HistoricalBalance -> "Ending balances (historical)"
|
||||||
|
showcell (real, Nothing) = showamt real
|
||||||
|
showcell (real, Just budget) =
|
||||||
|
case percentage real budget of
|
||||||
|
Just pct -> printf "%s [%s%% of %s]" (showamt real) (show $ roundTo 0 pct) (showamt budget)
|
||||||
|
Nothing -> printf "%s [%s]" (showamt real) (showamt budget)
|
||||||
|
percentage real budget =
|
||||||
|
case (real, budget) of
|
||||||
|
(Mixed [a1], Mixed [a2]) | acommodity a1 == acommodity a2 && aquantity a2 /= 0 ->
|
||||||
|
Just $ 100 * aquantity a1 / aquantity a2
|
||||||
|
_ -> Nothing
|
||||||
|
showamt | color_ opts = cshowMixedAmountOneLineWithoutPrice
|
||||||
|
| otherwise = showMixedAmountOneLineWithoutPrice
|
||||||
|
-- combine reportTable budgetTable will combine them into a single table where cells
|
||||||
|
-- are tuples of (actual, Maybe budget) numbers. Main assumptions is that
|
||||||
|
-- row/column titles of budgetTable are subset of row/column titles or reportTable,
|
||||||
|
-- and there are now row/column titles in budgetTable that are not mentioned in reporTable.
|
||||||
|
-- Both of these are satisfied by construction of budget report and process of rolling up
|
||||||
|
-- account names.
|
||||||
|
combine (Table l t d) (Table l' t' d') = Table l t combinedRows
|
||||||
|
where
|
||||||
|
-- For all accounts that are present in the budget, zip real amounts with budget amounts
|
||||||
|
combinedRows = [ combineRow row budgetRow
|
||||||
|
| (acct, row) <- zip (headerContents l) d
|
||||||
|
, let budgetRow =
|
||||||
|
if acct == "" then [] -- "" is totals row
|
||||||
|
else fromMaybe [] $ Map.lookup acct budgetAccts
|
||||||
|
]
|
||||||
|
-- Budget could cover smaller interval of time than the whole journal.
|
||||||
|
-- Headers for budget row will always be a sublist of headers of row
|
||||||
|
combineRow r br =
|
||||||
|
let reportRow = zip (headerContents t) r
|
||||||
|
budgetRow = Map.fromList $ zip (headerContents t') br
|
||||||
|
findBudgetVal hdr = Map.lookup hdr budgetRow
|
||||||
|
in map (\(hdr, val) -> (val, findBudgetVal hdr)) reportRow
|
||||||
|
budgetAccts = Map.fromList $ zip (headerContents l') d'
|
||||||
|
|
||||||
-- | Given a table representing a multi-column balance report (for example,
|
-- | Given a table representing a multi-column balance report (for example,
|
||||||
-- made using 'balanceReportAsTable'), render it in a format suitable for
|
-- made using 'balanceReportAsTable'), render it in a format suitable for
|
||||||
-- console output.
|
-- console output.
|
||||||
renderBalanceReportTable :: ReportOpts -> Table String String MixedAmount -> String
|
renderBalanceReportTable :: ReportOpts -> Table String String MixedAmount -> String
|
||||||
renderBalanceReportTable (ReportOpts { pretty_tables_ = pretty, color_=usecolor }) =
|
renderBalanceReportTable ropts =
|
||||||
|
renderBalanceReportTable' ropts showamt
|
||||||
|
where
|
||||||
|
showamt | color_ ropts = cshowMixedAmountOneLineWithoutPrice
|
||||||
|
| otherwise = showMixedAmountOneLineWithoutPrice
|
||||||
|
|
||||||
|
renderBalanceReportTable' :: ReportOpts -> (a -> String) -> Table String String a -> String
|
||||||
|
renderBalanceReportTable' (ReportOpts { pretty_tables_ = pretty}) showCell =
|
||||||
unlines
|
unlines
|
||||||
. addtrailingblank
|
. addtrailingblank
|
||||||
. trimborder
|
. trimborder
|
||||||
. lines
|
. lines
|
||||||
. render pretty id id showamt
|
. render pretty id id showCell
|
||||||
. align
|
. align
|
||||||
where
|
where
|
||||||
addtrailingblank = (++[""])
|
addtrailingblank = (++[""])
|
||||||
@ -512,8 +619,6 @@ renderBalanceReportTable (ReportOpts { pretty_tables_ = pretty, color_=usecolor
|
|||||||
where
|
where
|
||||||
acctswidth = maximum' $ map strWidth (headerContents l)
|
acctswidth = maximum' $ map strWidth (headerContents l)
|
||||||
l' = padRightWide acctswidth <$> l
|
l' = padRightWide acctswidth <$> l
|
||||||
showamt | usecolor = cshowMixedAmountOneLineWithoutPrice
|
|
||||||
| otherwise = showMixedAmountOneLineWithoutPrice
|
|
||||||
|
|
||||||
-- | Build a 'Table' from a multi-column balance report.
|
-- | Build a 'Table' from a multi-column balance report.
|
||||||
balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount
|
balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import Data.List
|
|||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.IO as T
|
import qualified Data.Text.IO as T
|
||||||
import Data.Time (Day)
|
import Data.Time (Day, addDays)
|
||||||
import Data.Word
|
import Data.Word
|
||||||
import Numeric
|
import Numeric
|
||||||
import Safe (readMay)
|
import Safe (readMay)
|
||||||
@ -54,6 +54,7 @@ import Hledger.Data
|
|||||||
import Hledger.Read
|
import Hledger.Read
|
||||||
import Hledger.Reports
|
import Hledger.Reports
|
||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
|
import Hledger.Query (Query(Any))
|
||||||
|
|
||||||
|
|
||||||
-- | Parse the user's specified journal file, maybe apply some transformations
|
-- | Parse the user's specified journal file, maybe apply some transformations
|
||||||
@ -70,6 +71,8 @@ withJournalDo opts cmd = do
|
|||||||
. anonymiseByOpts opts
|
. anonymiseByOpts opts
|
||||||
. journalApplyAliases (aliasesFromOpts opts)
|
. journalApplyAliases (aliasesFromOpts opts)
|
||||||
<=< journalApplyValue (reportopts_ opts)
|
<=< journalApplyValue (reportopts_ opts)
|
||||||
|
<=< journalAddForecast opts
|
||||||
|
. generateAutomaticPostings (reportopts_ opts)
|
||||||
either error' f ej
|
either error' f ej
|
||||||
|
|
||||||
-- | Apply the pivot transformation on a journal, if option is present.
|
-- | Apply the pivot transformation on a journal, if option is present.
|
||||||
@ -117,6 +120,38 @@ journalApplyValue ropts j = do
|
|||||||
= id
|
= id
|
||||||
return $ convert j
|
return $ convert j
|
||||||
|
|
||||||
|
-- | Run PeriodicTransactions from journal from today or journal end to requested end day.
|
||||||
|
-- Add generated transactions to the journal
|
||||||
|
journalAddForecast :: CliOpts -> Journal -> IO Journal
|
||||||
|
journalAddForecast opts j = do
|
||||||
|
today <- getCurrentDay
|
||||||
|
-- Create forecast starting from end of journal + 1 day, and until the end of requested reporting period
|
||||||
|
-- If end is not provided, do 180 days of forecast.
|
||||||
|
-- Note that jdatespan already returns last day + 1
|
||||||
|
let startDate = fromMaybe today $ spanEnd (jdatespan j)
|
||||||
|
endDate = fromMaybe (addDays 180 today) $ periodEnd (period_ ropts)
|
||||||
|
dates = DateSpan (Just startDate) (Just endDate)
|
||||||
|
withForecast = [makeForecast t | pt <- jperiodictxns j, t <- runPeriodicTransaction pt dates, spanContainsDate dates (tdate t) ] ++ (jtxns j)
|
||||||
|
makeForecast t = txnTieKnot $ t { tdescription = T.pack "Forecast transaction" }
|
||||||
|
ropts = reportopts_ opts
|
||||||
|
if forecast_ ropts
|
||||||
|
then return $ journalBalanceTransactions' opts j { jtxns = withForecast }
|
||||||
|
else return j
|
||||||
|
where
|
||||||
|
journalBalanceTransactions' opts j =
|
||||||
|
let assrt = not . ignore_assertions_ $ inputopts_ opts
|
||||||
|
in
|
||||||
|
either error' id $ journalBalanceTransactions assrt j
|
||||||
|
|
||||||
|
-- | Generate Automatic postings and add them to the current journal.
|
||||||
|
generateAutomaticPostings :: ReportOpts -> Journal -> Journal
|
||||||
|
generateAutomaticPostings ropts j =
|
||||||
|
if auto_ ropts then j { jtxns = map modifier $ jtxns j } else j
|
||||||
|
where
|
||||||
|
modifier = foldr (flip (.) . runModifierTransaction') id mtxns
|
||||||
|
runModifierTransaction' = fmap txnTieKnot . runModifierTransaction Any
|
||||||
|
mtxns = jmodifiertxns j
|
||||||
|
|
||||||
-- | Write some output to stdout or to a file selected by --output-file.
|
-- | Write some output to stdout or to a file selected by --output-file.
|
||||||
-- If the file exists it will be overwritten.
|
-- If the file exists it will be overwritten.
|
||||||
writeOutput :: CliOpts -> String -> IO ()
|
writeOutput :: CliOpts -> String -> IO ()
|
||||||
|
|||||||
@ -239,21 +239,59 @@ Examples:
|
|||||||
`-p "quarterly"`
|
`-p "quarterly"`
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
|
Note that `weekly`, `monthly`, `quarterly` and `yearly` intervals will
|
||||||
|
always start on the first day on week, month, quarter or year
|
||||||
|
accordingly, and will end on the last day of same period, even if
|
||||||
|
associated period expression specifies different explicit start and end date.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
------------------------------------------
|
||||||
|
`-p "weekly from 2009/1/1 to 2009/4/1"` -- starts on 2008/12/29, closest preceeding Monday
|
||||||
|
`-p "monthly in 2008/11/25"` -- starts on 2018/11/01
|
||||||
|
`-p "quarterly from 2009-05-05 to 2009-06-01"` - starts on 2009/04/01, ends on 2009/06/30, which are first and last days of Q2 2009
|
||||||
|
`-p "yearly from 2009-12-29"` - starts on 2009/01/01, first day of 2009
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
The following more complex report intervals are also supported:
|
The following more complex report intervals are also supported:
|
||||||
`biweekly`,
|
`biweekly`,
|
||||||
`bimonthly`,
|
`bimonthly`,
|
||||||
`every N days|weeks|months|quarters|years`,
|
`every day|week|month|quarter|year`,
|
||||||
`every Nth day [of month]`,
|
`every N days|weeks|months|quarters|years`.
|
||||||
`every Nth day of week`.
|
|
||||||
|
|
||||||
|
All of these will start on the first day of the requested period and end on the last one, as described above.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
`-p "bimonthly from 2008"`
|
`-p "bimonthly from 2008"` -- periods will have boundaries on 2008/01/01, 2008/03/01, ...
|
||||||
`-p "every 2 weeks"`
|
`-p "every 2 weeks"` -- starts on closest preceeding Monday
|
||||||
`-p "every 5 days from 1/3"`
|
`-p "every 5 month from 2009/03"` -- periods will have boundaries on 2009/03/01, 2009/08/01, ...
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
|
If you want intervals that start on arbitrary day of your choosing and span a week, month or year, you need to use any of the following:
|
||||||
|
|
||||||
|
`every Nth day of week`,
|
||||||
|
`every <weekday>`,
|
||||||
|
`every Nth day [of month]`,
|
||||||
|
`every Nth weekday [of month]`,
|
||||||
|
`every MM/DD [of year]`,
|
||||||
|
`every Nth MMM [of year]`,
|
||||||
|
`every MMM Nth [of year]`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
`-p "every 2nd day of week"` -- periods will go from Tue to Tue
|
||||||
|
`-p "every Tue"` -- same
|
||||||
|
`-p "every 15th day"` -- period boundaries will be on 15th of each month
|
||||||
|
`-p "every 2nd Monday"` -- period boundaries will be on second Monday of each month
|
||||||
|
`-p "every 11/05"` -- yearly periods with boundaries on 5th of Nov
|
||||||
|
`-p "every 5th Nov"` -- same
|
||||||
|
`-p "every Nov 5th"` -- same
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
Show historical balances at end of 15th each month (N is exclusive end date):
|
Show historical balances at end of 15th each month (N is exclusive end date):
|
||||||
|
|
||||||
`hledger balance -H -p "every 16th day"`
|
`hledger balance -H -p "every 16th day"`
|
||||||
|
|||||||
@ -113,6 +113,7 @@ library
|
|||||||
, text >=0.11
|
, text >=0.11
|
||||||
, utf8-string >=0.3.5 && <1.1
|
, utf8-string >=0.3.5 && <1.1
|
||||||
, wizards ==1.0.*
|
, wizards ==1.0.*
|
||||||
|
, Decimal
|
||||||
if (!(os(windows))) && (flag(terminfo))
|
if (!(os(windows))) && (flag(terminfo))
|
||||||
build-depends:
|
build-depends:
|
||||||
terminfo
|
terminfo
|
||||||
@ -192,6 +193,7 @@ executable hledger
|
|||||||
, text >=0.11
|
, text >=0.11
|
||||||
, utf8-string >=0.3.5 && <1.1
|
, utf8-string >=0.3.5 && <1.1
|
||||||
, wizards ==1.0.*
|
, wizards ==1.0.*
|
||||||
|
, Decimal
|
||||||
if (!(os(windows))) && (flag(terminfo))
|
if (!(os(windows))) && (flag(terminfo))
|
||||||
build-depends:
|
build-depends:
|
||||||
terminfo
|
terminfo
|
||||||
@ -241,6 +243,7 @@ test-suite test
|
|||||||
, text >=0.11
|
, text >=0.11
|
||||||
, utf8-string >=0.3.5 && <1.1
|
, utf8-string >=0.3.5 && <1.1
|
||||||
, wizards ==1.0.*
|
, wizards ==1.0.*
|
||||||
|
, Decimal
|
||||||
, test-framework
|
, test-framework
|
||||||
, test-framework-hunit
|
, test-framework-hunit
|
||||||
if (!(os(windows))) && (flag(terminfo))
|
if (!(os(windows))) && (flag(terminfo))
|
||||||
|
|||||||
@ -149,6 +149,7 @@ library:
|
|||||||
- text >=0.11
|
- text >=0.11
|
||||||
- utf8-string >=0.3.5 && <1.1
|
- utf8-string >=0.3.5 && <1.1
|
||||||
- wizards ==1.0.*
|
- wizards ==1.0.*
|
||||||
|
- Decimal
|
||||||
|
|
||||||
executables:
|
executables:
|
||||||
hledger:
|
hledger:
|
||||||
@ -178,6 +179,7 @@ executables:
|
|||||||
- text >=0.11
|
- text >=0.11
|
||||||
- utf8-string >=0.3.5 && <1.1
|
- utf8-string >=0.3.5 && <1.1
|
||||||
- wizards ==1.0.*
|
- wizards ==1.0.*
|
||||||
|
- Decimal
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
test:
|
test:
|
||||||
@ -204,6 +206,7 @@ tests:
|
|||||||
- text >=0.11
|
- text >=0.11
|
||||||
- utf8-string >=0.3.5 && <1.1
|
- utf8-string >=0.3.5 && <1.1
|
||||||
- wizards ==1.0.*
|
- wizards ==1.0.*
|
||||||
|
- Decimal
|
||||||
- test-framework
|
- test-framework
|
||||||
- test-framework-hunit
|
- test-framework-hunit
|
||||||
|
|
||||||
|
|||||||
323
site/budgeting-and-forecasting.md
Normal file
323
site/budgeting-and-forecasting.md
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
# Budgeting and forecasting
|
||||||
|
|
||||||
|
Budgeting and forecasting allows you to keep better track of your expenses and future financial situation.
|
||||||
|
If you write down your expectations of what your income/expenses/investment yields/etc should be, you can use them to:
|
||||||
|
- check how far off are your expectations from reality (budgeting)
|
||||||
|
- project your future account activity or balances (forecasting)
|
||||||
|
|
||||||
|
(This section uses examples/bcexample.hledger from hledger source repository).
|
||||||
|
|
||||||
|
## Periodic budget
|
||||||
|
To start budgeting, you need to know what your average yearly or weekly expenditures are. Hledger could help you with that.
|
||||||
|
Usually the interval for which you compute budget figures will be the same as the interval between
|
||||||
|
your paychecks -- monthly or weekly.
|
||||||
|
|
||||||
|
Lets create monthly (-M) report for years 2013-2014 (-b 2013) of all
|
||||||
|
top-level expense categories (--depth 2 Expenses), looking for average
|
||||||
|
figures (-A) in the cost at the time of transaction (-B), limiting
|
||||||
|
ourselves to USD transactions only, to save screen space:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -MBA -b 2013 --depth 2 Expenses cur:USD
|
||||||
|
Balance changes in 2013/01/01-2014/10/31:
|
||||||
|
|
||||||
|
|| 2013/01 2013/02 2013/03 ... 2014/07 2014/08 2014/09 2014/10 Average
|
||||||
|
====================++========================================...==================================================================
|
||||||
|
Expenses:Financial || 4.00 USD 12.95 USD 39.80 USD ... 30.85 USD 21.90 USD 12.95 USD 4.00 USD 17.83 USD
|
||||||
|
Expenses:Food || 396.46 USD 481.48 USD 603.32 USD ... 871.20 USD 768.23 USD 466.72 USD 83.00 USD 562.10 USD
|
||||||
|
Expenses:Health || 290.70 USD 193.80 USD 193.80 USD ... 290.70 USD 193.80 USD 193.80 USD 96.90 USD 207.01 USD
|
||||||
|
Expenses:Home || 2544.98 USD 2545.02 USD 2544.97 USD ... 2545.12 USD 2545.01 USD 2545.10 USD 0 2429.33 USD
|
||||||
|
Expenses:Taxes || 5976.60 USD 3984.40 USD 4901.83 USD ... 5976.60 USD 3984.40 USD 3984.40 USD 1992.20 USD 4322.27 USD
|
||||||
|
Expenses:Transport || 120.00 USD 120.00 USD 120.00 USD ... 0 120.00 USD 120.00 USD 120.00 USD 109.09 USD
|
||||||
|
--------------------++----------------------------------------...------------------------------------------------------------------
|
||||||
|
|| 9332.74 USD 7337.65 USD 8403.72 USD ... 9714.47 USD 7633.34 USD 7322.97 USD 2296.10 USD 7647.64 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
This report is rather wide and portion of it had been cut out for
|
||||||
|
brevity. Most interesting column is the last one, it shows average
|
||||||
|
monthly expenses for each category. Expenses in Food, Health, Home and
|
||||||
|
Transport categories seem to roughly similar month to month, so lets
|
||||||
|
create a budget for them.
|
||||||
|
|
||||||
|
Budgets are described with periodic transactions. Periodic transaction
|
||||||
|
has `~` instead of date and period expression instead of description. In this case
|
||||||
|
we want to create a monthly budget that will come into effect starting from January 2013,
|
||||||
|
which will include income of 10000 USD that is partically spent on Food, Health, Home and Transport
|
||||||
|
and the rest becomes our Assets:
|
||||||
|
|
||||||
|
```journal
|
||||||
|
~ monthly from 2013/01
|
||||||
|
Expenses:Food 500 USD
|
||||||
|
Expenses:Health 200 USD
|
||||||
|
Expenses:Home 2545 USD
|
||||||
|
Expenses:Transport 120 USD
|
||||||
|
Income:US -10700 USD ;; Taken as monthy average of Income account group
|
||||||
|
Assets:US
|
||||||
|
```
|
||||||
|
|
||||||
|
This transaction could be put into separate file (budget.journal) or
|
||||||
|
could be kept in the main journal. Normally hledger will ignore it and
|
||||||
|
will not include it in any computations or reports.
|
||||||
|
|
||||||
|
To put it into action, you need to add `--budget` switch to your balance invocation. If you do that,
|
||||||
|
you would be able to see how your past expenses aligned with the budget that you just created. This
|
||||||
|
time, lets not limit accounts in any way:
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -MB -b 2013 --budget cur:USD
|
||||||
|
Balance changes in 2013/01/01-2014/10/31:
|
||||||
|
|
||||||
|
|| 2013/01 2013/02 2013/03
|
||||||
|
==========================++===========================================================================================================
|
||||||
|
<unbudgeted>:Expenses || 5980.60 USD 3997.35 USD 4941.63 USD
|
||||||
|
<unbudgeted>:Liabilities || 293.09 USD -147.51 USD -66.01 USD
|
||||||
|
Assets:US || 1893.32 USD [26% of 7335 USD] 2929.77 USD [40% of 7335 USD] -3898.89 USD [-53% of 7335 USD]
|
||||||
|
Expenses:Food || 396.46 USD [79% of 500 USD] 481.48 USD [96% of 500 USD] 603.32 USD [121% of 500 USD]
|
||||||
|
Expenses:Health || 290.70 USD [145% of 200 USD] 193.80 USD [97% of 200 USD] 193.80 USD [97% of 200 USD]
|
||||||
|
Expenses:Home || 2544.98 USD [100% of 2545 USD] 2545.02 USD [100% of 2545 USD] 2544.97 USD [100% of 2545 USD]
|
||||||
|
Expenses:Transport || 120.00 USD [100% of 120 USD] 120.00 USD [100% of 120 USD] 120.00 USD [100% of 120 USD]
|
||||||
|
Income:US || -15119.10 USD [141% of -10700 USD] -10331.21 USD [97% of -10700 USD] -11079.40 USD [104% of -10700 USD]
|
||||||
|
--------------------------++-----------------------------------------------------------------------------------------------------------
|
||||||
|
|| -3599.95 USD -211.30 USD -6640.58 USD
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Numbers in square brackets give you your budget estimate and percentage of it used by your real expenses. Numbers below 100% mean
|
||||||
|
that you have some of your budget left, numbers over 100% mean that you went over your budget.
|
||||||
|
|
||||||
|
You can notice that actual numbers for Assets:US seem to be well below computed budget of 7335 USD. Why? Answer to this is in the first
|
||||||
|
row of the report: we have quite a lot of unbudgeted Expenses!
|
||||||
|
|
||||||
|
Notice that even though we have not limited accounts in any way, report includes just those mentioned in the budget. This is on purpose,
|
||||||
|
assumption is that when you are checking your budgets you probably do not want unbudgeted accounts getting in your way. Another thing to
|
||||||
|
note is that budget numbers have been allocated to top-level expense subcategories (like Expenses:Food). Journal has subaccounts under
|
||||||
|
Food, but to compute budget report they have all been rolled up into a nearest parent with budget number associated with it. Accounts that
|
||||||
|
do not have such parent went into `<unbudgeted>` row.
|
||||||
|
|
||||||
|
Allright, it seems that for Jan 2013 we have ~3000 USD of budgeted expenses and almost twice as much unbudgeted. Lets figure out what they are.
|
||||||
|
We can see more details if we add `--show-unbudgeted` switch:
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -M -b 2013-01 -e 2013-02 --budget cur:USD --show-unbudgeted
|
||||||
|
Balance changes in 2013/01:
|
||||||
|
|
||||||
|
|| 2013/01
|
||||||
|
==================================++====================================
|
||||||
|
Assets:US || 1893.32 USD [26% of 7335 USD]
|
||||||
|
Expenses:Financial:Fees || 4.00 USD
|
||||||
|
Expenses:Food || 396.46 USD [79% of 500 USD]
|
||||||
|
Expenses:Health || 290.70 USD [145% of 200 USD]
|
||||||
|
Expenses:Home || 2544.98 USD [100% of 2545 USD]
|
||||||
|
Expenses:Taxes:Y2013:US:CityNYC || 524.76 USD
|
||||||
|
Expenses:Taxes:Y2013:US:Federal || 3188.76 USD
|
||||||
|
Expenses:Taxes:Y2013:US:Medicare || 319.86 USD
|
||||||
|
Expenses:Taxes:Y2013:US:SDI || 3.36 USD
|
||||||
|
Expenses:Taxes:Y2013:US:SocSec || 844.62 USD
|
||||||
|
Expenses:Taxes:Y2013:US:State || 1095.24 USD
|
||||||
|
Expenses:Transport || 120.00 USD [100% of 120 USD]
|
||||||
|
Income:US || -15119.10 USD [141% of -10700 USD]
|
||||||
|
Liabilities:US:Chase:Slate || 293.09 USD
|
||||||
|
----------------------------------++------------------------------------
|
||||||
|
|| -3599.95 USD
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
All the accounts that were rolled up into `<unbudgeted>` category are now shown with their original name, but budgeted accounts are still rolled up. It
|
||||||
|
is easy to see now that we forgot taxes. Lets add them to our budget:
|
||||||
|
```journal
|
||||||
|
~ monthly from 2013/01
|
||||||
|
Expenses:Food 500 USD
|
||||||
|
Expenses:Health 200 USD
|
||||||
|
Expenses:Home 2545 USD
|
||||||
|
Expenses:Transport 120 USD
|
||||||
|
Expenses:Taxes 4300 USD ;; Taken from monthly average report
|
||||||
|
Income:US -10700 USD
|
||||||
|
Assets:US
|
||||||
|
```
|
||||||
|
|
||||||
|
Lets try again for a couple of month with this updated budget:
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -M -b 2013-01 -e 2013-04 --budget cur:USD
|
||||||
|
Balance changes in 2013q1:
|
||||||
|
|
||||||
|
|| 2013/01 2013/02 2013/03
|
||||||
|
==========================++===========================================================================================================
|
||||||
|
<unbudgeted>:Expenses || 4.00 USD 12.95 USD 39.80 USD
|
||||||
|
<unbudgeted>:Liabilities || 293.09 USD -147.51 USD -66.01 USD
|
||||||
|
Assets:US || 1893.32 USD [62% of 3035 USD] 2929.77 USD [97% of 3035 USD] -3898.89 USD [-128% of 3035 USD]
|
||||||
|
Expenses:Food || 396.46 USD [79% of 500 USD] 481.48 USD [96% of 500 USD] 603.32 USD [121% of 500 USD]
|
||||||
|
Expenses:Health || 290.70 USD [145% of 200 USD] 193.80 USD [97% of 200 USD] 193.80 USD [97% of 200 USD]
|
||||||
|
Expenses:Home || 2544.98 USD [100% of 2545 USD] 2545.02 USD [100% of 2545 USD] 2544.97 USD [100% of 2545 USD]
|
||||||
|
Expenses:Taxes || 5976.60 USD [139% of 4300 USD] 3984.40 USD [93% of 4300 USD] 4901.83 USD [114% of 4300 USD]
|
||||||
|
Expenses:Transport || 120.00 USD [100% of 120 USD] 120.00 USD [100% of 120 USD] 120.00 USD [100% of 120 USD]
|
||||||
|
Income:US || -15119.10 USD [141% of -10700 USD] -10331.21 USD [97% of -10700 USD] -11079.40 USD [104% of -10700 USD]
|
||||||
|
--------------------------++-----------------------------------------------------------------------------------------------------------
|
||||||
|
|| -3599.95 USD -211.30 USD -6640.58 USD
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now unbudgeted amounts are much smaller and some of them could be dismissed as noise, and we can see that budget created is actually
|
||||||
|
close enough to the real numbers, meaning that they are usually close to average that we put in our budget.
|
||||||
|
|
||||||
|
## Envelope budget
|
||||||
|
|
||||||
|
Budget report that we have used so far assumes that any unused budget amount for a given (monthly) period will not contribute to the
|
||||||
|
budget of the next period. Alternative popular "envelope budget" strategy assumes that you put a certain amount of money into an envelope
|
||||||
|
each month, and any unused amount stays there for future expenses. This is easy to simulate by adding --cumulative switch. Lets redo
|
||||||
|
the last report with it:
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -M -b 2013-01 -e 2013-04 --cumulative --budget cur:USD
|
||||||
|
Ending balances (cumulative) in 2013q1:
|
||||||
|
|
||||||
|
|| 2013/01/31 2013/02/28 2013/03/31
|
||||||
|
==========================++============================================================================================================
|
||||||
|
<unbudgeted>:Expenses || 4.00 USD 16.95 USD 56.75 USD
|
||||||
|
<unbudgeted>:Liabilities || 293.09 USD 145.58 USD 79.57 USD
|
||||||
|
Assets:US || 1893.32 USD [62% of 3035 USD] 4823.09 USD [79% of 6070 USD] 924.20 USD [10% of 9105 USD]
|
||||||
|
Expenses:Food || 396.46 USD [79% of 500 USD] 877.94 USD [88% of 1000 USD] 1481.26 USD [99% of 1500 USD]
|
||||||
|
Expenses:Health || 290.70 USD [145% of 200 USD] 484.50 USD [121% of 400 USD] 678.30 USD [113% of 600 USD]
|
||||||
|
Expenses:Home || 2544.98 USD [100% of 2545 USD] 5090.00 USD [100% of 5090 USD] 7634.97 USD [100% of 7635 USD]
|
||||||
|
Expenses:Taxes || 5976.60 USD [139% of 4300 USD] 9961.00 USD [116% of 8600 USD] 14862.83 USD [115% of 12900 USD]
|
||||||
|
Expenses:Transport || 120.00 USD [100% of 120 USD] 240.00 USD [100% of 240 USD] 360.00 USD [100% of 360 USD]
|
||||||
|
Income:US || -15119.10 USD [141% of -10700 USD] -25450.31 USD [119% of -21400 USD] -36529.71 USD [114% of -32100 USD]
|
||||||
|
--------------------------++------------------------------------------------------------------------------------------------------------
|
||||||
|
|| -3599.95 USD -3811.25 USD -10451.83 USD
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If you look at Expenses:Food category, you will see that every month budget is increased by 500 USD, and by March total amount budgeted
|
||||||
|
is 1500 USD, of which 1481.26 USD is spent. If you look back at the previous non-cumulative monthly budget report, you will see that in March food expenses
|
||||||
|
were 121% of the budgeted amount, but cumulative report shows that taking into account budget carry-over from Jan and Feb we are well withing planned numbers.
|
||||||
|
|
||||||
|
# Forecasting
|
||||||
|
|
||||||
|
Budget transaction that was created could be used to predict what would be our financial situation in the future. If you add `--forecast` switch, you will
|
||||||
|
see how budgeted income and expense affects you past the last transaction in the journal. Since journal ends in Oct 2014, lets see next two month:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -M -b 2014-10 -e 2015 --forecast cur:USD
|
||||||
|
Balance changes in 2014q4:
|
||||||
|
|
||||||
|
|| 2014/10 2014/11 2014/12
|
||||||
|
====================================++======================================
|
||||||
|
Assets:US || 0 3035 USD 3035 USD
|
||||||
|
Assets:US:BofA:Checking || -2453.40 USD 0 0
|
||||||
|
Assets:US:ETrade:Cash || 5000.00 USD 0 0
|
||||||
|
Expenses:Financial:Fees || 4.00 USD 0 0
|
||||||
|
Expenses:Food || 0 500 USD 500 USD
|
||||||
|
Expenses:Food:Restaurant || 83.00 USD 0 0
|
||||||
|
Expenses:Health || 0 200 USD 200 USD
|
||||||
|
Expenses:Health:Dental:Insurance || 2.90 USD 0 0
|
||||||
|
Expenses:Health:Life:GroupTermLife || 24.32 USD 0 0
|
||||||
|
Expenses:Health:Medical:Insurance || 27.38 USD 0 0
|
||||||
|
Expenses:Health:Vision:Insurance || 42.30 USD 0 0
|
||||||
|
Expenses:Home || 0 2545 USD 2545 USD
|
||||||
|
Expenses:Taxes || 0 4300 USD 4300 USD
|
||||||
|
Expenses:Taxes:Y2014:US:CityNYC || 174.92 USD 0 0
|
||||||
|
Expenses:Taxes:Y2014:US:Federal || 1062.92 USD 0 0
|
||||||
|
Expenses:Taxes:Y2014:US:Medicare || 106.62 USD 0 0
|
||||||
|
Expenses:Taxes:Y2014:US:SDI || 1.12 USD 0 0
|
||||||
|
Expenses:Taxes:Y2014:US:SocSec || 281.54 USD 0 0
|
||||||
|
Expenses:Taxes:Y2014:US:State || 365.08 USD 0 0
|
||||||
|
Expenses:Transport || 0 120 USD 120 USD
|
||||||
|
Expenses:Transport:Tram || 120.00 USD 0 0
|
||||||
|
Income:US || 0 -10700 USD -10700 USD
|
||||||
|
Income:US:Hoogle:GroupTermLife || -24.32 USD 0 0
|
||||||
|
Income:US:Hoogle:Salary || -4615.38 USD 0 0
|
||||||
|
Liabilities:US:Chase:Slate || -203.00 USD 0 0
|
||||||
|
------------------------------------++--------------------------------------
|
||||||
|
|| 0 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this time there is no roll-up of accounts. Unlike `--budget`, which could be used with `balance` command only, `--forecast`
|
||||||
|
could be used with any report. Forecast transactions would be added to your real journal and would appear in the report you requested as
|
||||||
|
if you have entered them on the scheduled dates.
|
||||||
|
|
||||||
|
Since quite a lot of accounts do not have any budgeted transactions, lets limit the depth of the report to avoid seeing lots of zeroes:
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -M -b 2014-10 -e 2015 --forecast cur:USD --depth 2
|
||||||
|
Balance changes in 2014q4:
|
||||||
|
|
||||||
|
|| 2014/10 2014/11 2014/12
|
||||||
|
====================++======================================
|
||||||
|
Assets:US || 2546.60 USD 3035 USD 3035 USD
|
||||||
|
Expenses:Financial || 4.00 USD 0 0
|
||||||
|
Expenses:Food || 83.00 USD 500 USD 500 USD
|
||||||
|
Expenses:Health || 96.90 USD 200 USD 200 USD
|
||||||
|
Expenses:Home || 0 2545 USD 2545 USD
|
||||||
|
Expenses:Taxes || 1992.20 USD 4300 USD 4300 USD
|
||||||
|
Expenses:Transport || 120.00 USD 120 USD 120 USD
|
||||||
|
Income:US || -4639.70 USD -10700 USD -10700 USD
|
||||||
|
Liabilities:US || -203.00 USD 0 0
|
||||||
|
--------------------++--------------------------------------
|
||||||
|
|| 0 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we should expect 3035 USD to be added into Assets:US each month. It is quite easy to see how overal amount of Assets will change with time if you use
|
||||||
|
`--cumulative` switch:
|
||||||
|
```shell
|
||||||
|
$ hledger balance -f bcexample.hledger -f budget.journal -M -b 2014-10 -e 2015 --forecast cur:USD --depth 2 --cumulative
|
||||||
|
Ending balances (cumulative) in 2014q4:
|
||||||
|
|
||||||
|
|| 2014/10/31 2014/11/30 2014/12/31
|
||||||
|
====================++============================================
|
||||||
|
Assets:US || 2546.60 USD 5581.60 USD 8616.60 USD
|
||||||
|
Expenses:Financial || 4.00 USD 4.00 USD 4.00 USD
|
||||||
|
Expenses:Food || 83.00 USD 583.00 USD 1083.00 USD
|
||||||
|
Expenses:Health || 96.90 USD 296.90 USD 496.90 USD
|
||||||
|
Expenses:Home || 0 2545 USD 5090 USD
|
||||||
|
Expenses:Taxes || 1992.20 USD 6292.20 USD 10592.20 USD
|
||||||
|
Expenses:Transport || 120.00 USD 240.00 USD 360.00 USD
|
||||||
|
Income:US || -4639.70 USD -15339.70 USD -26039.70 USD
|
||||||
|
Liabilities:US || -203.00 USD -203.00 USD -203.00 USD
|
||||||
|
--------------------++--------------------------------------------
|
||||||
|
|| 0 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
According to forecast, assets are expected to grow to 8600+ USD by the end of 2014. However, our forecast does not include a couple
|
||||||
|
of big one-off year end expenses. First, we plan to buy prize turkey for the Christmas table every year from 2014, spending up to 500 USD on it.
|
||||||
|
And on 17th Nov 2014 we would celebrate birthday of significant other, spending up to 6000 USD in a fancy restaurant:
|
||||||
|
```journal
|
||||||
|
~ every 20th Dec from 2014
|
||||||
|
Expenses:Food 500 USD ; Prize turkey, the biggest of the big
|
||||||
|
Assets:US
|
||||||
|
|
||||||
|
~ 2014/11/17
|
||||||
|
Assets:US
|
||||||
|
Expenses:Food 6000 USD ; Birthday, lots of guests
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that turkey transaction is not entered as "yearly from 2014/12/20", since yearly/quarterly/monthy/weekly periodic expressions always generate
|
||||||
|
entries at the first day of the calendar year/quarter/month/week. Thus "monthly from 2014/12" will occur on 2014/12/01, 2015/01/01, ..., whereas
|
||||||
|
"every 20th of month from 2014/12" will happen on 2014/12/20, 2015/12/20, etc.
|
||||||
|
|
||||||
|
With latest additions forecast now looks like this:
|
||||||
|
```shell
|
||||||
|
hledger balance -f bcexample.hledger -f budget.journal -M -b 2014-10 -e 2015 --forecast cur:USD --depth 2 --cumulative
|
||||||
|
Ending balances (cumulative) in 2014q4:
|
||||||
|
|
||||||
|
|| 2014/10/31 2014/11/30 2014/12/31
|
||||||
|
====================++============================================
|
||||||
|
Assets:US || 2546.60 USD -418.40 USD 2116.60 USD
|
||||||
|
Expenses:Financial || 4.00 USD 4.00 USD 4.00 USD
|
||||||
|
Expenses:Food || 83.00 USD 6583.00 USD 7583.00 USD
|
||||||
|
Expenses:Health || 96.90 USD 296.90 USD 496.90 USD
|
||||||
|
Expenses:Home || 0 2545 USD 5090 USD
|
||||||
|
Expenses:Taxes || 1992.20 USD 6292.20 USD 10592.20 USD
|
||||||
|
Expenses:Transport || 120.00 USD 240.00 USD 360.00 USD
|
||||||
|
Income:US || -4639.70 USD -15339.70 USD -26039.70 USD
|
||||||
|
Liabilities:US || -203.00 USD -203.00 USD -203.00 USD
|
||||||
|
--------------------++--------------------------------------------
|
||||||
|
|| 0 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
It is easy to see that in Nov 2014 we will run out of Assets. Using `register` we can figure out when or why it would happen:
|
||||||
|
```shell
|
||||||
|
$ hledger register -f bcexample.hledger -f budget.journal -b 2014-10 -e 2014-12 --forecast cur:USD Assets
|
||||||
|
2014/10/04 "BANK FEES" | "Monthly bank fee" Assets:US:BofA:Checking -4.00 USD -4.00 USD
|
||||||
|
2014/10/09 "Hoogle" | "Payroll" Assets:US:BofA:Checking 2550.60 USD 2546.60 USD
|
||||||
|
2014/10/10 "Transfering accumulated savings to o.. Assets:US:BofA:Checking -5000.00 USD -2453.40 USD
|
||||||
|
Assets:US:ETrade:Cash 5000.00 USD 2546.60 USD
|
||||||
|
2014/11/01 Forecast transaction Assets:US 3035 USD 5581.60 USD
|
||||||
|
2014/11/17 Forecast transaction Assets:US -6000 USD -418.40 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
It is 6000 USD planned for birthday! Something will have to be done about the birthday plans.
|
||||||
78
tests/budget/auto.test
Normal file
78
tests/budget/auto.test
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Add proportional income tax (from documentation)
|
||||||
|
hledger print -f- --auto
|
||||||
|
<<<
|
||||||
|
2016/1/1 paycheck
|
||||||
|
income:remuneration $-100
|
||||||
|
income:donations $-15
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
2016/1/1 withdraw
|
||||||
|
assets:cash $20
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
= ^income
|
||||||
|
(liabilities:tax) *.33 ; income tax
|
||||||
|
>>>
|
||||||
|
2016/01/01 paycheck
|
||||||
|
income:remuneration $-100
|
||||||
|
income:donations $-15
|
||||||
|
assets:bank
|
||||||
|
(liabilities:tax) $-33 ; income tax
|
||||||
|
(liabilities:tax) $-5 ; income tax
|
||||||
|
|
||||||
|
2016/01/01 withdraw
|
||||||
|
assets:cash $20
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|
||||||
|
hledger register -f- --auto
|
||||||
|
<<<
|
||||||
|
2016/1/1 paycheck
|
||||||
|
income:remuneration $-100
|
||||||
|
income:donations $-15
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
2016/1/1 withdraw
|
||||||
|
assets:cash $20
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
= ^income
|
||||||
|
(liabilities:tax) *.33 ; income tax
|
||||||
|
>>>
|
||||||
|
2016/01/01 paycheck income:remuneration $-100 $-100
|
||||||
|
income:donations $-15 $-115
|
||||||
|
assets:bank $115 0
|
||||||
|
(liabilities:tax) $-33 $-33
|
||||||
|
(liabilities:tax) $-5 $-38
|
||||||
|
2016/01/01 withdraw assets:cash $20 $-18
|
||||||
|
assets:bank $-20 $-38
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|
||||||
|
hledger balance -f- --auto
|
||||||
|
<<<
|
||||||
|
2016/1/1 paycheck
|
||||||
|
income:remuneration $-100
|
||||||
|
income:donations $-15
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
2016/1/1 withdraw
|
||||||
|
assets:cash $20
|
||||||
|
assets:bank
|
||||||
|
|
||||||
|
= ^income
|
||||||
|
(liabilities:tax) *.33 ; income tax
|
||||||
|
>>>
|
||||||
|
$115 assets
|
||||||
|
$95 bank
|
||||||
|
$20 cash
|
||||||
|
$-115 income
|
||||||
|
$-15 donations
|
||||||
|
$-100 remuneration
|
||||||
|
$-38 liabilities:tax
|
||||||
|
--------------------
|
||||||
|
$-38
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
92
tests/budget/budget.test
Normal file
92
tests/budget/budget.test
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Test --budget switch
|
||||||
|
hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget
|
||||||
|
<<<
|
||||||
|
2016/12/01
|
||||||
|
expenses:food $10
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/02
|
||||||
|
expenses:food $9
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/03
|
||||||
|
expenses:food $11
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/02
|
||||||
|
expenses:leisure $5
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/03
|
||||||
|
expenses:movies $25
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/03
|
||||||
|
expenses:cab $15
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
~ daily from 2016/1/1
|
||||||
|
expenses:food $10
|
||||||
|
expenses:leisure $15
|
||||||
|
assets:cash
|
||||||
|
>>>
|
||||||
|
Balance changes in 2016/12/01-2016/12/03:
|
||||||
|
|
||||||
|
|| 2016/12/01 2016/12/02 2016/12/03
|
||||||
|
=======================++=============================================================
|
||||||
|
<unbudgeted>:expenses || 0 0 $40
|
||||||
|
assets:cash || $-10 [40% of $-25] $-14 [56% of $-25] $-51 [204% of $-25]
|
||||||
|
expenses:food || $10 [100% of $10] $9 [90% of $10] $11 [110% of $10]
|
||||||
|
expenses:leisure || 0 [$15] $5 [33% of $15] 0 [$15]
|
||||||
|
-----------------------++-------------------------------------------------------------
|
||||||
|
|| 0 0 0
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|
||||||
|
# --show-unbudgeted
|
||||||
|
hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget --show-unbudgeted
|
||||||
|
<<<
|
||||||
|
2016/12/01
|
||||||
|
expenses:food $10
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/02
|
||||||
|
expenses:food $9
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/03
|
||||||
|
expenses:food $11
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/02
|
||||||
|
expenses:leisure $5
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/03
|
||||||
|
expenses:movies $25
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/03
|
||||||
|
expenses:cab $15
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
~ daily from 2016/1/1
|
||||||
|
expenses:food $10
|
||||||
|
expenses:leisure $15
|
||||||
|
assets:cash
|
||||||
|
>>>
|
||||||
|
Balance changes in 2016/12/01-2016/12/03:
|
||||||
|
|
||||||
|
|| 2016/12/01 2016/12/02 2016/12/03
|
||||||
|
==================++=============================================================
|
||||||
|
assets:cash || $-10 [40% of $-25] $-14 [56% of $-25] $-51 [204% of $-25]
|
||||||
|
expenses:cab || 0 0 $15
|
||||||
|
expenses:food || $10 [100% of $10] $9 [90% of $10] $11 [110% of $10]
|
||||||
|
expenses:leisure || 0 [$15] $5 [33% of $15] 0 [$15]
|
||||||
|
expenses:movies || 0 0 $25
|
||||||
|
------------------++-------------------------------------------------------------
|
||||||
|
|| 0 0 0
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
102
tests/budget/forecast.test
Normal file
102
tests/budget/forecast.test
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# Test --forecast switch
|
||||||
|
hledger bal -M -b 2016-11 -e 2017-02 -f - --forecast
|
||||||
|
<<<
|
||||||
|
2016/12/31
|
||||||
|
expenses:housing $600
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
~ monthly from 2016/1
|
||||||
|
income $-1000
|
||||||
|
expenses:food $20
|
||||||
|
expenses:leisure $15
|
||||||
|
expenses:grocery $30
|
||||||
|
assets:cash
|
||||||
|
>>>
|
||||||
|
Balance changes in 2016/12/01-2017/01/31:
|
||||||
|
|
||||||
|
|| 2016/12 2017/01
|
||||||
|
==================++==================
|
||||||
|
assets:cash || $-600 $935
|
||||||
|
expenses:food || 0 $20
|
||||||
|
expenses:grocery || 0 $30
|
||||||
|
expenses:housing || $600 0
|
||||||
|
expenses:leisure || 0 $15
|
||||||
|
income || 0 $-1000
|
||||||
|
------------------++------------------
|
||||||
|
|| 0 0
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|
||||||
|
|
||||||
|
hledger print -b 2016-11 -e 2017-02 -f - --forecast
|
||||||
|
<<<
|
||||||
|
2016/12/31
|
||||||
|
expenses:housing $600
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
~ monthly from 2016/1
|
||||||
|
income $-1000
|
||||||
|
expenses:food $20
|
||||||
|
expenses:leisure $15
|
||||||
|
expenses:grocery $30
|
||||||
|
assets:cash
|
||||||
|
>>>
|
||||||
|
2016/12/31
|
||||||
|
expenses:housing $600
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2017/01/01 Forecast transaction
|
||||||
|
income $-1000
|
||||||
|
expenses:food $20
|
||||||
|
expenses:leisure $15
|
||||||
|
expenses:grocery $30
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|
||||||
|
|
||||||
|
hledger register -b 2016-11 -e 2017-02 -f - --forecast
|
||||||
|
<<<
|
||||||
|
2016/12/31
|
||||||
|
expenses:housing $600
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
~ monthly from 2016/1
|
||||||
|
income $-1000
|
||||||
|
expenses:food $20
|
||||||
|
expenses:leisure $15
|
||||||
|
expenses:grocery $30
|
||||||
|
assets:cash
|
||||||
|
>>>
|
||||||
|
2016/12/31 expenses:housing $600 $600
|
||||||
|
assets:cash $-600 0
|
||||||
|
2017/01/01 Forecast transact.. income $-1000 $-1000
|
||||||
|
expenses:food $20 $-980
|
||||||
|
expenses:leisure $15 $-965
|
||||||
|
expenses:grocery $30 $-935
|
||||||
|
assets:cash $935 0
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|
||||||
|
# Check that --forecast generates transactions only after last transaction in journal
|
||||||
|
hledger register -b 2015-12 -e 2017-02 -f - assets:cash --forecast
|
||||||
|
<<<
|
||||||
|
2016/01/01
|
||||||
|
expenses:fun $10 ; more fireworks
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
2016/12/02
|
||||||
|
expenses:housing $600
|
||||||
|
assets:cash
|
||||||
|
|
||||||
|
~ yearly from 2016
|
||||||
|
income $-10000 ; bonus
|
||||||
|
assets:cash
|
||||||
|
>>>
|
||||||
|
2016/01/01 assets:cash $-10 $-10
|
||||||
|
2016/12/02 assets:cash $-600 $-610
|
||||||
|
2017/01/01 Forecast transact.. assets:cash $10000 $9390
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
Loading…
Reference in New Issue
Block a user