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 | ||||
| -- amount, runs the supplied fallback action. | ||||
| addAmountAndCheckBalanceAssertionB ::  | ||||
|      (Posting -> Balancing s Posting) -- ^ fallback action | ||||
|      (Posting -> Balancing s Posting) -- ^ fallback action  XXX why ? | ||||
|   -> Posting | ||||
|   -> Balancing s Posting | ||||
| addAmountAndCheckBalanceAssertionB _ p | hasAmount p = do | ||||
|   newAmt <- addToBalanceB (paccount p) (pamount p) | ||||
|   assrt <- R.reader bsAssrt | ||||
|   lift $ when assrt $ ExceptT $ return $ checkBalanceAssertion p newAmt | ||||
|   when assrt $ checkBalanceAssertionB p newAmt | ||||
|   return p | ||||
| addAmountAndCheckBalanceAssertionB fallback p = fallback p | ||||
| 
 | ||||
| @ -747,66 +747,81 @@ addAmountAndCheckBalanceAssertionB fallback p = fallback p | ||||
| -- return an error if the assertion is not satisfied. | ||||
| -- If the assertion is partial, unasserted commodities in the actual balance | ||||
| -- are ignored; if it is total, they will cause the assertion to fail. | ||||
| checkBalanceAssertion :: Posting -> MixedAmount -> Either String () | ||||
| checkBalanceAssertion p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal = | ||||
|   foldl' f (Right ()) assertedamts | ||||
|     where | ||||
|       f (Right _) assertedamt = checkBalanceAssertionOneCommodity p assertedamt actualbal | ||||
|       f err _                 = err | ||||
|       assertedamts = baamount : otheramts | ||||
|         where | ||||
|           assertedcomm = acommodity baamount | ||||
|           otheramts | batotal   = map (\a -> a{ aquantity = 0 }) $ amounts $ filterMixedAmount (\a -> acommodity a /= assertedcomm) actualbal | ||||
|                     | otherwise = [] | ||||
| checkBalanceAssertion _ _ = Right () | ||||
| checkBalanceAssertionB :: Posting -> MixedAmount -> Balancing s () | ||||
| checkBalanceAssertionB p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal = | ||||
|   forM_ assertedamts $ \amt -> checkBalanceAssertionOneCommodityB p amt actualbal | ||||
|   where | ||||
|     assertedamts = baamount : otheramts | ||||
|       where | ||||
|         assertedcomm = acommodity baamount | ||||
|         otheramts | batotal   = map (\a -> a{aquantity=0}) $ amounts $ filterMixedAmount ((/=assertedcomm).acommodity) actualbal | ||||
|                   | otherwise = [] | ||||
| checkBalanceAssertionB _ _ = return () | ||||
| 
 | ||||
| -- | Does this (single commodity) expected balance match the amount of that | ||||
| -- commodity in the given (multicommodity) actual balance ? If not, returns a | ||||
| -- balance assertion failure message based on the provided posting.  To match, | ||||
| -- the amounts must be exactly equal (display precision is ignored here). | ||||
| checkBalanceAssertionOneCommodity :: Posting -> Amount -> MixedAmount -> Either String () | ||||
| checkBalanceAssertionOneCommodity p assertedamt actualbal | ||||
|   | pass      = Right () | ||||
|   | otherwise = Left errmsg | ||||
|     where | ||||
|       assertedcomm = acommodity assertedamt | ||||
|       actualbalincommodity = fromMaybe nullamt $ find ((== assertedcomm) . acommodity) (amounts actualbal) | ||||
|       pass = | ||||
|         aquantity | ||||
|           -- traceWith (("asserted:"++).showAmountDebug) | ||||
|           assertedamt == | ||||
|         aquantity | ||||
|           -- traceWith (("actual:"++).showAmountDebug) | ||||
|           actualbalincommodity | ||||
|       errmsg = printf (unlines | ||||
|                     [ "balance assertion: %s", | ||||
|                       "\nassertion details:", | ||||
|                       "date:       %s", | ||||
|                       "account:    %s", | ||||
|                       "commodity:  %s", | ||||
|                       -- "display precision:  %d", | ||||
|                       "calculated: %s", -- (at display precision: %s)", | ||||
|                       "asserted:   %s", -- (at display precision: %s)", | ||||
|                       "difference: %s" | ||||
|                     ]) | ||||
|         (case ptransaction p of | ||||
|            Nothing -> "?" -- shouldn't happen | ||||
|            Just t ->  printf "%s\ntransaction:\n%s" | ||||
|                         (showGenericSourcePos pos) | ||||
|                         (chomp $ showTransaction t) | ||||
|                         :: String | ||||
|                         where | ||||
|                           pos = baposition $ fromJust $ pbalanceassertion p | ||||
|         ) | ||||
|         (showDate $ postingDate p) | ||||
|         (T.unpack $ paccount p) -- XXX pack | ||||
|         assertedcomm | ||||
|         -- (asprecision $ astyle actualbalincommodity)  -- should be the standard display precision I think | ||||
|         (show $ aquantity actualbalincommodity) | ||||
|         -- (showAmount actualbalincommodity) | ||||
|         (show $ aquantity assertedamt) | ||||
|         -- (showAmount assertedamt) | ||||
|         (show $ aquantity assertedamt - aquantity actualbalincommodity) | ||||
| -- If the assertion is inclusive, the expected amount is compared with the account's | ||||
| -- subaccount-inclusive balance; otherwise, with the subaccount-exclusive balance. | ||||
| checkBalanceAssertionOneCommodityB :: Posting -> Amount -> MixedAmount -> Balancing s () | ||||
| checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt actualbal = do | ||||
|   -- 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 | ||||
|     actualbalincomm = headDef 0 $ amounts $ filterMixedAmountByCommodity assertedcomm actualbal' | ||||
|     pass = | ||||
|       aquantity | ||||
|         -- traceWith (("asserted:"++).showAmountDebug) | ||||
|         assertedamt == | ||||
|       aquantity | ||||
|         -- traceWith (("actual:"++).showAmountDebug) | ||||
|         actualbalincomm | ||||
| 
 | ||||
