mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-25 19:31:44 +03:00
;refactor transaction balancing/checking (#1207)
This commit is contained in:
parent
9b349d9d41
commit
5ed6fe586a
@ -585,7 +585,29 @@ journalModifyTransactions j = j{ jtxns = modifyTransactions (jtxnmodifiers j) (j
|
||||
journalCheckBalanceAssertions :: Journal -> Maybe String
|
||||
journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTransactions True
|
||||
|
||||
-- "Transaction balancing" - inferring missing amounts and checking transaction balancedness and balance assertions
|
||||
-- "Transaction balancing", including: inferring missing amounts,
|
||||
-- applying balance assignments, checking transaction balancedness,
|
||||
-- checking balance assertions, respecting posting dates. These things
|
||||
-- are all interdependent.
|
||||
-- WARN tricky algorithm and code ahead.
|
||||
--
|
||||
-- Code overview as of 20190219, this could/should be simplified/documented more:
|
||||
-- parseAndFinaliseJournal['] (Cli/Utils.hs), journalAddForecast (Common.hs), budgetJournal (BudgetReport.hs), tests (BalanceReport.hs)
|
||||
-- journalBalanceTransactions
|
||||
-- runST
|
||||
-- runExceptT
|
||||
-- balanceTransaction (Transaction.hs)
|
||||
-- balanceTransactionHelper
|
||||
-- runReaderT
|
||||
-- balanceTransactionAndCheckAssertionsB
|
||||
-- addAmountAndCheckAssertionB
|
||||
-- addOrAssignAmountAndCheckAssertionB
|
||||
-- balanceTransactionHelper (Transaction.hs)
|
||||
-- uiCheckBalanceAssertions d ui@UIState{aopts=UIOpts{cliopts_=copts}, ajournal=j} (ErrorScreen.hs)
|
||||
-- journalCheckBalanceAssertions
|
||||
-- journalBalanceTransactions
|
||||
-- transactionWizard, postingsBalanced (Add.hs), tests (Transaction.hs)
|
||||
-- balanceTransaction (Transaction.hs) XXX hledger add won't allow balance assignments + missing amount ?
|
||||
|
||||
-- | Monad used for statefully balancing/amount-inferring/assertion-checking
|
||||
-- a sequence of transactions.
|
||||
@ -601,37 +623,40 @@ data BalancingState s = BalancingState {
|
||||
,bsAssrt :: Bool -- ^ whether to check balance assertions
|
||||
-- mutable
|
||||
,bsBalances :: H.HashTable s AccountName MixedAmount -- ^ running account balances, initially empty
|
||||
,bsTransactions :: STArray s Integer Transaction -- ^ the transactions being balanced
|
||||
,bsTransactions :: STArray s Integer Transaction -- ^ a mutable array of the transactions being balanced
|
||||
-- (for efficiency ? journalBalanceTransactions says: not strictly necessary but avoids a sort at the end I think)
|
||||
}
|
||||
|
||||
-- | Access the current balancing state, and possibly modify the mutable bits,
|
||||
-- lifting through the Except and Reader layers into the Balancing monad.
|
||||
withB :: (BalancingState s -> ST s a) -> Balancing s a
|
||||
withB f = ask >>= lift . lift . f
|
||||
withRunningBalance :: (BalancingState s -> ST s a) -> Balancing s a
|
||||
withRunningBalance f = ask >>= lift . lift . f
|
||||
|
||||
-- | Get an account's running balance so far.
|
||||
getAmountB :: AccountName -> Balancing s MixedAmount
|
||||
getAmountB acc = withB $ \BalancingState{bsBalances} -> do
|
||||
-- | Get this account's current running balance (exclusive).
|
||||
getRunningBalanceB :: AccountName -> Balancing s MixedAmount
|
||||
getRunningBalanceB acc = withRunningBalance $ \BalancingState{bsBalances} -> do
|
||||
fromMaybe 0 <$> H.lookup bsBalances acc
|
||||
|
||||
-- | Add an amount to an account's running balance, and return the new running balance.
|
||||
addAmountB :: AccountName -> MixedAmount -> Balancing s MixedAmount
|
||||
addAmountB acc amt = withB $ \BalancingState{bsBalances} -> do
|
||||
-- | Add this amount to this account's running balance,
|
||||
-- and return the new running balance (exclusive).
|
||||
addToRunningBalanceB :: AccountName -> MixedAmount -> Balancing s MixedAmount
|
||||
addToRunningBalanceB acc amt = withRunningBalance $ \BalancingState{bsBalances} -> do
|
||||
old <- fromMaybe 0 <$> H.lookup bsBalances acc
|
||||
let new = old + amt
|
||||
H.insert bsBalances acc new
|
||||
return new
|
||||
|
||||
-- | Set an account's running balance to this amount, and return the difference from the old.
|
||||
setAmountB :: AccountName -> MixedAmount -> Balancing s MixedAmount
|
||||
setAmountB acc amt = withB $ \BalancingState{bsBalances} -> do
|
||||
-- | Set this account's running balance (exclusive) to this amount,
|
||||
-- and return the difference from the previous value.
|
||||
setRunningBalanceB :: AccountName -> MixedAmount -> Balancing s MixedAmount
|
||||
setRunningBalanceB acc amt = withRunningBalance $ \BalancingState{bsBalances} -> do
|
||||
old <- fromMaybe 0 <$> H.lookup bsBalances acc
|
||||
H.insert bsBalances acc amt
|
||||
return $ amt - old
|
||||
|
||||
-- | Update (overwrite) this transaction with a new one.
|
||||
storeTransactionB :: Transaction -> Balancing s ()
|
||||
storeTransactionB t = withB $ \BalancingState{bsTransactions} ->
|
||||
-- | Update (overwrite) this transaction in the balancing state.
|
||||
updateTransactionB :: Transaction -> Balancing s ()
|
||||
updateTransactionB t = withRunningBalance $ \BalancingState{bsTransactions} ->
|
||||
void $ writeArray bsTransactions (tindex t) t
|
||||
|
||||
-- | Infer any missing amounts (to satisfy balance assignments and
|
||||
@ -639,30 +664,11 @@ storeTransactionB t = withB $ \BalancingState{bsTransactions} ->
|
||||
-- and (optional) all balance assertions pass. Or return an error message
|
||||
-- (just the first error encountered).
|
||||
--
|
||||
-- Assumes journalInferCommodityStyles has been called, since those affect transaction balancing.
|
||||
-- Assumes journalInferCommodityStyles has been called, since those
|
||||
-- affect transaction balancing.
|
||||
--
|
||||
-- This does multiple things because amount inferring, balance assignments,
|
||||
-- balance assertions and posting dates are interdependent.
|
||||
--
|
||||
-- This can be simplified further. Overview as of 20190219:
|
||||
-- @
|
||||
-- ****** parseAndFinaliseJournal['] (Cli/Utils.hs), journalAddForecast (Common.hs), budgetJournal (BudgetReport.hs), tests (BalanceReport.hs)
|
||||
-- ******* journalBalanceTransactions
|
||||
-- ******** runST
|
||||
-- ********* runExceptT
|
||||
-- ********** balanceTransaction (Transaction.hs)
|
||||
-- *********** balanceTransactionHelper
|
||||
-- ********** runReaderT
|
||||
-- *********** balanceTransactionAndCheckAssertionsB
|
||||
-- ************ addAmountAndCheckAssertionB
|
||||
-- ************ addOrAssignAmountAndCheckAssertionB
|
||||
-- ************ balanceTransactionHelper (Transaction.hs)
|
||||
-- ****** uiCheckBalanceAssertions d ui@UIState{aopts=UIOpts{cliopts_=copts}, ajournal=j} (ErrorScreen.hs)
|
||||
-- ******* journalCheckBalanceAssertions
|
||||
-- ******** journalBalanceTransactions
|
||||
-- ****** transactionWizard, postingsBalanced (Add.hs), tests (Transaction.hs)
|
||||
-- ******* balanceTransaction (Transaction.hs) XXX hledger add won't allow balance assignments + missing amount ?
|
||||
-- @
|
||||
-- This does multiple things at once because amount inferring, balance
|
||||
-- assignments, balance assertions and posting dates are interdependent.
|
||||
journalBalanceTransactions :: Bool -> Journal -> Either String Journal
|
||||
journalBalanceTransactions assrt j' =
|
||||
let
|
||||
@ -714,11 +720,9 @@ journalBalanceTransactions assrt j' =
|
||||
-- Transaction prices are removed, which helps eg balance-assertions.test: 15. Mix different commodities and assignments.
|
||||
-- This stores the balanced transactions in case 2 but not in case 1.
|
||||
balanceTransactionAndCheckAssertionsB :: Either Posting Transaction -> Balancing s ()
|
||||
|
||||
balanceTransactionAndCheckAssertionsB (Left p@Posting{}) =
|
||||
-- update the account's running balance and check the balance assertion if any
|
||||
void $ addAmountAndCheckAssertionB $ removePrices p
|
||||
|
||||
balanceTransactionAndCheckAssertionsB (Right t@Transaction{tpostings=ps}) = do
|
||||
-- make sure we can handle the balance assignments
|
||||
mapM_ checkIllegalBalanceAssignmentB ps
|
||||
@ -731,9 +735,9 @@ balanceTransactionAndCheckAssertionsB (Right t@Transaction{tpostings=ps}) = do
|
||||
Left err -> throwError err
|
||||
Right (t', inferredacctsandamts) -> do
|
||||
-- for each amount just inferred, update the running balance
|
||||
mapM_ (uncurry addAmountB) inferredacctsandamts
|
||||
mapM_ (uncurry addToRunningBalanceB) inferredacctsandamts
|
||||
-- and save the balanced transaction.
|
||||
storeTransactionB t'
|
||||
updateTransactionB t'
|
||||
|
||||
-- | If this posting has an explicit amount, add it to the account's running balance.
|
||||
-- If it has a missing amount and a balance assignment, infer the amount from, and
|
||||
@ -742,28 +746,32 @@ balanceTransactionAndCheckAssertionsB (Right t@Transaction{tpostings=ps}) = do
|
||||
-- Then test the balance assertion if any.
|
||||
addOrAssignAmountAndCheckAssertionB :: Posting -> Balancing s Posting
|
||||
addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanceassertion=mba}
|
||||
-- an explicit posting amount
|
||||
| hasAmount p = do
|
||||
newbal <- addAmountB acc amt
|
||||
newbal <- addToRunningBalanceB acc amt
|
||||
whenM (R.reader bsAssrt) $ checkBalanceAssertionB p newbal
|
||||
return p
|
||||
|
||||
-- no explicit posting amount, but there is a balance assignment
|
||||
| Just BalanceAssertion{baamount,batotal} <- mba = do
|
||||
(diff,newbal) <- case batotal of
|
||||
-- a total balance assignment (==, all commodities)
|
||||
True -> do
|
||||
-- a total balance assignment
|
||||
let newbal = Mixed [baamount]
|
||||
diff <- setAmountB acc newbal
|
||||
diff <- setRunningBalanceB acc newbal
|
||||
return (diff,newbal)
|
||||
-- a partial balance assignment (=, one commodity)
|
||||
False -> do
|
||||
-- a partial balance assignment
|
||||
oldbalothercommodities <- filterMixedAmount ((acommodity baamount /=) . acommodity) <$> getAmountB acc
|
||||
oldbalothercommodities <- filterMixedAmount ((acommodity baamount /=) . acommodity) <$> getRunningBalanceB acc
|
||||
let assignedbalthiscommodity = Mixed [baamount]
|
||||
newbal = oldbalothercommodities + assignedbalthiscommodity
|
||||
diff <- setAmountB acc newbal
|
||||
diff <- setRunningBalanceB acc newbal
|
||||
return (diff,newbal)
|
||||
let p' = p{pamount=diff, poriginal=Just $ originalPosting p}
|
||||
whenM (R.reader bsAssrt) $ checkBalanceAssertionB p' newbal
|
||||
return p'
|
||||
-- no amount, no balance assertion (GHC 7 doesn't like Nothing <- mba here)
|
||||
|
||||
-- no explicit posting amount, no balance assignment
|
||||
| otherwise = return p
|
||||
|
||||
-- | Add the posting's amount to its account's running balance, and
|
||||
@ -773,7 +781,7 @@ addOrAssignAmountAndCheckAssertionB p@Posting{paccount=acc, pamount=amt, pbalanc
|
||||
-- need to see the balance as it stands after each individual posting.
|
||||
addAmountAndCheckAssertionB :: Posting -> Balancing s Posting
|
||||
addAmountAndCheckAssertionB p | hasAmount p = do
|
||||
newbal <- addAmountB (paccount p) (pamount p)
|
||||
newbal <- addToRunningBalanceB (paccount p) (pamount p)
|
||||
whenM (R.reader bsAssrt) $ checkBalanceAssertionB p newbal
|
||||
return p
|
||||
addAmountAndCheckAssertionB p = return p
|
||||
@ -806,7 +814,8 @@ checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt
|
||||
if isinclusive
|
||||
then
|
||||
-- sum the running balances of this account and any of its subaccounts seen so far
|
||||
withB $ \BalancingState{bsBalances} ->
|
||||
-- XXX something wrong here, #1207
|
||||
withRunningBalance $ \BalancingState{bsBalances} ->
|
||||
H.foldM
|
||||
(\ibal (acc, amt) -> return $ ibal +
|
||||
if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc then amt else 0)
|
||||
|
Loading…
Reference in New Issue
Block a user