diff --git a/hledger-lib/Hledger/Reports/ReportOptions.hs b/hledger-lib/Hledger/Reports/ReportOptions.hs index 654436b4d..010dbd28a 100644 --- a/hledger-lib/Hledger/Reports/ReportOptions.hs +++ b/hledger-lib/Hledger/Reports/ReportOptions.hs @@ -665,11 +665,17 @@ journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceo CalcGain -> id _ -> journalToCost costop where costop = fromMaybe NoConversionOp $ conversionop_ ropts - -- Find the end of the period containing this posting + -- Find the "end" valuation date for this posting. + -- With a report interval, this is the last day of the report subperiod containing this posting; + -- with no interval it's the last date of the overall report period + -- (which for an end value report may have been extended to include the latest non-future P directive). + -- To get the period's last day, we subtract one from the (exclusive) period end date. postingperiodend = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts) - mPeriodEnd = case interval_ ropts of - NoInterval -> const . spanEnd . fst $ reportSpan j rspec - _ -> spanEnd <=< latestSpanContaining (historical : spans) + where + mPeriodEnd = case interval_ ropts of + NoInterval -> const . spanEnd . fst $ reportSpan j rspec + _ -> spanEnd <=< latestSpanContaining (historical : spans) + historical = DateSpan Nothing $ (fmap Exact . spanStart) =<< headMay spans spans = snd $ reportSpanBothDates j rspec styles = journalCommodityStyles j @@ -766,48 +772,59 @@ sortKeysDescription = "date, desc, account, amount, absamount" -- 'description' -- Report dates. --- | The effective report span is the start and end dates specified by --- options or queries, or otherwise the earliest and latest transaction or --- posting dates in the journal. If no dates are specified by options/queries --- and the journal is empty, returns the null date span. --- Also return the intervals if they are requested. +-- | The effective report span is the start and end dates requested by options or queries. +-- If the start date is unspecified, the earliest transaction or posting date is used. +-- If the end date is unspecified, the latest transaction or posting date +-- (or non-future market price date, when doing an end value report) is used. +-- If none of these things are present, the null date span is returned. +-- The report sub-periods caused by a report interval, if any, are also returned. reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan]) reportSpan = reportSpanHelper False +-- Note: In end value reports, the report end date and valuation date are the same. +-- If valuation date ever needs to be different, journalApplyValuationFromOptsWith is the place. --- | Like reportSpan, but uses both primary and secondary dates when calculating --- the span. +-- | Like reportSpan, but considers both primary and secondary dates, not just one or the other. reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [DateSpan]) reportSpanBothDates = reportSpanHelper True --- | A helper for reportSpan, which takes a Bool indicating whether to use both --- primary and secondary dates. reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, [DateSpan]) -reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} = - (reportspan, if not (null intervalspans) then intervalspans else [reportspan]) +reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts, _rsDay=today} = + (enlargedreportspan, if not (null intervalspans) then intervalspans else [enlargedreportspan]) where -- The date span specified by -b/-e/-p options and query args if any. - requestedspan = dbg3 "requestedspan" $ if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query - -- If we are requesting period-end valuation, the journal date span should - -- include price directives after the last transaction - journalspan = dbg3 "journalspan" $ if bothdates then journalDateSpanBothDates j else journalDateSpan (date2_ ropts) j - pricespan = dbg3 "pricespan" . DateSpan Nothing $ case value_ ropts of - Just (AtEnd _) -> fmap (Exact . addDays 1) . maximumMay . map pddate $ jpricedirectives j - _ -> Nothing - -- If the requested span has open ends, fill those with the journal's start and/or end dates, if possible. - requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanValidDefaultsFrom` (journalspan `spanExtend` pricespan) + requestedspan = dbg3 "requestedspan" $ + if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query + + -- If the requested span has open ends, fill them with defaults. + reportspan = dbg3 "reportspan" $ requestedspan `spanValidDefaultsFrom` txnsorpricespan + where + txnsorpricespan = dbg3 "txnsorpricespan" $ DateSpan mfirsttxn mlatesttxnorprice + where + DateSpan mfirsttxn mlasttxn = dbg3 "txnsspan" $ + if bothdates then journalDateSpanBothDates j else journalDateSpan (date2_ ropts) j + mlatesttxnorprice = + case value_ ropts of + Just (AtEnd _) -> mlasttxn `max` mlatestnonfutureprice + _ -> mlasttxn + where + mlatestnonfutureprice = dbg3 "latestnonfutureprice" $ -- #2445 + fmap (Exact . addDays 1) . maximumMay . filter (not . (> today)) . map pddate $ jpricedirectives j + -- The list of interval spans enclosing the requested span. -- This list can be empty if the journal was empty, -- or if hledger-ui has added its special date:-tomorrow to the query -- and all txns are in the future. - intervalspans = dbg3 "intervalspans" $ splitSpan adjust (interval_ ropts) requestedspan' + intervalspans = dbg3 "intervalspans" $ splitSpan adjust (interval_ ropts) reportspan where -- When calculating report periods, we will adjust the start date back to the nearest interval boundary -- unless a start date was specified explicitly. adjust = isNothing $ spanStart requestedspan + -- The requested span enlarged to enclose a whole number of intervals. -- This can be the null span if there were no intervals. - reportspan = dbg3 "reportspan" $ DateSpan (fmap Exact . spanStart =<< headMay intervalspans) - (fmap Exact . spanEnd =<< lastMay intervalspans) + enlargedreportspan = dbg3 "enlargedreportspan" $ + DateSpan (fmap Exact . spanStart =<< headMay intervalspans) + (fmap Exact . spanEnd =<< lastMay intervalspans) reportStartDate :: Journal -> ReportSpec -> Maybe Day reportStartDate j = spanStart . fst . reportSpan j diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 2c96c8be3..86a58a3c5 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -6154,8 +6154,8 @@ hledger will use the prices on a particular valuation date (or on more than one By default hledger uses "end" dates for valuation. More specifically: - For single period reports (including normal print and register reports): - - If an explicit [report end date](#report-start-end-date) is specified, that is used - - Otherwise the latest transaction date or P directive date is used (even if it's in the future) + - If an explicit [report end date](#report-start-end-date) is specified, that is used. + - Otherwise the latest transaction date or non-future P directive date is used. - For [multiperiod reports](#report-intervals), each period is valued on its last day. diff --git a/hledger/test/journal/valuation.test b/hledger/test/journal/valuation.test index fb027504b..39dd9c630 100644 --- a/hledger/test/journal/valuation.test +++ b/hledger/test/journal/valuation.test @@ -126,6 +126,7 @@ P 2000/01/15 A 5 B P 2000/02/01 A 2 B P 2000/03/01 A 3 B P 2000/04/01 A 4 B +P 3000/01/01 A 9 B 2000/01/01 (a) 1 A @ 6 B @@ -215,6 +216,20 @@ $ hledger -f- print --value=now >=0 +# ** 15. print value using prices on default end date, which is the later of +# txn dates and market price dates, excluding future market prices in the future. +$ hledger -f- print -V --today=2025-09-01 +2000-01-01 + (a) 4 B + +2000-02-01 + (a) 4 B + +2000-03-01 + (a) 4 B + +>=0 + # ** register # ** 15. register report valued at cost.