Repository: beancount/beancount

Issue:
Allow "balance" check directives against non-leaf accounts
**[Original report](https://bitbucket.org/blais/beancount/issue/396) by Denys Kurylenko (Bitbucket: [dkurilenko](https://bitbucket.org/dkurilenko)).**

----------------------------------------

Allow "balance" check directives against non-leaf accounts when leafonly plugin is enabled.

Use case: when someone starts and don’t want to let's say import entire history of Paychecks for the given year, but rather imports YTD numbers. Paychecks conveniently have GROSS YTD amount, one can use it to reconcile that all numbers are entered properly.

‌

```shell
2019-05-03 * "Hooli 2019 YTD Payroll"
  Income:US:Hooli:AnnualBonus              -2,000.00 USD
  Income:US:Hooli:GroupTermLife               -24.64 USD
  Income:US:Hooli:Salary                   -9,126.89 USD
  Equity:Retained-Earnings                  9,126.89 USD

; reconcile agains YTD/Gross
2019-05-24 balance Income:US:Hooli       -9,126.89 USD
```

One option is to reconcile against generic `Equity:Retained-Earnings` , however it might already have earnings from another employer.



Discussion:

Patch:
diff --git a/beancount/plugins/leafonly.py b/beancount/plugins/leafonly.py
index 98b7ae576..73b3ea239 100644
--- a/beancount/plugins/leafonly.py
+++ b/beancount/plugins/leafonly.py
@@ -37,20 +37,24 @@ def validate_leaf_only(entries, unused_options_map):
     open_close_map = None  # Lazily computed.
     errors = []
     for real_account in realization.iter_children(real_root):
-        if len(real_account) > 0 and real_account.txn_postings:
-            if open_close_map is None:
-                open_close_map = getters.get_account_open_close(entries)
-
-            try:
-                open_entry = open_close_map[real_account.account][0]
-            except KeyError:
-                open_entry = None
-            errors.append(
-                LeafOnlyError(
-                    open_entry.meta if open_entry else default_meta,
-                    "Non-leaf account '{}' has postings on it".format(real_account.account),
-                    open_entry,
-                )
+        if len(real_account) == 0 or not real_account.txn_postings:
+            continue
+        allowed_types = (data.Open, data.Balance)
+        postings = real_account.txn_postings
+        if all(isinstance(post, allowed_types) for post in postings):
+            continue
+        if open_close_map is None:
+            open_close_map = getters.get_account_open_close(entries)
+        try:
+            open_entry = open_close_map[real_account.account][0]
+        except KeyError:
+            open_entry = None
+        errors.append(
+            LeafOnlyError(
+                open_entry.meta if open_entry else default_meta,
+                "Non-leaf account '{}' has postings on it".format(real_account.account),
+                open_entry,
             )
+        )
 
     return entries, errors


Test patch:
diff --git a/beancount/plugins/leafonly_test.py b/beancount/plugins/leafonly_test.py
index d6e4ae2b1..70f5abeeb 100644
--- a/beancount/plugins/leafonly_test.py
+++ b/beancount/plugins/leafonly_test.py
@@ -49,6 +49,23 @@ def test_leaf_only2(self, _, errors, __):
         for error in errors:
             self.assertRegex(error.message, "Expenses:Food")
 
+    @loader.load_doc(expect_errors=False)
+    def test_leaf_only3(self, _, errors, __):
+        """
+        plugin "beancount.plugins.leafonly"
+
+        2011-01-01 open Expenses:Food
+        2011-01-01 open Expenses:Food:Restaurant
+        2011-01-01 open Assets:Other
+
+        2011-05-17 * "Something"
+          Expenses:Food:Restaurant   1.00 USD
+          Assets:Other              -1.00 USD
+
+        2011-05-18 balance Expenses:Food 1.00 USD  ;; Allowed balance posting.
+        """
+        self.assertEqual(0, len(errors))
+
 
 if __name__ == "__main__":
     unittest.main()