cli: --forecast adds periodic transactions to reports

Ledger-style periodic transactions, previously supported only by
hledger-budget, have landed as a first-class feature.  The --forecast
flag activates them, so that any transactions they generate are
included in reports.
This commit is contained in:
Dmitry Astapov 2017-11-18 00:40:10 +00:00
parent 50b4d76ce9
commit f101d5b515
6 changed files with 133 additions and 2 deletions

View File

@ -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...]

View File

@ -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

View File

@ -104,6 +104,7 @@ 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
} deriving (Show, Data, Typeable) } deriving (Show, Data, Typeable)
instance Default ReportOpts where def = defreportopts instance Default ReportOpts where def = defreportopts
@ -134,6 +135,7 @@ defreportopts = ReportOpts
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 +166,7 @@ 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'
} }
-- | 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.

View File

@ -155,6 +155,7 @@ 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"] (\opts -> setboolopt "forecast" opts) "generate forecast transactions"
] ]
-- | Common output-related flags: --output-file, --output-format... -- | Common output-related flags: --output-file, --output-format...

View File

@ -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)
@ -70,6 +70,7 @@ withJournalDo opts cmd = do
. anonymiseByOpts opts . anonymiseByOpts opts
. journalApplyAliases (aliasesFromOpts opts) . journalApplyAliases (aliasesFromOpts opts)
<=< journalApplyValue (reportopts_ opts) <=< journalApplyValue (reportopts_ opts)
<=< journalAddForecast 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 +118,29 @@ 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
-- | 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 ()

102
tests/budget/forecast.test Normal file
View 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