Compare commits
2 Commits
dd83e36d75
...
0f3d48e0b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f3d48e0b9 | ||
|
|
e908bad8a1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Exclude virtual environment directories
|
||||
.venv/
|
||||
venv/
|
||||
Extracted/
|
||||
data/
|
||||
|
||||
# Exclude other common files
|
||||
__pycache__/
|
||||
|
||||
15
config.py
15
config.py
@@ -1,15 +0,0 @@
|
||||
# config.py
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Voeg de map van dit bestand toe aan de Python-zoekpaden.
|
||||
# Dit zorgt ervoor dat je importer gevonden kan worden.
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
# De importregel hieronder zal nu correct werken
|
||||
from postbank_csv_importer import MyCSVImporter
|
||||
|
||||
CONFIG = [
|
||||
MyCSVImporter()
|
||||
]
|
||||
14
import.py
Normal file
14
import.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from beangulp import register_importers
|
||||
from importers.csvbank import Importer as CSVBankImporter
|
||||
|
||||
def main():
|
||||
ledger_file = 'ledger.beancount'
|
||||
|
||||
register_importers([
|
||||
CSVBankImporter('Assets:Bank:Checking', 'EUR'),
|
||||
])
|
||||
from beangulp.testing import main as beangulp_main
|
||||
beangulp_main(ledger_file)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
35
importers/csvbank.py
Normal file
35
importers/csvbank.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
from beangulp import mimetypes
|
||||
from beangulp.importers import csvbase
|
||||
from beangulp.testing import main as testing_main
|
||||
|
||||
class Importer(csvbase.Importer):
|
||||
# TODO Deutscher CSV header überprüfen: Posting Date ; Description ; Note ; Amount, Balance
|
||||
date = csvbase.Date('Posting Date', '%Y-%m-%d') # ! date-Format anpassen
|
||||
narration = csvbase.Columns('Description', 'Note', sep='; ')
|
||||
amount = csvbase.Amount('Amount')
|
||||
balance = csvbase.Amount('Balance')
|
||||
|
||||
""" Date,Posting Date,Description,Note,Amount,Balance
|
||||
2025-08-01,2025-08-01,Grocery Store,, -54.20,1245.80
|
||||
2025-08-03,2025-08-03,Salary,, 2500.00,3745.80
|
||||
"""
|
||||
|
||||
def identify(self, filepath):
|
||||
mimetype, _ = mimetypes.guess_type(filepath)
|
||||
if mimetype != 'text/csv':
|
||||
return false
|
||||
|
||||
with open(filepath, encoding='utf-8') as f:
|
||||
header = f.readline()
|
||||
# ! TODO
|
||||
return header.startswith('Date, Posting Date, Description, Amount')
|
||||
|
||||
def filename(self, filepath):
|
||||
name = os.path.basename(filepath)
|
||||
return f'csvbank.{name}'
|
||||
|
||||
# ? Testing
|
||||
__name__ '__main__':
|
||||
# Test-Runner und CLI
|
||||
testing_main(Importer('Assets:Bank:Checking', 'EUR'))
|
||||
@@ -1,164 +0,0 @@
|
||||
from beancount.ingest import importer
|
||||
from beancount.core import data
|
||||
from beancount.core.number import D
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
|
||||
class MyCSVImporter(importer.ImporterProtocol):
|
||||
"""A Beancount importer for CSV files"""
|
||||
|
||||
def identify(self, file):
|
||||
return file.name.endswith('.csv')
|
||||
|
||||
def file_account(self, file):
|
||||
return "Assets:Bank:PostbankGiro"
|
||||
|
||||
def file_date(self, file):
|
||||
"""Returns the unique date of the file, if any."""
|
||||
# Optioneel: als je bestandsnaam een datum bevat
|
||||
return None
|
||||
|
||||
def file_name(self, file):
|
||||
"""Returns the unique name of the file, if any."""
|
||||
# Optioneel: als je een unieke naam wilt baseren op het bestand
|
||||
return None
|
||||
|
||||
def extract(self, file, existing_entries=None):
|
||||
"""Extracts entries from a files."""
|
||||
entries = []
|
||||
|
||||
# file.contents
|
||||
with open(file.name, encoding='utf-8') as f:
|
||||
csv_reader = csv.reader(f, delimiter=';')
|
||||
# Gebruik enumerate() om een teller (index) te krijgen
|
||||
for index, row in enumerate(csv_reader):
|
||||
if len(row) < 18 or row[17] != "EUR":
|
||||
continue
|
||||
|
||||
# De index begint bij 0, dus de 12e kolom is index 11.
|
||||
# Boekingsdatum = index 0
|
||||
# Ontvanger = index 3
|
||||
# Omschrijving = index 4
|
||||
# Bedrag = index 11
|
||||
# Valuta = index 17
|
||||
datum_str = row[0]
|
||||
ontvanger = row[3]
|
||||
omschrijving_str = row[4]
|
||||
bedrag_str = row[11]
|
||||
valuta = row[17]
|
||||
|
||||
dag, maand, jaar = datum_str.split('.')
|
||||
transactie_datum = datetime.date(int(jaar), int(maand), int(dag))
|
||||
|
||||
payee = ontvanger
|
||||
narration = omschrijving_str
|
||||
# Verwijder eerst de duizendtallen-scheidingstekens (de punten)
|
||||
bedrag_str = bedrag_str.replace('.', '')
|
||||
# Vervang daarna de komma door een punt
|
||||
bedrag_str = bedrag_str.replace(',', '.')
|
||||
bedrag = D(bedrag_str)
|
||||
currency = valuta
|
||||
|
||||
meta = data.new_metadata(file.name, index)
|
||||
|
||||
# Bepaal of het een inkomst of uitgave is op basis van het voorteken van het bedrag.
|
||||
if bedrag < D(0):
|
||||
# Uitgave: bedrag is negatief.
|
||||
# Geld gaat van de bankrekening naar een uitgavenrekening.
|
||||
tegenrekening = self._map_payee_to_account(payee + " " + omschrijving_str)
|
||||
|
||||
#FORMAT: data.Posting(account, units, cost=None, price=None, flag=None, meta=None)
|
||||
postings = [
|
||||
#data.Posting(self.file_account(file), bedrag, currency, None, None, None),
|
||||
data.Posting(self.file_account(file), data.Amount(bedrag, currency), None, None, None, None),
|
||||
#data.Posting(tegenrekening, -bedrag, currency, None, None, None),
|
||||
data.Posting(tegenrekening, data.Amount(bedrag, currency), None, None, None, None),
|
||||
]
|
||||
else:
|
||||
# Inkomsten: bedrag is positief.
|
||||
# Geld gaat van een inkomstenrekening naar de bankrekening.
|
||||
tegenrekening = self._map_payee_to_account(payee + " " + omschrijving_str)
|
||||
|
||||
#FORMAT: data.Posting(account, units, cost=None, price=None, flag=None, meta=None)
|
||||
postings = [
|
||||
#data.Posting(tegenrekening, -bedrag, currency, None, None, None),
|
||||
data.Posting(tegenrekening, data.Amount(-bedrag, currency), None, None, None, None),
|
||||
#data.Posting(self.file_account(file), bedrag, currency, None, None, None),
|
||||
data.Posting(self.file_account(file), data.Amount(bedrag, currency), None, None, None, None),
|
||||
]
|
||||
|
||||
transaction = data.Transaction(
|
||||
meta=meta,
|
||||
date=transactie_datum,
|
||||
flag='*',
|
||||
payee=payee,
|
||||
narration=narration,
|
||||
tags=frozenset(),
|
||||
links=frozenset(),
|
||||
postings=postings
|
||||
)
|
||||
|
||||
entries.append(transaction)
|
||||
|
||||
return entries
|
||||
|
||||
def _map_payee_to_account(self, payee):
|
||||
mapping = {
|
||||
#INCOME postings
|
||||
"Lohn": "Income:Salaris",
|
||||
"Gehalt": "Income:Salaris",
|
||||
"Landkreis Meissen":"Income:BasicIncome",
|
||||
|
||||
#EXPENSES postings
|
||||
"Miete": "Expenses:Rent",
|
||||
"Sachsen":"Expenses:Electricity",
|
||||
|
||||
"Kontoführung":"Expenses:Banking",
|
||||
"AMAZON":"Expenses:Subscriptions",
|
||||
"Allianz":"Expenses:Insurance",
|
||||
|
||||
"Autohof":"Expenses:Driving",
|
||||
"Tankstelle":"Expenses:Driving",
|
||||
"ESSO":"Expenses:Driving",
|
||||
"ARAL":"Expenses:Driving",
|
||||
"Yellowbrick":"Expenses:Driving:Parking",
|
||||
"PH":"Expenses:Driving:Parking", # Narrow down
|
||||
|
||||
"eBay":"Expenses:Gadgets", # Differentiate
|
||||
"MEDIA MARKT":"Expenses:Gadgets",
|
||||
"Logic Pro":"Expenses:Gadgets",
|
||||
|
||||
"Thomas Klotsche":"Expenses:Household",
|
||||
"POCO":"Expenses:Furniture",
|
||||
"Tapete":"Expenses:Furniture",
|
||||
|
||||
"Deutsche Post AG":"Expenses:Postdelivery",
|
||||
"Echtzeitüberw":"Expenses:Banking",
|
||||
"Apotheke":"Expenses:Drugs",
|
||||
"ALDI":"Expenses:Food",
|
||||
"Lidl":"Expenses:Food",
|
||||
"Bosch":"Expenses:Food", #BOSCH catering
|
||||
"TRANSGOURMET":"Expenses:Food",
|
||||
"Netto Marken":"Expenses:Food",
|
||||
"Rewe":"Expenses:Food",
|
||||
|
||||
#Creditcards
|
||||
"AMERICAN EXPRESS":"Expenses:Creditcard",
|
||||
"CONSORS":"Expenses:Creditcard",
|
||||
|
||||
#SAVINGS postings
|
||||
"Bitpanda":"Assets:Savings:Trade"
|
||||
|
||||
#DEBTS
|
||||
#"111649731":"Debts:Basic income",
|
||||
# Actually booked from Dutch Account
|
||||
#"Duo studieschuld":"Debts:Student loan (NL)",
|
||||
#"DUO Studienschuld":"Debts:Student loan (NL)",
|
||||
#"Bundeskasse Halle":"Debts:Student loan (DE)"
|
||||
}
|
||||
for sleutelwoord, rekening in mapping.items():
|
||||
if sleutelwoord.lower() in payee.lower():
|
||||
return rekening
|
||||
|
||||
return "Expenses:Uncategorized"
|
||||
@@ -1,35 +0,0 @@
|
||||
anyio==4.9.0
|
||||
babel==2.17.0
|
||||
beancount==3.1.0
|
||||
beangulp==0.2.0
|
||||
beanquery==0.2.0
|
||||
beautifulsoup4==4.13.4
|
||||
blinker==1.9.0
|
||||
chardet==5.2.0
|
||||
cheroot==10.0.1
|
||||
click==8.2.1
|
||||
fava==1.30.5
|
||||
Flask==3.1.1
|
||||
flask-babel==4.0.0
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
jaraco.functools==4.2.1
|
||||
Jinja2==3.1.6
|
||||
lxml==6.0.0
|
||||
markdown2==2.5.4
|
||||
MarkupSafe==3.0.2
|
||||
more-itertools==10.7.0
|
||||
ply==3.11
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.1.1
|
||||
python-magic==0.4.27
|
||||
pytz==2025.2
|
||||
regex==2025.7.34
|
||||
simplejson==3.20.1
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
soupsieve==2.7
|
||||
TatSu-LTS==5.13.1
|
||||
typing_extensions==4.14.1
|
||||
watchfiles==1.1.0
|
||||
Werkzeug==3.1.3
|
||||
@@ -1,7 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
print("Python executable:", sys.executable)
|
||||
print("sys.path (zoekpaden):")
|
||||
for p in sys.path:
|
||||
print(" ", p)
|
||||
@@ -1,20 +0,0 @@
|
||||
import unittest
|
||||
from beancount.core.amount import D
|
||||
|
||||
class TestAmountParsing(unittest.TestCase):
|
||||
|
||||
def clean_bedrag_str(self, bedrag_str):
|
||||
bedrag_str = bedrag_str.replace('.','')
|
||||
bedrag_str = bedrag_str.replace(',', '.')
|
||||
return bedrag_str
|
||||
|
||||
# Europese notatie - het werkt dus prima
|
||||
def test_correct_amount_parsing(self):
|
||||
test_bedrag = "1.699,62"
|
||||
expected_decimal = D("1699.62")
|
||||
cleaned_string = self.clean_bedrag_str(test_bedrag)
|
||||
|
||||
self.assertEqual(D(cleaned_string), expected_decimal)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,33 +0,0 @@
|
||||
import os
|
||||
from beancount.ingest import importer
|
||||
from beancount.core import data
|
||||
from beancount.ingest.importers import csv
|
||||
from beancount.format import format_entry
|
||||
from beancount.ingest import identify
|
||||
|
||||
# Een dummy-klasse die een bestand representeert, zoals Beancount dat zou doen.
|
||||
class DummyFile:
|
||||
def __init__(self, filename):
|
||||
self.name = filename
|
||||
|
||||
from postbank_csv_importer import MyCSVImporter
|
||||
importer = MyCSVImporter()
|
||||
bestand_pad = 'PostbankGiro_25-01_08.csv'
|
||||
|
||||
# Controleer of de importer het bestand herkent.
|
||||
if importer.identify(bestand_pad):
|
||||
print("Importer identified the file. Extracting entries...")
|
||||
# Roep de extract methode aan om de transacties te verwerken.
|
||||
extracted_entries = importer.extract(bestand_pad)
|
||||
else:
|
||||
# We maken een dummy-bestand aan met testdata
|
||||
with open(bestand_pad, 'w', encoding='utf-8') as f:
|
||||
f.write("Datum;Omschrijving;Rekening;Tegenrekening;Bedrag;Valuta\n")
|
||||
f.write("01-01-2025;Boodschappen;Bank;Supermarkt;50.00;EUR\n")
|
||||
f.write("02-01-2025;Salaris;Werkgever;Bank;2500.00;EUR\n")
|
||||
f.write("03-01-2025;Huur;Bank;Huisbaas;750.00;EUR\n")
|
||||
extracted_entries = importer_instance.extract(DummyFile(bestand_pad), None)
|
||||
|
||||
print("; Geëxtraheerde transacties:")
|
||||
for entry in extracted_entries:
|
||||
print(format_entry(entry))
|
||||
Reference in New Issue
Block a user