|     errmsg = printf (unlines | ||||
|                   [ "balance assertion: %s", | ||||
|                     "\nassertion details:", | ||||
|                     "date:       %s", | ||||
|                     "account:    %s%s", | ||||
|                     "commodity:  %s", | ||||
|                     -- "display precision:  %d", | ||||
|                     "calculated: %s", -- (at display precision: %s)", | ||||
|                     "asserted:   %s", -- (at display precision: %s)", | ||||
|                     "difference: %s" | ||||
|                   ]) | ||||
|       (case ptransaction p of | ||||
|          Nothing -> "?" -- shouldn't happen | ||||
|          Just t ->  printf "%s\ntransaction:\n%s" | ||||
|                       (showGenericSourcePos pos) | ||||
|                       (chomp $ showTransaction t) | ||||
|                       :: String | ||||
|                       where | ||||
|                         pos = baposition $ fromJust $ pbalanceassertion p | ||||
|       ) | ||||
|       (showDate $ postingDate p) | ||||
|       (T.unpack $ paccount p) -- XXX pack | ||||
|       (if isinclusive then " (and subs)" else "" :: String) | ||||
|       assertedcomm | ||||
|       -- (asprecision $ astyle actualbalincommodity)  -- should be the standard display precision I think | ||||
|       (show $ aquantity actualbalincomm) | ||||
|       -- (showAmount actualbalincommodity) | ||||
|       (show $ aquantity assertedamt) | ||||
|       -- (showAmount assertedamt) | ||||
|       (show $ aquantity assertedamt - aquantity actualbalincomm) | ||||
| 
 | ||||
|   when (not pass) $ throwError errmsg | ||||
| 
 | ||||
| -- | Choose and apply a consistent display format to the posting | ||||
| -- amounts in each commodity. Each commodity's format is specified by | ||||
|  | ||||
| @ -105,6 +105,7 @@ nullassertion, assertion :: BalanceAssertion | ||||
| nullassertion = BalanceAssertion | ||||
|                   {baamount=nullamt | ||||
|                   ,batotal=False | ||||
|                   ,bainclusive=False | ||||
|                   ,baposition=nullsourcepos | ||||
|                   } | ||||
| assertion = nullassertion | ||||
|  | ||||
| @ -235,7 +235,7 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [ | ||||
|     | postingblock <- postingblocks] | ||||
|   where | ||||
|     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 | ||||
|         where | ||||
|           -- 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 []   -> ("",[]) | ||||
|                                               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. | ||||
| -- showPostingLine p = | ||||
| --   indent $ | ||||
| @ -374,6 +378,9 @@ storeTransactionB t = liftB $ \bs -> | ||||
| -- by inferring a missing amount or conversion price(s) if needed.  | ||||
| -- Or if balancing is not possible, because of unbalanced amounts or  | ||||
| -- 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, | ||||
| -- so those can optionally be provided. | ||||
| balanceTransaction :: | ||||
|  | ||||
| @ -278,6 +278,7 @@ instance Show Status where -- custom show.. bad idea.. don't do it.. | ||||
| data BalanceAssertion = BalanceAssertion { | ||||
|       baamount    :: Amount,             -- ^ the expected balance in a particular commodity | ||||
|       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 | ||||
|     } deriving (Eq,Typeable,Data,Generic,Show) | ||||
| 
 | ||||
