packages feed

hledger-lib 1.22.1 → 1.22.2

raw patch · 7 files changed

+153/−97 lines, 7 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Hledger.Reports.ReportOptions: [forecast_] :: ReportOpts -> Maybe DateSpan
- Hledger.Reports.ReportOptions: forecastPeriodFromRawOpts :: Day -> RawOpts -> Maybe DateSpan
+ Hledger.Data.Posting: postingApplyCommodityStyles :: Map CommoditySymbol AmountStyle -> Posting -> Posting
+ Hledger.Query: parseQueryTerm :: Day -> Text -> Either String (Either Query QueryOpt)
+ Hledger.Read.Common: [forecast_] :: InputOpts -> Maybe DateSpan
+ Hledger.Read.Common: forecastPeriodFromRawOpts :: Day -> RawOpts -> Maybe DateSpan
+ Hledger.Read.Common: journalAddAutoPostings :: Day -> BalancingOpts -> Journal -> Either String Journal
+ Hledger.Read.Common: journalAddForecast :: Day -> Maybe DateSpan -> Journal -> Journal
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Bool -> BalancingOpts -> Bool -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> Bool -> BalancingOpts -> Bool -> InputOpts
- Hledger.Read.Common: rawOptsToInputOpts :: RawOpts -> InputOpts
+ Hledger.Read.Common: rawOptsToInputOpts :: RawOpts -> IO InputOpts
- Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Costing -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> [Text] -> Bool -> Bool -> Bool -> ReportType -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Maybe DateSpan -> Bool -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Costing -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> [Text] -> Bool -> Bool -> Bool -> ReportType -> BalanceType -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> ReportOpts
- Hledger.Reports.ReportOptions: rawOptsToReportOpts :: RawOpts -> IO ReportOpts
+ Hledger.Reports.ReportOptions: rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts

Files

