mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-08 15:14:49 +03:00
; refactor: centralise valuation logic #131
This commit is contained in:
parent
ad83919c6a
commit
42ce95aaef
@ -62,7 +62,8 @@ module Hledger.Data.Amount (
|
||||
multiplyAmount,
|
||||
divideAmountAndPrice,
|
||||
multiplyAmountAndPrice,
|
||||
amountValue,
|
||||
amountValueAtDate,
|
||||
amountApplyValuation,
|
||||
amountTotalPriceToUnitPrice,
|
||||
-- ** rendering
|
||||
amountstyle,
|
||||
@ -107,7 +108,8 @@ module Hledger.Data.Amount (
|
||||
isZeroMixedAmount,
|
||||
isReallyZeroMixedAmount,
|
||||
isReallyZeroMixedAmountCost,
|
||||
mixedAmountValue,
|
||||
mixedAmountValueAtDate,
|
||||
mixedAmountApplyValuation,
|
||||
mixedAmountTotalPriceToUnitPrice,
|
||||
-- ** rendering
|
||||
styleMixedAmount,
|
||||
@ -223,6 +225,35 @@ costOfAmount a@Amount{aquantity=q, aprice=price} =
|
||||
amountToCost :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount
|
||||
amountToCost styles = styleAmount styles . costOfAmount
|
||||
|
||||
-- | Find the market value of this amount on the given valuation date
|
||||
-- in its default valuation commodity (that of the latest applicable
|
||||
-- market price before the valuation date).
|
||||
-- The given market prices are expected to be in parse order.
|
||||
-- If no default valuation commodity can be found, the amount is left
|
||||
-- unchanged.
|
||||
amountValueAtDate :: Prices -> Day -> Amount -> Amount
|
||||
amountValueAtDate prices d a =
|
||||
case priceLookup prices d (acommodity a) of
|
||||
Just v -> v{aquantity=aquantity v * aquantity a}
|
||||
Nothing -> a
|
||||
|
||||
-- | Alternate implementation.
|
||||
-- Apply a specified valuation to this amount, using the provided
|
||||
-- prices db, commodity styles, period-end/current dates,
|
||||
-- and whether this is for a multiperiod report or not.
|
||||
-- Currently ignores the specified valuation commodity and always uses
|
||||
-- the default valuation commodity.
|
||||
amountApplyValuation :: Prices -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> ValuationType -> Amount -> Amount
|
||||
amountApplyValuation prices styles periodend today ismultiperiod v a =
|
||||
-- will use _mc later
|
||||
case v of
|
||||
AtCost _mc -> amountToCost styles a
|
||||
AtEnd _mc -> amountValueAtDate prices periodend a
|
||||
AtNow _mc -> amountValueAtDate prices today a
|
||||
AtDefault _mc | ismultiperiod -> amountValueAtDate prices periodend a
|
||||
AtDefault _mc -> amountValueAtDate prices today a
|
||||
AtDate d _mc -> amountValueAtDate prices d a
|
||||
|
||||
-- | Replace an amount's TotalPrice, if it has one, with an equivalent UnitPrice.
|
||||
-- Has no effect on amounts without one.
|
||||
-- Also increases the unit price's display precision to show one extra decimal place,
|
||||
@ -451,18 +482,6 @@ canonicaliseAmount styles a@Amount{acommodity=c, astyle=s} = a{astyle=s'}
|
||||
where
|
||||
s' = findWithDefault s c styles
|
||||
|
||||
-- | Find the market value of this amount on the given valuation date
|
||||
-- in its default valuation commodity (that of the latest applicable
|
||||
-- market price before the valuation date).
|
||||
-- The given market prices are expected to be in parse order.
|
||||
-- If no default valuation commodity can be found, the amount is left
|
||||
-- unchanged.
|
||||
amountValue :: Prices -> Day -> Amount -> Amount
|
||||
amountValue prices d a =
|
||||
case priceLookup prices d (acommodity a) of
|
||||
Just v -> v{aquantity=aquantity v * aquantity a}
|
||||
Nothing -> a
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- MixedAmount
|
||||
|
||||
@ -724,8 +743,17 @@ canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styl
|
||||
-- in its default valuation commodity, using the given market prices
|
||||
-- which are expected to be in parse order. When no default valuation
|
||||
-- commodity can be found, amounts are left unchanged.
|
||||
mixedAmountValue :: Prices -> Day -> MixedAmount -> MixedAmount
|
||||
mixedAmountValue prices d (Mixed as) = Mixed $ map (amountValue prices d) as
|
||||
mixedAmountValueAtDate :: Prices -> Day -> MixedAmount -> MixedAmount
|
||||
mixedAmountValueAtDate prices d (Mixed as) = Mixed $ map (amountValueAtDate prices d) as
|
||||
|
||||
-- Apply a specified valuation to this mixed amount, using the provided
|
||||
-- prices db, commodity styles, period-end/current dates,
|
||||
-- and whether this is for a multiperiod report or not.
|
||||
-- Currently ignores the specified valuation commodity and always uses
|
||||
-- the default valuation commodity.
|
||||
mixedAmountApplyValuation :: Prices -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> ValuationType -> MixedAmount -> MixedAmount
|
||||
mixedAmountApplyValuation prices styles periodend today ismultiperiod v (Mixed as) =
|
||||
Mixed $ map (amountApplyValuation prices styles periodend today ismultiperiod v) as
|
||||
|
||||
-- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice.
|
||||
-- Has no effect on amounts without one.
|
||||
|
@ -65,7 +65,7 @@ module Hledger.Data.Posting (
|
||||
-- * misc.
|
||||
showComment,
|
||||
postingTransformAmount,
|
||||
postingValue,
|
||||
postingApplyValuation,
|
||||
postingToCost,
|
||||
tests_Posting
|
||||
)
|
||||
@ -347,20 +347,36 @@ aliasReplace (BasicAlias old new) a
|
||||
| otherwise = a
|
||||
aliasReplace (RegexAlias re repl) a = T.pack $ regexReplaceCIMemo re repl $ T.unpack a -- XXX
|
||||
|
||||
-- | Apply a transform function to this posting's amount.
|
||||
postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting
|
||||
postingTransformAmount f p@Posting{pamount=a} = p{pamount=f a}
|
||||
-- Apply a specified valuation to this posting's amount, using the provided
|
||||
-- prices db, commodity styles, period-end/current dates, and whether
|
||||
-- this is for a multiperiod report or not.
|
||||
-- Currently ignores the specified valuation commodity and always uses
|
||||
-- the default valuation commodity.
|
||||
postingApplyValuation :: Prices -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Bool -> Posting -> ValuationType -> Posting
|
||||
postingApplyValuation prices styles periodend today ismultiperiod p v =
|
||||
-- will use _mc later
|
||||
case v of
|
||||
AtCost _mc -> postingToCost styles p
|
||||
AtEnd _mc -> postingValueAtDate prices periodend p
|
||||
AtNow _mc -> postingValueAtDate prices today p
|
||||
AtDefault _mc | ismultiperiod -> postingValueAtDate prices periodend p
|
||||
AtDefault _mc -> postingValueAtDate prices today p
|
||||
AtDate d _mc -> postingValueAtDate prices d p
|
||||
|
||||
-- | Convert this posting's amount to cost, and apply the appropriate amount styles.
|
||||
postingToCost :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting
|
||||
postingToCost styles p@Posting{pamount=a} = p{pamount=mixedAmountToCost styles a}
|
||||
|
||||
-- | Convert this posting's amount to market value in its default
|
||||
-- valuation commodity on the given date using the given market prices.
|
||||
-- If no default valuation commodity can be found, amounts are left unchanged.
|
||||
-- The prices are expected to be in parse order.
|
||||
postingValue :: Prices -> Day -> Posting -> Posting
|
||||
postingValue prices d p = postingTransformAmount (mixedAmountValue prices d) p
|
||||
postingValueAtDate :: Prices -> Day -> Posting -> Posting
|
||||
postingValueAtDate prices d p = postingTransformAmount (mixedAmountValueAtDate prices d) p
|
||||
|
||||
-- | Convert this posting's amount to cost, and apply the appropriate amount styles.
|
||||
postingToCost :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting
|
||||
postingToCost styles p@Posting{pamount=a} = p{pamount=mixedAmountToCost styles a}
|
||||
-- | Apply a transform function to this posting's amount.
|
||||
postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting
|
||||
postingTransformAmount f p@Posting{pamount=a} = p{pamount=f a}
|
||||
|
||||
|
||||
-- tests
|
||||
|
@ -431,6 +431,16 @@ data MarketPrice = MarketPrice {
|
||||
|
||||
instance NFData MarketPrice
|
||||
|
||||
-- | What kind of value conversion should be done on amounts ?
|
||||
-- UI: --value=cost|end|now|DATE[,COMM]
|
||||
data ValuationType =
|
||||
AtCost (Maybe CommoditySymbol) -- ^ convert to cost commodity using transaction prices, then optionally to given commodity using market prices at posting date
|
||||
| AtEnd (Maybe CommoditySymbol) -- ^ convert to default valuation commodity or given commodity, using market prices at period end(s)
|
||||
| AtNow (Maybe CommoditySymbol) -- ^ convert to default valuation commodity or given commodity, using current market prices
|
||||
| AtDate Day (Maybe CommoditySymbol) -- ^ convert to default valuation commodity or given commodity, using market prices on some date
|
||||
| AtDefault (Maybe CommoditySymbol) -- ^ works like AtNow in single period reports, like AtEnd in multiperiod reports
|
||||
deriving (Show,Data,Eq) -- Typeable
|
||||
|
||||
-- | A Journal, containing transactions and various other things.
|
||||
-- The basic data model for hledger.
|
||||
--
|
||||
|
@ -74,6 +74,7 @@ balanceReport ropts@ReportOpts{..} q j =
|
||||
today = fromMaybe (error' "balanceReport: ReportOpts today_ is unset so could not satisfy --value=now") today_
|
||||
multiperiod = interval_ /= NoInterval
|
||||
prices = journalPrices j
|
||||
styles = journalCommodityStyles j
|
||||
|
||||
-- Get all the summed accounts & balances, according to the query, as an account tree.
|
||||
-- If doing cost valuation, amounts will be converted to cost first.
|
||||
@ -84,14 +85,7 @@ balanceReport ropts@ReportOpts{..} q j =
|
||||
where
|
||||
valueaccount a@Account{..} = a{aebalance=val aebalance, aibalance=val aibalance}
|
||||
where
|
||||
val = case value_ of
|
||||
Nothing -> id
|
||||
Just (AtCost _mc) -> id
|
||||
Just (AtEnd _mc) -> mixedAmountValue prices periodlastday
|
||||
Just (AtNow _mc) -> mixedAmountValue prices today
|
||||
Just (AtDefault _mc) | multiperiod -> mixedAmountValue prices periodlastday
|
||||
Just (AtDefault _mc) -> mixedAmountValue prices today
|
||||
Just (AtDate d _mc) -> mixedAmountValue prices d
|
||||
val = maybe id (mixedAmountApplyValuation prices styles periodlastday today multiperiod) value_
|
||||
where
|
||||
periodlastday =
|
||||
fromMaybe (error' "balanceReport: expected a non-empty journal") $ -- XXX shouldn't happen
|
||||
|
@ -41,33 +41,22 @@ entriesReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
prices = journalPrices j
|
||||
styles = journalCommodityStyles j
|
||||
tvalue t@Transaction{..} = t{tpostings=map pvalue tpostings}
|
||||
pvalue p@Posting{..} = case value_ of
|
||||
Nothing -> p
|
||||
Just (AtCost _mc) -> postingToCost styles p
|
||||
Just (AtEnd _mc) -> valueend p
|
||||
Just (AtNow _mc) -> valuenow p
|
||||
Just (AtDefault _mc) -> valuenow p
|
||||
Just (AtDate d _mc) -> postingValue prices d p
|
||||
pvalue p = maybe p (postingApplyValuation prices styles end today False p) value_
|
||||
where
|
||||
valueend p = postingValue prices (
|
||||
fromMaybe (postingDate p) -- XXX shouldn't happen
|
||||
mperiodorjournallastday
|
||||
) p
|
||||
valuenow p = postingValue prices (
|
||||
case today_ of Just d -> d
|
||||
Nothing -> error' "erValue: ReportOpts today_ is unset so could not satisfy --value=now"
|
||||
) p
|
||||
mperiodorjournallastday = mperiodlastday <|> journalEndDate False j
|
||||
today = fromMaybe (error' "erValue: ReportOpts today_ is unset so could not satisfy --value=now") today_
|
||||
end = fromMaybe (postingDate p) mperiodorjournallastday
|
||||
where
|
||||
-- The last day of the report period.
|
||||
-- Will be Nothing if no report period is specified, or also
|
||||
-- if ReportOpts does not have today_ set, since we need that
|
||||
-- to get the report period robustly.
|
||||
mperiodlastday :: Maybe Day = do
|
||||
t <- today_
|
||||
let q = queryFromOpts t ropts
|
||||
qend <- queryEndDate False q
|
||||
return $ addDays (-1) qend
|
||||
mperiodorjournallastday = mperiodlastday <|> journalEndDate False j
|
||||
where
|
||||
-- The last day of the report period.
|
||||
-- Will be Nothing if no report period is specified, or also
|
||||
-- if ReportOpts does not have today_ set, since we need that
|
||||
-- to get the report period robustly.
|
||||
mperiodlastday :: Maybe Day = do
|
||||
t <- today_
|
||||
let q = queryFromOpts t ropts
|
||||
qend <- queryEndDate False q
|
||||
return $ addDays (-1) qend
|
||||
|
||||
tests_EntriesReport = tests "EntriesReport" [
|
||||
tests "entriesReport" [
|
||||
|
@ -157,8 +157,9 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
-- end: summed/averaged row amounts
|
||||
-- date: summed/averaged row amounts
|
||||
today = fromMaybe (error' "postingsReport: ReportOpts today_ is unset so could not satisfy --value=now") today_
|
||||
-- Market prices.
|
||||
-- Market prices, commodity display styles.
|
||||
prices = journalPrices j
|
||||
styles = journalCommodityStyles j
|
||||
-- The last day of each column subperiod.
|
||||
lastdays :: [Day] =
|
||||
map ((maybe
|
||||
@ -273,17 +274,8 @@ multiBalanceReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
CumulativeChange -> drop 1 $ scanl (+) 0 changes
|
||||
HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor a) changes
|
||||
-- The row amounts valued according to --value if needed.
|
||||
, let rowbalsendvalue = [mixedAmountValue prices periodlastday amt | (amt,periodlastday) <- zip rowbals lastdays]
|
||||
, let rowbalsdatevalue d = [mixedAmountValue prices d amt | amt <- rowbals]
|
||||
, let valuedrowbals = dbg1 "valuedrowbals" $ case value_ of
|
||||
Nothing -> rowbals
|
||||
Just (AtCost _mc) -> rowbals -- cost valuation was handled earlier
|
||||
Just (AtEnd _mc) -> rowbalsendvalue
|
||||
Just (AtNow _mc) -> rowbalsdatevalue today
|
||||
Just (AtDefault _mc) | multiperiod -> rowbalsendvalue
|
||||
Just (AtDefault _mc) -> rowbalsdatevalue today
|
||||
Just (AtDate d _mc) -> rowbalsdatevalue d
|
||||
|
||||
, let val end = maybe id (mixedAmountApplyValuation prices styles end today multiperiod) value_
|
||||
, let valuedrowbals = dbg1 "valuedrowbals" $ [val periodlastday amt | (amt,periodlastday) <- zip rowbals lastdays]
|
||||
-- The total and average for the row, and their values.
|
||||
-- Total for a cumulative/historical report is always zero.
|
||||
, let rowtot = if balancetype_==PeriodChange then sum valuedrowbals else 0
|
||||
|
@ -74,6 +74,7 @@ postingsReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
whichdate = whichDateFromOpts ropts
|
||||
depth = queryDepth q
|
||||
prices = journalPrices j
|
||||
styles = journalCommodityStyles j
|
||||
|
||||
-- postings to be included in the report, and similarly-matched postings before the report start date
|
||||
(precedingps, reportps) = matchedPostingsBeforeAndDuring ropts q j reportspan
|
||||
@ -91,49 +92,26 @@ postingsReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
--
|
||||
-- In all cases, the running total/average is calculated from the above numbers.
|
||||
-- "Day before report start" is a bit arbitrary.
|
||||
|
||||
today = fromMaybe (error' "postingsReport: ReportOpts today_ is unset so could not satisfy --value=now") today_
|
||||
|
||||
today =
|
||||
fromMaybe (error' "postingsReport: ReportOpts today_ is unset so could not satisfy --value=now")
|
||||
today_
|
||||
reportperiodlastday =
|
||||
fromMaybe (error' "postingsReport: expected a non-empty journal") $ -- XXX shouldn't happen
|
||||
reportPeriodOrJournalLastDay ropts j
|
||||
multiperiod = interval_ /= NoInterval
|
||||
showempty = empty_ || average_
|
||||
pvalue p end = maybe p (postingApplyValuation prices styles end today multiperiod p) value_
|
||||
|
||||
-- Postings, or summary postings along with their subperiod's end date, to be displayed.
|
||||
displayps :: [(Posting, Maybe Day)] =
|
||||
if multiperiod then
|
||||
let
|
||||
showempty = empty_ || average_
|
||||
summaryps = summarisePostingsByInterval interval_ whichdate depth showempty reportspan reportps
|
||||
summaryps' = [(p, Just e) | (p,e) <- summaryps]
|
||||
summarypsendvalue = [ (postingValue prices periodlastday p, Just periodend)
|
||||
| (p,periodend) <- summaryps
|
||||
, let periodlastday = addDays (-1) periodend
|
||||
]
|
||||
summarypsdatevalue d = [(postingValue prices d p, Just periodend) | (p,periodend) <- summaryps]
|
||||
in case value_ of
|
||||
Nothing -> summaryps'
|
||||
Just (AtCost _mc) -> summaryps' -- conversion to cost was done earlier
|
||||
Just (AtEnd _mc) -> summarypsendvalue
|
||||
Just (AtNow _mc) -> summarypsdatevalue today
|
||||
Just (AtDefault _mc) | multiperiod -> summarypsendvalue
|
||||
Just (AtDefault _mc) -> summarypsdatevalue today
|
||||
Just (AtDate d _mc) -> summarypsdatevalue d
|
||||
else
|
||||
let
|
||||
reportperiodlastday =
|
||||
fromMaybe (error' "postingsReport: expected a non-empty journal") -- XXX shouldn't happen
|
||||
$ reportPeriodOrJournalLastDay ropts j
|
||||
reportpsdatevalue d = [(postingValue prices d p, Nothing) | p <- reportps]
|
||||
reportpsnovalue = [(p, Nothing) | p <- reportps]
|
||||
in case value_ of
|
||||
Nothing -> reportpsnovalue
|
||||
Just (AtCost _mc) -> reportpsnovalue -- conversion to cost was done earlier
|
||||
Just (AtEnd _mc) -> reportpsdatevalue reportperiodlastday
|
||||
Just (AtNow _mc) -> reportpsdatevalue today
|
||||
Just (AtDefault _mc) | multiperiod -> reportpsdatevalue reportperiodlastday
|
||||
Just (AtDefault _mc) -> reportpsdatevalue today
|
||||
Just (AtDate d _mc) -> reportpsdatevalue d
|
||||
-- Postings, or summary postings with their subperiod's end date, to be displayed.
|
||||
displayps :: [(Posting, Maybe Day)]
|
||||
| multiperiod =
|
||||
let summaryps = summarisePostingsByInterval interval_ whichdate depth showempty reportspan reportps
|
||||
in [(pvalue p lastday, Just periodend) | (p, periodend) <- summaryps, let lastday = addDays (-1) periodend]
|
||||
| otherwise =
|
||||
[(pvalue p reportperiodlastday, Nothing) | p <- reportps]
|
||||
|
||||
-- posting report items ready for display
|
||||
items = dbg1 "postingsReport items" $ postingsReportItems displayps (nullposting,Nothing) whichdate depth valuedstartbal runningcalc startnum
|
||||
items = dbg1 "postingsReport items" $ postingsReportItems displayps (nullposting,Nothing) whichdate depth startbalvalued runningcalc startnum
|
||||
where
|
||||
historical = balancetype_ == HistoricalBalance
|
||||
precedingsum = sumPostings precedingps
|
||||
@ -142,20 +120,14 @@ postingsReport ropts@ReportOpts{..} q j@Journal{..} =
|
||||
startbal | average_ = if historical then precedingavg else 0
|
||||
| otherwise = if historical then precedingsum else 0
|
||||
-- For --value=end/now/DATE, convert the initial running total/average to value.
|
||||
startbaldatevalue d = mixedAmountValue prices d startbal
|
||||
valuedstartbal = case value_ of
|
||||
Nothing -> startbal
|
||||
Just (AtCost _mc) -> startbal -- conversion to cost was done earlier
|
||||
Just (AtEnd _mc) -> startbaldatevalue daybeforereportstart
|
||||
Just (AtNow _mc) -> startbaldatevalue today
|
||||
Just (AtDefault _mc) | multiperiod -> startbaldatevalue daybeforereportstart
|
||||
Just (AtDefault _mc) -> startbaldatevalue today
|
||||
Just (AtDate d _mc) -> startbaldatevalue d
|
||||
startbalvalued = val startbal
|
||||
where
|
||||
daybeforereportstart = maybe
|
||||
(error' "postingsReport: expected a non-empty journal") -- XXX shouldn't happen
|
||||
(addDays (-1))
|
||||
$ reportPeriodOrJournalStart ropts j
|
||||
val = maybe id (mixedAmountApplyValuation prices styles daybeforereportstart today multiperiod) value_
|
||||
where
|
||||
daybeforereportstart = maybe
|
||||
(error' "postingsReport: expected a non-empty journal") -- XXX shouldn't happen
|
||||
(addDays (-1))
|
||||
$ reportPeriodOrJournalStart ropts j
|
||||
|
||||
startnum = if historical then length precedingps + 1 else 1
|
||||
runningcalc = registerRunningCalculationFn ropts
|
||||
|
@ -78,18 +78,6 @@ data AccountListMode = ALDefault | ALTree | ALFlat deriving (Eq, Show, Data, Typ
|
||||
|
||||
instance Default AccountListMode where def = ALDefault
|
||||
|
||||
-- | What kind of value conversion should be done on amounts ?
|
||||
-- UI: --value=cost|end|now|DATE[,COMM]
|
||||
data ValuationType =
|
||||
AtCost (Maybe CommoditySymbol) -- ^ convert to cost commodity using transaction prices, then optionally to given commodity using market prices at posting date
|
||||
| AtEnd (Maybe CommoditySymbol) -- ^ convert to default valuation commodity or given commodity, using market prices at period end(s)
|
||||
| AtNow (Maybe CommoditySymbol) -- ^ convert to default valuation commodity or given commodity, using current market prices
|
||||
| AtDate Day (Maybe CommoditySymbol) -- ^ convert to default valuation commodity or given commodity, using market prices on some date
|
||||
| AtDefault (Maybe CommoditySymbol) -- ^ works like AtNow in single period reports, like AtEnd in multiperiod reports
|
||||
deriving (Show,Data,Eq) -- Typeable
|
||||
|
||||
-- instance Default ValuationType where def = AtNow Nothing
|
||||
|
||||
-- | Standard options for customising report filtering and output.
|
||||
-- Most of these correspond to standard hledger command-line options
|
||||
-- or query arguments, but not all. Some are used only by certain
|
||||
|
Loading…
Reference in New Issue
Block a user