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 System.Console.CmdArgs
|
||||
import Hledger.Cli
|
||||
import Hledger.Data.AutoTransaction
|
||||
|
||||
-- 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.Timeclock,
|
||||
module Hledger.Data.Transaction,
|
||||
module Hledger.Data.AutoTransaction,
|
||||
module Hledger.Data.Types,
|
||||
tests_Hledger_Data
|
||||
)
|
||||
@ -42,6 +43,7 @@ import Hledger.Data.RawOptions
|
||||
import Hledger.Data.StringFormat
|
||||
import Hledger.Data.Timeclock
|
||||
import Hledger.Data.Transaction
|
||||
import Hledger.Data.AutoTransaction
|
||||
import Hledger.Data.Types
|
||||
|
||||
tests_Hledger_Data :: Test
|
||||
|
||||
@ -136,7 +136,8 @@ renderPostingCommentDates p = p { pcomment = comment' }
|
||||
--
|
||||
-- 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
|
||||
-- hi $1.00
|
||||
-- <BLANKLINE>
|
||||
@ -146,6 +147,86 @@ renderPostingCommentDates p = p { pcomment = comment' }
|
||||
-- 2017/03/01
|
||||
-- hi $1.00
|
||||
-- <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 pt = generate where
|
||||
base = nulltransaction { tpostings = ptpostings pt }
|
||||
@ -154,5 +235,18 @@ runPeriodicTransaction pt = generate where
|
||||
(interval, effectspan) =
|
||||
case parsePeriodExpr errCurrent periodExpr of
|
||||
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]
|
||||
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 Safe (headMay, lastMay, readMay)
|
||||
import Text.Megaparsec.Compat
|
||||
import Text.Megaparsec.Perm
|
||||
import Text.Printf
|
||||
|
||||
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"
|
||||
-- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]
|
||||
-- >>> 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"
|
||||
-- [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 _ (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 (Quarters n) s = splitspan startofquarter (applyN n nextquarter) 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 (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 (MonthOfYear n) s = splitspan startofmonth (applyN n nextmonth) 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
|
||||
-- the provided reference date, or return a parse error.
|
||||
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 refdate = either (const Nothing) Just . parsePeriodExpr refdate
|
||||
@ -447,6 +456,7 @@ thismonth = startofmonth
|
||||
prevmonth = startofmonth . addGregorianMonthsClip (-1)
|
||||
nextmonth = startofmonth . addGregorianMonthsClip 1
|
||||
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
|
||||
prevquarter = startofquarter . addGregorianMonthsClip (-3)
|
||||
@ -461,18 +471,106 @@ prevyear = startofyear . addGregorianYearsClip (-1)
|
||||
nextyear = startofyear . addGregorianYearsClip 1
|
||||
startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day
|
||||
|
||||
nthdayofmonthcontaining n d | d1 >= d = d1
|
||||
| otherwise = d2
|
||||
where d1 = addDays (fromIntegral n-1) s
|
||||
d2 = addDays (fromIntegral n-1) $ nextmonth s
|
||||
-- | For given date d find year-long interval that starts on given MM/DD of year
|
||||
-- and covers it.
|
||||
--
|
||||
-- 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
|
||||
|
||||
nthdayofweekcontaining n d | d1 >= d = d1
|
||||
| otherwise = d2
|
||||
where d1 = addDays (fromIntegral n-1) s
|
||||
d2 = addDays (fromIntegral n-1) $ nextweek s
|
||||
-- | For given date d find week-long interval that starts on nth day of week
|
||||
-- and covers it.
|
||||
--
|
||||
-- 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
|
||||
|
||||
-- | 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
|
||||
|
||||
@ -633,17 +731,11 @@ md = do
|
||||
months = ["january","february","march","april","may","june",
|
||||
"july","august","september","october","november","december"]
|
||||
monthabbrevs = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]
|
||||
-- weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
||||
-- weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
||||
weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
||||
weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
||||
|
||||
#if MIN_VERSION_megaparsec(6,0,0)
|
||||
lc = T.toLower
|
||||
#else
|
||||
lc = lowercase
|
||||
#endif
|
||||
|
||||
monthIndex t = maybe 0 (+1) $ lc t `elemIndex` months
|
||||
monIndex t = maybe 0 (+1) $ lc t `elemIndex` monthabbrevs
|
||||
monthIndex t = maybe 0 (+1) $ t `elemIndex` months
|
||||
monIndex t = maybe 0 (+1) $ t `elemIndex` monthabbrevs
|
||||
|
||||
month :: SimpleTextParser SmartDate
|
||||
month = do
|
||||
@ -657,6 +749,12 @@ mon = do
|
||||
let i = monIndex m
|
||||
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 = string "today" >> return ("","","today")
|
||||
yesterday = string "yesterday" >> return ("","","yesterday")
|
||||
@ -683,17 +781,43 @@ lastthisnextthing = do
|
||||
return ("", T.unpack r, T.unpack p)
|
||||
|
||||
-- |
|
||||
-- >>> let p = parsewith (periodexpr (parsedate "2008/11/26")) :: T.Text -> Either (ParseError Char MPErr) (Interval, DateSpan)
|
||||
-- >>> p "from aug to oct"
|
||||
-- >>> let p s = parsewith (periodexpr (parsedate "2008/11/26") <* eof) (T.toLower s) :: Either (ParseError Char MPErr) (Interval, DateSpan)
|
||||
-- >>> p "from Aug to Oct"
|
||||
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
||||
-- >>> p "aug to oct"
|
||||
-- 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)
|
||||
-- >>> p "daily from aug"
|
||||
-- Right (Days 1,DateSpan 2008/08/01-)
|
||||
-- >>> p "every week to 2009"
|
||||
-- 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 rdate = choice $ map try [
|
||||
intervalanddateperiodexpr rdate,
|
||||
@ -736,31 +860,53 @@ reportinginterval = choice' [
|
||||
return $ Months 2,
|
||||
do string "every"
|
||||
many spacenonewline
|
||||
n <- fmap read $ some digitChar
|
||||
thsuffix
|
||||
n <- nth
|
||||
many spacenonewline
|
||||
string "day"
|
||||
many spacenonewline
|
||||
string "of"
|
||||
many spacenonewline
|
||||
string "week"
|
||||
of_ "week"
|
||||
return $ DayOfWeek n,
|
||||
do string "every"
|
||||
many spacenonewline
|
||||
n <- fmap read $ some digitChar
|
||||
thsuffix
|
||||
n <- weekday
|
||||
return $ DayOfWeek n,
|
||||
do string "every"
|
||||
many spacenonewline
|
||||
n <- nth
|
||||
many spacenonewline
|
||||
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
|
||||
string "of"
|
||||
many spacenonewline
|
||||
string "month"
|
||||
return $ DayOfMonth n
|
||||
]
|
||||
where
|
||||
string period
|
||||
|
||||
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".
|
||||
tryinterval :: String -> String -> (Int -> Interval) -> SimpleTextParser Interval
|
||||
|
||||
@ -89,7 +89,9 @@ data Interval =
|
||||
| Quarters Int
|
||||
| Years Int
|
||||
| DayOfMonth Int
|
||||
| WeekdayOfMonth Int Int
|
||||
| DayOfWeek Int
|
||||
| DayOfYear Int Int -- Month, Day
|
||||
-- WeekOfYear Int
|
||||
-- MonthOfYear Int
|
||||
-- QuarterOfYear Int
|
||||
|
||||
@ -104,6 +104,8 @@ data ReportOpts = ReportOpts {
|
||||
-- eg in the income section of an income statement, this helps --sort-amount know
|
||||
-- how to sort negative numbers.
|
||||
,color_ :: Bool
|
||||
,forecast_ :: Bool
|
||||
,auto_ :: Bool
|
||||
} deriving (Show, Data, Typeable)
|
||||
|
||||
instance Default ReportOpts where def = defreportopts
|
||||
@ -134,6 +136,8 @@ defreportopts = ReportOpts
|
||||
def
|
||||
def
|
||||
def
|
||||
def
|
||||
def
|
||||
|
||||
rawOptsToReportOpts :: RawOpts -> IO ReportOpts
|
||||
rawOptsToReportOpts rawopts = checkReportOpts <$> do
|
||||
@ -164,6 +168,8 @@ rawOptsToReportOpts rawopts = checkReportOpts <$> do
|
||||
,sort_amount_ = boolopt "sort-amount" rawopts'
|
||||
,pretty_tables_ = boolopt "pretty-tables" rawopts'
|
||||
,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.
|
||||
|
||||
@ -155,6 +155,8 @@ reportflags = [
|
||||
,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 ["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...
|
||||
|
||||
@ -246,11 +246,13 @@ module Hledger.Cli.Commands.Balance (
|
||||
,tests_Hledger_Cli_Commands_Balance
|
||||
) where
|
||||
|
||||
import Data.List (intercalate)
|
||||
import Data.List (intercalate, nub)
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
-- import Data.Monoid
|
||||
import qualified Data.Text as T
|
||||
import System.Console.CmdArgs.Explicit as C
|
||||
import Data.Decimal (roundTo)
|
||||
import Text.CSV
|
||||
import Test.HUnit
|
||||
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)"
|
||||
,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 ["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
|
||||
,groupHidden = []
|
||||
@ -293,7 +297,7 @@ balancemode = (defCommandMode $ ["balance"] ++ aliases) { -- also accept but don
|
||||
|
||||
-- | The balance command, prints a balance report.
|
||||
balance :: CliOpts -> Journal -> IO ()
|
||||
balance opts@CliOpts{reportopts_=ropts} j = do
|
||||
balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
||||
d <- getCurrentDay
|
||||
case lineFormatFromOpts ropts of
|
||||
Left err -> error' $ unlines [err]
|
||||
@ -319,12 +323,58 @@ balance opts@CliOpts{reportopts_=ropts} j = do
|
||||
"csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r
|
||||
_ -> balanceReportAsText
|
||||
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
|
||||
render = case format of
|
||||
"csv" -> \ropts r -> (++ "\n") $ printCSV $ multiBalanceReportAsCsv ropts r
|
||||
_ -> multiBalanceReportAsText
|
||||
writeOutput opts $ render ropts report
|
||||
"csv" -> (++ "\n") . printCSV . multiBalanceReportAsCsv ropts
|
||||
_ -> multiBalanceReportAsText ropts
|
||||
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
|
||||
|
||||
@ -494,16 +544,73 @@ multiBalanceReportAsText opts r =
|
||||
CumulativeChange -> "Ending balances (cumulative)"
|
||||
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,
|
||||
-- made using 'balanceReportAsTable'), render it in a format suitable for
|
||||
-- console output.
|
||||
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
|
||||
. addtrailingblank
|
||||
. trimborder
|
||||
. lines
|
||||
. render pretty id id showamt
|
||||
. render pretty id id showCell
|
||||
. align
|
||||
where
|
||||
addtrailingblank = (++[""])
|
||||
@ -512,8 +619,6 @@ renderBalanceReportTable (ReportOpts { pretty_tables_ = pretty, color_=usecolor
|
||||
where
|
||||
acctswidth = maximum' $ map strWidth (headerContents l)
|
||||
l' = padRightWide acctswidth <$> l
|
||||
showamt | usecolor = cshowMixedAmountOneLineWithoutPrice
|
||||
| otherwise = showMixedAmountOneLineWithoutPrice
|
||||
|
||||
-- | Build a 'Table' from a multi-column balance report.
|
||||
balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount
|
||||
|
||||
@ -31,7 +31,7 @@ import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.IO as T
|
||||
import Data.Time (Day)
|
||||
import Data.Time (Day, addDays)
|
||||
import Data.Word
|
||||
import Numeric
|
||||
import Safe (readMay)
|
||||
@ -54,6 +54,7 @@ import Hledger.Data
|
||||
import Hledger.Read
|
||||
import Hledger.Reports
|
||||
import Hledger.Utils
|
||||
import Hledger.Query (Query(Any))
|
||||
|
||||
|
||||
-- | Parse the user's specified journal file, maybe apply some transformations
|
||||
@ -70,6 +71,8 @@ withJournalDo opts cmd = do
|
||||
. anonymiseByOpts opts
|
||||
. journalApplyAliases (aliasesFromOpts opts)
|
||||
<=< journalApplyValue (reportopts_ opts)
|
||||
<=< journalAddForecast opts
|
||||
. generateAutomaticPostings (reportopts_ opts)
|
||||
either error' f ej
|
||||
|
||||
-- | Apply the pivot transformation on a journal, if option is present.
|
||||
@ -117,6 +120,38 @@ journalApplyValue ropts j = do
|
||||
= id
|
||||
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.
|
||||
-- If the file exists it will be overwritten.
|
||||
writeOutput :: CliOpts -> String -> IO ()
|
||||
|
||||
@ -239,21 +239,59 @@ Examples:
|
||||
`-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:
|
||||
`biweekly`,
|
||||
`bimonthly`,
|
||||
`every N days|weeks|months|quarters|years`,
|
||||
`every Nth day [of month]`,
|
||||
`every Nth day of week`.
|
||||
`every day|week|month|quarter|year`,
|
||||
`every N days|weeks|months|quarters|years`.
|
||||
|
||||
|
||||
All of these will start on the first day of the requested period and end on the last one, as described above.
|
||||
|
||||
Examples:
|
||||
|
||||
------------------------------------------
|
||||
`-p "bimonthly from 2008"`
|
||||
`-p "every 2 weeks"`
|
||||
`-p "every 5 days from 1/3"`
|
||||
`-p "bimonthly from 2008"` -- periods will have boundaries on 2008/01/01, 2008/03/01, ...
|
||||
`-p "every 2 weeks"` -- starts on closest preceeding Monday
|
||||
`-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):
|
||||
|
||||
`hledger balance -H -p "every 16th day"`
|
||||
|
||||
@ -113,6 +113,7 @@ library
|
||||
, text >=0.11
|
||||
, utf8-string >=0.3.5 && <1.1
|
||||
, wizards ==1.0.*
|
||||
, Decimal
|
||||
if (!(os(windows))) && (flag(terminfo))
|
||||
build-depends:
|
||||
terminfo
|
||||
@ -192,6 +193,7 @@ executable hledger
|
||||
, text >=0.11
|
||||
, utf8-string >=0.3.5 && <1.1
|
||||
, wizards ==1.0.*
|
||||
, Decimal
|
||||
if (!(os(windows))) && (flag(terminfo))
|
||||
build-depends:
|
||||
terminfo
|
||||
@ -241,6 +243,7 @@ test-suite test
|
||||
, text >=0.11
|
||||
, utf8-string >=0.3.5 && <1.1
|
||||
, wizards ==1.0.*
|
||||
, Decimal
|
||||
, test-framework
|
||||
, test-framework-hunit
|
||||
if (!(os(windows))) && (flag(terminfo))
|
||||
|
||||
@ -149,6 +149,7 @@ library:
|
||||
- text >=0.11
|
||||
- utf8-string >=0.3.5 && <1.1
|
||||
- wizards ==1.0.*
|
||||
- Decimal
|
||||
|
||||
executables:
|
||||
hledger:
|
||||
@ -178,6 +179,7 @@ executables:
|
||||
- text >=0.11
|
||||
- utf8-string >=0.3.5 && <1.1
|
||||
- wizards ==1.0.*
|
||||
- Decimal
|
||||
|
||||
tests:
|
||||
test:
|
||||
@ -204,6 +206,7 @@ tests:
|
||||
- text >=0.11
|
||||
- utf8-string >=0.3.5 && <1.1
|
||||
- wizards ==1.0.*
|
||||
- Decimal
|
||||
- test-framework
|
||||
- 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