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:
@@ -247,3 +247,61 @@ def test_zwei_gleiche_zahlungen_verbrauchen_je_einen_eintrag(admin):
|
||||
assert gruende.count("inbox") == 2, gruende
|
||||
# Die dritte ist echt und wird gebucht.
|
||||
assert sum(1 for r in v["rows"] if r["include"]) == 1
|
||||
|
||||
|
||||
def test_auszug_vom_telefon_nutzt_die_zuletzt_benutzte_vorlage(admin):
|
||||
"""Am Telefon kann man keine Spalten zuordnen. Also wird die Einstellung
|
||||
wiederverwendet, mit der dieses Konto zuletzt eingelesen wurde."""
|
||||
client = _fremd()
|
||||
konto = _konto(admin, name="Handykonto", currency="CHF")
|
||||
marke = _marke(admin, konto["id"])
|
||||
kopf = {"X-Certain-Token": marke["token"]}
|
||||
import io
|
||||
|
||||
csv_text = KOPF + "01.04.2026;-12,00;Bäckerei;Brot;\n02.04.2026;-8,00;Kiosk;Zeitung;\n"
|
||||
|
||||
# Ohne vorherigen Import am Rechner: klare Ansage statt Rätselraten.
|
||||
r = client.post("/api/quick/statement",
|
||||
files={"file": ("a.csv", io.BytesIO(csv_text.encode()), "text/csv")},
|
||||
data={"account_id": str(konto["id"])}, headers=kopf)
|
||||
assert r.status_code == 400
|
||||
assert "am Rechner" in r.json()["detail"]
|
||||
|
||||
# Einmal am Rechner einlesen legt die Einstellung fest.
|
||||
import json as _json
|
||||
vorlage = admin.post("/api/imports/templates", json={
|
||||
"name": "Handytest " + secrets.token_hex(3), "delimiter": ";", "decimal_sep": ",",
|
||||
"date_format": "%d.%m.%Y", "encoding": "utf-8", "has_header": True, "skip_rows": 0,
|
||||
"amount_mode": "signed",
|
||||
"column_map": {"date": 0, "amount": 1, "payee": 2, "purpose": 3},
|
||||
})
|
||||
assert vorlage.status_code == 201, vorlage.text
|
||||
v = _import(admin, konto["id"], csv_text)
|
||||
admin.post("/api/imports/commit", json={
|
||||
"account_id": konto["id"], "filename": "april.csv",
|
||||
"template_id": vorlage.json()["id"],
|
||||
"rows": [x for x in v["rows"] if x["include"]]})
|
||||
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 100000 - 2000
|
||||
|
||||
# Ab jetzt geht es vom Telefon: neue Zeilen kommen an, alte nicht doppelt.
|
||||
mehr = csv_text + "03.04.2026;-5,50;Coop;Milch;\n"
|
||||
r = client.post("/api/quick/statement",
|
||||
files={"file": ("april.csv", io.BytesIO(mehr.encode()), "text/csv")},
|
||||
data={"account_id": str(konto["id"])}, headers=kopf)
|
||||
assert r.status_code == 201, r.text
|
||||
assert r.json()["gebucht"] == 1
|
||||
assert r.json()["duplikate"] == 2
|
||||
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 100000 - 2550
|
||||
|
||||
# Und der Stapel steht im Verlauf, also mit einem Klick ruecknehmbar.
|
||||
stapel = admin.get("/api/imports/batches").json()
|
||||
assert any(b["filename"] == "april.csv" and b["imported_count"] == 1 for b in stapel)
|
||||
|
||||
|
||||
def test_auszug_vom_telefon_braucht_eine_gueltige_marke(admin):
|
||||
import io
|
||||
konto = _konto(admin, name="Fremdes Konto")
|
||||
r = _fremd().post("/api/quick/statement",
|
||||
files={"file": ("a.csv", io.BytesIO(b"x"), "text/csv")},
|
||||
data={"account_id": str(konto["id"])})
|
||||
assert r.status_code == 401
|
||||
|
||||
Reference in New Issue
Block a user