Ticket Billing
Ticket- und Abrechnungsverwaltung für ERPNext 16: E-Mail-Tickets mit Zuordnung nach Abteilung, austauschbarer Zuweisungsregel, Zeiterfassung im Vier-Augen-Prinzip und eigener Vue-3-Oberfläche.
- Author: saschafo
- Repository: https://github.com/saschafo/ticket_billing
- GitHub stars: 1
- Forks: 0
- License: NOASSERTION
- Category: HR & Payroll
- Maintenance: Actively Maintained
Install Ticket Billing
bench get-app https://github.com/saschafo/ticket_billing
Tags
- agpl-3-0
- billing
- erpnext
- frappe
- frappe-app
- german
- helpdesk
- ticketing
- time-tracking
- vue3
Add the Frappe Gems badge to your README
Maintain Ticket Billing? Paste this into your README:
[](https://frappegems.com/gems/apps/saschafo/ticket_billing)
About Ticket Billing
Ticket Billing
Deutsch · English
Frappe-App für Ticket- und Abrechnungsverwaltung, aufbauend auf ERPNext 16 und Frappe Framework 16.
Tickets entstehen aus E-Mails externer Kunden oder als interne Anfragen zwischen Abteilungen. Der Zugriff ist nach Abteilung und Rolle getrennt, neue Tickets werden automatisch zugewiesen, erfasste Zeiten laufen über ein Vier-Augen-Prinzip in die ERPNext-Rechnungsstellung.
Das dazugehörige Docker-Setup liegt in einem eigenen Repo:
ticket_billing_docker.
Inhalt
- Datenmodell
- Rollen und Rechte
- Automatische Zuweisung
- Zeiterfassung
- Vier-Augen-Prinzip
- Realtime
- Oberfläche
- Mehrsprachigkeit
- Kennzahlen
- Abrechnung
- Demo-Daten
- Sicherheitshinweise
- Grenzen des Entwurfs
- Einrichtung, die zur Anlage gehört
- Lizenz
Datenmodell
Es entstehen keine eigenen Ticket-Doctypes — die App erweitert ERPNext:
| Doctype | Feld | Zweck |
|---|---|---|
Issue |
tb_department |
zuständige Abteilung (Pflicht) |
Issue |
tb_origin |
Internal / External |
Issue |
tb_assigned_employee |
zugewiesener Mitarbeiter |
Issue |
tb_first_response_on |
Zeitpunkt der ersten Reaktion |
Issue |
tb_resolved_on |
Zeitpunkt der Erledigung |
Issue |
customer (ERPNext) |
nur bei externer Herkunft zulässig |
Timesheet Detail |
tb_issue |
verknüpft den Zeiteintrag mit dem Ticket |
Email Account |
tb_department |
ein Postfach je Abteilung |
Eigene Doctypes: Ticket Billing Settings (Einstellungen), Ticket Timer
(ein laufender Timer) und Ticket Billing Demo Record (Nachverfolgung der
Demo-Daten).
Die Felder legt setup.py bei after_install und
bei jedem after_migrate idempotent an — nicht als Fixture. Fixtures auf
fremden Doctypes brechen, sobald ERPNext dort etwas ändert.
Rollen und Rechte
| Rolle | Sicht |
|---|---|
Mitarbeiter |
die ihm zugewiesenen Tickets, dazu die selbst angelegten |
Abteilungsleiter |
alle Tickets seiner Abteilung, Umverteilung, Team-Auswertung, Zeitfreigabe |
Geschäftsführung |
abteilungsübergreifend lesend, kein Schreibrecht |
Abgesichert wird auf drei Ebenen, alle im Backend:
- DocPerms als Obergrenze (was eine Rolle überhaupt darf).
- User Permission auf
Department— wird ausEmployee.departmentgespiegelt (doc_events/employee.py) und von Frappe automatisch auf jede Abfrage angewandt. permission_query_conditionsundhas_permission(permissions.py) für die zeilenweise Einschränkung. Das greift für Desk, Report,frappe.get_listund REST — nicht nur für die Oberfläche.
Jeder Whitelisted-Endpunkt prüft zusätzlich selbst. Die Auswertungen umgehen die Zeilenfilter bewusst (eine Kennzahl muss zählen, was man einzeln nicht sehen darf) — deshalb steht dort vor jeder Abfrage eine ausdrückliche Prüfung.
Wichtig:
tb_assigned_employeeträgtignore_user_permissions. ERPNext legt zu jedem Employee eine User Permission auf sich selbst an; ohne dieses Flag sähe ein Abteilungsleiter nur die Tickets, die auf ihn selbst laufen.
Automatische Zuweisung
Beim Anlegen eines Tickets — auch bei einem aus dem Posteingang — greift
after_insert. Die Regel ist austauschbar:
assignment/
├── base.py Schnittstelle (Candidate, AssignmentStrategy)
├── registry.py Verzeichnis, @register
├── strategies/
│ ├── __init__.py importiert die Regeln
│ └── by_workload.py Startregel: wenigste offene Tickets
└── __init__.py Rahmen: Kandidaten → Regel → anwenden
Eine neue Regel (Round-Robin, kategoriebasiert, manuelle Bestätigung):
# assignment/strategies/round_robin.py
from ticket_billing.assignment.base import AssignmentStrategy
from ticket_billing.assignment.registry import register
@register
class RoundRobinStrategy(AssignmentStrategy):
key = "round_robin"
label = "Reihum"
def select(self, issue, candidates):
... # None heißt: nicht zuweisen, der Leiter entscheidet
Dazu eine Importzeile in strategies/__init__.py, und in den Einstellungen
assignment_strategy auf round_robin setzen. Am übrigen Code ändert sich
nichts.
Fehler in der Zuweisung werden geloggt, nicht geworfen: Ein unzugewiesenes Ticket ist sichtbar und nachträglich verteilbar, eine am Pflichtfeld gescheiterte E-Mail nicht.
Systemabsender (mailer-daemon@, postmaster@, noreply@ …) werden
übersprungen — ein Rückläufer ist keine Arbeit für jemanden.
Der vollständige Weg: Eine Mail an ein Abteilungspostfach wird zum Ticket, der Bearbeiter antwortet aus dem Ticket heraus, die Antwort des Kunden hängt sich an denselben Vorgang.
Eingang: das Postfach bestimmt die Abteilung
Je Abteilung ein Email Account mit tb_department. Beim IMAP-Abruf muss
append_to = Issue am Ordner stehen (IMAP Folder), nicht am Konto —
bei aktiviertem IMAP wertet Frappe nur die Ordnerzeile aus.
Die Abteilung wird in dieser Reihenfolge bestimmt
(doc_events/issue.py):
- Abteilung des empfangenden Postfachs — hat Vorrang, auch wenn im Feld schon etwas steht.
- was am Ticket steht (bei Anlage in der Oberfläche),
- die Standardabteilung aus den Einstellungen,
- sonst ein Abbruch mit verständlicher Meldung.
Warum das Postfach vorgeht: Frappe belegt Verweisfelder aus den Benutzerrechten vor. Wer genau eine Abteilung sehen darf, bekommt sie beim Anlegen automatisch eingetragen. Ruft ein Mitarbeiter die Post ab, entstünde das Ticket dadurch in seiner Abteilung statt in der des Postfachs — eine Mail an die Buchhaltung landete beim Support. Eine Vorbelegung ist keine Entscheidung.
Damit das Postfach überhaupt am Ticket ankommt, setzt die App den Property
Setter Issue.recipient_account_field = email_account (siehe unten).
Herkunft: intern oder extern
tb_origin entscheidet mehr als eine Beschriftung: Zeiten auf externen
Tickets gehen als abrechenbar in die Rechnungsstellung, interne nicht.
Als intern gilt ein Absender, wenn eine der drei Bedingungen zutrifft:
- die Adresse ist eines der eigenen Postfächer,
- sie gehört einem aktiven Mitarbeiter (
Employee.user_id), - sie liegt auf derselben Domain wie eines der eigenen Postfächer.
Bewusst Employee und nicht User: Ein Kunde mit Portalzugang ist auch ein
Benutzer, gehört aber nicht ins Haus.
Die Domain-Regel setzt voraus, dass unter den eigenen Domains niemand von außen schreibt. Wer Kunden auf einer eigenen Domain hat, muss das anders lösen.
Verlauf und Antworten
Der Text einer eingegangenen Mail steht nicht in Issue.description,
sondern in verknüpften Communication-Sätzen. Die Detailansicht zeigt sie im
Reiter Verlauf mit Absender, Zeitpunkt und Anhängen — als Text, nicht als
HTML (fremde Mails werden nicht ungeprüft gerendert).
Antworten geht direkt aus dem Ticket: Empfänger sind vorbelegt (Absender der
letzten eingegangenen Nachricht, sonst der ursprüngliche Absender), Absender
ist das Postfach der Abteilung. Die Antwort trägt in_reply_to, wodurch die
Rückantwort des Kunden wieder an demselben Ticket landet. Der Status
wechselt auf Replied.
Antworten gehen sofort raus. Frappe legt ausgehende Mail sonst nur in die Warteschlange, die der Zeitplan alle vier Minuten leert — gemessen 11 bis 244 Sekunden. Der Endpunkt stößt die zugehörigen Einträge gezielt an, als Hintergrundjob (der SMTP-Griff darf die Oberfläche nicht anhalten). Schlägt das fehl, holt der reguläre Sammellauf sie nach; die Mail geht nicht verloren.
In der Ticketliste trägt eine Zeile den Hinweis Neue Antwort, sobald die jüngste Nachricht von außen kam — aber nur bei einer Nachricht nach der Eröffnung. Sonst trüge ihn jedes neue Ticket, und ein Hinweis, den alle tragen, sagt nichts.
Rückläufer
Ein Zustellfehler kommt als Mail vom Mailsystem zurück. Ohne Zutun legt Frappe dafür ein eigenes Ticket an und die Zuweisung schiebt es jemandem in die Liste — eine Störungsmeldung sähe aus wie eine Kundenanfrage.
mail_filter.py erkennt Systemabsender am
Teil vor dem @ und hängt die Nachricht an das Ticket, dessen Antwort
zurückkam. Den Ticketnamen liefert der Abmeldelink in der zitierten
Originalmail, also ein systemseitiger Anker. Zusätzlich muss die Adresse des
Ausstellers im Rückläufer vorkommen — sonst ging die gescheiterte Mail an
jemand anderen (etwa an eine interne Benachrichtigung) und gehört nicht in
den Kundenverlauf. Lässt sich nichts zuordnen, wird das Ticket geschlossen
statt gelöscht.
Abruf
Der Zeitplan holt alle zehn Minuten (0/10 * * * *), gemessen 74 bis 482
Sekunden bis zur Ankunft. Wer nicht warten will, nutzt Mail abrufen in
den Listenansichten: ein Abruf dauert rund 0,25 Sekunden je Postfach und
meldet sofort, was dazugekommen ist.
Eine gemeinsame Sperre von zehn Sekunden bremst das — nicht eine je Benutzer: Belastet wird der Mailserver, und gleichzeitige Klicks sollen daraus keine Vervielfachung machen. Die Rechteprüfung sitzt im Endpunkt; ein ausgeblendeter Knopf ist keine Sperre.
Voraussetzungen für den Versand
Ausgehende Mail wird von großen Anbietern abgelehnt, wenn die Absenderdomain sich nicht ausweisen kann. Gmail verlangt seit 2024 SPF oder DKIM:
550-5.7.26 Your email has been blocked because the sender is unauthenticated.
550-5.7.26 Gmail requires all senders to authenticate with either SPF or DKIM.
Im DNS der Absenderdomain gehören deshalb:
| Eintrag | Beispiel |
|---|---|
A |
die IP des Mailservers |
TXT (SPF) |
v=spf1 a:mail.example.org include:… ~all |
TXT default._domainkey |
der DKIM-Schlüssel des Servers |
TXT _dmarc |
v=DMARC1; p=none; rua=mailto:postmaster@example.org |
SPF muss den tatsächlich sendenden Server nennen. Ein Eintrag, der noch auf einen früheren Mailanbieter zeigt, ist der häufigste Grund für Ablehnung.
Zeiterfassung
Timer starten/stoppen oder eine Dauer von Hand buchen. Beides erzeugt eine Zeile in einem ERPNext-Timesheet, verknüpft mit Ticket, Mitarbeiter und — bei externen Tickets — dem Kunden. Der Timer liegt auf dem Server, überlebt also Neuladen und Gerätewechsel.
Nur ein Timer je Mitarbeiter. Abgesichert durch einen eindeutigen Index auf
Ticket Timer.employee. Die Prüfung im Controller liefert nur die
verständliche Meldung — zwei fast gleichzeitige Anfragen kämen an ihr vorbei,
am Index nicht.
Beim Stoppen wird gefragt. Der Dialog zeigt die gemessene Dauer, sie ist
editierbar (1:30 oder 1,5), daneben steht Verwerfen. Eine gekürzte Dauer
wird am Timer-Start verankert, nicht rückwärts von jetzt: ERPNext lässt
überlappende Zeiteinträge desselben Mitarbeiters nicht zu, und rückwärts
gerechnet liefe eine Korrektur regelmäßig in einen vorher gebuchten Eintrag.
Grenzen: mindestens eine Minute, höchstens 24 Stunden je Eintrag.
Warnschwelle: Läuft ein Timer länger als timer_warning_hours (Standard 4,
in den Einstellungen änderbar), färben sich Kopfleiste und Detailansicht.
Die Laufzeit zählt im Browser hoch — ohne Server-Abfrage. Gerechnet wird aus
der Dauer, die der Server beim letzten Abruf gemeldet hat, plus verstrichener
Browserzeit. Nicht aus start_time: Frappe liefert den Zeitstempel ohne
Zonenangabe, und der Browser liest ihn als seine eigene Zeit — bei
abweichender Zone läge die Anzeige um genau diese Differenz daneben.
Ein Timesheet je Eintrag. In ERPNext wird pro Dokument gebuc
Related HR & Payroll apps for Frappe & ERPNext
- Hrms — Open Source HR and Payroll Software
- Huf — Open-source, self-hosted multi-agent AI infrastructure for teams and apps with support for cloud and local models, tool integrations, workflows, and automation across business systems including Slack, ERPNext, Discord & Gmail.
- Bookings — Hotel Management App for Erpnext
- Employee Self Service — This is the backend component for Nesscale ESS - a mobile app that brings ERPNext to your phone. Employees can manage their HR tasks, sales activities, and projects right from their mobile devices.
- Inventory Tools — A collection of features to streamline and enhance inventory management and manufacturing workflows in ERPNext.
- Check Run — Payables utility for ERPNext
- Next Ai — NextAI is an AI-powered app for Frappe and ERPNext, delivering seamless content generation, automation, and productivity enhancements.
- Projectit — Open Source PWA mobile app to track the Employees out in the field. This mobile app is developed on Frappe Framework and it is integrated with the Project functionalities of ERPNext and integrated tightly with Frappe HR.