ofxstatement-nordigen 0.2.2__tar.gz
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.
- ofxstatement_nordigen-0.2.2/MANIFEST.in +1 -0
- ofxstatement_nordigen-0.2.2/PKG-INFO +94 -0
- ofxstatement_nordigen-0.2.2/README.rst +71 -0
- ofxstatement_nordigen-0.2.2/pyproject.toml +49 -0
- ofxstatement_nordigen-0.2.2/setup.cfg +4 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen/__init__.py +0 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen/plugin.py +93 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen/schemas.py +111 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen.egg-info/PKG-INFO +94 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen.egg-info/SOURCES.txt +16 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen.egg-info/dependency_links.txt +1 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen.egg-info/entry_points.txt +2 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen.egg-info/requires.txt +5 -0
- ofxstatement_nordigen-0.2.2/src/ofxstatement_nordigen.egg-info/top_level.txt +1 -0
- ofxstatement_nordigen-0.2.2/tests/test_CAISSEDEPARGNE_ILE_DE_FRANCE_CEPAFRPP751.py +48 -0
- ofxstatement_nordigen-0.2.2/tests/test_banquepopulaire_rives_de_paris.py +67 -0
- ofxstatement_nordigen-0.2.2/tests/test_sample.py +61 -0
- ofxstatement_nordigen-0.2.2/tests/test_schemas.py +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.rst
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ofxstatement-nordigen
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: ofxstatement plugin for Nordigen bank statements
|
|
5
|
+
Author-email: Jimmy Stammers <jimmy.stammers@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/jstammers/ofxstatement-nordigen/
|
|
7
|
+
Keywords: ofx,banking,statement,plugin,ofxstatement
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/x-rst
|
|
18
|
+
Requires-Dist: ofxstatement
|
|
19
|
+
Requires-Dist: pydantic
|
|
20
|
+
Requires-Dist: requests
|
|
21
|
+
Requires-Dist: requests-cache
|
|
22
|
+
Requires-Dist: wheel>=0.45.1
|
|
23
|
+
|
|
24
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
25
|
+
ofxstatement-nordigen
|
|
26
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
27
|
+
|
|
28
|
+
A plugin for `ofxstatement`_ to parse transaction data from GoCardless (previously known as Nordigen).
|
|
29
|
+
|
|
30
|
+
`ofxstatement`_ is a tool to convert proprietary bank statement to OFX format,
|
|
31
|
+
suitable for importing to GnuCash. Plugin for ofxstatement parses a
|
|
32
|
+
particular proprietary bank statement format and produces common data
|
|
33
|
+
structure, that is then formatted into an OFX file.
|
|
34
|
+
|
|
35
|
+
.. _ofxstatement: https://github.com/kedder/ofxstatement
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Installation
|
|
39
|
+
================
|
|
40
|
+
|
|
41
|
+
To install the plugin, you can use `pip`_:
|
|
42
|
+
|
|
43
|
+
.. _pip: https://pypi.org/project/pip/
|
|
44
|
+
|
|
45
|
+
.. code-block:: shell
|
|
46
|
+
|
|
47
|
+
pip install ofxstatement-nordigen
|
|
48
|
+
|
|
49
|
+
or, if you want to install it in editable mode (for development), use:
|
|
50
|
+
|
|
51
|
+
.. code-block:: shell
|
|
52
|
+
|
|
53
|
+
pip install -e ./
|
|
54
|
+
|
|
55
|
+
To verify that the plugin is installed correctly, you can run:
|
|
56
|
+
|
|
57
|
+
.. code-block:: shell
|
|
58
|
+
|
|
59
|
+
ofxstatement --list-plugins
|
|
60
|
+
|
|
61
|
+
This should list the ``nordigen`` plugin among other plugins.
|
|
62
|
+
|
|
63
|
+
Usage
|
|
64
|
+
================
|
|
65
|
+
|
|
66
|
+
To use the plugin, you can run the ``ofxstatement`` command with the ``--plugin`` option:
|
|
67
|
+
|
|
68
|
+
.. code-block:: shell
|
|
69
|
+
|
|
70
|
+
ofxstatement convert -t nordigen <input_file> <output_file>
|
|
71
|
+
|
|
72
|
+
Replace ``<input_file>`` with the path to your input file and ``<output_file>`` with the desired output file name.
|
|
73
|
+
|
|
74
|
+
The input file should be a JSON of transactions from GoCardless that has the schema defined `here`_.
|
|
75
|
+
|
|
76
|
+
.. _here: https://developer.gocardless.com/bank-account-data/transactions
|
|
77
|
+
|
|
78
|
+
The output file will be an OFX file that can be imported into GnuCash or other financial software.
|
|
79
|
+
|
|
80
|
+
Configuration
|
|
81
|
+
================
|
|
82
|
+
|
|
83
|
+
Configuration can be edited using the ``ofxstatement edit-config`` command.
|
|
84
|
+
The following parameters are available:
|
|
85
|
+
|
|
86
|
+
- ``account_id``: The account ID to use for the transactions. This is required.
|
|
87
|
+
- ``currency``: The currency to use for the account. If not specified, the currency will be determined from the transactions.
|
|
88
|
+
|
|
89
|
+
After you are done
|
|
90
|
+
==================
|
|
91
|
+
|
|
92
|
+
After your plugin is ready, feel free to open an issue on `ofxstatement`_
|
|
93
|
+
project to include your plugin in "known plugin list". That would hopefully
|
|
94
|
+
make life of other clients of your bank easier.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
2
|
+
ofxstatement-nordigen
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
A plugin for `ofxstatement`_ to parse transaction data from GoCardless (previously known as Nordigen).
|
|
6
|
+
|
|
7
|
+
`ofxstatement`_ is a tool to convert proprietary bank statement to OFX format,
|
|
8
|
+
suitable for importing to GnuCash. Plugin for ofxstatement parses a
|
|
9
|
+
particular proprietary bank statement format and produces common data
|
|
10
|
+
structure, that is then formatted into an OFX file.
|
|
11
|
+
|
|
12
|
+
.. _ofxstatement: https://github.com/kedder/ofxstatement
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Installation
|
|
16
|
+
================
|
|
17
|
+
|
|
18
|
+
To install the plugin, you can use `pip`_:
|
|
19
|
+
|
|
20
|
+
.. _pip: https://pypi.org/project/pip/
|
|
21
|
+
|
|
22
|
+
.. code-block:: shell
|
|
23
|
+
|
|
24
|
+
pip install ofxstatement-nordigen
|
|
25
|
+
|
|
26
|
+
or, if you want to install it in editable mode (for development), use:
|
|
27
|
+
|
|
28
|
+
.. code-block:: shell
|
|
29
|
+
|
|
30
|
+
pip install -e ./
|
|
31
|
+
|
|
32
|
+
To verify that the plugin is installed correctly, you can run:
|
|
33
|
+
|
|
34
|
+
.. code-block:: shell
|
|
35
|
+
|
|
36
|
+
ofxstatement --list-plugins
|
|
37
|
+
|
|
38
|
+
This should list the ``nordigen`` plugin among other plugins.
|
|
39
|
+
|
|
40
|
+
Usage
|
|
41
|
+
================
|
|
42
|
+
|
|
43
|
+
To use the plugin, you can run the ``ofxstatement`` command with the ``--plugin`` option:
|
|
44
|
+
|
|
45
|
+
.. code-block:: shell
|
|
46
|
+
|
|
47
|
+
ofxstatement convert -t nordigen <input_file> <output_file>
|
|
48
|
+
|
|
49
|
+
Replace ``<input_file>`` with the path to your input file and ``<output_file>`` with the desired output file name.
|
|
50
|
+
|
|
51
|
+
The input file should be a JSON of transactions from GoCardless that has the schema defined `here`_.
|
|
52
|
+
|
|
53
|
+
.. _here: https://developer.gocardless.com/bank-account-data/transactions
|
|
54
|
+
|
|
55
|
+
The output file will be an OFX file that can be imported into GnuCash or other financial software.
|
|
56
|
+
|
|
57
|
+
Configuration
|
|
58
|
+
================
|
|
59
|
+
|
|
60
|
+
Configuration can be edited using the ``ofxstatement edit-config`` command.
|
|
61
|
+
The following parameters are available:
|
|
62
|
+
|
|
63
|
+
- ``account_id``: The account ID to use for the transactions. This is required.
|
|
64
|
+
- ``currency``: The currency to use for the account. If not specified, the currency will be determined from the transactions.
|
|
65
|
+
|
|
66
|
+
After you are done
|
|
67
|
+
==================
|
|
68
|
+
|
|
69
|
+
After your plugin is ready, feel free to open an issue on `ofxstatement`_
|
|
70
|
+
project to include your plugin in "known plugin list". That would hopefully
|
|
71
|
+
make life of other clients of your bank easier.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ofxstatement-nordigen"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Jimmy Stammers", email="jimmy.stammers@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "ofxstatement plugin for Nordigen bank statements"
|
|
12
|
+
readme = "README.rst"
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Natural Language :: English",
|
|
18
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
19
|
+
"Topic :: Utilities",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
23
|
+
]
|
|
24
|
+
keywords = ["ofx", "banking", "statement", "plugin", "ofxstatement"]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"ofxstatement",
|
|
27
|
+
"pydantic",
|
|
28
|
+
"requests",
|
|
29
|
+
"requests-cache",
|
|
30
|
+
"wheel>=0.45.1",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/jstammers/ofxstatement-nordigen/"
|
|
35
|
+
|
|
36
|
+
[project.entry-points."ofxstatement"]
|
|
37
|
+
nordigen = "ofxstatement_nordigen.plugin:NordigenPlugin"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"black>=25.11.0",
|
|
42
|
+
"build>=1.4.0",
|
|
43
|
+
"exceptiongroup>=1.3.1",
|
|
44
|
+
"mypy>=1.19.1",
|
|
45
|
+
"pytest>=8.4.2",
|
|
46
|
+
"pytest-cov>=7.0.0",
|
|
47
|
+
"ruff>=0.14.11",
|
|
48
|
+
"tomli>=2.4.0",
|
|
49
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Iterable, Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from ofxstatement.plugin import Plugin
|
|
5
|
+
from ofxstatement.parser import StatementParser
|
|
6
|
+
from ofxstatement.statement import Statement, StatementLine
|
|
7
|
+
|
|
8
|
+
from ofxstatement_nordigen.schemas import NordigenTransactionModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NordigenPlugin(Plugin):
|
|
12
|
+
"""Retrieves Nordigen transactions and converts them to OFX format."""
|
|
13
|
+
|
|
14
|
+
def get_parser(self, filename: str) -> "NordigenParser":
|
|
15
|
+
default_ccy = self.settings.get("currency")
|
|
16
|
+
account_id = self.settings.get("account")
|
|
17
|
+
return NordigenParser(filename, default_ccy, account_id)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NordigenParser(StatementParser[str]):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
filename: str,
|
|
24
|
+
currency: Optional[str] = None,
|
|
25
|
+
account_id: Optional[str] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
if not filename.endswith(".json"):
|
|
29
|
+
raise ValueError("Only JSON files are supported")
|
|
30
|
+
self.filename = filename
|
|
31
|
+
self.currency = currency
|
|
32
|
+
self.account_id = account_id
|
|
33
|
+
|
|
34
|
+
def parse(self) -> Statement:
|
|
35
|
+
"""Main entry point for parsers
|
|
36
|
+
|
|
37
|
+
super() implementation will call to split_records and parse_record to
|
|
38
|
+
process the file.
|
|
39
|
+
"""
|
|
40
|
+
with open(self.filename, "r"):
|
|
41
|
+
statement = super().parse()
|
|
42
|
+
dates = [
|
|
43
|
+
line.date for line in statement.lines if isinstance(line.date, datetime)
|
|
44
|
+
]
|
|
45
|
+
if len(dates) > 0:
|
|
46
|
+
statement.start_date = min(dates)
|
|
47
|
+
statement.end_date = max(dates)
|
|
48
|
+
statement.account_id = self.account_id
|
|
49
|
+
statement.currency = self.currency or statement.currency
|
|
50
|
+
return statement
|
|
51
|
+
|
|
52
|
+
def split_records(self) -> Iterable[str]:
|
|
53
|
+
"""Return iterable object consisting of a line per transaction"""
|
|
54
|
+
data = json.load(open(self.filename, "r"))
|
|
55
|
+
transactions = data.get("transactions", {})
|
|
56
|
+
booked_transactions = transactions.get("booked", [])
|
|
57
|
+
return [json.dumps(transaction) for transaction in booked_transactions]
|
|
58
|
+
|
|
59
|
+
def parse_record(self, line: str) -> StatementLine:
|
|
60
|
+
"""Parse given transaction line and return StatementLine object"""
|
|
61
|
+
|
|
62
|
+
# TODO: Infer transaction type from transaction data
|
|
63
|
+
statement = StatementLine()
|
|
64
|
+
transaction = json.loads(line)
|
|
65
|
+
transaction_data = NordigenTransactionModel(**transaction)
|
|
66
|
+
statement.id = (
|
|
67
|
+
transaction_data.transactionId or transaction_data.internalTransactionId
|
|
68
|
+
)
|
|
69
|
+
# Use bookingDateTime if available, otherwise convert bookingDate to datetime
|
|
70
|
+
if transaction_data.bookingDateTime:
|
|
71
|
+
statement.date = transaction_data.bookingDateTime
|
|
72
|
+
elif transaction_data.bookingDate:
|
|
73
|
+
statement.date = datetime.combine(
|
|
74
|
+
transaction_data.bookingDate, datetime.min.time()
|
|
75
|
+
)
|
|
76
|
+
statement.amount = transaction_data.transactionAmount.amount
|
|
77
|
+
# Handle different types of remittance information
|
|
78
|
+
if transaction_data.remittanceInformationUnstructured:
|
|
79
|
+
statement.memo = transaction_data.remittanceInformationUnstructured
|
|
80
|
+
elif transaction_data.remittanceInformationUnstructuredArray:
|
|
81
|
+
statement.memo = " ".join(
|
|
82
|
+
transaction_data.remittanceInformationUnstructuredArray
|
|
83
|
+
)
|
|
84
|
+
statement.payee = transaction_data.creditorName or transaction_data.debtorName
|
|
85
|
+
statement.date_user = transaction_data.valueDateTime
|
|
86
|
+
statement.check_no = transaction_data.checkId
|
|
87
|
+
statement.refnum = transaction_data.internalTransactionId
|
|
88
|
+
statement.currency = transaction_data.transactionAmount.currency
|
|
89
|
+
if transaction_data.currencyExchange and hasattr(
|
|
90
|
+
transaction_data.currencyExchange, "sourceCurrency"
|
|
91
|
+
):
|
|
92
|
+
statement.orig_currency = transaction_data.currencyExchange.sourceCurrency
|
|
93
|
+
return statement
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from ofxstatement.statement import Currency
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, field_validator, ConfigDict
|
|
9
|
+
from pydantic.alias_generators import to_snake
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Amount(BaseModel):
|
|
13
|
+
amount: Decimal
|
|
14
|
+
currency: Currency
|
|
15
|
+
|
|
16
|
+
@field_validator("currency", mode="before")
|
|
17
|
+
def validate_currency(cls, value):
|
|
18
|
+
if isinstance(value, str):
|
|
19
|
+
return Currency(value)
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Account(BaseModel):
|
|
26
|
+
bban: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReportExchangeRate(BaseModel):
|
|
30
|
+
sourceCurrency: Optional[Currency] = None
|
|
31
|
+
targetCurrency: Optional[Currency] = None
|
|
32
|
+
unitCurrency: Optional[Currency] = None
|
|
33
|
+
exchangeRate: Optional[float] = None
|
|
34
|
+
quotationDate: Optional[datetime.date] = None
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
37
|
+
|
|
38
|
+
@field_validator("sourceCurrency", "targetCurrency", "unitCurrency", mode="before")
|
|
39
|
+
def validate_currency(cls, value):
|
|
40
|
+
if isinstance(value, str):
|
|
41
|
+
return Currency(value)
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CurrencyExchangeAmex(BaseModel):
|
|
46
|
+
"""
|
|
47
|
+
Context: Issue #11
|
|
48
|
+
|
|
49
|
+
Temporary Fix:
|
|
50
|
+
This addresses the lack of normalization of American Express data to the GoCardless specification.
|
|
51
|
+
|
|
52
|
+
Note: This class should be removed once the normalization process is completed.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
sourceCurrency: Optional[Currency] = None
|
|
56
|
+
targetCurrency: Optional[Currency] = None
|
|
57
|
+
unitCurrency: Optional[Currency] = None
|
|
58
|
+
exchangeRate: Optional[float] = None
|
|
59
|
+
instructedAmount: Optional[Amount] = None
|
|
60
|
+
|
|
61
|
+
@field_validator("sourceCurrency", "targetCurrency", "unitCurrency", mode="before")
|
|
62
|
+
def validate_currency(cls, value):
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
return Currency(value)
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class NordigenTransactionModel(BaseModel):
|
|
71
|
+
"""
|
|
72
|
+
Nordigen data transaction model.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
balanceAfterTransaction: Optional[float] = None
|
|
76
|
+
bankTransactionCode: Optional[str] = None
|
|
77
|
+
bookingDate: Optional[datetime.date] = None
|
|
78
|
+
bookingDateTime: Optional[datetime.datetime] = None
|
|
79
|
+
checkId: Optional[str] = None
|
|
80
|
+
creditorAccount: Optional[Account] = None
|
|
81
|
+
creditorAgent: Optional[str] = None
|
|
82
|
+
creditorId: Optional[str] = None
|
|
83
|
+
creditorName: Optional[str] = None
|
|
84
|
+
currencyExchange: Optional[List[ReportExchangeRate] | CurrencyExchangeAmex] = None
|
|
85
|
+
debtorAccount: Optional[Account] = None
|
|
86
|
+
debtorAgent: Optional[str] = None
|
|
87
|
+
debtorName: Optional[str] = None
|
|
88
|
+
endToEndId: Optional[str] = None
|
|
89
|
+
entryReference: Optional[str] = None
|
|
90
|
+
internalTransactionId: Optional[str] = None
|
|
91
|
+
mandateId: Optional[str] = None
|
|
92
|
+
merchantCategoryCode: Optional[str] = None
|
|
93
|
+
proprietaryBankTransactionCode: Optional[str] = None
|
|
94
|
+
purposeCode: Optional[str] = None
|
|
95
|
+
remittanceInformationStructured: Optional[str] = None
|
|
96
|
+
remittanceInformationStructuredArray: Optional[List[str]] = None
|
|
97
|
+
remittanceInformationUnstructured: Optional[str] = None
|
|
98
|
+
remittanceInformationUnstructuredArray: Optional[List[str]] = None
|
|
99
|
+
transactionAmount: Amount
|
|
100
|
+
transactionId: Optional[str] = None
|
|
101
|
+
ultimateCreditor: Optional[str] = None
|
|
102
|
+
ultimateDebtor: Optional[str] = None
|
|
103
|
+
valueDate: Optional[datetime.date] = None
|
|
104
|
+
valueDateTime: Optional[datetime.datetime] = None
|
|
105
|
+
|
|
106
|
+
# class Config:
|
|
107
|
+
# alias_generator = to_snake
|
|
108
|
+
|
|
109
|
+
model_config = ConfigDict(
|
|
110
|
+
arbitrary_types_allowed=True, alias_generator=to_snake, populate_by_name=True
|
|
111
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ofxstatement-nordigen
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: ofxstatement plugin for Nordigen bank statements
|
|
5
|
+
Author-email: Jimmy Stammers <jimmy.stammers@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/jstammers/ofxstatement-nordigen/
|
|
7
|
+
Keywords: ofx,banking,statement,plugin,ofxstatement
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/x-rst
|
|
18
|
+
Requires-Dist: ofxstatement
|
|
19
|
+
Requires-Dist: pydantic
|
|
20
|
+
Requires-Dist: requests
|
|
21
|
+
Requires-Dist: requests-cache
|
|
22
|
+
Requires-Dist: wheel>=0.45.1
|
|
23
|
+
|
|
24
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
25
|
+
ofxstatement-nordigen
|
|
26
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
27
|
+
|
|
28
|
+
A plugin for `ofxstatement`_ to parse transaction data from GoCardless (previously known as Nordigen).
|
|
29
|
+
|
|
30
|
+
`ofxstatement`_ is a tool to convert proprietary bank statement to OFX format,
|
|
31
|
+
suitable for importing to GnuCash. Plugin for ofxstatement parses a
|
|
32
|
+
particular proprietary bank statement format and produces common data
|
|
33
|
+
structure, that is then formatted into an OFX file.
|
|
34
|
+
|
|
35
|
+
.. _ofxstatement: https://github.com/kedder/ofxstatement
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Installation
|
|
39
|
+
================
|
|
40
|
+
|
|
41
|
+
To install the plugin, you can use `pip`_:
|
|
42
|
+
|
|
43
|
+
.. _pip: https://pypi.org/project/pip/
|
|
44
|
+
|
|
45
|
+
.. code-block:: shell
|
|
46
|
+
|
|
47
|
+
pip install ofxstatement-nordigen
|
|
48
|
+
|
|
49
|
+
or, if you want to install it in editable mode (for development), use:
|
|
50
|
+
|
|
51
|
+
.. code-block:: shell
|
|
52
|
+
|
|
53
|
+
pip install -e ./
|
|
54
|
+
|
|
55
|
+
To verify that the plugin is installed correctly, you can run:
|
|
56
|
+
|
|
57
|
+
.. code-block:: shell
|
|
58
|
+
|
|
59
|
+
ofxstatement --list-plugins
|
|
60
|
+
|
|
61
|
+
This should list the ``nordigen`` plugin among other plugins.
|
|
62
|
+
|
|
63
|
+
Usage
|
|
64
|
+
================
|
|
65
|
+
|
|
66
|
+
To use the plugin, you can run the ``ofxstatement`` command with the ``--plugin`` option:
|
|
67
|
+
|
|
68
|
+
.. code-block:: shell
|
|
69
|
+
|
|
70
|
+
ofxstatement convert -t nordigen <input_file> <output_file>
|
|
71
|
+
|
|
72
|
+
Replace ``<input_file>`` with the path to your input file and ``<output_file>`` with the desired output file name.
|
|
73
|
+
|
|
74
|
+
The input file should be a JSON of transactions from GoCardless that has the schema defined `here`_.
|
|
75
|
+
|
|
76
|
+
.. _here: https://developer.gocardless.com/bank-account-data/transactions
|
|
77
|
+
|
|
78
|
+
The output file will be an OFX file that can be imported into GnuCash or other financial software.
|
|
79
|
+
|
|
80
|
+
Configuration
|
|
81
|
+
================
|
|
82
|
+
|
|
83
|
+
Configuration can be edited using the ``ofxstatement edit-config`` command.
|
|
84
|
+
The following parameters are available:
|
|
85
|
+
|
|
86
|
+
- ``account_id``: The account ID to use for the transactions. This is required.
|
|
87
|
+
- ``currency``: The currency to use for the account. If not specified, the currency will be determined from the transactions.
|
|
88
|
+
|
|
89
|
+
After you are done
|
|
90
|
+
==================
|
|
91
|
+
|
|
92
|
+
After your plugin is ready, feel free to open an issue on `ofxstatement`_
|
|
93
|
+
project to include your plugin in "known plugin list". That would hopefully
|
|
94
|
+
make life of other clients of your bank easier.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.rst
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ofxstatement_nordigen/__init__.py
|
|
5
|
+
src/ofxstatement_nordigen/plugin.py
|
|
6
|
+
src/ofxstatement_nordigen/schemas.py
|
|
7
|
+
src/ofxstatement_nordigen.egg-info/PKG-INFO
|
|
8
|
+
src/ofxstatement_nordigen.egg-info/SOURCES.txt
|
|
9
|
+
src/ofxstatement_nordigen.egg-info/dependency_links.txt
|
|
10
|
+
src/ofxstatement_nordigen.egg-info/entry_points.txt
|
|
11
|
+
src/ofxstatement_nordigen.egg-info/requires.txt
|
|
12
|
+
src/ofxstatement_nordigen.egg-info/top_level.txt
|
|
13
|
+
tests/test_CAISSEDEPARGNE_ILE_DE_FRANCE_CEPAFRPP751.py
|
|
14
|
+
tests/test_banquepopulaire_rives_de_paris.py
|
|
15
|
+
tests/test_sample.py
|
|
16
|
+
tests/test_schemas.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ofxstatement_nordigen
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import pytest
|
|
4
|
+
from datetime import datetime, date
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
from ofxstatement.ui import UI
|
|
8
|
+
from ofxstatement import ofx
|
|
9
|
+
from ofxstatement.statement import StatementLine, Currency
|
|
10
|
+
|
|
11
|
+
from ofxstatement_nordigen.plugin import NordigenPlugin, NordigenParser
|
|
12
|
+
from ofxstatement_nordigen.schemas import NordigenTransactionModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("filename", ["CAISSEDEPARGNE_ILE_DE_FRANCE_CEPAFRPP751.json"])
|
|
16
|
+
def test_CAISSEDEPARGNE_ILE_DE_FRANCE(filename: str) -> None:
|
|
17
|
+
"""Test parsing the CAISSEDEPARGNE_ILE_DE_FRANCE_CEPAFRPP751.json file."""
|
|
18
|
+
here = os.path.dirname(__file__)
|
|
19
|
+
sample_filename = os.path.join(here, "data", filename)
|
|
20
|
+
expected_filename = sample_filename.replace(".json", ".ofx")
|
|
21
|
+
|
|
22
|
+
parser = NordigenParser(sample_filename)
|
|
23
|
+
statement = parser.parse()
|
|
24
|
+
|
|
25
|
+
# Verify the statement properties
|
|
26
|
+
assert len(statement.lines) == 1
|
|
27
|
+
|
|
28
|
+
# Verify the transaction details
|
|
29
|
+
transaction = statement.lines[0]
|
|
30
|
+
assert transaction.id == "6666666"
|
|
31
|
+
|
|
32
|
+
assert (
|
|
33
|
+
transaction.date is not None
|
|
34
|
+
) # Fix for mypy: Check that date is not None before accessing date() method
|
|
35
|
+
assert transaction.date.date() == date(2025, 5, 13)
|
|
36
|
+
|
|
37
|
+
assert transaction.amount == Decimal("-1")
|
|
38
|
+
|
|
39
|
+
assert (
|
|
40
|
+
transaction.currency is not None
|
|
41
|
+
) # Fix for mypy: Check that currency is not None before accessing symbol attribute
|
|
42
|
+
assert transaction.currency.symbol == "EUR"
|
|
43
|
+
|
|
44
|
+
# Check if the memo contains the combined information from remittanceInformationUnstructuredArray
|
|
45
|
+
assert (
|
|
46
|
+
transaction.memo is not None
|
|
47
|
+
) # Fix for mypy: Check that memo is not None before using 'in' operator
|
|
48
|
+
assert "PRLV assurance" in transaction.memo
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import pytest
|
|
6
|
+
from datetime import datetime, date
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from ofxstatement.ui import UI
|
|
10
|
+
from ofxstatement import ofx
|
|
11
|
+
from ofxstatement.statement import StatementLine, Currency
|
|
12
|
+
|
|
13
|
+
from ofxstatement_nordigen.plugin import NordigenPlugin, NordigenParser
|
|
14
|
+
from ofxstatement_nordigen.schemas import NordigenTransactionModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
"filename", ["BANQUEPOPULAIRE_RIVES_DE_PARIS_CCBPFRPPMTG.json"]
|
|
19
|
+
)
|
|
20
|
+
def test_banquepopulaire_rives_de_paris(filename: str) -> None:
|
|
21
|
+
"""Test parsing the BANQUEPOPULAIRE_RIVES_DE_PARIS_CCBPFRPPMTG.json file."""
|
|
22
|
+
here = os.path.dirname(__file__)
|
|
23
|
+
sample_filename = os.path.join(here, "data", filename)
|
|
24
|
+
expected_filename = sample_filename.replace(".json", ".ofx")
|
|
25
|
+
|
|
26
|
+
parser = NordigenParser(sample_filename)
|
|
27
|
+
statement = parser.parse()
|
|
28
|
+
|
|
29
|
+
# Verify the statement properties
|
|
30
|
+
assert len(statement.lines) == 1
|
|
31
|
+
|
|
32
|
+
# Verify the transaction details
|
|
33
|
+
transaction = statement.lines[0]
|
|
34
|
+
assert transaction.id == "202500400015"
|
|
35
|
+
|
|
36
|
+
# Fix for mypy: Check that date is not None before accessing date() method
|
|
37
|
+
assert transaction.date is not None
|
|
38
|
+
assert transaction.date.date() == date(2025, 5, 2)
|
|
39
|
+
|
|
40
|
+
assert transaction.amount == Decimal("-8.43")
|
|
41
|
+
|
|
42
|
+
# Fix for mypy: Check that currency is not None before accessing symbol attribute
|
|
43
|
+
assert transaction.currency is not None
|
|
44
|
+
assert transaction.currency.symbol == "EUR"
|
|
45
|
+
|
|
46
|
+
assert transaction.refnum == "YYYYYYYYYYYYYY"
|
|
47
|
+
|
|
48
|
+
# Check if the memo contains the combined information from remittanceInformationUnstructuredArray
|
|
49
|
+
# Fix for mypy: Check that memo is not None before using 'in' operator
|
|
50
|
+
assert transaction.memo is not None
|
|
51
|
+
assert "CB****2222" in transaction.memo
|
|
52
|
+
assert "Food Restaurant" in transaction.memo
|
|
53
|
+
|
|
54
|
+
# Compare with expected OFX output
|
|
55
|
+
expected = open(expected_filename, "r").read()
|
|
56
|
+
writer = ofx.OfxWriter(statement)
|
|
57
|
+
result = writer.toxml(pretty=True)
|
|
58
|
+
|
|
59
|
+
# Get everything between the <STMTTRN> and </STMTTRN> tags ignoring \r characters
|
|
60
|
+
result = result[
|
|
61
|
+
result.index("<STMTTRN>") : result.index("</STMTTRN>") + len("</STMTTRN>")
|
|
62
|
+
].replace("\r", "")
|
|
63
|
+
expected = expected[
|
|
64
|
+
expected.index("<STMTTRN>") : expected.index("</STMTTRN>") + len("</STMTTRN>")
|
|
65
|
+
].replace("\r", "")
|
|
66
|
+
|
|
67
|
+
assert result == expected
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from ofxstatement.ui import UI
|
|
4
|
+
|
|
5
|
+
from ofxstatement_nordigen.plugin import NordigenPlugin, NordigenParser
|
|
6
|
+
from ofxstatement import ofx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_config_sets_account_and_currency() -> None:
|
|
10
|
+
"""Test that account and currency are set correctly when provided in config."""
|
|
11
|
+
# Create a plugin with specific account and currency settings
|
|
12
|
+
settings = {"account": "test-account-123", "currency": "EUR"}
|
|
13
|
+
plugin = NordigenPlugin(UI(), settings)
|
|
14
|
+
|
|
15
|
+
here = os.path.dirname(__file__)
|
|
16
|
+
sample_filename = os.path.join(here, "sample-statement.json")
|
|
17
|
+
parser = plugin.get_parser(sample_filename)
|
|
18
|
+
|
|
19
|
+
assert parser.account_id == settings["account"]
|
|
20
|
+
assert parser.currency == settings["currency"]
|
|
21
|
+
|
|
22
|
+
statement = parser.parse()
|
|
23
|
+
assert statement.account_id == settings["account"]
|
|
24
|
+
assert statement.currency == settings["currency"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_sample() -> None:
|
|
28
|
+
plugin = NordigenPlugin(UI(), {})
|
|
29
|
+
here = os.path.dirname(__file__)
|
|
30
|
+
for filename in os.listdir(here):
|
|
31
|
+
if filename.endswith(".json"):
|
|
32
|
+
sample_filename = os.path.join(here, filename)
|
|
33
|
+
parser = plugin.get_parser(sample_filename)
|
|
34
|
+
statement = parser.parse()
|
|
35
|
+
assert len(statement.lines) > 0
|
|
36
|
+
assert statement.start_date is not None
|
|
37
|
+
assert statement.end_date is not None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("filename", ["test_date.json", "test_snake_case.json"])
|
|
41
|
+
def test_parse_record(filename: str) -> None:
|
|
42
|
+
here = os.path.dirname(__file__)
|
|
43
|
+
sample_filename = os.path.join(here, "data", filename)
|
|
44
|
+
expected_filename = sample_filename.replace(".json", ".ofx")
|
|
45
|
+
|
|
46
|
+
parser = NordigenParser(sample_filename)
|
|
47
|
+
statement = parser.parse()
|
|
48
|
+
|
|
49
|
+
expected = open(expected_filename, "r").read()
|
|
50
|
+
writer = ofx.OfxWriter(statement)
|
|
51
|
+
result = writer.toxml(pretty=True)
|
|
52
|
+
|
|
53
|
+
# Get everything between the <STMTTRN> and </STMTTRN> tags ignoring \r characters
|
|
54
|
+
result = result[
|
|
55
|
+
result.index("<STMTTRN>") : result.index("</STMTTRN>") + len("</STMTTRN>")
|
|
56
|
+
].replace("\r", "")
|
|
57
|
+
expected = expected[
|
|
58
|
+
expected.index("<STMTTRN>") : expected.index("</STMTTRN>") + len("</STMTTRN>")
|
|
59
|
+
].replace("\r", "")
|
|
60
|
+
|
|
61
|
+
assert result == expected
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ofxstatement_nordigen.schemas import NordigenTransactionModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize(
|
|
7
|
+
"data",
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
"transactionId": "123456789",
|
|
11
|
+
"transactionAmount": {"amount": 100.0, "currency": "EUR"},
|
|
12
|
+
"valueDate": "2023-10-01",
|
|
13
|
+
"valueDateTime": "2023-10-01T12:00:00Z",
|
|
14
|
+
"remittanceInformationStructured": "Payment for invoice #12345",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"transactionId": "987654321",
|
|
18
|
+
"entryReference": "REF123456",
|
|
19
|
+
"bookingDate": "2025-03-31",
|
|
20
|
+
"bookingDateTime": "2025-03-31T00:00:00+00:00",
|
|
21
|
+
"transactionAmount": {"amount": "-1521.00", "currency": "GBP"},
|
|
22
|
+
"remittanceInformationUnstructured": "Payment for invoice #67890",
|
|
23
|
+
"additionalInformation": "Payment received",
|
|
24
|
+
"proprietaryBankTransactionCode": "BP",
|
|
25
|
+
"internalTransactionId": "INT123456",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"transactionId": "anonymized_transaction_id",
|
|
29
|
+
"entryReference": "anonymized_entry_reference",
|
|
30
|
+
"bookingDate": "2025-04-05",
|
|
31
|
+
"valueDate": "2025-04-05",
|
|
32
|
+
"bookingDateTime": "2025-04-05T00:00:00+00:00",
|
|
33
|
+
"valueDateTime": "2025-04-05T00:00:00+00:00",
|
|
34
|
+
"transactionAmount": {"amount": "0.00", "currency": "XXX"},
|
|
35
|
+
"currencyExchange": [{"sourceCurrency": "XXX", "exchangeRate": "0.0"}],
|
|
36
|
+
"remittanceInformationUnstructured": "anonymized_remittance_information",
|
|
37
|
+
"additionalInformation": "anonymized_additional_information",
|
|
38
|
+
"additionalDataStructured": {
|
|
39
|
+
"CardSchemeName": "anonymized_card_scheme",
|
|
40
|
+
"Name": "anonymized_name",
|
|
41
|
+
"Identification": "anonymized_identification",
|
|
42
|
+
},
|
|
43
|
+
"internalTransactionId": "anonymized_internal_transaction_id",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"transactionId": "anonymized_transaction_id",
|
|
47
|
+
"bookingDate": "2025-04-05",
|
|
48
|
+
"bookingDateTime": "2025-05-05T07:20:42.19Z",
|
|
49
|
+
"transactionAmount": {"amount": "-100.0000", "currency": "GBP"},
|
|
50
|
+
"currencyExchange": [
|
|
51
|
+
{
|
|
52
|
+
"quotationDate": "2025-04-05",
|
|
53
|
+
"sourceCurrency": "AUD",
|
|
54
|
+
"exchangeRate": "2.04501",
|
|
55
|
+
"unitCurrency": "GBP",
|
|
56
|
+
"targetCurrency": "GBP",
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"remittanceInformationUnstructured": "anonymized_remittance_information",
|
|
60
|
+
"proprietaryBankTransactionCode": "anonymized_code",
|
|
61
|
+
"internalTransactionId": "anonymized_internal_transaction_id",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
def test_go_cardless_transaction_model(data):
|
|
66
|
+
validated = NordigenTransactionModel(**data)
|
|
67
|
+
print(validated)
|
|
68
|
+
assert validated is not None
|