lib: inclusive balance assertions (=* and ==*)
This commit is contained in:
parent
3b47b58aec
commit
8789a442a8
@ -733,13 +733,13 @@ inferFromAssignmentB p@Posting{paccount=acc} =
|
|||||||
-- checks the posting's balance assertion if any. Or if the posting has no
|
-- checks the posting's balance assertion if any. Or if the posting has no
|
||||||
-- amount, runs the supplied fallback action.
|
-- amount, runs the supplied fallback action.
|
||||||
addAmountAndCheckBalanceAssertionB ::
|
addAmountAndCheckBalanceAssertionB ::
|
||||||
(Posting -> Balancing s Posting) -- ^ fallback action
|
(Posting -> Balancing s Posting) -- ^ fallback action XXX why ?
|
||||||
-> Posting
|
-> Posting
|
||||||
-> Balancing s Posting
|
-> Balancing s Posting
|
||||||
addAmountAndCheckBalanceAssertionB _ p | hasAmount p = do
|
addAmountAndCheckBalanceAssertionB _ p | hasAmount p = do
|
||||||
newAmt <- addToBalanceB (paccount p) (pamount p)
|
newAmt <- addToBalanceB (paccount p) (pamount p)
|
||||||
assrt <- R.reader bsAssrt
|
assrt <- R.reader bsAssrt
|
||||||
lift $ when assrt $ ExceptT $ return $ checkBalanceAssertion p newAmt
|
when assrt $ checkBalanceAssertionB p newAmt
|
||||||
return p
|
return p
|
||||||
addAmountAndCheckBalanceAssertionB fallback p = fallback p
|
addAmountAndCheckBalanceAssertionB fallback p = fallback p
|
||||||
|
|
||||||
@ -747,42 +747,54 @@ addAmountAndCheckBalanceAssertionB fallback p = fallback p
|
|||||||
-- return an error if the assertion is not satisfied.
|
-- return an error if the assertion is not satisfied.
|
||||||
-- If the assertion is partial, unasserted commodities in the actual balance
|
-- If the assertion is partial, unasserted commodities in the actual balance
|
||||||
-- are ignored; if it is total, they will cause the assertion to fail.
|
-- are ignored; if it is total, they will cause the assertion to fail.
|
||||||
checkBalanceAssertion :: Posting -> MixedAmount -> Either String ()
|
checkBalanceAssertionB :: Posting -> MixedAmount -> Balancing s ()
|
||||||
checkBalanceAssertion p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal =
|
checkBalanceAssertionB p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal =
|
||||||
foldl' f (Right ()) assertedamts
|
forM_ assertedamts $ \amt -> checkBalanceAssertionOneCommodityB p amt actualbal
|
||||||
where
|
where
|
||||||
f (Right _) assertedamt = checkBalanceAssertionOneCommodity p assertedamt actualbal
|
|
||||||
f err _ = err
|
|
||||||
assertedamts = baamount : otheramts
|
assertedamts = baamount : otheramts
|
||||||
where
|
where
|
||||||
assertedcomm = acommodity baamount
|
assertedcomm = acommodity baamount
|
||||||
otheramts | batotal = map (\a -> a{ aquantity = 0 }) $ amounts $ filterMixedAmount (\a -> acommodity a /= assertedcomm) actualbal
|
otheramts | batotal = map (\a -> a{aquantity=0}) $ amounts $ filterMixedAmount ((/=assertedcomm).acommodity) actualbal
|
||||||
| otherwise = []
|
| otherwise = []
|
||||||
checkBalanceAssertion _ _ = Right ()
|
checkBalanceAssertionB _ _ = return ()
|
||||||
|
|
||||||
-- | Does this (single commodity) expected balance match the amount of that
|
-- | Does this (single commodity) expected balance match the amount of that
|
||||||
-- commodity in the given (multicommodity) actual balance ? If not, returns a
|
-- commodity in the given (multicommodity) actual balance ? If not, returns a
|
||||||
-- balance assertion failure message based on the provided posting. To match,
|
-- balance assertion failure message based on the provided posting. To match,
|
||||||
-- the amounts must be exactly equal (display precision is ignored here).
|
-- the amounts must be exactly equal (display precision is ignored here).
|
||||||
checkBalanceAssertionOneCommodity :: Posting -> Amount -> MixedAmount -> Either String ()
|
-- If the assertion is inclusive, the expected amount is compared with the account's
|
||||||
checkBalanceAssertionOneCommodity p assertedamt actualbal
|
-- subaccount-inclusive balance; otherwise, with the subaccount-exclusive balance.
|
||||||
| pass = Right ()
|
checkBalanceAssertionOneCommodityB :: Posting -> Amount -> MixedAmount -> Balancing s ()
|
||||||
| otherwise = Left errmsg
|
checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt actualbal = do
|
||||||
where
|
-- sum the running balances of this account and any subaccounts seen so far
|
||||||
|
bals <- R.asks bsBalances
|
||||||
|
actualibal <- liftB $ const $ H.foldM
|
||||||
|
(\bal (acc, amt) -> return $
|
||||||
|
if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc
|
||||||
|
then bal + amt
|
||||||
|
else bal)
|
||||||
|
0
|
||||||
|
bals
|
||||||
|
let
|
||||||
|
isinclusive = maybe False bainclusive $ pbalanceassertion p
|
||||||
|
actualbal'
|
||||||
|
| isinclusive = actualibal
|
||||||
|
| otherwise = actualbal
|
||||||
assertedcomm = acommodity assertedamt
|
assertedcomm = acommodity assertedamt
|
||||||
actualbalincommodity = fromMaybe nullamt $ find ((== assertedcomm) . acommodity) (amounts actualbal)
|
actualbalincomm = headDef 0 $ amounts $ filterMixedAmountByCommodity assertedcomm actualbal'
|
||||||
pass =
|
pass =
|
||||||
aquantity
|
aquantity
|
||||||
-- traceWith (("asserted:"++).showAmountDebug)
|
-- traceWith (("asserted:"++).showAmountDebug)
|
||||||
assertedamt ==
|
assertedamt ==
|
||||||
aquantity
|
aquantity
|
||||||
-- traceWith (("actual:"++).showAmountDebug)
|
-- traceWith (("actual:"++).showAmountDebug)
|
||||||
actualbalincommodity
|
actualbalincomm
|
||||||
|
|
||||||
errmsg = printf (unlines
|
errmsg = printf (unlines
|
||||||
[ "balance assertion: %s",
|
[ "balance assertion: %s",
|
||||||
"\nassertion details:",
|
"\nassertion details:",
|
||||||
"date: %s",
|
"date: %s",
|
||||||
"account: %s",
|
"account: %s%s",
|
||||||
"commodity: %s",
|
"commodity: %s",
|
||||||
-- "display precision: %d",
|
-- "display precision: %d",
|
||||||
"calculated: %s", -- (at display precision: %s)",
|
"calculated: %s", -- (at display precision: %s)",
|
||||||
@ -800,13 +812,16 @@ checkBalanceAssertionOneCommodity p assertedamt actualbal
|
|||||||
)
|
)
|
||||||
(showDate $ postingDate p)
|
(showDate $ postingDate p)
|
||||||
(T.unpack $ paccount p) -- XXX pack
|
(T.unpack $ paccount p) -- XXX pack
|
||||||
|
(if isinclusive then " (and subs)" else "" :: String)
|
||||||
assertedcomm
|
assertedcomm
|
||||||
-- (asprecision $ astyle actualbalincommodity) -- should be the standard display precision I think
|
-- (asprecision $ astyle actualbalincommodity) -- should be the standard display precision I think
|
||||||
(show $ aquantity actualbalincommodity)
|
(show $ aquantity actualbalincomm)
|
||||||
-- (showAmount actualbalincommodity)
|
-- (showAmount actualbalincommodity)
|
||||||
(show $ aquantity assertedamt)
|
(show $ aquantity assertedamt)
|
||||||
-- (showAmount assertedamt)
|
-- (showAmount assertedamt)
|
||||||
(show $ aquantity assertedamt - aquantity actualbalincommodity)
|
(show $ aquantity assertedamt - aquantity actualbalincomm)
|
||||||
|
|
||||||
|
when (not pass) $ throwError errmsg
|
||||||
|
|
||||||
-- | Choose and apply a consistent display format to the posting
|
-- | Choose and apply a consistent display format to the posting
|
||||||
-- amounts in each commodity. Each commodity's format is specified by
|
-- amounts in each commodity. Each commodity's format is specified by
|
||||||
|
|||||||
@ -105,6 +105,7 @@ nullassertion, assertion :: BalanceAssertion
|
|||||||
nullassertion = BalanceAssertion
|
nullassertion = BalanceAssertion
|
||||||
{baamount=nullamt
|
{baamount=nullamt
|
||||||
,batotal=False
|
,batotal=False
|
||||||
|
,bainclusive=False
|
||||||
,baposition=nullsourcepos
|
,baposition=nullsourcepos
|
||||||
}
|
}
|
||||||
assertion = nullassertion
|
assertion = nullassertion
|
||||||
|
|||||||
@ -235,7 +235,7 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [
|
|||||||
| postingblock <- postingblocks]
|
| postingblock <- postingblocks]
|
||||||
where
|
where
|
||||||
postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amount, assertion, samelinecomment] | amount <- shownAmounts]
|
postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amount, assertion, samelinecomment] | amount <- shownAmounts]
|
||||||
assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p
|
assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p
|
||||||
statusandaccount = indent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
|
statusandaccount = indent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
|
||||||
where
|
where
|
||||||
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
|
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
|
||||||
@ -259,6 +259,10 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [
|
|||||||
case renderCommentLines (pcomment p) of [] -> ("",[])
|
case renderCommentLines (pcomment p) of [] -> ("",[])
|
||||||
c:cs -> (c,cs)
|
c:cs -> (c,cs)
|
||||||
|
|
||||||
|
-- | Render a balance assertion, as the =[=][*] symbol and expected amount.
|
||||||
|
showBalanceAssertion BalanceAssertion{..} =
|
||||||
|
"=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount
|
||||||
|
|
||||||
-- | Render a posting, simply. Used in balance assertion errors.
|
-- | Render a posting, simply. Used in balance assertion errors.
|
||||||
-- showPostingLine p =
|
-- showPostingLine p =
|
||||||
-- indent $
|
-- indent $
|
||||||
@ -374,6 +378,9 @@ storeTransactionB t = liftB $ \bs ->
|
|||||||
-- by inferring a missing amount or conversion price(s) if needed.
|
-- by inferring a missing amount or conversion price(s) if needed.
|
||||||
-- Or if balancing is not possible, because of unbalanced amounts or
|
-- Or if balancing is not possible, because of unbalanced amounts or
|
||||||
-- more than one missing amount, returns an error message.
|
-- more than one missing amount, returns an error message.
|
||||||
|
-- Note this function may be unable to balance some transactions
|
||||||
|
-- that journalBalanceTransactions/balanceTransactionB can balance
|
||||||
|
-- (eg ones with balance assignments).
|
||||||
-- Whether postings "sum to 0" depends on commodity display precisions,
|
-- Whether postings "sum to 0" depends on commodity display precisions,
|
||||||
-- so those can optionally be provided.
|
-- so those can optionally be provided.
|
||||||
balanceTransaction ::
|
balanceTransaction ::
|
||||||
|
|||||||
@ -278,6 +278,7 @@ instance Show Status where -- custom show.. bad idea.. don't do it..
|
|||||||
data BalanceAssertion = BalanceAssertion {
|
data BalanceAssertion = BalanceAssertion {
|
||||||
baamount :: Amount, -- ^ the expected balance in a particular commodity
|
baamount :: Amount, -- ^ the expected balance in a particular commodity
|
||||||
batotal :: Bool, -- ^ disallow additional non-asserted commodities ?
|
batotal :: Bool, -- ^ disallow additional non-asserted commodities ?
|
||||||
|
bainclusive :: Bool, -- ^ include subaccounts when calculating the actual balance ?
|
||||||
baposition :: GenericSourcePos -- ^ the assertion's file position, for error reporting
|
baposition :: GenericSourcePos -- ^ the assertion's file position, for error reporting
|
||||||
} deriving (Eq,Typeable,Data,Generic,Show)
|
} deriving (Eq,Typeable,Data,Generic,Show)
|
||||||
|
|
||||||
|
|||||||
@ -729,6 +729,7 @@ balanceassertionp = do
|
|||||||
sourcepos <- genericSourcePos <$> lift getSourcePos
|
sourcepos <- genericSourcePos <$> lift getSourcePos
|
||||||
char '='
|
char '='
|
||||||
istotal <- fmap isJust $ optional $ try $ char '='
|
istotal <- fmap isJust $ optional $ try $ char '='
|
||||||
|
isinclusive <- fmap isJust $ optional $ try $ char '*'
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
-- this amount can have a price; balance assertions ignore it,
|
-- this amount can have a price; balance assertions ignore it,
|
||||||
-- but balance assignments will use it
|
-- but balance assignments will use it
|
||||||
@ -736,6 +737,7 @@ balanceassertionp = do
|
|||||||
return BalanceAssertion
|
return BalanceAssertion
|
||||||
{ baamount = a
|
{ baamount = a
|
||||||
, batotal = istotal
|
, batotal = istotal
|
||||||
|
, bainclusive = isinclusive
|
||||||
, baposition = sourcepos
|
, baposition = sourcepos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -337,8 +337,8 @@ Virtual postings have some legitimate uses, but those are few. You can usually f
|
|||||||
hledger supports
|
hledger supports
|
||||||
[Ledger-style balance assertions](http://ledger-cli.org/3.0/doc/ledger3.html#Balance-assertions)
|
[Ledger-style balance assertions](http://ledger-cli.org/3.0/doc/ledger3.html#Balance-assertions)
|
||||||
in journal files.
|
in journal files.
|
||||||
These look like `=EXPECTEDBALANCE` following a posting's amount. Eg in
|
These look like, for example, `= EXPECTEDBALANCE` following a posting's amount.
|
||||||
this example we assert the expected dollar balance in accounts a and b after
|
Eg here we assert the expected dollar balance in accounts a and b after
|
||||||
each posting:
|
each posting:
|
||||||
|
|
||||||
```journal
|
```journal
|
||||||
@ -355,7 +355,7 @@ After reading a journal file, hledger will check all balance
|
|||||||
assertions and report an error if any of them fail. Balance assertions
|
assertions and report an error if any of them fail. Balance assertions
|
||||||
can protect you from, eg, inadvertently disrupting reconciled balances
|
can protect you from, eg, inadvertently disrupting reconciled balances
|
||||||
while cleaning up old entries. You can disable them temporarily with
|
while cleaning up old entries. You can disable them temporarily with
|
||||||
the `--ignore-assertions` flag, which can be useful for
|
the `-I/--ignore-assertions` flag, which can be useful for
|
||||||
troubleshooting or for reading Ledger files.
|
troubleshooting or for reading Ledger files.
|
||||||
|
|
||||||
### Assertions and ordering
|
### Assertions and ordering
|
||||||
@ -399,10 +399,10 @@ We could call this a "partial" balance assertion.
|
|||||||
To assert the balance of more than one commodity in an account,
|
To assert the balance of more than one commodity in an account,
|
||||||
you can write multiple postings, each asserting one commodity's balance.
|
you can write multiple postings, each asserting one commodity's balance.
|
||||||
|
|
||||||
You can make a stronger kind of balance assertion, by writing a
|
You can make a stronger "total" balance assertion by writing a
|
||||||
double equals sign (`== EXPECTEDBALANCE`).
|
double equals sign (`== EXPECTEDBALANCE`).
|
||||||
This "complete" balance assertion asserts the absence of other commodities
|
This asserts that there are no other unasserted commodities in the account
|
||||||
(or, that their balance is 0, which to hledger is equivalent.)
|
(or, that their balance is 0).
|
||||||
|
|
||||||
``` {.journal}
|
``` {.journal}
|
||||||
2013/1/1
|
2013/1/1
|
||||||
@ -453,21 +453,16 @@ and because [balance *assignments*](#balance-assignments) do use them (see below
|
|||||||
|
|
||||||
### Assertions and subaccounts
|
### Assertions and subaccounts
|
||||||
|
|
||||||
Balance assertions do not count the balance from subaccounts; they check
|
The balance assertions above (`=` and `==`) do not count the balance
|
||||||
the posted account's exclusive balance. For example:
|
from subaccounts; they check the account's exclusive balance only.
|
||||||
|
You can assert the balance including subaccounts by writing `=*` or `==*`, eg:
|
||||||
|
|
||||||
```journal
|
```journal
|
||||||
1/1
|
2019/1/1
|
||||||
checking:fund 1 = 1 ; post to this subaccount, its balance is now 1
|
equity:opening balances
|
||||||
checking 1 = 1 ; post to the parent account, its exclusive balance is now 1
|
checking:a 5
|
||||||
equity
|
checking:b 5
|
||||||
```
|
checking 1 ==* 11
|
||||||
The balance report's flat mode shows these exclusive balances more clearly:
|
|
||||||
```shell
|
|
||||||
$ hledger bal checking --flat
|
|
||||||
1 checking
|
|
||||||
1 checking:fund
|
|
||||||
--------------------
|
|
||||||
2
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Assertions and virtual postings
|
### Assertions and virtual postings
|
||||||
|
|||||||
@ -311,7 +311,7 @@ hledger -f - stats
|
|||||||
>>>2
|
>>>2
|
||||||
>>>=0
|
>>>=0
|
||||||
|
|
||||||
# 17. Exact assertions parse correctly
|
# 17. Total assertions (==) parse correctly
|
||||||
hledger -f - stats
|
hledger -f - stats
|
||||||
<<<
|
<<<
|
||||||
2016/1/1
|
2016/1/1
|
||||||
@ -324,7 +324,7 @@ hledger -f - stats
|
|||||||
>>>2
|
>>>2
|
||||||
>>>=0
|
>>>=0
|
||||||
|
|
||||||
# 18. Exact assertions consider entire account
|
# 18. Total assertions consider entire multicommodity amount
|
||||||
hledger -f - stats
|
hledger -f - stats
|
||||||
<<<
|
<<<
|
||||||
2016/1/1
|
2016/1/1
|
||||||
@ -340,7 +340,7 @@ hledger -f - stats
|
|||||||
>>>2 /balance assertion.*line 10, column 15/
|
>>>2 /balance assertion.*line 10, column 15/
|
||||||
>>>=1
|
>>>=1
|
||||||
|
|
||||||
# 19. Mix different commodities and exact assignments
|
# 19. Mix different commodities and total assignments
|
||||||
hledger -f - stats
|
hledger -f - stats
|
||||||
<<<
|
<<<
|
||||||
2016/1/1
|
2016/1/1
|
||||||
@ -440,3 +440,27 @@ commodity $1000.00
|
|||||||
|
|
||||||
>>>2 /difference: 0\.0001/
|
>>>2 /difference: 0\.0001/
|
||||||
>>>=1
|
>>>=1
|
||||||
|
|
||||||
|
# 26. Inclusive assertions include balances from subaccounts.
|
||||||
|
hledger -f- print
|
||||||
|
<<<
|
||||||
|
2019/1/1
|
||||||
|
(a) X1
|
||||||
|
(a) Y3
|
||||||
|
(a:b) Y7
|
||||||
|
(a) 0 =* X1
|
||||||
|
(a) 0 =* Y10
|
||||||
|
(a:b) 0 =* Y7
|
||||||
|
(a:b) 0 ==* Y7
|
||||||
|
>>>
|
||||||
|
2019/01/01
|
||||||
|
(a) X1
|
||||||
|
(a) Y3
|
||||||
|
(a:b) Y7
|
||||||
|
(a) 0 =* X1
|
||||||
|
(a) 0 =* Y10
|
||||||
|
(a:b) 0 =* Y7
|
||||||
|
(a:b) 0 ==* Y7
|
||||||
|
|
||||||
|
>>>2
|
||||||
|
>>>=0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user