feat: Import-API mit Vorschau, Saldo-Pruefung und Duplikat-Schutz
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
99
finance/app/services/importer.py
Normal file
99
finance/app/services/importer.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.tables import Account, Statement, Transaction
|
||||
from app.parsers.base import ParserError
|
||||
from app.parsers.registry import parse_pdf
|
||||
from app.parsers.validate import balance_difference, dedup_hash
|
||||
from app.services.categorize import apply_rules
|
||||
|
||||
|
||||
def _find_or_create_account(session: Session, bank: str, iban: str | None, filename: str) -> Account:
|
||||
if iban:
|
||||
acc = session.execute(select(Account).where(Account.iban == iban)).scalar()
|
||||
if acc is not None:
|
||||
return acc
|
||||
else:
|
||||
iban = f"UNBEKANNT-{filename}"
|
||||
acc = Account(bank=bank, iban=iban, name=iban, type="giro")
|
||||
session.add(acc)
|
||||
session.flush()
|
||||
return acc
|
||||
|
||||
|
||||
def process_pdf(session: Session, path: Path) -> Statement:
|
||||
settings = get_settings()
|
||||
filename = path.name
|
||||
|
||||
try:
|
||||
parsed = parse_pdf(path)
|
||||
except ParserError as exc:
|
||||
stmt = Statement(filename=filename, bank="unbekannt", account_id=None,
|
||||
status="error", error_message=str(exc))
|
||||
session.add(stmt)
|
||||
session.commit()
|
||||
session.refresh(stmt)
|
||||
return stmt
|
||||
|
||||
account = _find_or_create_account(session, parsed.bank, parsed.iban, filename)
|
||||
|
||||
stmt = Statement(
|
||||
filename=filename,
|
||||
bank=parsed.bank,
|
||||
account_id=account.id,
|
||||
period_start=parsed.period_start,
|
||||
period_end=parsed.period_end,
|
||||
opening_balance=parsed.opening_balance,
|
||||
closing_balance=parsed.closing_balance,
|
||||
status="draft",
|
||||
)
|
||||
session.add(stmt)
|
||||
session.flush()
|
||||
|
||||
diff = balance_difference(parsed)
|
||||
if diff != 0:
|
||||
stmt.status = "error"
|
||||
stmt.error_message = f"Saldo-Differenz {diff} EUR"
|
||||
session.commit()
|
||||
session.refresh(stmt)
|
||||
return stmt
|
||||
|
||||
drafts = []
|
||||
for t in parsed.transactions:
|
||||
h = dedup_hash(account.id, t.booking_date, t.amount, t.purpose)
|
||||
is_duplicate = session.execute(
|
||||
select(Transaction).where(
|
||||
Transaction.dedup_hash == h,
|
||||
Transaction.status == "confirmed",
|
||||
)
|
||||
).scalar() is not None
|
||||
tx = Transaction(
|
||||
account_id=account.id,
|
||||
statement_id=stmt.id,
|
||||
booking_date=t.booking_date,
|
||||
value_date=t.value_date,
|
||||
amount=t.amount,
|
||||
purpose=t.purpose,
|
||||
counterparty=t.counterparty,
|
||||
status="draft",
|
||||
dedup_hash=h,
|
||||
is_duplicate=is_duplicate,
|
||||
)
|
||||
session.add(tx)
|
||||
drafts.append(tx)
|
||||
|
||||
session.flush()
|
||||
apply_rules(session, drafts)
|
||||
|
||||
stmt.status = "draft"
|
||||
session.commit()
|
||||
session.refresh(stmt)
|
||||
|
||||
uploads_dir = settings.uploads_dir
|
||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
path.replace(uploads_dir / filename)
|
||||
|
||||
return stmt
|
||||
Reference in New Issue
Block a user