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.

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:

[![Listed on Frappe Gems](https://frappegems.com/api/method/frappe_gems.seo.badge?app=saschafo%2Fticket_billing)](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

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:

  1. DocPerms als Obergrenze (was eine Rolle überhaupt darf).
  2. User Permission auf Department — wird aus Employee.department gespiegelt (doc_events/employee.py) und von Frappe automatisch auf jede Abfrage angewandt.
  3. permission_query_conditions und has_permission (permissions.py) für die zeilenweise Einschränkung. Das greift für Desk, Report, frappe.get_list und 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_employee trägt ignore_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.


E-Mail

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):

  1. Abteilung des empfangenden Postfachs — hat Vorrang, auch wenn im Feld schon etwas steht.
  2. was am Ticket steht (bei Anlage in der Oberfläche),
  3. die Standardabteilung aus den Einstellungen,
  4. 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.