beancount-gocardless 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- beancount_gocardless/__init__.py +10 -2
- beancount_gocardless/cli.py +70 -44
- beancount_gocardless/client.py +364 -308
- beancount_gocardless/importer.py +246 -125
- beancount_gocardless/models.py +538 -0
- beancount_gocardless/openapi/swagger.json +5344 -0
- beancount_gocardless/tui.py +771 -0
- beancount_gocardless/tui2.py +17 -0
- beancount_gocardless-0.1.9.dist-info/METADATA +126 -0
- beancount_gocardless-0.1.9.dist-info/RECORD +13 -0
- {beancount_gocardless-0.1.7.dist-info → beancount_gocardless-0.1.9.dist-info}/WHEEL +1 -1
- beancount_gocardless-0.1.9.dist-info/entry_points.txt +3 -0
- beancount_gocardless-0.1.9.dist-info/licenses/LICENSE +24 -0
- beancount_gocardless-0.1.7.dist-info/LICENSE +0 -19
- beancount_gocardless-0.1.7.dist-info/METADATA +0 -92
- beancount_gocardless-0.1.7.dist-info/RECORD +0 -9
- beancount_gocardless-0.1.7.dist-info/entry_points.txt +0 -3
beancount_gocardless/importer.py
CHANGED
|
@@ -1,37 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import date, timedelta
|
|
2
3
|
from os import path
|
|
4
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
3
5
|
import beangulp
|
|
4
6
|
import yaml
|
|
5
7
|
from beancount.core import amount, data, flags
|
|
6
8
|
from beancount.core.number import D
|
|
7
|
-
from .client import NordigenClient
|
|
8
9
|
|
|
10
|
+
from .client import GoCardlessClient
|
|
11
|
+
from .models import BankTransaction
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReferenceDuplicatesComparator:
|
|
17
|
+
def __init__(self, refs: List[str] = ["ref"]) -> None:
|
|
18
|
+
self.refs = refs
|
|
19
|
+
|
|
20
|
+
def __call__(self, entry1: data.Transaction, entry2: data.Transaction) -> bool:
|
|
21
|
+
entry1Refs = set()
|
|
22
|
+
entry2Refs = set()
|
|
23
|
+
for ref in self.refs:
|
|
24
|
+
if ref in entry1.meta:
|
|
25
|
+
entry1Refs.add(entry1.meta[ref])
|
|
26
|
+
if ref in entry2.meta:
|
|
27
|
+
entry2Refs.add(entry2.meta[ref])
|
|
28
|
+
|
|
29
|
+
return bool(entry1Refs & entry2Refs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GoCardLessImporter(beangulp.Importer):
|
|
11
33
|
"""
|
|
12
|
-
An importer for
|
|
34
|
+
An importer for GoCardless API with improved structure and extensibility.
|
|
13
35
|
|
|
14
36
|
Attributes:
|
|
15
|
-
config (
|
|
16
|
-
_client (
|
|
17
|
-
|
|
37
|
+
config (Optional[Dict[str, Any]]): Configuration loaded from the YAML file.
|
|
38
|
+
_client (Optional[GoCardlessClient]): Instance of the GoCardless API client.
|
|
18
39
|
"""
|
|
19
40
|
|
|
20
|
-
def __init__(self):
|
|
21
|
-
"""Initialize the
|
|
22
|
-
|
|
23
|
-
self.
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
"""Initialize the GoCardLessImporter."""
|
|
43
|
+
logger.debug("Initializing GoCardLessImporter")
|
|
44
|
+
self.config: Optional[Dict[str, Any]] = None
|
|
45
|
+
self._client: Optional[GoCardlessClient] = None
|
|
24
46
|
|
|
25
47
|
@property
|
|
26
|
-
def client(self):
|
|
48
|
+
def client(self) -> GoCardlessClient:
|
|
27
49
|
"""
|
|
28
|
-
Lazily initializes and returns the
|
|
50
|
+
Lazily initializes and returns the GoCardless API client.
|
|
29
51
|
|
|
30
52
|
Returns:
|
|
31
|
-
|
|
53
|
+
GoCardlessClient: The initialized GoCardless API client.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If config is not loaded.
|
|
32
57
|
"""
|
|
33
58
|
if not self._client:
|
|
34
|
-
self.
|
|
59
|
+
if not self.config:
|
|
60
|
+
raise ValueError("Config not loaded. Call load_config() first.")
|
|
61
|
+
self._client = GoCardlessClient(
|
|
35
62
|
self.config["secret_id"],
|
|
36
63
|
self.config["secret_key"],
|
|
37
64
|
cache_options=self.config.get("cache_options", None),
|
|
@@ -41,29 +68,32 @@ class NordigenImporter(beangulp.Importer):
|
|
|
41
68
|
|
|
42
69
|
def identify(self, filepath: str) -> bool:
|
|
43
70
|
"""
|
|
44
|
-
Identifies if the given file is a
|
|
71
|
+
Identifies if the given file is a GoCardless configuration file.
|
|
45
72
|
|
|
46
73
|
Args:
|
|
47
74
|
filepath (str): The path to the file.
|
|
48
75
|
|
|
49
76
|
Returns:
|
|
50
|
-
bool: True if the file is a
|
|
77
|
+
bool: True if the file is a GoCardless configuration file, False otherwise.
|
|
51
78
|
"""
|
|
52
|
-
|
|
79
|
+
result = path.basename(filepath).endswith("gocardless.yaml")
|
|
80
|
+
logger.debug("Identifying file %s: %s", filepath, result)
|
|
81
|
+
return result
|
|
53
82
|
|
|
54
83
|
def account(self, filepath: str) -> str:
|
|
55
84
|
"""
|
|
56
85
|
Returns an empty string as account (not directly used in this importer).
|
|
57
86
|
|
|
58
87
|
Args:
|
|
59
|
-
filepath (str): The path to the file.
|
|
88
|
+
filepath (str): The path to the file. Not used in this implementation.
|
|
60
89
|
|
|
61
90
|
Returns:
|
|
62
91
|
str: An empty string.
|
|
63
92
|
"""
|
|
93
|
+
logger.debug("Returning account for %s: ''", filepath)
|
|
64
94
|
return "" # We get the account from the config file
|
|
65
95
|
|
|
66
|
-
def load_config(self, filepath: str):
|
|
96
|
+
def load_config(self, filepath: str) -> Optional[Dict[str, Any]]:
|
|
67
97
|
"""
|
|
68
98
|
Loads configuration from the specified YAML file.
|
|
69
99
|
|
|
@@ -71,8 +101,9 @@ class NordigenImporter(beangulp.Importer):
|
|
|
71
101
|
filepath (str): The path to the YAML configuration file.
|
|
72
102
|
|
|
73
103
|
Returns:
|
|
74
|
-
|
|
104
|
+
Dict[str, Any]: The loaded configuration dictionary. Also sets the `self.config` attribute.
|
|
75
105
|
"""
|
|
106
|
+
logger.debug("Loading config from %s", filepath)
|
|
76
107
|
with open(filepath, "r") as f:
|
|
77
108
|
raw_config = f.read()
|
|
78
109
|
expanded_config = path.expandvars(
|
|
@@ -82,142 +113,145 @@ class NordigenImporter(beangulp.Importer):
|
|
|
82
113
|
|
|
83
114
|
return self.config
|
|
84
115
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
account_id (str): The Nordigen account ID.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
dict: The transaction data returned by the API.
|
|
94
|
-
"""
|
|
95
|
-
transactions_data = self.client.get_transactions(account_id)
|
|
96
|
-
|
|
97
|
-
return transactions_data
|
|
98
|
-
|
|
99
|
-
def get_all_transactions(self, transactions_data):
|
|
116
|
+
def get_all_transactions(
|
|
117
|
+
self, booked: List[BankTransaction], pending: List[BankTransaction]
|
|
118
|
+
) -> List[Tuple[BankTransaction, str]]:
|
|
100
119
|
"""
|
|
101
120
|
Combines booked and pending transactions and sorts them by date.
|
|
102
121
|
|
|
103
122
|
Args:
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
booked (List[BankTransaction]): The booked transaction data from the API.
|
|
124
|
+
pending (List[BankTransaction]): The pending transaction data from the API.
|
|
106
125
|
|
|
107
126
|
Returns:
|
|
108
|
-
|
|
109
|
-
a transaction
|
|
127
|
+
List[Tuple[BankTransaction, str]]: A sorted list of tuples, where each tuple contains
|
|
128
|
+
a transaction and its status ('booked' or 'pending').
|
|
110
129
|
"""
|
|
111
|
-
all_transactions = [
|
|
112
|
-
(tx, "
|
|
113
|
-
]
|
|
130
|
+
all_transactions = [(tx, "booked") for tx in booked] + [
|
|
131
|
+
(tx, "pending") for tx in pending
|
|
132
|
+
]
|
|
114
133
|
return sorted(
|
|
115
134
|
all_transactions,
|
|
116
|
-
key=lambda x: x[0].
|
|
135
|
+
key=lambda x: x[0].value_date or x[0].booking_date or "",
|
|
117
136
|
)
|
|
118
137
|
|
|
119
|
-
def add_metadata(
|
|
138
|
+
def add_metadata(
|
|
139
|
+
self, transaction: BankTransaction, custom_metadata: Dict[str, Any]
|
|
140
|
+
) -> Dict[str, Any]:
|
|
120
141
|
"""
|
|
121
142
|
Extracts metadata from a transaction and returns it as a dictionary.
|
|
122
143
|
|
|
123
144
|
This method can be overridden in subclasses to customize metadata extraction.
|
|
124
145
|
|
|
125
146
|
Args:
|
|
126
|
-
transaction (
|
|
127
|
-
custom_metadata (
|
|
147
|
+
transaction (BankTransaction): The transaction data from the API.
|
|
148
|
+
custom_metadata (Dict[str, Any]): Custom metadata from the config file.
|
|
128
149
|
|
|
129
150
|
Returns:
|
|
130
|
-
|
|
151
|
+
Dict[str, Any]: A dictionary of metadata key-value pairs.
|
|
131
152
|
"""
|
|
132
|
-
metakv = {}
|
|
153
|
+
metakv: Dict[str, Any] = {}
|
|
133
154
|
|
|
134
155
|
# Transaction ID
|
|
135
|
-
if
|
|
136
|
-
metakv["nordref"] = transaction
|
|
156
|
+
if transaction.transaction_id:
|
|
157
|
+
metakv["nordref"] = transaction.transaction_id
|
|
137
158
|
|
|
138
159
|
# Names
|
|
139
|
-
if
|
|
140
|
-
metakv["creditorName"] = transaction
|
|
141
|
-
if
|
|
142
|
-
metakv["debtorName"] = transaction
|
|
160
|
+
if transaction.creditor_name:
|
|
161
|
+
metakv["creditorName"] = transaction.creditor_name
|
|
162
|
+
if transaction.debtor_name:
|
|
163
|
+
metakv["debtorName"] = transaction.debtor_name
|
|
143
164
|
|
|
144
165
|
# Currency exchange
|
|
145
|
-
if
|
|
146
|
-
|
|
166
|
+
if (
|
|
167
|
+
transaction.currency_exchange
|
|
168
|
+
and transaction.currency_exchange[0].instructed_amount
|
|
169
|
+
):
|
|
170
|
+
instructedAmount = transaction.currency_exchange[0].instructed_amount
|
|
147
171
|
metakv["original"] = (
|
|
148
|
-
f"{instructedAmount
|
|
172
|
+
f"{instructedAmount.currency} {instructedAmount.amount}"
|
|
149
173
|
)
|
|
150
174
|
|
|
151
|
-
if transaction.
|
|
152
|
-
metakv["bookingDate"] = transaction
|
|
175
|
+
if transaction.booking_date:
|
|
176
|
+
metakv["bookingDate"] = transaction.booking_date
|
|
153
177
|
|
|
154
178
|
metakv.update(custom_metadata)
|
|
155
179
|
|
|
156
180
|
return metakv
|
|
157
181
|
|
|
158
|
-
def get_narration(self, transaction):
|
|
182
|
+
def get_narration(self, transaction: BankTransaction) -> str:
|
|
159
183
|
"""
|
|
160
184
|
Extracts the narration from a transaction.
|
|
161
185
|
|
|
162
186
|
This method can be overridden in subclasses to customize narration extraction.
|
|
163
187
|
|
|
164
188
|
Args:
|
|
165
|
-
transaction (
|
|
189
|
+
transaction (BankTransaction): The transaction data from the API.
|
|
166
190
|
|
|
167
191
|
Returns:
|
|
168
192
|
str: The extracted narration.
|
|
169
193
|
"""
|
|
170
194
|
narration = ""
|
|
171
195
|
|
|
172
|
-
if
|
|
173
|
-
narration += transaction
|
|
196
|
+
if transaction.remittance_information_unstructured:
|
|
197
|
+
narration += transaction.remittance_information_unstructured
|
|
174
198
|
|
|
175
|
-
if
|
|
176
|
-
narration += " ".join(transaction
|
|
199
|
+
if transaction.remittance_information_unstructured_array:
|
|
200
|
+
narration += " ".join(transaction.remittance_information_unstructured_array)
|
|
177
201
|
|
|
178
202
|
return narration
|
|
179
203
|
|
|
180
|
-
def get_payee(self, transaction):
|
|
204
|
+
def get_payee(self, transaction: BankTransaction) -> str:
|
|
181
205
|
"""
|
|
182
206
|
Extracts the payee from a transaction.
|
|
183
207
|
|
|
184
|
-
This method can be overridden in subclasses to customize payee extraction.
|
|
208
|
+
This method can be overridden in subclasses to customize payee extraction. The default
|
|
185
209
|
implementation returns an empty string.
|
|
186
210
|
|
|
187
211
|
Args:
|
|
188
|
-
transaction (
|
|
212
|
+
transaction (Dict[str, Any]): The transaction data from the API.
|
|
189
213
|
|
|
190
214
|
Returns:
|
|
191
215
|
str: The extracted payee (or an empty string by default).
|
|
192
|
-
|
|
193
216
|
"""
|
|
194
217
|
return ""
|
|
195
218
|
|
|
196
|
-
def get_transaction_date(self, transaction):
|
|
219
|
+
def get_transaction_date(self, transaction: BankTransaction) -> Optional[date]:
|
|
197
220
|
"""
|
|
198
|
-
Extracts the transaction date from a transaction.
|
|
221
|
+
Extracts the transaction date from a transaction. Prefers 'valueDate',
|
|
199
222
|
falls back to 'bookingDate'.
|
|
200
223
|
|
|
201
224
|
This method can be overridden in subclasses to customize date extraction.
|
|
202
225
|
|
|
203
226
|
Args:
|
|
204
|
-
transaction (
|
|
227
|
+
transaction (BankTransaction): The transaction data from the API.
|
|
205
228
|
|
|
206
229
|
Returns:
|
|
207
|
-
date: The extracted transaction date, or None if no date is found.
|
|
230
|
+
Optional[date]: The extracted transaction date, or None if no date is found.
|
|
208
231
|
"""
|
|
209
|
-
date_str = transaction.
|
|
232
|
+
date_str = transaction.value_date or transaction.booking_date
|
|
210
233
|
return date.fromisoformat(date_str) if date_str else None
|
|
211
234
|
|
|
212
|
-
def get_transaction_status(
|
|
235
|
+
def get_transaction_status(
|
|
236
|
+
self,
|
|
237
|
+
transaction: BankTransaction,
|
|
238
|
+
status: str,
|
|
239
|
+
metakv: Dict[str, Any],
|
|
240
|
+
tx_amount: amount.Amount,
|
|
241
|
+
asset_account: str,
|
|
242
|
+
) -> str:
|
|
213
243
|
"""
|
|
214
|
-
Determines the Beancount transaction flag based on
|
|
244
|
+
Determines the Beancount transaction flag based on transaction context.
|
|
215
245
|
|
|
216
246
|
This method can be overridden in subclasses to customize flag assignment. The default
|
|
217
|
-
implementation returns FLAG_OKAY for
|
|
247
|
+
implementation returns FLAG_OKAY for booked transactions and FLAG_WARNING for pending.
|
|
218
248
|
|
|
219
249
|
Args:
|
|
250
|
+
transaction (Dict[str, Any]): The transaction data from the API.
|
|
220
251
|
status (str): The transaction status ('booked' or 'pending').
|
|
252
|
+
metakv (Dict[str, Any]): Transaction metadata.
|
|
253
|
+
tx_amount (amount.Amount): Transaction amount.
|
|
254
|
+
asset_account (str): The Beancount asset account.
|
|
221
255
|
|
|
222
256
|
Returns:
|
|
223
257
|
str: The Beancount transaction flag.
|
|
@@ -225,34 +259,55 @@ class NordigenImporter(beangulp.Importer):
|
|
|
225
259
|
return flags.FLAG_OKAY if status == "booked" else flags.FLAG_WARNING
|
|
226
260
|
|
|
227
261
|
def create_transaction_entry(
|
|
228
|
-
self,
|
|
229
|
-
|
|
262
|
+
self,
|
|
263
|
+
transaction: BankTransaction,
|
|
264
|
+
status: str,
|
|
265
|
+
asset_account: str,
|
|
266
|
+
custom_metadata: Dict[str, Any],
|
|
267
|
+
) -> Optional[data.Transaction]:
|
|
230
268
|
"""
|
|
231
|
-
Creates a Beancount transaction entry from a
|
|
269
|
+
Creates a Beancount transaction entry from a GoCardless transaction.
|
|
232
270
|
|
|
233
271
|
This method can be overridden in subclasses to customize entry creation.
|
|
234
272
|
|
|
235
273
|
Args:
|
|
236
|
-
transaction (
|
|
274
|
+
transaction (Dict[str, Any]): The transaction data from the API.
|
|
237
275
|
status (str): The transaction status ('booked' or 'pending').
|
|
238
276
|
asset_account (str): The Beancount asset account.
|
|
239
|
-
custom_metadata (
|
|
277
|
+
custom_metadata (Dict[str, Any]): Custom metadata from config
|
|
240
278
|
|
|
241
279
|
Returns:
|
|
242
|
-
data.Transaction: The created Beancount transaction entry.
|
|
280
|
+
Optional[data.Transaction]: The created Beancount transaction entry, or None if date is invalid.
|
|
243
281
|
"""
|
|
282
|
+
logger.debug(
|
|
283
|
+
"Creating entry for transaction %s (%s)", transaction.transaction_id, status
|
|
284
|
+
)
|
|
244
285
|
metakv = self.add_metadata(transaction, custom_metadata)
|
|
245
286
|
meta = data.new_metadata("", 0, metakv)
|
|
246
287
|
|
|
247
288
|
trx_date = self.get_transaction_date(transaction)
|
|
289
|
+
if trx_date is None:
|
|
290
|
+
logger.debug(
|
|
291
|
+
"Skipping transaction %s with invalid date", transaction.transaction_id
|
|
292
|
+
)
|
|
293
|
+
return None
|
|
294
|
+
|
|
248
295
|
narration = self.get_narration(transaction)
|
|
249
296
|
payee = self.get_payee(transaction)
|
|
250
|
-
flag = self.get_transaction_status(status)
|
|
251
297
|
|
|
252
298
|
# Get transaction amount
|
|
299
|
+
if transaction.transaction_amount is None:
|
|
300
|
+
logger.debug(
|
|
301
|
+
"Skipping transaction %s with no amount", transaction.transaction_id
|
|
302
|
+
)
|
|
303
|
+
return None
|
|
253
304
|
tx_amount = amount.Amount(
|
|
254
|
-
D(str(transaction
|
|
255
|
-
transaction
|
|
305
|
+
D(str(transaction.transaction_amount.amount)),
|
|
306
|
+
transaction.transaction_amount.currency,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
flag = self.get_transaction_status(
|
|
310
|
+
transaction, status, metakv, tx_amount, asset_account
|
|
256
311
|
)
|
|
257
312
|
|
|
258
313
|
return data.Transaction(
|
|
@@ -277,63 +332,129 @@ class NordigenImporter(beangulp.Importer):
|
|
|
277
332
|
|
|
278
333
|
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
|
279
334
|
"""
|
|
280
|
-
Extracts Beancount entries from
|
|
335
|
+
Extracts Beancount entries from GoCardless transactions.
|
|
281
336
|
|
|
282
337
|
Args:
|
|
283
338
|
filepath (str): The path to the YAML configuration file.
|
|
284
|
-
existing (data.Entries):
|
|
339
|
+
existing (data.Entries): Existing Beancount entries (not used in this implementation).
|
|
285
340
|
|
|
286
341
|
Returns:
|
|
287
342
|
data.Entries: A list of Beancount transaction entries.
|
|
288
343
|
"""
|
|
344
|
+
logger.info("Starting extraction from %s", filepath)
|
|
289
345
|
self.load_config(filepath)
|
|
290
346
|
|
|
291
|
-
|
|
292
|
-
|
|
347
|
+
if not self.config:
|
|
348
|
+
raise ValueError("No config loaded from YAML file")
|
|
349
|
+
|
|
350
|
+
entries: data.Entries = []
|
|
351
|
+
accounts = self.config.get("accounts", [])
|
|
352
|
+
total_transactions = 0
|
|
353
|
+
logger.info("Processing %d accounts", len(accounts))
|
|
354
|
+
for account in accounts:
|
|
293
355
|
account_id = account["id"]
|
|
294
356
|
asset_account = account["asset_account"]
|
|
295
357
|
# Use get() with a default empty dict for custom_metadata
|
|
296
358
|
custom_metadata = account.get("metadata", {})
|
|
297
359
|
|
|
298
|
-
|
|
299
|
-
|
|
360
|
+
logger.debug("Fetching transactions for account %s", account_id)
|
|
361
|
+
account_transactions = self.client.get_account_transactions(account_id)
|
|
362
|
+
booked = account_transactions.transactions.get("booked", [])
|
|
363
|
+
pending = account_transactions.transactions.get("pending", [])
|
|
364
|
+
all_transactions = self.get_all_transactions(booked, pending)
|
|
365
|
+
logger.debug(
|
|
366
|
+
"Fetched %d booked and %d pending transactions for account %s",
|
|
367
|
+
len(booked),
|
|
368
|
+
len(pending),
|
|
369
|
+
account_id,
|
|
370
|
+
)
|
|
371
|
+
total_transactions += len(booked) + len(pending)
|
|
300
372
|
|
|
373
|
+
skipped = 0
|
|
301
374
|
for transaction, status in all_transactions:
|
|
302
375
|
entry = self.create_transaction_entry(
|
|
303
376
|
transaction, status, asset_account, custom_metadata
|
|
304
377
|
)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
378
|
+
if entry is not None:
|
|
379
|
+
entries.append(entry)
|
|
380
|
+
else:
|
|
381
|
+
skipped += 1
|
|
382
|
+
if skipped > 0:
|
|
383
|
+
logger.warning(
|
|
384
|
+
"Skipped %d invalid transactions for account %s",
|
|
385
|
+
skipped,
|
|
386
|
+
account_id,
|
|
387
|
+
)
|
|
312
388
|
|
|
313
|
-
|
|
314
|
-
|
|
389
|
+
# Add balance assertion at the end of the account's transactions
|
|
390
|
+
balances = self.client.get_account_balances(account_id)
|
|
391
|
+
logger.debug(
|
|
392
|
+
"Available balances for account %s: %s",
|
|
393
|
+
account_id,
|
|
394
|
+
[
|
|
395
|
+
(b.balance_type, b.balance_amount.amount, b.balance_amount.currency)
|
|
396
|
+
for b in balances.balances
|
|
397
|
+
],
|
|
398
|
+
)
|
|
399
|
+
expected_balance = None
|
|
400
|
+
other_balances = []
|
|
401
|
+
for bal in balances.balances:
|
|
402
|
+
if bal.balance_type == "expected":
|
|
403
|
+
expected_balance = bal
|
|
404
|
+
else:
|
|
405
|
+
other_balances.append(bal)
|
|
406
|
+
|
|
407
|
+
if expected_balance:
|
|
408
|
+
balance_amount = amount.Amount(
|
|
409
|
+
D(str(expected_balance.balance_amount.amount)),
|
|
410
|
+
expected_balance.balance_amount.currency,
|
|
411
|
+
)
|
|
412
|
+
balance_meta = {}
|
|
413
|
+
detail_parts = [
|
|
414
|
+
f"{b.balance_type}: {b.balance_amount.amount} {b.balance_amount.currency}"
|
|
415
|
+
for b in balances.balances
|
|
416
|
+
]
|
|
417
|
+
detail = " / ".join(detail_parts)
|
|
418
|
+
balance_meta["detail"] = detail
|
|
419
|
+
# Include custom metadata from config for consistency with transactions
|
|
420
|
+
balance_meta.update(custom_metadata)
|
|
421
|
+
meta = data.new_metadata("", 0, balance_meta)
|
|
422
|
+
balance_entry = data.Balance(
|
|
423
|
+
meta=meta,
|
|
424
|
+
date=date.today() + timedelta(days=1),
|
|
425
|
+
account=asset_account,
|
|
426
|
+
amount=balance_amount,
|
|
427
|
+
tolerance=None,
|
|
428
|
+
diff_amount=None,
|
|
429
|
+
)
|
|
430
|
+
entries.append(balance_entry)
|
|
431
|
+
logger.debug(
|
|
432
|
+
"Added balance assertion for account %s using expected balance: %s %s",
|
|
433
|
+
account_id,
|
|
434
|
+
balance_amount,
|
|
435
|
+
date.today() + timedelta(days=1),
|
|
436
|
+
)
|
|
315
437
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
438
|
+
# Log other balances if they differ from expected
|
|
439
|
+
expected_amount = D(str(expected_balance.balance_amount.amount))
|
|
440
|
+
for bal in other_balances:
|
|
441
|
+
other_amount = D(str(bal.balance_amount.amount))
|
|
442
|
+
if other_amount != expected_amount:
|
|
443
|
+
logger.info(
|
|
444
|
+
"Account %s has different balance for type %s: %s %s vs expected %s",
|
|
445
|
+
account_id,
|
|
446
|
+
bal.balance_type,
|
|
447
|
+
other_amount,
|
|
448
|
+
bal.balance_amount.currency,
|
|
449
|
+
expected_amount,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
logger.info(
|
|
453
|
+
"Processed %d total transactions across %d accounts, created %d entries",
|
|
454
|
+
total_transactions,
|
|
455
|
+
len(accounts),
|
|
456
|
+
len(entries),
|
|
457
|
+
)
|
|
458
|
+
return entries
|
|
319
459
|
|
|
320
|
-
|
|
321
|
-
int: -1 if entry1 < entry2, 0 if entry1 == entry2, 1 if entry1 > entry2.
|
|
322
|
-
Returns 0 if 'nordref' is not present in both.
|
|
323
|
-
"""
|
|
324
|
-
if (
|
|
325
|
-
"nordref" in entry1.meta
|
|
326
|
-
and "nordref" in entry2.meta
|
|
327
|
-
and entry1.meta["nordref"] == entry2.meta["nordref"]
|
|
328
|
-
):
|
|
329
|
-
return 0 # Consider them equal if nordref matches
|
|
330
|
-
elif (
|
|
331
|
-
"nordref" in entry1.meta
|
|
332
|
-
and "nordref" in entry2.meta
|
|
333
|
-
and entry1.meta["nordref"] < entry2.meta["nordref"]
|
|
334
|
-
):
|
|
335
|
-
return -1
|
|
336
|
-
elif "nordref" in entry1.meta and "nordref" in entry2.meta:
|
|
337
|
-
return 1
|
|
338
|
-
else:
|
|
339
|
-
return 0
|
|
460
|
+
cmp = ReferenceDuplicatesComparator(["nordref"])
|