aboutsummaryrefslogtreecommitdiffstats
path: root/beancount_extras_kris7t/importers/utils.py
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-01-25 01:14:28 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-01-25 01:14:28 +0100
commita1c2a999e449054d6641bbb633954e45fcd63f90 (patch)
tree47628c10ded721d66e47b5f87f501293cd8af003 /beancount_extras_kris7t/importers/utils.py
parentInitialize package (diff)
downloadbeancount-extras-kris7t-a1c2a999e449054d6641bbb633954e45fcd63f90.tar.gz
beancount-extras-kris7t-a1c2a999e449054d6641bbb633954e45fcd63f90.tar.zst
beancount-extras-kris7t-a1c2a999e449054d6641bbb633954e45fcd63f90.zip
Add plugins and importers from private config
The importers are missing tests, because not having any specifications for the import formats means we must use real, private data as test inputs
Diffstat (limited to 'beancount_extras_kris7t/importers/utils.py')
-rw-r--r--beancount_extras_kris7t/importers/utils.py125
1 files changed, 125 insertions, 0 deletions
diff --git a/beancount_extras_kris7t/importers/utils.py b/beancount_extras_kris7t/importers/utils.py
new file mode 100644
index 0000000..f0a8134
--- /dev/null
+++ b/beancount_extras_kris7t/importers/utils.py
@@ -0,0 +1,125 @@
1'''
2Utilities for custom importers.
3'''
4__copyright__ = 'Copyright (c) 2020 Kristóf Marussy <kristof@marussy.com>'
5__license__ = 'GNU GPLv2'
6
7from abc import ABC, abstractmethod
8import datetime as dt
9from decimal import Decimal
10from typing import cast, Callable, Iterable, List, NamedTuple, Optional, Set, TypeVar, Union
11
12from beancount.core import amount as am, data
13from beancount.core.amount import Amount
14from beancount.core.flags import FLAG_OKAY, FLAG_WARNING
15from beancount.core.number import D, ZERO
16
17MISSING_AMOUNT = cast(Amount, None)
18COMMENT_META = 'import-raw-comment'
19PAYEE_META = 'import-raw-payee'
20
21
22class InvalidEntry(Exception):
23 pass
24
25
26class Posting(NamedTuple):
27 account: str
28 amount: Amount
29
30
31class Row(ABC):
32 entry_type: Optional[str]
33 payee: Optional[str]
34 comment: str
35 meta: data.Meta
36 flag: str
37 tags: Set[str]
38 links: Set[str]
39 _postings: Optional[List[Posting]]
40
41 def __init__(self,
42 file_name: str,
43 line_number: int,
44 entry_type: Optional[str],
45 payee: Optional[str],
46 comment: str):
47 self.entry_type = entry_type
48 self.payee = payee
49 self.comment = comment
50 self.meta = data.new_metadata(file_name, line_number)
51 self.flag = FLAG_OKAY
52 self.tags = set()
53 self.links = set()
54 self._postings = None
55
56 @property
57 @abstractmethod
58 def transacted_amount(self) -> Amount:
59 pass
60
61 @property
62 def transacted_currency(self) -> str:
63 return self.transacted_amount.currency
64
65 @property
66 def postings(self) -> Optional[List[Posting]]:
67 return self._postings
68
69 def assign_to_accounts(self, *postings: Posting) -> None:
70 if self.done:
71 raise InvalidEntry('Transaction is alrady done processing')
72 self._postings = list(postings)
73 if not self._postings:
74 raise InvalidEntry('Not assigned to any accounts')
75 head, *rest = self._postings
76 sum = head.amount
77 for posting in rest:
78 sum = am.add(sum, posting.amount)
79 if sum != self.transacted_amount:
80 self.flag = FLAG_WARNING
81
82 def assign_to_account(self, account: str) -> None:
83 self.assign_to_accounts(Posting(account, self.transacted_amount))
84
85 @property
86 def done(self) -> bool:
87 return self._postings is not None
88
89
90Extractor = Callable[[Row], None]
91TRow = TypeVar('TRow', bound=Row)
92
93
94def run_row_extractors(row: TRow, extractors: Iterable[Callable[[TRow], None]]) -> None:
95 for extractor in extractors:
96 extractor(row)
97 if row.done:
98 return
99
100
101def extract_unknown(expenses_account: str, income_account: str) -> Extractor:
102 def do_extract(row: Row) -> None:
103 if row.transacted_amount.number < ZERO:
104 row.assign_to_account(expenses_account)
105 else:
106 row.assign_to_account(income_account)
107 row.flag = FLAG_WARNING
108 return do_extract
109
110
111def parse_date(date_str: str, format_string: str) -> dt.date:
112 try:
113 return dt.datetime.strptime(date_str, format_string).date()
114 except ValueError as exc:
115 raise InvalidEntry(f'Cannot parse date: {date_str}') from exc
116
117
118def parse_number(in_amount: Union[str, int, float, Decimal]) -> Decimal:
119 try:
120 value = D(in_amount)
121 except ValueError as exc:
122 raise InvalidEntry(f'Cannot parse number: {in_amount}') from exc
123 if value is None:
124 raise InvalidEntry(f'Parse number returned None: {in_amount}')
125 return value