Skip to content

Commit

Permalink
imp: cost: Generate totally balanced conversion postings for amounts …
Browse files Browse the repository at this point in the history
…with costs.

Introduce --infer-equity option which will generate conversion postings.
--cost will override --infer-equity.

This means there will no longer be unbalanced transactions, but will be
offsetting conversion postings to balance things out. For example.

2000-01-01
  a   1 AAA @@ 2 BBB
  b  -2 BBB

When converting to cost, this is treated the same as before.
When used with --infer-equity, this is now treated as:

2000-01-01
  a                               1 AAA
  equity:conversion:AAA-BBB:AAA  -1 AAA
  equity:conversion:AAA-BBB:BBB   2 BBB
  b                              -2 BBB

The equity:conversion account name can be changed with an account
declaration of type `conversion`.

This also removes show_costs_ option in ReportOpts, replacing its
functionality with a richer cost_ option.
  • Loading branch information
Xitian9 committed Dec 11, 2021
1 parent 9c1c7ac commit 8652855
Show file tree
Hide file tree
Showing 20 changed files with 284 additions and 135 deletions.
23 changes: 17 additions & 6 deletions hledger-lib/Hledger/Data/Journal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module Hledger.Data.Journal (
journalLiabilityAccountQuery,
journalEquityAccountQuery,
journalCashAccountQuery,
journalConversionAccount,
-- * Misc
canonicalStyleFrom,
nulljournal,
Expand Down Expand Up @@ -120,9 +121,10 @@ import Hledger.Utils
import Hledger.Data.Types
import Hledger.Data.AccountName
import Hledger.Data.Amount
import Hledger.Data.Posting
import Hledger.Data.Transaction
import Hledger.Data.TransactionModifier
import Hledger.Data.Posting
import Hledger.Data.Valuation
import Hledger.Query


Expand Down Expand Up @@ -395,7 +397,8 @@ letterPairs _ = []
-- queries for standard account types

-- | Get a query for accounts of the specified types in this journal.
-- Account types include Asset, Liability, Equity, Revenue, Expense, Cash.
-- Account types include:
-- Asset, Liability, Equity, Revenue, Expense, Cash, Conversion.
-- For each type, if no accounts were declared with this type, the query
-- will instead match accounts with names matched by the case-insensitive
-- regular expression provided as a fallback.
Expand Down Expand Up @@ -506,6 +509,13 @@ journalProfitAndLossAccountQuery j = Or [journalRevenueAccountQuery j
,journalExpenseAccountQuery j
]

-- | The 'AccountName' to use for automatically generated conversion postings.
journalConversionAccount :: Journal -> AccountName
journalConversionAccount =
headDef (T.pack "equity:conversion")
. M.findWithDefault [] Conversion
. jdeclaredaccounttypes

-- Various kinds of filtering on journals. We do it differently depending
-- on the command.

Expand Down Expand Up @@ -870,10 +880,11 @@ postingInferredmarketPrice p@Posting{pamount} =

-- | Convert all this journal's amounts to cost using the transaction prices, if any.
-- The journal's commodity styles are applied to the resulting amounts.
journalToCost :: Journal -> Journal
journalToCost j@Journal{jtxns=ts} = j{jtxns=map (transactionToCost styles) ts}
where
styles = journalCommodityStyles j
journalToCost :: Costing -> Journal -> Journal
journalToCost cost j@Journal{jtxns=ts} =
j{jtxns=map (transactionToCost (journalConversionAccount j) styles cost) ts}
where
styles = journalCommodityStyles j

-- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol.
-- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol
Expand Down
30 changes: 27 additions & 3 deletions hledger-lib/Hledger/Data/Posting.hs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ import Data.Foldable (asum)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, isJust)
import Data.MemoUgly (memo)
import Data.List (foldl')
import Data.List (foldl', sort)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
Expand Down Expand Up @@ -481,8 +481,32 @@ postingApplyValuation priceoracle styles periodlast today v p =
postingTransformAmount (mixedAmountApplyValuation priceoracle styles periodlast today (postingDate p) v) p

-- | Convert this posting's amount to cost, and apply the appropriate amount styles.
postingToCost :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting
postingToCost styles = postingTransformAmount (styleMixedAmount styles . mixedAmountCost)
postingToCost :: Text -> M.Map CommoditySymbol AmountStyle -> Costing -> Posting -> [Posting]
postingToCost _ _ NoCost p = [p]
postingToCost _ styles Cost p = [postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p]
postingToCost equityAcct styles ConversionCost p = postingStripPrices p : concatMap conversionPostings (amountsRaw $ pamount p)
where
conversionPostings amt = case aprice amt of
Nothing -> []
Just _ -> [ cp{ paccount = accountPrefix <> amtCommodity
, pamount = mixedAmount . negate $ amountStripPrices amt
}
, cp{ paccount = accountPrefix <> costCommodity
, pamount = styleMixedAmount styles $ mixedAmount cost
}
]
where
cost = amountCost amt
amtCommodity = commodity amt
costCommodity = commodity cost
cp = p{ pcomment = pcomment p `commentAddTag` ("generated-posting","")
, ptags = [("generated-posting", ""), ("_generated-posting", "")]
, pbalanceassertion = Nothing
, poriginal = Nothing
}
accountPrefix = mconcat [ equityAcct, ":", T.intercalate "-" $ sort [amtCommodity, costCommodity], ":"]
-- Take the commodity of an amount and collapse consecutive spaces to a single space
commodity = T.unwords . filter (not . T.null) . T.words . acommodity

-- | Apply a transform function to this posting's amount.
postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting
Expand Down
4 changes: 2 additions & 2 deletions hledger-lib/Hledger/Data/Transaction.hs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ transactionApplyValuation priceoracle styles periodlast today v =
transactionTransformPostings (postingApplyValuation priceoracle styles periodlast today v)

-- | Convert this transaction's amounts to cost, and apply the appropriate amount styles.
transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction
transactionToCost styles = transactionTransformPostings (postingToCost styles)
transactionToCost :: Text -> M.Map CommoditySymbol AmountStyle -> Costing -> Transaction -> Transaction
transactionToCost equityAcct styles cost t = t{tpostings=concatMap (postingToCost equityAcct styles cost) $ tpostings t}

-- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases.
-- This can fail due to a bad replacement pattern in a regular expression alias.
Expand Down
1 change: 1 addition & 0 deletions hledger-lib/Hledger/Data/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ data AccountType =
| Revenue
| Expense
| Cash -- ^ a subtype of Asset - liquid assets to show in cashflow report
| Conversion -- ^ a subtype of Equity - account in which to generate conversion postings for transaction prices
deriving (Show,Eq,Ord,Generic)

-- not worth the trouble, letters defined in accountdirectivep for now
Expand Down
15 changes: 8 additions & 7 deletions hledger-lib/Hledger/Data/Valuation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ import Text.Printf (printf)
------------------------------------------------------------------------------
-- Types

-- | Whether to convert amounts to cost.
data Costing = Cost | NoCost
-- | Whether to convert amounts to cost or create conversion postings.
data Costing = Cost | ConversionCost | NoCost
deriving (Show,Eq)

-- | What kind of value conversion should be done on amounts ?
Expand Down Expand Up @@ -98,8 +98,8 @@ priceDirectiveToMarketPrice PriceDirective{..} =
-- Converting things to value

-- | Convert all component amounts to cost/selling price if requested, and style them.
mixedAmountToCost :: Costing -> M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
mixedAmountToCost cost styles = mapMixedAmount (amountToCost cost styles)
mixedAmountToCost :: M.Map CommoditySymbol AmountStyle -> Costing -> MixedAmount -> MixedAmount
mixedAmountToCost styles cost = mapMixedAmount (amountToCost styles cost)

-- | Apply a specified valuation to this mixed amount, using the
-- provided price oracle, commodity styles, and reference dates.
Expand All @@ -109,9 +109,10 @@ mixedAmountApplyValuation priceoracle styles periodlast today postingdate v =
mapMixedAmount (amountApplyValuation priceoracle styles periodlast today postingdate v)

-- | Convert an Amount to its cost if requested, and style it appropriately.
amountToCost :: Costing -> M.Map CommoditySymbol AmountStyle -> Amount -> Amount
amountToCost NoCost _ = id
amountToCost Cost styles = styleAmount styles . amountCost
amountToCost :: M.Map CommoditySymbol AmountStyle -> Costing -> Amount -> Amount
amountToCost styles Cost = styleAmount styles . amountCost
amountToCost _ ConversionCost = amountStripPrices
amountToCost _ NoCost = id

-- | Apply a specified valuation to this amount, using the provided
-- price oracle, and reference dates. Also fix up its display style
Expand Down
32 changes: 17 additions & 15 deletions hledger-lib/Hledger/Read/JournalReader.hs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ accountdirectivep = do
-- XXX added in 1.11, deprecated in 1.13, remove in 1.14
mtypecode :: Maybe Char <- lift $ optional $ try $ do
skipNonNewlineSpaces1 -- at least one more space in addition to the one consumed by modifiedaccountp
choice $ map char "ALERX"
choice $ map char "ALERXV"

-- maybe a comment, on this and/or following lines
(cmt, tags) <- lift transactioncommentp
Expand All @@ -378,22 +378,24 @@ accountTypeTagName = "type"
parseAccountTypeCode :: Text -> Either String AccountType
parseAccountTypeCode s =
case T.toLower s of
"asset" -> Right Asset
"a" -> Right Asset
"liability" -> Right Liability
"l" -> Right Liability
"equity" -> Right Equity
"e" -> Right Equity
"revenue" -> Right Revenue
"r" -> Right Revenue
"expense" -> Right Expense
"x" -> Right Expense
"cash" -> Right Cash
"c" -> Right Cash
_ -> Left err
"asset" -> Right Asset
"a" -> Right Asset
"liability" -> Right Liability
"l" -> Right Liability
"equity" -> Right Equity
"e" -> Right Equity
"revenue" -> Right Revenue
"r" -> Right Revenue
"expense" -> Right Expense
"x" -> Right Expense
"cash" -> Right Cash
"c" -> Right Cash
"conversion" -> Right Conversion
"v" -> Right Conversion
_ -> Left err
where
err = T.unpack $ "invalid account type code "<>s<>", should be one of " <>
T.intercalate ", " ["A","L","E","R","X","C","Asset","Liability","Equity","Revenue","Expense","Cash"]
T.intercalate ", " ["A","L","E","R","X","C","V","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"]

-- Add an account declaration to the journal, auto-numbering it.
addAccountDeclaration :: (AccountName,Text,[Tag]) -> JournalParser m ()
Expand Down
2 changes: 1 addition & 1 deletion hledger-lib/Hledger/Reports/BalanceReport.hs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ tests_BalanceReport = testGroup "BalanceReport" [
," a:b 10h @ $50"
," c:d "
]) >>= either error' return
let j' = journalCanonicaliseAmounts $ journalToCost j -- enable cost basis adjustment
let j' = journalCanonicaliseAmounts $ journalToCost Cost j -- enable cost basis adjustment
balanceReportAsText defreportopts (balanceReport defreportopts Any j') `is`
[" $500 a:b"
," $-500 c:d"
Expand Down
8 changes: 4 additions & 4 deletions hledger-lib/Hledger/Reports/BudgetReport.hs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ budgetReportAsText ropts@ReportOpts{..} budgetr = TB.toLazyText $
where
title = "Budget performance in " <> showDateSpan (periodicReportSpan budgetr)
<> (case cost_ of
Cost -> ", converted to cost"
NoCost -> "")
Just Cost -> ", converted to cost"
_ -> "")
<> (case value_ of
Just (AtThen _mc) -> ", valued at posting date"
Just (AtEnd _mc) -> ", valued at period ends"
Expand Down Expand Up @@ -387,8 +387,8 @@ budgetReportAsTable
Nothing
where
costedAmounts = case cost_ of
Cost -> amounts . mixedAmountCost
NoCost -> amounts
Just Cost -> amounts . mixedAmountCost
_ -> amounts

-- | Calculate the percentage of actual change to budget goal for a particular commodity
percentage' :: Change -> BudgetGoal -> CommoditySymbol -> Maybe Percentage
Expand Down
2 changes: 1 addition & 1 deletion hledger-lib/Hledger/Reports/EntriesReport.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type EntriesReportItem = Transaction
entriesReport :: ReportSpec -> Journal -> EntriesReport
entriesReport rspec@ReportSpec{_rsReportOpts=ropts} =
sortBy (comparing $ transactionDateFn ropts) . jtxns
. journalApplyValuationFromOpts rspec{_rsReportOpts=ropts{show_costs_=True}}
. journalApplyValuationFromOpts (setDefaultCosting NoCost rspec)
. filterJournalTransactions (_rsQuery rspec)

tests_EntriesReport = testGroup "EntriesReport" [
Expand Down
4 changes: 2 additions & 2 deletions hledger-lib/Hledger/Reports/MultiBalanceReport.hs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ getPostings rspec@ReportSpec{_rsQuery=query, _rsReportOpts=ropts} j priceoracle
where
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
then ropts{value_=Nothing, cost_=Just 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
Expand Down Expand Up @@ -432,7 +432,7 @@ displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} valuedaccts
balance = maybeStripPrices . case accountlistmode_ ropts of
ALTree | d == depth -> aibalance
_ -> aebalance
where maybeStripPrices = if show_costs_ ropts then id else mixedAmountStripPrices
where maybeStripPrices = if cost_ ropts == Just NoCost then id else mixedAmountStripPrices

-- Accounts interesting because they are a fork for interesting subaccounts
interestingParents = dbg5 "interestingParents" $ case accountlistmode_ ropts of
Expand Down
Loading

0 comments on commit 8652855

Please sign in to comment.