feat: Kontoauszug vom Telefon per Teilen-Menue einlesen
Revolut und Raiffeisen lassen die CSV auch am Handy exportieren; nur N26 verlangt die Web-Version. Ein zweiter Kurzbefehl nimmt die Datei aus dem Teilen-Menue entgegen und schickt sie mit dem gewaehlten Konto an /quick/statement. Am Telefon laesst sich keine Spaltenzuordnung vornehmen. Der Endpunkt verwendet deshalb die Einstellung, mit der dieses Konto zuletzt erfolgreich eingelesen wurde – und sagt klar Bescheid, wenn es noch keine gibt, statt mit falschen Spalten zu raten. Gebucht wird sofort: Ein Auszug hat hunderte Zeilen, die einzeln abzunicken waere keine Erleichterung. Sicher ist das durch die Duplikaterkennung und weil der Stapel im Import-Verlauf mit einem Klick zurueckzunehmen ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,9 @@ Auswahl braucht – sie liefert nur Name, Symbol und Farbe, keine Salden.
|
||||
from datetime import date as date_t
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
@@ -21,6 +23,9 @@ from ..deps import require_quick_token
|
||||
from ..models import (
|
||||
Account,
|
||||
ApiToken,
|
||||
ImportBatch,
|
||||
ImportStatus,
|
||||
ImportTemplate,
|
||||
InboxItem,
|
||||
InboxSource,
|
||||
InboxStatus,
|
||||
@@ -144,3 +149,82 @@ def quick_booking(
|
||||
message=f"{betrag / 100:.2f} {konto.currency} auf {konto.name}{wohin}",
|
||||
item=InboxItemOut.model_validate(eintrag),
|
||||
)
|
||||
|
||||
|
||||
def _letzte_vorlage(db: Session, konto: Account) -> ImportTemplate | None:
|
||||
"""Die Einstellung, mit der dieses Konto zuletzt erfolgreich eingelesen wurde.
|
||||
|
||||
Am Telefon lässt sich keine Spaltenzuordnung vornehmen. Der Kurzbefehl
|
||||
setzt deshalb voraus, dass die Datei am Rechner schon einmal für dieses
|
||||
Konto eingelesen wurde – dann steht im Import-Verlauf, wie es ging.
|
||||
"""
|
||||
stapel = (
|
||||
db.query(ImportBatch)
|
||||
.filter(ImportBatch.owner_id == konto.owner_id,
|
||||
ImportBatch.account_id == konto.id,
|
||||
ImportBatch.status == ImportStatus.committed)
|
||||
.order_by(ImportBatch.created_at.desc(), ImportBatch.id.desc())
|
||||
.first()
|
||||
)
|
||||
if stapel is None:
|
||||
return None
|
||||
if stapel.template_id:
|
||||
vorlage = db.get(ImportTemplate, stapel.template_id)
|
||||
if vorlage is not None and vorlage.owner_id == konto.owner_id:
|
||||
return vorlage
|
||||
if stapel.preset:
|
||||
for p in csv_import.PRESETS:
|
||||
if p["name"] == stapel.preset:
|
||||
felder = {k: v for k, v in p.items() if k not in ("name", "column_map")}
|
||||
return ImportTemplate(owner_id=konto.owner_id, name="_kurzbefehl",
|
||||
column_map=json.dumps(p["column_map"]), **felder)
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/statement", status_code=status.HTTP_201_CREATED)
|
||||
async def quick_statement(
|
||||
file: UploadFile = File(...),
|
||||
account_id: int = Form(...),
|
||||
marke: ApiToken = Depends(require_quick_token),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Kontoauszug vom Telefon aus einlesen (Teilen-Menü).
|
||||
|
||||
Gebucht wird sofort – ein Auszug hat schnell hunderte Zeilen, die einzeln
|
||||
abzunicken wäre keine Erleichterung. Sicher ist das, weil die
|
||||
Duplikaterkennung greift und der ganze Stapel im Import-Verlauf mit einem
|
||||
Klick rückgängig zu machen ist.
|
||||
"""
|
||||
konto = _konto(db, marke, account_id)
|
||||
user = db.get(User, marke.owner_id)
|
||||
vorlage = _letzte_vorlage(db, konto)
|
||||
if vorlage is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=(f"Für „{konto.name}“ gibt es noch keine Einstellung. Bitte einen Auszug "
|
||||
"dieses Kontos einmal am Rechner einlesen – danach geht es vom Telefon."),
|
||||
)
|
||||
|
||||
daten = await file.read()
|
||||
try:
|
||||
kandidaten = csv_import.parse_file_to_candidates(daten, vorlage)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Datei nicht lesbar: {e}") from e
|
||||
|
||||
zeilen = csv_import.detect_rows(db, user, konto, kandidaten)
|
||||
zu_buchen = [z for z in zeilen if z.get("include")]
|
||||
stapel = csv_import.commit_import(
|
||||
db, user, konto, vorlage.id if vorlage.id else None,
|
||||
(file.filename or "Vom Telefon")[:255], zu_buchen,
|
||||
preset=None if vorlage.id else vorlage.name,
|
||||
)
|
||||
dubletten = sum(1 for z in zeilen if z.get("is_duplicate"))
|
||||
return {
|
||||
"gelesen": len(zeilen),
|
||||
"gebucht": stapel.imported_count,
|
||||
"duplikate": dubletten,
|
||||
"konto": konto.name,
|
||||
"message": f"{stapel.imported_count} gebucht auf {konto.name}"
|
||||
f" · {dubletten} schon vorhanden",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user