|  | ||||
| @ -729,14 +729,16 @@ balanceassertionp = do | ||||
|   sourcepos <- genericSourcePos <$> lift getSourcePos | ||||
|   char '=' | ||||
|   istotal <- fmap isJust $ optional $ try $ char '=' | ||||
|   isinclusive <- fmap isJust $ optional $ try $ char '*' | ||||
|   lift (skipMany spacenonewline) | ||||
|   -- this amount can have a price; balance assertions ignore it, | ||||
|   -- but balance assignments will use it | ||||
|   a <- amountp <?> "amount (for a balance assertion or assignment)" | ||||
|   return BalanceAssertion | ||||
|     { baamount   = a | ||||
|     , batotal    = istotal | ||||
|     , baposition = sourcepos | ||||
|     { baamount    = a | ||||
|     , batotal     = istotal | ||||
|     , bainclusive = isinclusive | ||||
|     , baposition  = sourcepos | ||||
|     } | ||||
| 
 | ||||
| -- Parse a Ledger-style fixed lot price: {=PRICE} | ||||
|  | ||||
| @ -337,8 +337,8 @@ Virtual postings have some legitimate uses, but those are few. You can usually f | ||||
| hledger supports  | ||||
| [Ledger-style balance assertions](http://ledger-cli.org/3.0/doc/ledger3.html#Balance-assertions) | ||||
| in journal files. | ||||
| These look like `=EXPECTEDBALANCE` following a posting's amount. Eg in | ||||
| this example we assert the expected dollar balance in accounts a and b after | ||||
| These look like, for example, `= EXPECTEDBALANCE` following a posting's amount.  | ||||
| Eg here we assert the expected dollar balance in accounts a and b after | ||||
| each posting: | ||||
| 
 | ||||
| ```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 | ||||
| can protect you from, eg, inadvertently disrupting reconciled balances | ||||
| 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. | ||||
| 
 | ||||
| ### 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,  | ||||
| you can write multiple postings, each asserting one commodity's balance. | ||||
| 
 | ||||
| You can make a stronger kind of balance assertion, by writing a  | ||||
| double equals sign (`==EXPECTEDBALANCE`).  | ||||
| This "complete" balance assertion asserts the absence of other commodities  | ||||
| (or, that their balance is 0, which to hledger is equivalent.)  | ||||
| You can make a stronger "total" balance assertion by writing a  | ||||
| double equals sign (`== EXPECTEDBALANCE`).  | ||||
| This asserts that there are no other unasserted commodities in the account  | ||||
| (or, that their balance is 0).  | ||||
| 
 | ||||
| ``` {.journal} | ||||
| 2013/1/1 | ||||
| @ -453,21 +453,16 @@ and because [balance *assignments*](#balance-assignments) do use them (see below | ||||
| 
 | ||||
| ### Assertions and subaccounts | ||||
| 
 | ||||
| Balance assertions do not count the balance from subaccounts; they check | ||||
| the posted account's exclusive balance. For example: | ||||
| The balance assertions above (`=` and `==`) do not count the balance  | ||||
| from subaccounts; they check the account's exclusive balance only.  | ||||
| You can assert the balance including subaccounts by writing `=*` or `==*`, eg:  | ||||
| 
 | ||||
| ```journal | ||||
| 1/1 | ||||
|   checking:fund   1 = 1  ; post to this subaccount, its balance is now 1 | ||||
|   checking        1 = 1  ; post to the parent account, its exclusive balance is now 1 | ||||
|   equity | ||||
| ``` | ||||
| The balance report's flat mode shows these exclusive balances more clearly: | ||||
| ```shell | ||||
| $ hledger bal checking --flat | ||||
|                    1  checking | ||||
|                    1  checking:fund | ||||
| -------------------- | ||||
|                    2 | ||||
| 2019/1/1 | ||||
|   equity:opening balances | ||||
|   checking:a       5 | ||||
|   checking:b       5 | ||||
|   checking         1  ==* 11 | ||||
| ``` | ||||
| 
 | ||||
| ### Assertions and virtual postings | ||||
|  | ||||
| @ -311,7 +311,7 @@ hledger -f - stats | ||||
| >>>2 | ||||
| >>>=0 | ||||
| 
 | ||||
| # 17. Exact assertions parse correctly | ||||
| # 17. Total assertions (==) parse correctly | ||||
| hledger -f - stats | ||||
| <<< | ||||
| 2016/1/1 | ||||
| @ -324,7 +324,7 @@ hledger -f - stats | ||||
| >>>2 | ||||
| >>>=0 | ||||
| 
 | ||||
| # 18. Exact assertions consider entire account | ||||
| # 18. Total assertions consider entire multicommodity amount | ||||
| hledger -f - stats | ||||
| <<< | ||||
| 2016/1/1 | ||||
| @ -340,7 +340,7 @@ hledger -f - stats | ||||
| >>>2 /balance assertion.*line 10, column 15/ | ||||
| >>>=1 | ||||
| 
 | ||||
| # 19. Mix different commodities and exact assignments | ||||
| # 19. Mix different commodities and total assignments | ||||
| hledger -f - stats | ||||
| <<< | ||||
| 2016/1/1 | ||||
| @ -440,3 +440,27 @@ commodity $1000.00 | ||||
| 
 | ||||
| >>>2 /difference: 0\.0001/ | ||||
| >>>=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