journal: fix txn modifier multipliers with total-priced amounts (#928)

Transaction modifier multipliers have never multiplied total-priced amounts
correctly (and prior to hledger 1.10, this could generate unbalanced
transactions).

Now, the generated postings in this situation will have unit prices,
and an extra digit of display precision. This helps ensure that
the modified transaction will remain balanced. I'm not sure yet if
it's guaranteed.
This commit is contained in:
Simon Michael 2018-11-13 15:37:42 -08:00
parent 4b5b9f46db
commit 9e8b1612b0
3 changed files with 101 additions and 5 deletions

View File

@ -60,6 +60,7 @@ module Hledger.Data.Amount (
divideAmount,
multiplyAmount,
amountValue,
amountTotalPriceToUnitPrice,
-- ** rendering
amountstyle,
styleAmount,
@ -99,6 +100,7 @@ module Hledger.Data.Amount (
isReallyZeroMixedAmount,
isReallyZeroMixedAmountCost,
mixedAmountValue,
mixedAmountTotalPriceToUnitPrice,
-- ** rendering
styleMixedAmount,
showMixedAmount,
@ -209,6 +211,17 @@ costOfAmount a@Amount{aquantity=q, aprice=price} =
UnitPrice p@Amount{aquantity=pq} -> p{aquantity=pq * q}
TotalPrice p@Amount{aquantity=pq} -> p{aquantity=pq * signum q}
-- | 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,
-- to help the unit-priced amounts to still balance.
-- Does Decimal division, might be some rounding/irrational number issues.
amountTotalPriceToUnitPrice :: Amount -> Amount
amountTotalPriceToUnitPrice
a@Amount{aquantity=q, aprice=TotalPrice pa@Amount{aquantity=pq, astyle=ps@AmountStyle{asprecision=pp}}}
= a{aprice = UnitPrice pa{aquantity=abs (pq/q), astyle=ps{asprecision=pp+1}}}
amountTotalPriceToUnitPrice a = a
-- | Divide an amount's quantity by a constant.
divideAmount :: Amount -> Quantity -> Amount
divideAmount a@Amount{aquantity=q} d = a{aquantity=q/d}
@ -665,6 +678,12 @@ canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styl
mixedAmountValue :: Journal -> Day -> MixedAmount -> MixedAmount
mixedAmountValue j d (Mixed as) = Mixed $ map (amountValue j d) as
-- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice.
-- Has no effect on amounts without one.
-- Does Decimal division, might be some rounding/irrational number issues.
mixedAmountTotalPriceToUnitPrice :: MixedAmount -> MixedAmount
mixedAmountTotalPriceToUnitPrice (Mixed as) = Mixed $ map amountTotalPriceToUnitPrice as
-------------------------------------------------------------------------------
-- tests

View File

@ -24,7 +24,7 @@ import Hledger.Data.Amount
import Hledger.Data.Transaction
import Hledger.Query
import Hledger.Utils.UTF8IOCompat (error')
-- import Hledger.Utils.Debug
import Hledger.Utils.Debug
-- $setup
-- >>> :set -XOverloadedStrings
@ -78,6 +78,8 @@ tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt)
-- | Converts a 'TransactionModifier''s posting rule to a 'Posting'-generating function,
-- which will be used to make a new posting based on the old one (an "automated posting").
-- The new posting's amount can optionally be the old posting's amount multiplied by a constant.
-- If the old posting had a total-priced amount, the new posting's multiplied amount will be unit-priced.
tmPostingRuleToFunction :: TMPostingRule -> (Posting -> Posting)
tmPostingRuleToFunction pr =
\p -> renderPostingCommentDates $ pr
@ -88,10 +90,21 @@ tmPostingRuleToFunction pr =
where
amount' = case postingRuleMultiplier pr of
Nothing -> const $ pamount pr
Just n -> \p -> withAmountType (head $ amounts $ pamount pr) $ pamount p `multiplyMixedAmount` n
withAmountType pramount (Mixed as) = case acommodity pramount of
"" -> Mixed as
c -> Mixed [a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount} | a <- as]
Just n -> \p ->
-- Multiply the old posting's amount by the posting rule's multiplier.
-- Its display precision will be increased if needed to show all digits.
let
pramount = dbg6 "pramount" $ head $ amounts $ pamount pr
matchedamount = dbg6 "matchedamount" $ pamount p
unitpricedmatchedamount = dbg6 "unitpricedmatchedamount" $ mixedAmountTotalPriceToUnitPrice matchedamount
Mixed as = dbg6 "scaledmatchedamount" $ unitpricedmatchedamount `multiplyMixedAmount` n
in
case acommodity pramount of
"" -> Mixed as
-- TODO multipliers with commodity symbols are not yet a documented feature.
-- For now: in addition to multiplying the quantity, it also replaces the
-- matched amount's commodity, display style, and price with those of the posting rule.
c -> Mixed [a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount} | a <- as]
postingRuleMultiplier :: TMPostingRule -> Maybe Quantity
postingRuleMultiplier p =

View File

@ -0,0 +1,64 @@
# Issue #928
# Generating auto postings from a unit-priced amount.
<
= ^Expenses:Joint
Expenses:Joint *-1
Liabilities:Joint:Bob *0.5
Liabilities:Joint:Bill *0.5
2018/01/01
Expenses:Joint:Widgets $100.00 @ £0.50
Assets:Joint:Bank -£50.00
$ hledger -f- print --auto
2018/01/01
Expenses:Joint:Widgets $100.00 @ £0.50
Expenses:Joint $-100.00 @ £0.50
Liabilities:Joint:Bob $50.00 @ £0.50
Liabilities:Joint:Bill $50.00 @ £0.50
Assets:Joint:Bank £-50.00
>=0
# Generating auto postings from a total-priced amount.
<
= ^Expenses:Joint
Expenses:Joint *-1
Liabilities:Joint:Bob *0.5
Liabilities:Joint:Bill *0.5
2018/01/01
Expenses:Joint:Widgets $100.00 @@ £50
Assets:Joint:Bank -£50.00
$ hledger -f- print --auto
2018/01/01
Expenses:Joint:Widgets $100.00 @@ £50
Expenses:Joint $-100.00 @ £0.5
Liabilities:Joint:Bob $50.00 @ £0.5
Liabilities:Joint:Bill $50.00 @ £0.5
Assets:Joint:Bank £-50.00
>=0
# Generating auto postings from an implicitly-priced amount. Same as above.
<
= ^Expenses:Joint
Expenses:Joint *-1
Liabilities:Joint:Bob *0.5
Liabilities:Joint:Bill *0.5
2018/01/01
Expenses:Joint:Widgets $100.00
Assets:Joint:Bank -£50.00
$ hledger -f- print --auto
2018/01/01
Expenses:Joint:Widgets $100.00
Expenses:Joint $-100.00 @ £0.5
Liabilities:Joint:Bob $50.00 @ £0.5
Liabilities:Joint:Bill $50.00 @ £0.5
Assets:Joint:Bank £-50.00
>=0