diff --git a/hledger-lib/Hledger/Data/Journal.hs b/hledger-lib/Hledger/Data/Journal.hs index cc4aa4ae1..3ebad0f46 100644 --- a/hledger-lib/Hledger/Data/Journal.hs +++ b/hledger-lib/Hledger/Data/Journal.hs @@ -521,19 +521,26 @@ filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionPost filterJournalRelatedPostings :: Query -> Journal -> Journal filterJournalRelatedPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionRelatedPostings q) ts} --- | Within each posting's amount, keep only the parts matching the query. +-- | Within each posting's amount, keep only the parts matching the query, and +-- remove any postings with all amounts removed. -- This can leave unbalanced transactions. filterJournalAmounts :: Query -> Journal -> Journal filterJournalAmounts q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionAmounts q) ts} --- | Filter out all parts of this transaction's amounts which do not match the query. +-- | Filter out all parts of this transaction's amounts which do not match the +-- query, and remove any postings with all amounts removed. -- This can leave the transaction unbalanced. filterTransactionAmounts :: Query -> Transaction -> Transaction -filterTransactionAmounts q t@Transaction{tpostings=ps} = t{tpostings=map (filterPostingAmount q) ps} +filterTransactionAmounts q t@Transaction{tpostings=ps} = t{tpostings=mapMaybe (filterPostingAmount q) ps} --- | Filter out all parts of this posting's amount which do not match the query. -filterPostingAmount :: Query -> Posting -> Posting -filterPostingAmount q p@Posting{pamount=as} = p{pamount=filterMixedAmount (q `matchesAmount`) as} +-- | Filter out all parts of this posting's amount which do not match the query, and remove the posting +-- if this removes all amounts. +filterPostingAmount :: Query -> Posting -> Maybe Posting +filterPostingAmount q p@Posting{pamount=as} + | null newamt = Nothing + | otherwise = Just p{pamount=Mixed newamt} + where + Mixed newamt = filterMixedAmount (q `matchesAmount`) as filterTransactionPostings :: Query -> Transaction -> Transaction filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps} diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index f3e094466..9595dcdf3 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -250,21 +250,18 @@ getPostingsByColumn rspec j priceoracle reportspan = -- | Gather postings matching the query within the report period. getPostings :: ReportSpec -> Journal -> PriceOracle -> [Posting] -getPostings rspec@ReportSpec{_rsQuery=query,_rsReportOpts=ropts} j priceoracle = - journalPostings . - valueJournal . - filterJournalAmounts symq $ -- remove amount parts excluded by cur: - filterJournalPostings reportq j -- remove postings not matched by (adjusted) query +getPostings rspec@ReportSpec{_rsQuery=query, _rsReportOpts=ropts} j priceoracle = + journalPostings $ journalValueAndFilterPostingsWith rspec' j priceoracle where - symq = dbg3 "symq" . filterQuery queryIsSym $ dbg3 "requested q" query + rspec' = rspec{_rsQuery=depthless, _rsReportOpts = ropts'} + ropts' = if isJust (valuationAfterSum ropts) + then ropts{value_=Nothing, cost_=NoCost} -- If we're valuing after the sum, don't do it now + else ropts + -- The user's query with no depth limit, and expanded to the report span -- if there is one (otherwise any date queries are left as-is, which -- handles the hledger-ui+future txns case above). - reportq = dbg3 "reportq" $ depthless query - depthless = dbg3 "depthless" . filterQuery (not . queryIsDepth) - valueJournal j' | isJust (valuationAfterSum ropts) = j' - | otherwise = journalApplyValuationFromOptsWith rspec j' priceoracle - + depthless = dbg3 "depthless" $ filterQuery (not . queryIsDepth) query -- | Given a set of postings, eg for a single report column, gather -- the accounts that have postings and calculate the change amount for diff --git a/hledger-lib/Hledger/Reports/PostingsReport.hs b/hledger-lib/Hledger/Reports/PostingsReport.hs index f9905b9e5..c72951218 100644 --- a/hledger-lib/Hledger/Reports/PostingsReport.hs +++ b/hledger-lib/Hledger/Reports/PostingsReport.hs @@ -113,29 +113,21 @@ registerRunningCalculationFn ropts -- A helper for the postings report. matchedPostingsBeforeAndDuring :: ReportSpec -> Journal -> DateSpan -> ([Posting],[Posting]) matchedPostingsBeforeAndDuring rspec@ReportSpec{_rsReportOpts=ropts,_rsQuery=q} j reportspan = - dbg5 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps + dbg5 "beforeps, duringps" $ span (beforestartq `matchesPosting`) beforeandduringps where beforestartq = dbg3 "beforestartq" $ dateqtype $ DateSpan Nothing $ spanStart reportspan - beforeandduringps = - dbg5 "ps4" $ sortOn sortdate $ -- sort postings by date or date2 - dbg5 "ps3" $ (if invert_ ropts then map negatePostingAmount else id) $ -- with --invert, invert amounts - journalPostings $ - journalApplyValuationFromOpts rspec $ -- convert to cost and apply valuation - dbg5 "ps2" $ filterJournalAmounts symq $ -- remove amount parts which the query's cur: terms would exclude - dbg5 "ps1" $ filterJournal beforeandduringq j -- filter postings by the query, with no start date or depth limit + beforeandduringps = sortOn (if date2_ ropts then postingDate2 else postingDate) -- sort postings by date or date2 + . (if invert_ ropts then map negatePostingAmount else id) -- with --invert, invert amounts + . journalPostings $ journalValueAndFilterPostings rspec{_rsQuery=beforeandduringq} j + -- filter postings by the query, with no start date or depth limit beforeandduringq = dbg4 "beforeandduringq" $ And [depthless $ dateless q, beforeendq] where depthless = filterQuery (not . queryIsDepth) dateless = filterQuery (not . queryIsDateOrDate2) beforeendq = dateqtype $ DateSpan Nothing $ spanEnd reportspan - sortdate = if date2_ ropts then postingDate2 else postingDate - filterJournal = if related_ ropts then filterJournalRelatedPostings else filterJournalPostings -- with -r, replace each posting with its sibling postings - symq = dbg4 "symq" $ filterQuery queryIsSym q - dateqtype - | queryIsDate2 dateq || (queryIsDate dateq && date2_ ropts) = Date2 - | otherwise = Date + dateqtype = if queryIsDate2 dateq || (queryIsDate dateq && date2_ ropts) then Date2 else Date where dateq = dbg4 "dateq" $ filterQuery queryIsDateOrDate2 $ dbg4 "q" q -- XXX confused by multiple date:/date2: ? diff --git a/hledger-lib/Hledger/Reports/ReportOptions.hs b/hledger-lib/Hledger/Reports/ReportOptions.hs index 70b27cac6..3be6878ea 100644 --- a/hledger-lib/Hledger/Reports/ReportOptions.hs +++ b/hledger-lib/Hledger/Reports/ReportOptions.hs @@ -39,6 +39,8 @@ module Hledger.Reports.ReportOptions ( reportOptsToggleStatus, simplifyStatuses, whichDateFromOpts, + journalValueAndFilterPostings, + journalValueAndFilterPostingsWith, journalApplyValuationFromOpts, journalApplyValuationFromOptsWith, mixedAmountApplyValuationAfterSumFromOptsWith, @@ -59,7 +61,7 @@ module Hledger.Reports.ReportOptions ( ) where -import Control.Applicative (Const(..), (<|>)) +import Control.Applicative (Const(..), (<|>), liftA2) import Control.Monad ((<=<), join) import Data.Either (fromRight) import Data.Either.Extra (eitherToMaybe) @@ -498,6 +500,31 @@ flat_ = not . tree_ -- depthFromOpts :: ReportOpts -> Int -- depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts) +-- | Convert a 'Journal''s amounts to cost and/or to value (see +-- 'journalApplyValuationFromOpts'), and filter by the 'ReportSpec' 'Query'. +-- +-- We make sure to first filter by amt: and cur: terms, then value the +-- 'Journal', then filter by the remaining terms. +journalValueAndFilterPostings :: ReportSpec -> Journal -> Journal +journalValueAndFilterPostings rspec j = journalValueAndFilterPostingsWith rspec j priceoracle + where priceoracle = journalPriceOracle (infer_prices_ $ _rsReportOpts rspec) j + +-- | Like 'journalValueAndFilterPostings', but takes a 'PriceOracle' as an argument. +journalValueAndFilterPostingsWith :: ReportSpec -> Journal -> PriceOracle -> Journal +journalValueAndFilterPostingsWith rspec@ReportSpec{_rsQuery=q, _rsReportOpts=ropts} j = + -- Filter by the remainder of the query + filterJournal reportq + -- Apply valuation and costing + . journalApplyValuationFromOptsWith rspec + -- Filter by amount and currency, so it matches pre-valuation/costing + (if queryIsNull amtsymq then j else filterJournalAmounts amtsymq j) + where + -- with -r, replace each posting with its sibling postings + filterJournal = if related_ ropts then filterJournalRelatedPostings else filterJournalPostings + amtsymq = dbg3 "amtsymq" $ filterQuery queryIsAmtOrSym q + reportq = dbg3 "reportq" $ filterQuery (not . queryIsAmtOrSym) q + queryIsAmtOrSym = liftA2 (||) queryIsAmt queryIsSym + -- | Convert this journal's postings' amounts to cost and/or to value, if specified -- by options (-B/--cost/-V/-X/--value etc.). Strip prices if not needed. This -- should be the main stop for performing costing and valuation. The exception is diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 378b8b703..139358e06 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -1244,6 +1244,22 @@ $ hledger print -X A ``` +## Interaction of valuation and queries + +When matching postings based on queries in the presence of valuation, the +following happens. + +1. The query is separated into two parts: + 1. the currency (`cur:`) or amount (`amt:`). + 2. all other parts. +2. The postings are matched to the currency and amount queries based on pre-valued amounts. +3. Valuation is applied to the postings. +4. The postings are matched to the other parts of the query based on post-valued amounts. + +See: +[1625](https://github.com/simonmichael/hledger/issues/1625) + + ## Effect of valuation on reports Here is a reference for how valuation is supposed to affect each part of hledger's reports (and a glossary).