Don't infer a txn price with same-sign amounts (#1551)

This commit is contained in:
Simon Michael 2021-06-02 15:22:57 -10:00
commit 1e2ff1315b
3 changed files with 28 additions and 27 deletions

View File

@ -552,41 +552,42 @@ inferBalancingPrices t@Transaction{tpostings=ps} = t{tpostings=ps'}
-- posting type (real or balanced virtual). If we cannot or should not infer -- posting type (real or balanced virtual). If we cannot or should not infer
-- prices, just act as the identity on postings. -- prices, just act as the identity on postings.
priceInferrerFor :: Transaction -> PostingType -> (Posting -> Posting) priceInferrerFor :: Transaction -> PostingType -> (Posting -> Posting)
priceInferrerFor t pt = maybe id inferprice $ inferFromAndTo sumamounts priceInferrerFor t pt = maybe id inferprice inferFromAndTo
where where
postings = filter ((==pt).ptype) $ tpostings t postings = filter ((==pt).ptype) $ tpostings t
pcommodities = map acommodity $ concatMap (amounts . pamount) postings pcommodities = map acommodity $ concatMap (amounts . pamount) postings
sumamounts = amounts $ sumPostings postings -- amounts normalises to one amount per commodity & price sumamounts = amounts $ sumPostings postings -- amounts normalises to one amount per commodity & price
noprices = all (isNothing . aprice) sumamounts
-- We can infer prices if there are no prices given, and exactly two commodities in the -- We can infer prices if there are no prices given, exactly two commodities in the normalised
-- normalised sum of postings in this transaction. The amount we are converting from is -- sum of postings in this transaction, and these two have opposite signs. The amount we are
-- the first commodity to appear in the ordered list of postings, and the commodity we -- converting from is the first commodity to appear in the ordered list of postings, and the
-- are converting to is the other. If we cannot infer prices, return Nothing. -- commodity we are converting to is the other. If we cannot infer prices, return Nothing.
inferFromAndTo [a,b] | noprices = asum $ map orderIfMatches pcommodities inferFromAndTo = case sumamounts of
where orderIfMatches x | x == acommodity a = Just (a,b) [a,b] | noprices, oppositesigns -> asum $ map orderIfMatches pcommodities
| x == acommodity b = Just (b,a) where
| otherwise = Nothing noprices = all (isNothing . aprice) sumamounts
inferFromAndTo _ = Nothing oppositesigns = signum (aquantity a) /= signum (aquantity b)
orderIfMatches x | x == acommodity a = Just (a,b)
| x == acommodity b = Just (b,a)
| otherwise = Nothing
_ -> Nothing
-- For each posting, if the posting type matches, there is only a single amount in the posting, -- For each posting, if the posting type matches, there is only a single amount in the posting,
-- and the commodity of the amount matches the amount we're converting from, -- and the commodity of the amount matches the amount we're converting from,
-- then set its price based on the ratio between fromamount and toamount. -- then set its price based on the ratio between fromamount and toamount.
inferprice (fromamount, toamount) posting inferprice (fromamount, toamount) posting
| [a] <- amounts (pamount posting), ptype posting == pt, acommodity a == acommodity fromamount | [a] <- amounts (pamount posting), ptype posting == pt, acommodity a == acommodity fromamount
, let totalpricesign = if aquantity a < 0 then negate else id = posting{ pamount = mixedAmount a{aprice=Just conversionprice}
= posting{ pamount = mixedAmount a{aprice=Just $ conversionprice totalpricesign}
, poriginal = Just $ originalPosting posting } , poriginal = Just $ originalPosting posting }
| otherwise = posting | otherwise = posting
where where
-- If only one Amount in the posting list matches fromamount we can use TotalPrice, -- If only one Amount in the posting list matches fromamount we can use TotalPrice.
-- but we need to know the sign. Otherwise divide the conversion equally among the -- Otherwise divide the conversion equally among the Amounts by using a unit price.
-- Amounts by using a unit price. conversionprice = case filter (== acommodity fromamount) pcommodities of
conversionprice sign = case filter (== acommodity fromamount) pcommodities of [_] -> TotalPrice $ negate toamount
[_] -> TotalPrice $ sign (abs toamount) `withPrecision` NaturalPrecision _ -> UnitPrice $ negate unitprice `withPrecision` unitprecision
_ -> UnitPrice $ abs unitprice `withPrecision` unitprecision
unitprice = (aquantity fromamount) `divideAmount` toamount unitprice = aquantity fromamount `divideAmount` toamount
unitprecision = case (asprecision $ astyle fromamount, asprecision $ astyle toamount) of unitprecision = case (asprecision $ astyle fromamount, asprecision $ astyle toamount) of
(Precision a, Precision b) -> Precision . max 2 $ saturatedAdd a b (Precision a, Precision b) -> Precision . max 2 $ saturatedAdd a b
_ -> NaturalPrecision _ -> NaturalPrecision
@ -917,7 +918,7 @@ tests_Transaction =
[ posting {paccount = "a", pamount = mixedAmount (usd 1.35)} [ posting {paccount = "a", pamount = mixedAmount (usd 1.35)}
, posting {paccount = "b", pamount = mixedAmount (eur (-1))} , posting {paccount = "b", pamount = mixedAmount (eur (-1))}
])) @?= ])) @?=
Right (mixedAmount $ usd 1.35 @@ (eur 1 `withPrecision` NaturalPrecision)) Right (mixedAmount $ usd 1.35 @@ eur 1)
,test "balanceTransaction balances based on cost if there are unit prices" $ ,test "balanceTransaction balances based on cost if there are unit prices" $
assertRight $ assertRight $
balanceTransaction balanceTransaction

View File

@ -230,11 +230,11 @@ $ hledger -f- print --auto
# 12. # 12.
$ hledger -f- print --auto $ hledger -f- print --auto
2018-01-01 ; modified: 2018-01-01 ; modified:
Expenses:Joint:Widgets $100.00 Expenses:Joint:Widgets $100.00
Expenses:Joint $-100.00 @@ £50 ; generated-posting: = ^Expenses:Joint Expenses:Joint $-100.00 @@ £50.00 ; generated-posting: = ^Expenses:Joint
Liabilities:Joint:Bob $50.00 @@ £25 ; generated-posting: = ^Expenses:Joint Liabilities:Joint:Bob $50.00 @@ £25.00 ; generated-posting: = ^Expenses:Joint
Liabilities:Joint:Bill $50.00 @@ £25 ; generated-posting: = ^Expenses:Joint Liabilities:Joint:Bill $50.00 @@ £25.00 ; generated-posting: = ^Expenses:Joint
Assets:Joint:Bank £-50.00 Assets:Joint:Bank £-50.00
>=0 >=0

View File

@ -127,7 +127,7 @@ hledger: "-" (lines 1-3)
could not balance this transaction: could not balance this transaction:
real postings all have the same sign real postings all have the same sign
2020-01-01 2020-01-01
a 1A @@ 1B a 1A
b 1B b 1B
>=1 >=1