CHANGES.md view
@@ -9,6 +9,16 @@ Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. +# 1.22.2 2021-08-07++- forecast_ has moved from ReportOpts to InputOpts. (Stephen Morgan)++- Generate forecast transactions at journal finalisation, rather than as a postprocessing step.+  This allows us to have a uniform procedure for balancing transactions,+  whether they are normal transactions or forecast transactions, including+  dealing with balance assignments, balance assertions, and auto postings.+  ([#1638](https://github.com/simonmichael/hledger/issues/1638), Stephen Morgan)+ # 1.22.1 2021-08-02  - Allow megaparsec 9.1
Hledger/Data/Posting.hs view
@@ -38,6 +38,7 @@   relatedPostings,   postingStripPrices,   postingApplyAliases,+  postingApplyCommodityStyles,   -- * date operations   postingDate,   postingDate2,@@ -297,6 +298,14 @@       where         err = "problem while applying account aliases:\n" ++ pshow aliases            ++ "\n to account name: "++T.unpack paccount++"\n "++e++-- | Choose and apply a consistent display style to the posting+-- amounts in each commodity (see journalCommodityStyles).+postingApplyCommodityStyles :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting+postingApplyCommodityStyles styles p = p{pamount=styleMixedAmount styles $ pamount p+                                        ,pbalanceassertion=fixbalanceassertion <$> pbalanceassertion p}+  where+    fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba}  -- | Rewrite an account name using all matching aliases from the given list, in sequence. -- Each alias sees the result of applying the previous aliases.
Hledger/Query.hs view
@@ -20,6 +20,7 @@   -- * parsing   parseQuery,   parseQueryList,+  parseQueryTerm,   simplifyQuery,   filterQuery,   -- * accessors
Hledger/Read/Common.hs view
@@ -32,6 +32,7 @@   InputOpts (..),   definputopts,   rawOptsToInputOpts,+  forecastPeriodFromRawOpts,    -- * parsing utilities   runTextParser,@@ -48,6 +49,8 @@   journalCheckAccountsDeclared,   journalCheckCommoditiesDeclared,   journalCheckPayeesDeclared,+  journalAddForecast,+  journalAddAutoPostings,   setYear,   getYear,   setDefaultCommodityAndStyle,@@ -127,13 +130,15 @@ import "base-compat-batteries" Prelude.Compat hiding (fail, readFile) import Control.Applicative.Permutations (runPermutation, toPermutationWithDefault) import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (fail)-import Control.Monad.Except (ExceptT(..), runExceptT, throwError)+import Control.Monad.Except (ExceptT(..), liftEither, runExceptT, throwError) import Control.Monad.State.Strict hiding (fail) import Data.Bifunctor (bimap, second) import Data.Char (digitToInt, isDigit, isSpace) import Data.Decimal (DecimalRaw (Decimal), Decimal) import Data.Default (Default(..))+import Data.Either (lefts, rights) import Data.Function ((&))+import Data.Functor ((<&>)) import Data.Functor.Identity (Identity) import "base-compat-batteries" Data.List.Compat import Data.List.NonEmpty (NonEmpty(..))@@ -142,7 +147,7 @@ import qualified Data.Semigroup as Sem import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar (Day, fromGregorianValid, toGregorian)+import Data.Time.Calendar (Day, addDays, fromGregorianValid, toGregorian) import Data.Time.LocalTime (LocalTime(..), TimeOfDay(..)) import Data.Word (Word8) import System.Time (getClockTime)@@ -154,6 +159,8 @@   finalErrorBundlePretty, parseErrorAt, parseErrorAtRegion)  import Hledger.Data+import Hledger.Query (Query(..), filterQuery, parseQueryTerm, queryEndDate, queryIsDate, simplifyQuery)+import Hledger.Reports.ReportOptions (ReportOpts(..), queryFromFlags, rawOptsToReportOpts) import Hledger.Utils import Text.Printf (printf) @@ -203,6 +210,7 @@     ,new_               :: Bool                 -- ^ read only new transactions since this file was last read     ,new_save_          :: Bool                 -- ^ save latest new transactions state for next time     ,pivot_             :: String               -- ^ use the given field's value as the account name+    ,forecast_          :: Maybe DateSpan       -- ^ span in which to generate forecast transactions     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed     ,balancingopts_     :: BalancingOpts        -- ^ options for balancing transactions     ,strict_            :: Bool                 -- ^ do extra error checking (eg, all posted accounts are declared, no prices are inferred)@@ -219,29 +227,56 @@     , new_               = False     , new_save_          = True     , pivot_             = ""+    , forecast_          = Nothing     , auto_              = False     , balancingopts_     = def     , strict_            = False     } -rawOptsToInputOpts :: RawOpts -> InputOpts-rawOptsToInputOpts rawopts = InputOpts{-     -- files_             = listofstringopt "file" rawopts-     mformat_           = Nothing-    ,mrules_file_       = maybestringopt "rules-file" rawopts-    ,aliases_           = listofstringopt "alias" rawopts-    ,anon_              = boolopt "anon" rawopts-    ,new_               = boolopt "new" rawopts-    ,new_save_          = True-    ,pivot_             = stringopt "pivot" rawopts-    ,auto_              = boolopt "auto" rawopts-    ,balancingopts_     = def{ ignore_assertions_ = boolopt "ignore-assertions" rawopts-                             , infer_prices_      = not noinferprice-                             }-    ,strict_            = boolopt "strict" rawopts-    }+-- | Parse an InputOpts from a RawOpts and the current date.+-- This will fail with a usage error if the forecast period expression cannot be parsed.+rawOptsToInputOpts :: RawOpts -> IO InputOpts+rawOptsToInputOpts rawopts = do+    d <- getCurrentDay++    return InputOpts{+       -- files_             = listofstringopt "file" rawopts+       mformat_           = Nothing+      ,mrules_file_       = maybestringopt "rules-file" rawopts+      ,aliases_           = listofstringopt "alias" rawopts+      ,anon_              = boolopt "anon" rawopts+      ,new_               = boolopt "new" rawopts+      ,new_save_          = True+      ,pivot_             = stringopt "pivot" rawopts+      ,forecast_          = forecastPeriodFromRawOpts d rawopts+      ,auto_              = boolopt "auto" rawopts+      ,balancingopts_     = def{ ignore_assertions_ = boolopt "ignore-assertions" rawopts+                               , infer_prices_      = not noinferprice+                               }+      ,strict_            = boolopt "strict" rawopts+      }   where noinferprice = boolopt "strict" rawopts || stringopt "args" rawopts == "balancednoautoconversion" +-- | Get period expression from --forecast option.+-- This will fail with a usage error if the forecast period expression cannot be parsed.+forecastPeriodFromRawOpts :: Day -> RawOpts -> Maybe DateSpan+forecastPeriodFromRawOpts d rawopts = case maybestringopt "forecast" rawopts of+    Nothing -> Nothing+    Just "" -> Just forecastspanDefault+    Just str -> either (\e -> usageError $ "could not parse forecast period : "++customErrorBundlePretty e)+                       (\(_,requestedspan) -> Just $ requestedspan `spanDefaultsFrom` forecastspanDefault) $+                  parsePeriodExpr d $ stripquotes $ T.pack str+  where+    -- "They end on or before the specified report end date, or 180 days from today if unspecified."+    mspecifiedend = dbg2 "specifieddates" $ queryEndDate False datequery+    forecastendDefault = dbg2 "forecastendDefault" $ addDays 180 d+    forecastspanDefault = DateSpan Nothing $ mspecifiedend <|> Just forecastendDefault+    -- Do we really need to do all this work just to get the requested end date? This is duplicating+    -- much of reportOptsToSpec.+    ropts = rawOptsToReportOpts d rawopts+    argsquery = lefts . rights . map (parseQueryTerm d) $ querystring_ ropts+    datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery+ --- ** parsing utilities  -- | Run a text parser in the identity monad. See also: parseWithState.@@ -319,6 +354,8 @@ -- -- - save misc info and reverse transactions into their original parse order, --+-- - add forecast transactions,+-- -- - evaluate balance assignments and balance each transaction, -- -- - apply transaction modifiers (auto postings) if enabled,@@ -328,52 +365,65 @@ -- - infer transaction-implied market prices from transaction prices -- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal-journalFinalise InputOpts{auto_,balancingopts_,strict_} f txt pj = do-  t <- liftIO getClockTime-  d <- liftIO getCurrentDay-  let pj' =-        pj{jglobalcommoditystyles=fromMaybe M.empty $ commodity_styles_ balancingopts_}  -- save any global commodity styles-        & journalAddFile (f, txt)  -- save the main file's info-        & journalSetLastReadTime t -- save the last read time-        & journalReverse -- convert all lists to the order they were parsed+journalFinalise InputOpts{forecast_,auto_,balancingopts_,strict_} f txt pj = do+    t <- liftIO getClockTime+    d <- liftIO getCurrentDay+    -- Infer and apply canonical styles for each commodity (or throw an error).+    -- This affects transaction balancing/assertions/assignments, so needs to be done early.+    liftEither $ checkAddAndBalance d <=< journalApplyCommodityStyles $+        pj{jglobalcommoditystyles=fromMaybe mempty $ commodity_styles_ balancingopts_}  -- save any global commodity styles+        & journalAddFile (f, txt)           -- save the main file's info+        & journalSetLastReadTime t          -- save the last read time+        & journalReverse                    -- convert all lists to the order they were parsed+  where+    checkAddAndBalance d j = do+        when strict_ $ do+          -- If in strict mode, check all postings are to declared accounts+          journalCheckAccountsDeclared j+          -- and using declared commodities+          journalCheckCommoditiesDeclared j -  -- If in strict mode, check all postings are to declared accounts-  case if strict_ then journalCheckAccountsDeclared pj' else Right () of-    Left e   -> throwError e-    Right () ->+        -- Add forecast transactions if enabled+        journalAddForecast d forecast_ j+        -- Add auto postings if enabled+          & (if auto_ && not (null $ jtxnmodifiers j) then journalAddAutoPostings d balancingopts_ else pure)+        -- Balance all transactions and maybe check balance assertions.+          >>= journalBalanceTransactions balancingopts_+        -- infer market prices from commodity-exchanging transactions+          <&> journalInferMarketPricesFromTransactions -      -- and using declared commodities-      case if strict_ then journalCheckCommoditiesDeclared pj' else Right () of-        Left e   -> throwError e-        Right () ->+journalAddAutoPostings :: Day -> BalancingOpts -> Journal -> Either String Journal+journalAddAutoPostings d bopts =+    -- Balance all transactions without checking balance assertions,+    journalBalanceTransactions bopts{ignore_assertions_=True}+    -- then add the auto postings+    -- (Note adding auto postings after balancing means #893b fails;+    -- adding them before balancing probably means #893a, #928, #938 fail.)+    >=> journalModifyTransactions d+    >=> journalApplyCommodityStyles -          -- Infer and apply canonical styles for each commodity (or throw an error).-          -- This affects transaction balancing/assertions/assignments, so needs to be done early.-          case journalApplyCommodityStyles pj' of-            Left e     -> throwError e-            Right pj'' -> either throwError return $-              pj''-              & (if not auto_ || null (jtxnmodifiers pj'')-                then-                  -- Auto postings are not active.-                  -- Balance all transactions and maybe check balance assertions.-                  journalBalanceTransactions balancingopts_-                else \j -> do  -- Either monad-                  -- Auto postings are active.-                  -- Balance all transactions without checking balance assertions,-                  j' <- journalBalanceTransactions balancingopts_{ignore_assertions_=True} j-                  -- then add the auto postings-                  -- (Note adding auto postings after balancing means #893b fails;-                  -- adding them before balancing probably means #893a, #928, #938 fail.)-                  case journalModifyTransactions d j' of-                    Left e -> throwError e-                    Right j'' -> do-                      -- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?)-                      j''' <- journalApplyCommodityStyles j''-                      -- then check balance assertions.-                      journalBalanceTransactions balancingopts_ j'''-                )-            & fmap journalInferMarketPricesFromTransactions  -- infer market prices from commodity-exchanging transactions+-- | Generate periodic transactions from all periodic transaction rules in the journal.+-- These transactions are added to the in-memory Journal (but not the on-disk file).+--+-- The start & end date for generated periodic transactions are determined in+-- a somewhat complicated way; see the hledger manual -> Periodic transactions.+journalAddForecast :: Day -> Maybe DateSpan -> Journal -> Journal+journalAddForecast _ Nothing              j = j+journalAddForecast d (Just requestedspan) j = j{jtxns = jtxns j ++ forecasttxns}+  where+    forecasttxns =+        map (txnTieKnot . transactionTransformPostings (postingApplyCommodityStyles $ journalCommodityStyles j))+      . filter (spanContainsDate forecastspan . tdate)+      . concatMap (`runPeriodicTransaction` forecastspan)+      $ jperiodictxns j++    -- "They can start no earlier than: the day following the latest normal transaction in the journal (or today if there are none)."+    mjournalend   = dbg2 "journalEndDate" $ journalEndDate False j  -- ignore secondary dates+    forecastbeginDefault = dbg2 "forecastbeginDefault" $ mjournalend <|> Just d++    -- "They end on or before the specified report end date, or 180 days from today if unspecified."+    forecastspan = dbg2 "forecastspan" $ dbg2 "forecastspan flag" requestedspan+        `spanDefaultsFrom` DateSpan forecastbeginDefault (Just $ addDays 180 d)  -- | Check that all the journal's transactions have payees declared with -- payee directives, returning an error message otherwise.
Hledger/Reports/AccountTransactionsReport.hs view
@@ -21,7 +21,7 @@ import Data.Ord (Down(..), comparing) import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar (Day, addDays)+import Data.Time.Calendar (Day)  import Hledger.Data import Hledger.Query@@ -83,17 +83,11 @@   where     -- A depth limit should not affect the account transactions report; it should show all transactions in/below this account.     -- Queries on currency or amount are also ignored at this stage; they are handled earlier, before valuation.-    reportq = simplifyQuery $ And [aregisterq, periodq, excludeforecastq (forecast_ ropts)]+    reportq = simplifyQuery $ And [aregisterq, periodq]       where         aregisterq = filterQuery (not . queryIsCurOrAmt) $ filterQuery (not . queryIsDepth) reportq'         periodq = Date . periodAsDateSpan $ period_ ropts-        -- Except in forecast mode, exclude future/forecast transactions.-        excludeforecastq (Just _) = Any-        excludeforecastq Nothing  =  -- not:date:tomorrow- not:tag:generated-transaction-          And [ Not . Date $ DateSpan (Just . addDays 1 $ rsToday rspec) Nothing-              , Not generatedTransactionTag-              ]-    amtq = filterQuery queryIsCurOrAmt reportq'+    amtq = filterQuery queryIsCurOrAmt $ rsQuery rspec     queryIsCurOrAmt q = queryIsSym q || queryIsAmt q      -- Note that within this functions, we are only allowed limited@@ -138,9 +132,9 @@      items =         accountTransactionsReportItems reportq thisacctq startbal maNegate-      -- sort by the transaction's register date, for accurate starting balance+      -- sort by the transaction's register date, then index, for accurate starting balance       . ptraceAtWith 5 (("ts4:\n"++).pshowTransactions.map snd)-      . sortBy (comparing $ Down . fst)+      . sortBy (comparing (Down . fst) <> comparing (Down . tindex . snd))       . map (\t -> (transactionRegisterDate reportq thisacctq t, t))       $ jtxns acctJournal 
Hledger/Reports/ReportOptions.hs view
@@ -33,7 +33,6 @@   mixedAmountApplyValuationAfterSumFromOptsWith,   valuationAfterSum,   intervalFromRawOpts,-  forecastPeriodFromRawOpts,   queryFromFlags,   transactionDateFn,   postingDateFn,@@ -138,7 +137,6 @@       --   Influenced by the --color/colour flag (cf CliOptions),       --   whether stdout is an interactive terminal, and the value of       --   TERM and existence of NO_COLOR environment variables.-    ,forecast_       :: Maybe DateSpan     ,transpose_      :: Bool  } deriving (Show) @@ -175,24 +173,27 @@     , invert_          = False     , normalbalance_   = Nothing     , color_           = False-    , forecast_        = Nothing     , transpose_       = False     } -rawOptsToReportOpts :: RawOpts -> IO ReportOpts-rawOptsToReportOpts rawopts = do-    d <- getCurrentDay+-- | Generate a ReportOpts from raw command-line input, given a day.+-- This will fail with a usage error if it is passed+-- - an invalid --format argument,+-- - an invalid --value argument,+-- - if --valuechange is called with a valuation type other than -V/--value=end.+rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts+rawOptsToReportOpts d rawopts =      let formatstring = T.pack <$> maybestringopt "format" rawopts         querystring  = map T.pack $ listofstringopt "args" rawopts  -- doesn't handle an arg like "" right         (costing, valuation) = valuationTypeFromRawOpts rawopts -    format <- case parseStringFormat <$> formatstring of-        Nothing         -> return defaultBalanceLineFormat-        Just (Right x)  -> return x-        Just (Left err) -> fail $ "could not parse format option: " ++ err+        format = case parseStringFormat <$> formatstring of+            Nothing         -> defaultBalanceLineFormat+            Just (Right x)  -> x+            Just (Left err) -> usageError $ "could not parse format option: " ++ err -    return defreportopts+    in defreportopts           {period_      = periodFromRawOpts d rawopts           ,interval_    = intervalFromRawOpts rawopts           ,statuses_    = statusesFromRawOpts rawopts@@ -221,7 +222,6 @@           ,invert_      = boolopt "invert" rawopts           ,pretty_tables_ = boolopt "pretty-tables" rawopts           ,color_       = useColorOnStdout -- a lower-level helper-          ,forecast_    = forecastPeriodFromRawOpts d rawopts           ,transpose_   = boolopt "transpose" rawopts           } @@ -275,7 +275,7 @@ rawOptsToReportSpec :: RawOpts -> IO ReportSpec rawOptsToReportSpec rawopts = do     d <- getCurrentDay-    ropts <- rawOptsToReportOpts rawopts+    let ropts = rawOptsToReportOpts d rawopts     either fail return $ reportOptsToSpec d ropts  accountlistmodeopt :: RawOpts -> AccountListMode@@ -388,17 +388,6 @@       | n == "yearly"    = Just $ Years 1       | otherwise = Nothing --- | get period expression from --forecast option-forecastPeriodFromRawOpts :: Day -> RawOpts -> Maybe DateSpan-forecastPeriodFromRawOpts d opts =-  case maybestringopt "forecast" opts-  of-    Nothing -> Nothing-    Just "" -> Just nulldatespan-    Just str ->-      either (\e -> usageError $ "could not parse forecast period : "++customErrorBundlePretty e) (Just . snd) $ -      parsePeriodExpr d $ stripquotes $ T.pack str- -- | Extract the interval from the parsed -p/--period expression. -- Return Nothing if an interval is not explicitly defined. extractIntervalOrNothing :: (Interval, DateSpan) -> Maybe Interval@@ -435,6 +424,9 @@ -- specified by -B/--cost, -V, -X/--exchange, or --value flags. It is -- allowed to combine -B/--cost with any other valuation type. If -- there's more than one valuation type, the rightmost flag wins.+-- This will fail with a usage error if an invalid argument is passed+-- to --value, or if --valuechange is called with a valuation type+-- other than -V/--value=end. valuationTypeFromRawOpts :: RawOpts -> (Costing, Maybe ValuationType) valuationTypeFromRawOpts rawopts = (costing, valuation)   where
hledger-lib.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        1.22.1+version:        1.22.2 synopsis:       A reusable library providing the core functionality of hledger description:    A reusable library containing hledger's core functionality.                 This is used by most hledger* packages so that they support the same