diff options
Diffstat (limited to 'beancount_extras_kris7t/plugins/closing_balance.py')
-rw-r--r-- | beancount_extras_kris7t/plugins/closing_balance.py | 70 |
1 files changed, 70 insertions, 0 deletions
diff --git a/beancount_extras_kris7t/plugins/closing_balance.py b/beancount_extras_kris7t/plugins/closing_balance.py new file mode 100644 index 0000000..a22e712 --- /dev/null +++ b/beancount_extras_kris7t/plugins/closing_balance.py | |||
@@ -0,0 +1,70 @@ | |||
1 | ''' | ||
2 | Plugin that closes an account by transferring its whole balance to another account. | ||
3 | ''' | ||
4 | __copyright__ = 'Copyright (c) 2020 Kristóf Marussy <kristof@marussy.com>' | ||
5 | __license__ = 'GNU GPLv2' | ||
6 | |||
7 | from typing import Any, Dict, List, NamedTuple, Optional, Tuple | ||
8 | |||
9 | from beancount.core.data import Balance, Close, Directive, Entries, Meta, Posting, Transaction | ||
10 | from beancount.core.flags import FLAG_OKAY | ||
11 | from beancount.core.number import ZERO | ||
12 | |||
13 | __plugins__ = ('close_with_balance_assertions',) | ||
14 | |||
15 | CLOSE_TO_META = 'close-to' | ||
16 | CLOSING_META = 'closing' | ||
17 | |||
18 | |||
19 | class ClosingBalanceError(NamedTuple): | ||
20 | source: Optional[Meta] | ||
21 | message: str | ||
22 | entry: Directive | ||
23 | |||
24 | |||
25 | def close_with_balance_assertions(entries: Entries, | ||
26 | options_map: Dict[str, Any], | ||
27 | config_str: Optional[str] = None) -> \ | ||
28 | Tuple[Entries, List[ClosingBalanceError]]: | ||
29 | new_entries: Entries = [] | ||
30 | errors: List[ClosingBalanceError] = [] | ||
31 | for entry in entries: | ||
32 | new_entries.append(entry) | ||
33 | if isinstance(entry, Balance) and CLOSE_TO_META in entry.meta: | ||
34 | close_to_account = entry.meta[CLOSE_TO_META] | ||
35 | if not isinstance(close_to_account, str): | ||
36 | errors.append(ClosingBalanceError( | ||
37 | entry.meta, | ||
38 | f'{CLOSE_TO_META} must be a string, got {close_to_account} instead', | ||
39 | entry)) | ||
40 | continue | ||
41 | if entry.tolerance is not None and entry.tolerance != ZERO: | ||
42 | errors.append(ClosingBalanceError( | ||
43 | entry.meta, | ||
44 | f'Closing an account requires {ZERO} tolerance, got {entry.tolerance} instead', | ||
45 | entry)) | ||
46 | continue | ||
47 | if entry.diff_amount is not None: | ||
48 | errors.append(ClosingBalanceError( | ||
49 | entry.meta, | ||
50 | f'Not closing {entry.account} with {entry.diff_amount} failed balance check', | ||
51 | entry)) | ||
52 | continue | ||
53 | new_meta = dict(entry.meta) | ||
54 | del new_meta[CLOSE_TO_META] | ||
55 | if entry.amount.number != ZERO: | ||
56 | new_entries.append(Transaction( | ||
57 | new_meta, | ||
58 | entry.date, | ||
59 | FLAG_OKAY, | ||
60 | None, | ||
61 | f'Closing {entry.account}', | ||
62 | set(), | ||
63 | set(), | ||
64 | [ | ||
65 | Posting(entry.account, -entry.amount, None, None, None, | ||
66 | {CLOSING_META: True}), | ||
67 | Posting(close_to_account, entry.amount, None, None, None, None) | ||
68 | ])) | ||
69 | new_entries.append(Close(new_meta, entry.date, entry.account)) | ||
70 | return new_entries, errors | ||