DATEV Dışa Aktarımını Otomatikleştirme (Mali Müşavirler/Bürolar)¶
DATEV dışa aktarımı tamamen gözetimsiz olarak otomatikleştirilebilir: bir kez yapılandırın, ardından bir betik/cronjob ile aylık olarak tetikleyin, durumu sorgulayın ve sonucu indirin veya doğrudan büroya e-posta ile gönderilmesini sağlayın. Admin alanında manuel tıklama gerekmez.
Uç Noktalar¶
DatevController — Temel api/v1/datev:
| Yöntem | Uç Nokta | Amaç |
|---|---|---|
GET |
/config |
Güncel DATEV yapılandırmasını okur (danışman/müvekkil no., hesaplar, büro e-postası) |
PUT |
/config |
Yapılandırmayı ayarlar (tam değiştirme) |
POST |
/export |
Bir dönem için dışa aktarımı tetikler → sessionId döner |
GET |
/export/{sessionId}/status |
İlerlemeyi/durumu sorgular |
GET |
/export/{sessionId}/download |
Tamamlanmış dışa aktarım dosyasını ZIP olarak indirir |
POST |
/send |
Bir dönemin dışa aktarımını doğrudan büroya e-posta ile gönderir |
Admin yetkisi gereklidir
POST /export (ve /send), Admin veya SupportAdmin rolünü gerektirir — normal bir
kasa kullanıcısı 403 FORBIDDEN alır. Otomasyon için kullanılan hesabın buna uygun olarak
bu rollerden birine sahip olması gerekir.
Yapılandırmayı okuma/ayarlama¶
{
"success": true,
"data": {
"id": "datevcfg_1",
"mandatennummer": 12345,
"beraterId": 6789,
"kontoMwstNormal": 4400,
"kontoMwstReduziert": 4300,
"kontoMwstFrei": 4200,
"srcKonto": 1600,
"srcKontoKarte": 1360,
"barKontoSrc": 1000,
"barKontoTarget": 1600,
"barOnly": false,
"includeDocuments": true,
"includeSpendings": true,
"spendingKonto": 6300,
"pfandKonto": 1590,
"kundenguthabenKonto": 1701,
"gutscheinKonto": 1702,
"sachkontenLaenge": 4,
"exportMode": 0,
"stbEmail": "kanzlei@steuerberater.example",
"stbName": "Steuerberatung Musterfrau",
"zipPassword": "***",
"byPaymentTyp": [],
"wgrAccountMappings": []
}
}
PUT /api/v1/datev/config, aynı alan kümesini (id/zaman damgası hariç) tam bir
değiştirme olarak bekler — eksik alanlar birleştirilmez, DTO varsayılanına sıfırlanır. Yazmadan
önce dolayısıyla önce GET /config okuyun, değerleri devralın, hedefli değişiklik yapın, sonra
PUT yapın.
Dışa aktarımı başlatma¶
POST /api/v1/datev/export
Authorization: Bearer eyJ...
Content-Type: application/json
{ "startDate": "2026-06-01T00:00:00Z", "endDate": "2026-06-30T23:59:59Z", "mode": 0 }
{ "success": true, "data": { "sessionId": "datev_a1b2c3", "isComplete": false, "progress": 0, "downloadUrl": null } }
mode, Admin alanında açılır menü olarak sunulan dışa aktarım türlerini temsil eden 0–6
arası bir tam sayıdır (ör. belgeli/belgesiz, yalnızca nakit). Düzenli bir aylık EXTF paketi için
mode: 0 yeterlidir.
Durumu sorgulama¶
{ "success": true, "data": { "sessionId": "datev_a1b2c3", "isComplete": true, "progress": 100, "error": null,
"downloadUrl": "/api/v1/datev/export/datev_a1b2c3/download" } }
Bilinmeyen bir sessionId → errorCode: "NOT_FOUND" ile 404 döner.
İndirme¶
Yanıt: 200 OK, Content-Type: application/zip, dosya datev_export.zip (DATEV-EXTF biçimi,
includeDocuments'a göre isteğe bağlı belgelerle, zipPassword'a göre isteğe bağlı parola
korumalı).
Doğrudan büroya gönderme¶
POST /api/v1/datev/send
Authorization: Bearer eyJ...
Content-Type: application/json
{ "startDate": "2026-06-01T00:00:00Z", "endDate": "2026-06-30T23:59:59Z", "mode": 0 }
Tamamlanmış dışa aktarımı, config.stbEmail/stbName içinde kayıtlı büro adresine e-posta ile
gönderir — ayrı bir indirme adımı gerekmez.
Tarif: Dönem dışa aktarma ve indirme¶
API="https://ihre-instanz.dikas.de/api/v1"
TOKEN="eyJ..."
# 1. Einmalig: Konfiguration setzen (siehe oben, hier ausgelassen)
# 2. Export für Juni 2026 anstoßen
SESSION=$(curl -s -X POST "$API/datev/export" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "startDate": "2026-06-01T00:00:00Z", "endDate": "2026-06-30T23:59:59Z", "mode": 0 }' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['sessionId'])")
# 3. Status pollen, bis fertig
while true; do
STATUS=$(curl -s "$API/datev/export/$SESSION/status" -H "Authorization: Bearer $TOKEN")
DONE=$(echo "$STATUS" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['isComplete'])")
[ "$DONE" = "True" ] && break
sleep 5
done
# 4a. Herunterladen ...
curl -s "$API/datev/export/$SESSION/download" -H "Authorization: Bearer $TOKEN" -o "datev_2026-06.zip"
# 4b. ... ODER direkt an die Kanzlei senden
curl -s -X POST "$API/datev/send" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "startDate": "2026-06-01T00:00:00Z", "endDate": "2026-06-30T23:59:59Z", "mode": 0 }'
Kopyala-Yapıştır Cron Betiği: Otomatik Aylık Dışa Aktarım¶
Her ayın 1'inde çalışır (ör. Crontab'da 0 6 1 * *), tam olarak önceki ayı dışa aktarır ve
send ile büroya gönderir. Yeniden giriş yapar, zaman aşımıyla sorgular, success/errorCode
değerlendirir ve anlamlı bir çıkış kodu belirler (izleme/uyarı için).
#!/usr/bin/env bash
# datev-monatsexport.sh — am 1. jedes Monats per Cron: Vormonat exportieren + an Kanzlei senden.
# Crontab-Beispiel: 0 6 1 * * /pfad/zu/datev-monatsexport.sh >> /var/log/datev-export.log 2>&1
set -euo pipefail
API="https://ihre-instanz.dikas.de/api/v1"
DIKAS_USER="${DIKAS_USER:?DIKAS_USER env var fehlt}"
DIKAS_PASSWORD="${DIKAS_PASSWORD:?DIKAS_PASSWORD env var fehlt}"
POLL_INTERVAL=5
POLL_TIMEOUT=300 # Sekunden, bevor der Export als hängengeblieben gilt
# Vormonat als [Anfang, Ende] in UTC ISO-8601 berechnen
START_DATE=$(date -u -d "$(date +%Y-%m-01) -1 month" +%Y-%m-%dT00:00:00Z)
END_DATE=$(date -u -d "$(date +%Y-%m-01) -1 second" +%Y-%m-%dT23:59:59Z)
log() { echo "[$(date -u +%FT%TZ)] $*"; }
json_get() {
# json_get '<json>' '<data-key>' — liest ein Feld aus data{} per stdlib json (kein jq-Zwang).
# WICHTIG: Die API serialisiert null-Felder explizit mit (kein WhenWritingNull) — sowohl
# `data` selbst als auch das gesuchte Feld können JSON-`null` sein. Beides muss sauber auf
# einen leeren String fallen, NICHT auf den Python-Literal-String "None" (der in bash mit
# `[ -n "$X" ]` als NICHT-leer gilt und das Skript bei jedem gesunden Export abbrechen ließe).
python3 -c "
import sys, json
obj = json.loads(sys.argv[1])
v = obj.get('data') or {}
v = v.get(sys.argv[2])
print('' if v is None else v)
" "$1" "$2"
}
json_top() {
# json_top '<json>' '<top-level-key>' — liest TOP-LEVEL-Felder von ApiResponse<T>
# ({success, data, errorCode, errorMessage}) — errorCode/errorMessage stehen NEBEN data,
# nicht darin (bei einem Fehler ist data ohnehin null). Gleiche Null-Sicherheit wie json_get.
python3 -c "
import sys, json
obj = json.loads(sys.argv[1])
v = obj.get(sys.argv[2])
print('' if v is None else v)
" "$1" "$2"
}
fail() {
log "FEHLER: $*"
exit 1
}
# 1. Login
LOGIN_RESPONSE=$(curl -sS -X POST "$API/auth/login" -H "Content-Type: application/json" \
-d "{ \"username\": \"$DIKAS_USER\", \"password\": \"$DIKAS_PASSWORD\" }") || fail "Login-Request fehlgeschlagen (Netzwerk)"
SUCCESS=$(json_top "$LOGIN_RESPONSE" "success")
if [ "$SUCCESS" != "True" ]; then
ERR_CODE=$(json_top "$LOGIN_RESPONSE" "errorCode")
fail "Login abgelehnt (errorCode=$ERR_CODE)"
fi
ACCESS_TOKEN=$(json_get "$LOGIN_RESPONSE" "accessToken")
REFRESH_TOKEN=$(json_get "$LOGIN_RESPONSE" "refreshToken")
[ -n "$ACCESS_TOKEN" ] || fail "Kein accessToken in Login-Antwort"
log "Login OK. Exportiere Zeitraum $START_DATE bis $END_DATE."
# Hilfsfunktion: authentifizierter Request mit EINMALIGEM Refresh-Retry bei 401
api_call() {
local method="$1" path="$2" body="${3:-}"
local resp status
if [ -n "$body" ]; then
resp=$(curl -sS -w '\n%{http_code}' -X "$method" "$API$path" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d "$body")
else
resp=$(curl -sS -w '\n%{http_code}' -X "$method" "$API$path" \
-H "Authorization: Bearer $ACCESS_TOKEN")
fi
status=$(echo "$resp" | tail -n1)
body_out=$(echo "$resp" | sed '$d')
if [ "$status" = "401" ]; then
log "Access-Token abgelaufen — erneuere per Refresh-Token."
REFRESH_RESPONSE=$(curl -sS -X POST "$API/auth/refresh" -H "Content-Type: application/json" \
-d "{ \"refreshToken\": \"$REFRESH_TOKEN\" }") || fail "Refresh-Request fehlgeschlagen"
if [ "$(json_top "$REFRESH_RESPONSE" "success")" != "True" ]; then
fail "Refresh-Token ungültig/abgelaufen — Skript neu mit frischem Login starten."
fi
ACCESS_TOKEN=$(json_get "$REFRESH_RESPONSE" "accessToken")
REFRESH_TOKEN=$(json_get "$REFRESH_RESPONSE" "refreshToken")
# Einmal wiederholen mit neuem Token
if [ -n "$body" ]; then
resp=$(curl -sS -w '\n%{http_code}' -X "$method" "$API$path" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d "$body")
else
resp=$(curl -sS -w '\n%{http_code}' -X "$method" "$API$path" \
-H "Authorization: Bearer $ACCESS_TOKEN")
fi
status=$(echo "$resp" | tail -n1)
body_out=$(echo "$resp" | sed '$d')
fi
echo "$body_out"
return 0
}
# 2. Export starten
EXPORT_BODY="{ \"startDate\": \"$START_DATE\", \"endDate\": \"$END_DATE\", \"mode\": 0 }"
EXPORT_RESPONSE=$(api_call POST /datev/export "$EXPORT_BODY")
if [ "$(json_top "$EXPORT_RESPONSE" "success")" != "True" ]; then
ERR_CODE=$(json_top "$EXPORT_RESPONSE" "errorCode")
ERR_MSG=$(json_top "$EXPORT_RESPONSE" "errorMessage")
fail "Export-Start abgelehnt: $ERR_CODE — $ERR_MSG"
fi
SESSION_ID=$(json_get "$EXPORT_RESPONSE" "sessionId")
[ -n "$SESSION_ID" ] || fail "Keine sessionId in Export-Antwort"
log "Export gestartet, sessionId=$SESSION_ID"
# 3. Status pollen mit Timeout
ELAPSED=0
while true; do
STATUS_RESPONSE=$(api_call GET "/datev/export/$SESSION_ID/status")
if [ "$(json_top "$STATUS_RESPONSE" "success")" != "True" ]; then
ERR_CODE=$(json_top "$STATUS_RESPONSE" "errorCode")
fail "Status-Abfrage fehlgeschlagen: $ERR_CODE"
fi
# Fertig-Erkennung ausschließlich über isComplete (verifiziert gegen DatevExportStatusResponse) —
# NICHT über das optionale error-Feld, sonst würde ein gesunder, noch laufender Export nie
# zwischenzeitlich ein "error" haben, aber falsche Priorisierung könnte ihn trotzdem stoppen.
# error wird erst geprüft, NACHDEM isComplete=true ist (dann heißt es: fertig, aber fehlgeschlagen).
IS_COMPLETE=$(json_get "$STATUS_RESPONSE" "isComplete")
if [ "$IS_COMPLETE" = "True" ]; then
EXPORT_ERROR=$(json_get "$STATUS_RESPONSE" "error")
if [ -n "$EXPORT_ERROR" ]; then
fail "DATEV-Export abgeschlossen, aber mit Fehler: $EXPORT_ERROR"
fi
log "Export abgeschlossen nach ${ELAPSED}s."
break
fi
if [ "$ELAPSED" -ge "$POLL_TIMEOUT" ]; then
fail "Timeout ($POLL_TIMEOUT s) — Export ist nicht rechtzeitig fertig geworden (sessionId=$SESSION_ID)."
fi
sleep "$POLL_INTERVAL"
ELAPSED=$((ELAPSED + POLL_INTERVAL))
done
# 4. An die Kanzlei senden (Konfiguration muss stbEmail/stbName gesetzt haben)
SEND_RESPONSE=$(api_call POST /datev/send "$EXPORT_BODY")
if [ "$(json_top "$SEND_RESPONSE" "success")" != "True" ]; then
ERR_CODE=$(json_top "$SEND_RESPONSE" "errorCode")
ERR_MSG=$(json_top "$SEND_RESPONSE" "errorMessage")
fail "Versand an Kanzlei fehlgeschlagen: $ERR_CODE — $ERR_MSG"
fi
log "DATEV-Export für $START_DATE bis $END_DATE erfolgreich an die Kanzlei gesendet."
exit 0
Çıkış kodları: 0 = başarıyla dışa aktarıldı ve gönderildi, 1 = günlük satırıyla iptal (giriş,
dışa aktarım, zaman aşımı, gönderim) — sıfır olmayan çıkışta cron e-posta bildirimi için uygundur.
Kimlik Doğrulama¶
api/v1/datev/* yalnızca JWT ile korunur ([Authorize], export/send için Admin/SupportAdmin
rolleri). Hiçbir API anahtarı kısayolu yoktur: api-keys yönetimi (ApiKeysController) yalnızca
eski /rest/* uyumluluk katmanını besler, api/v1 denetleyicilerini değil. Gözetimsiz bir otomasyon
için betiğin dolayısıyla Admin rolüne sahip teknik bir kullanıcıya ihtiyacı vardır ve giriş/yenileme
döngüsünü kendisi yürütmelidir (yukarıdaki cron betiğine bakın).
Ayrıca bakınız¶
- DATEV Dışa Aktarımı (Kullanım Kılavuzu) — Admin alanındaki manuel kullanım.
- Mali Müşavir ve Danışmanlık Bürosu — API dışa aktarımına alternatif/ek olarak Büro Portalı.