Urlaube nach Personio exportieren
von Alexander Huber
Die Erfassung und Verwaltung von Arbeitszeiten ist ein wesentliches Element moderner Unternehmen, doch sie bringt auch Herausforderungen mit sich. Verschiedene Anforderungen führen oft dazu, dass mehrere Systeme parallel genutzt werden müssen: eines für allgemeine Arbeitszeiterfassung (Dokumentation von Arbeitszeit, Basis für Lohnverrechnung, Einhaltung gesetzlicher Vorschriften) und ein weiteres für projektbezogene Zeiterfassung.
Unsere Erfahrung zeigt, dass es in solchen Fällen entscheidend ist, die Zusammenarbeit dieser Systeme optimal zu gestalten. So können redundante Prozesse reduziert und Synergien geschaffen werden, die sowohl Effizienz als auch Transparenz fördern.
Herausforderungen und Potenziale der parallelen Zeiterfassungssysteme
Die Erfassung von Arbeitszeiten ist ein unverzichtbarer Bestandteil des betrieblichen Alltags. Arbeitgeber stehen jedoch oft vor der Herausforderung, unterschiedliche Systeme parallel zu betreiben: Ein Tool wie Personio verwaltet Anwesenheitszeiten, Urlaube und Abwesenheiten. Gleichzeitig bieten spezialisierte Projektzeiterfassungstools tiefere Einblicke in projektbezogene Arbeitszeiten. Diese Parallelstruktur bringt oft administrative Doppelarbeit mit sich, aber eröffnet auch Potenziale für Automatisierung und Prozessoptimierung.
In diesem Artikel beleuchten wir:
- die Hauptfunktionen von Personio und Projektzeiterfassungstools anhand von time cockpit,
- die Unterschiede zwischen Arbeitszeit- und Projektzeiterfassung,
- und wie Schnittstellen diese Welten verbinden können.
Personio – HR-Management im Fokus
Personio ist eine umfassende HR-Softwarelösung, die speziell für kleine und mittelständische Unternehmen entwickelt wurde. Die Plattform deckt alle wesentlichen Bereiche des Personalmanagements ab und bietet folgende Hauptfunktionen:
- Personalverwaltung: Zentralisierung aller Mitarbeiterdaten in einer digitalen Personalakte, Verwaltung von Abwesenheiten, Arbeitszeiterfassung und Dokumentenverwaltung (Personio – Personalverwaltung).
- Recruiting: Unterstützung des gesamten Bewerbungsprozesses, einschließlich Erstellung und Veröffentlichung von Stellenausschreibungen, Bewerbermanagement und Onboarding neuer Mitarbeiter (Personio – Recruiting).
- Lohn- und Gehaltsabrechnung: Vorbereitung der Lohnabrechnung mit Integration zu DATEV, um den Abrechnungsprozess zu optimieren und Fehler zu minimieren (Personio – Gehaltsabrechnung).
- Performance & Entwicklung: Verwaltung von Mitarbeiterleistungen und -entwicklungen durch Feedbackgespräche, Zielsetzungen und Schulungsorganisation (Personio – Performance & Entwicklung.
- Workflow-Automatisierung: Automatisierung wiederkehrender HR-Prozesse, um Effizienz zu steigern und manuelle Fehler zu reduzieren (Personio – Workflow-Automatisierung).
Zusätzlich bietet Personio über 200 Integrationen mit anderen Tools, um nahtlose Arbeitsabläufe zu gewährleisten (Personio – Integrationen). Die Software ist cloudbasiert und ermöglicht somit den Zugriff von überall, was besonders für Unternehmen mit Remote-Arbeitsplätzen von Vorteil ist.
Projektzeiterfassung allgemein
Die Projektzeiterfassung dokumentiert Arbeitszeiten systematisch für Aufgaben oder Projekte. Sie schafft Transparenz, optimiert Ressourcen und stellt sicher, dass Projekte effizient umgesetzt und Budgets eingehalten werden.
Basis für Abrechnung und Rentabilität
Für Dienstleister ist sie essenziell, um erbrachte Leistungen korrekt abzurechnen. Kunden erhalten klare Nachweise über den Zeitaufwand, während Unternehmen die Rentabilität ihrer Projekte analysieren können.
Erfolgsfaktoren
Einfache Tools, klare Prozesse und intuitive Software sind entscheidend für eine effiziente Umsetzung und Akzeptanz im Team. So wird Projektzeiterfassung praktisch und wertvoll.
Arbeitszeiterfassung vs. Projektzeiterfassung
Die Arbeitszeiterfassung konzentriert sich auf die Erfassung von Anwesenheitszeiten und Abwesenheiten, um gesetzliche und interne Vorgaben einzuhalten. Sie beantwortet grundlegende Fragen wie:
- Wann hat der Mitarbeiter begonnen und aufgehört zu arbeiten? (z. B. Arbeitsbeginn um 08:00 Uhr und Ende um 16:30 Uhr mit einer einstündigen Mittagspause).
- Wie viele Stunden wurden insgesamt pro Tag geleistet? (z. B. 7,5 Stunden an einem regulären Arbeitstag).
- Gab es Überstunden oder Fehlzeiten? (z. B. 2 Überstunden oder ein Fehltag aufgrund von Krankheit).
Die Projektzeiterfassung hingegen bietet eine detailliertere Perspektive und adressiert Fragestellungen wie:
- Welche Aufgaben wurden bearbeitet? (z. B. Erstellung einer Marketingkampagne oder Behebung eines Softwarefehlers).
- Wie lange wurde an einem bestimmten Projekt oder für einen Kunden gearbeitet? (z. B. 4 Stunden für Projekt „Webseiten-Redesign“ eines Kunden).
- Wie effizient wurde die Zeit genutzt? (z. B. 80 % der Zeit wurden produktiv genutzt, während 20 % für Meetings oder Wartezeiten auf Feedback anfielen).
- Welches Budget wurde für welche Projekte oder Kunden verbraucht? (z. B. 15 Stunden von insgesamt 40 budgetierten Stunden für ein Projekt verbraucht).
Ergänzende Integration
Eine Integration beider Systeme ermöglicht es Unternehmen, sowohl die Einhaltung von Arbeitszeitgesetzen als auch die Effizienz und Rentabilität von Projekten im Blick zu behalten. Beispielsweise können Mitarbeiterzeiten automatisch vom Projekt- ins Arbeitszeiterfassungssystem übernommen werden, wodurch doppelte Eingaben vermieden und Fehler reduziert werden.
Potenzielle Herausforderungen bei der parallelen Nutzung
Unternehmen, die sowohl Arbeitszeiterfassung als auch ein Projektzeiterfassungstool einsetzen, stehen häufig vor den folgenden Herausforderungen:
Redundante Dateneingaben: Mitarbeiter müssen ihre Arbeitszeiten separat in beiden Systemen erfassen, z. B. Anwesenheitszeiten in der Arbeitszeiterfassung und projektbezogene Stunden in der Projektzeiterfassung. Dies führt zu doppeltem Aufwand und Unzufriedenheit bei den Nutzern.
Fehleranfälligkeit: Mehrfache Eingaben erhöhen das Risiko von Abweichungen, z. B. wenn ein Mitarbeiter unterschiedliche Arbeitszeiten in beiden Systemen einträgt oder vergisst, eine Änderung zu synchronisieren. Dies kann zu Inkonsistenzen in Berichten und Abrechnungen führen.
Erhöhter Verwaltungsaufwand: Die fehlende Integration der Systeme bedeutet, dass Daten manuell abgeglichen werden müssen. Dies betrifft vor allem Personalabteilungen und Projektmanager, die sicherstellen müssen, dass alle Daten korrekt und synchron sind. Dies kostet Zeit und bindet Ressourcen.
Eine fehlende Integration der beiden Systeme kann die Effizienz beeinträchtigen, die Transparenz verringern und die Benutzerakzeptanz negativ beeinflussen.
Schnittstellen als Lösung
In diesem Blogartikel zeigen wir, wie die genannten Herausforderungen durch eine Integration von Arbeitszeit- und Projektzeiterfassungssystemen gelöst werden können. Als HR-Tool für die Arbeitszeiterfassung nutzen wir Personio, während time cockpit als Beispiel für ein Projektzeiterfassungssystem dient.
Eine Schnittstelle zwischen beiden Systemen ermöglicht es Unternehmen, Prozesse zu optimieren und die Effizienz zu steigern. Gleichzeitig werden Datensilos aufgebrochen, wodurch redundante Eingaben vermieden und eine konsistente Datenbasis geschaffen werden können.
Lösungsansatz
Time cockpit wird in diesem Lösungsansatz als führendes System für Zeitbuchungen und Urlaube eingesetzt, da Mitarbeiter ihre Zeiten nicht nur auf Projekte, sondern auch auf detaillierte Tätigkeiten erfassen. Diese granularen Buchungen sind entscheidend, um die Transparenz und Genauigkeit der projekt- und leistungsbezogenen Zeiterfassung zu gewährleisten.
In diesem Post zeigen wir den Export von Urlauben von time cockpit nach Personio. In einem Folge-Post zeigen wir wie Zeitbuchungen nach Personio übertragen werden können.
Die Umsetzung erfolgt mithilfe von Azure Functions, einer skalierbaren und kosteneffizienten Lösung. Die Azure Function synchronisiert stündlich Zeitbuchungen und Urlaube zwischen Time Cockpit und Personio. Die Architektur umfasst folgende Azure-Ressourcen:
- Azure Function: Für die Ausführung der regelmäßigen Synchronisationsjobs.
- Azure Key Vault: Für die sichere Speicherung von Geheimnissen wie dem Time Cockpit PAT und dem Personio Client Secret.
- Application Insights: Zur Protokollierung und Überwachung der Funktionsausführung.
Um die Kommunikation mit den beiden Systemen zu ermöglichen, wird jeweils ein Client für jedes System implementiert. Da der Funktionsumfang der Clients überschaubar ist, entwickeln wir diese manuell und verzichten auf die Generierung von Client-Bibliotheken, beispielsweise mit Tools wie AutoRest.
API-Integration: PersonioClient
Um sich bei Personio zu authentifizieren, muss zunächst eine Integration in Personio erstellt werden. Dazu sind folgende Schritte erforderlich:
- Navigieren zu den Einstellungen: Gehe zu Einstellungen → API-Zugriffsdaten.
- Integration erstellen: Erstelle eine individuelle Integration und vergebe einen aussagekräftigen Namen.
- Berechtigungen zuweisen: Wähle die benötigten Rechte aus. In unserem Fall sind dies:
- Mitarbeitende lesen
- Anwesenheiten lesen und bearbeiten
- Abwesenheiten lesen und bearbeiten
Nachdem das Client Secret für Personio generiert wurde, sollte dieses sicher abgelegt werden. Wir verwenden dafür Azure Key Vault, um eine sichere Speicherung zu gewährleisten.
Der folgende Code zeigt, wie der PersonioClient
für die Dependency Injection in .NET Core eingerichtet wird.
Wichtig: Stelle sicher, dass bei jedem Request die Partner ID und die App ID übermittelt werden.
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureAppConfiguration(builder =>
{
....
})
.ConfigureServices((hostContext, services) =>
{
...
services.AddHttpClient("Personio", httpClient =>
{
httpClient.BaseAddress = new Uri(config["PersonioBaseUri"]);
httpClient.DefaultRequestHeaders.Add("X-Personio-Partner-ID", "YOUR-PARTNER-ID");
httpClient.DefaultRequestHeaders.Add("X-Personio-App-ID", "TC");
SetToken(httpClient, config);
});
...
})
.Build();
host.Run();
Die Methode SetToken()
dient dazu, das Client Secret gegen ein Bearer Token auszutauschen, das anschließend bei jedem Request mitgegeben werden muss. Dabei wird das Client Secret an den entsprechenden Authentifizierungsendpunkt gesendet, um ein gültiges Bearer Token zu erhalten.
Nach erfolgreichem Austausch wird das Bearer Token im Authorization-Header des HTTP-Clients hinterlegt, sodass es bei allen nachfolgenden API-Anfragen automatisch mitgesendet wird. Es ist essenziell sicherzustellen, dass das Token vor dem Versenden von Requests gesetzt wird, da ohne ein gültiges Token keine authentifizierten Anfragen ausgeführt werden können.
Disclaimer: In diesem Beispiel wird das Refresh des Tokens nicht berücksichtigt. Dies liegt daran, dass die Laufzeit der Function relativ kurz ist und das Token in diesem Zeitraum in der Regel gültig bleibt.
void SetToken(HttpClient httpClient, IConfiguration configuration)
{
var body = new
{
client_id = configuration["PersonioClientId"],
client_secret = configuration["PersonioClientSecret"]
};
var json = JsonConvert.SerializeObject(body);
var payload = new StringContent(json, Encoding.UTF8, "application/json");
var response = httpClient.PostAsync("auth", payload).Result;
var responseContent = response.Content.ReadAsStringAsync().Result;
if (!response.IsSuccessStatusCode)
{
throw new Exception(responseContent);
}
JObject jsonObject = JObject.Parse(responseContent);
var token = (string)jsonObject["data"]["token"];
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
Aufgaben des PersonioClient
Der PersonioClient
dient als Verbindung zur Personio-API und ermöglicht die effiziente Verwaltung von Anwesenheiten und Abwesenheiten. Die Hauptaufgaben umfassen:
- Datenabruf: Abfragen von Urlaubs- und Anwesenheitsdaten spezifischer Mitarbeitender.
- Datenübertragung: Erstellen, Aktualisieren und Löschen von Einträgen in Personio.
Aufbau des Clients
Der PersonioClient
verwendet die Constructor Dependency Injection von .NET Core, um eine klare Trennung der Verantwortlichkeiten und eine einfache Testbarkeit zu gewährleisten.
Ein Beispiel für den Aufbau ist der Konstruktor:
Im Program.cs werden sowohl der Client für Personio als auch der für time cockpit initialisiert. Dabei kommt die HttpClientFactory
zum Einsatz, die den gewünschten HttpClient
bereitstellt und automatisch verwaltet. Dieser wird anschließend in den PersonioClient
injiziert.
Dieser Ansatz fördert die Wiederverwendbarkeit und sorgt dafür, dass alle Abhängigkeiten zentral konfiguriert und bereitgestellt werden können.
public PersonioClient(IHttpClientFactory httpClientFactory, ILogger<PersonioClient> logger, IOptions<Settings> configuration)
{
this.httpClient = httpClientFactory.CreateClient("Personio");
this.configuration = configuration;
this.logger = logger;
}
Die zentrale Methode des PersonioClient
ist GetVacationsOfUser
. Diese Methode ruft mithilfe der Personio-API die Abwesenheiten eines bestimmten Benutzers ab.
Ein besonderes Augenmerk liegt auf der Handhabung größerer Datenmengen: Die Methode ist so implementiert, dass sie Paging unterstützt. Dadurch wird sichergestellt, dass auch bei umfangreichen Datensätzen, wie beispielsweise einer hohen Anzahl von Abwesenheitseinträgen, die Abfrage effizient und performant bleibt.
Abrufen von Urlaubsdaten
internal async Task<List<TimeOffPeriod>> GetVacationsOfUser(
string personioId,
Func<TimeOffPeriod, bool> filterPredicate)
{
List<TimeOffPeriod> vacations = new List<TimeOffPeriod>();
const int limit = 200; // Max items per page
int offset = 1; // Start at the beginning
while (true)
{
var url = $"company/time-offs?limit={limit}&offset={offset}";
var response = await this.httpClient.GetAsync(url);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
// Assuming the API returns an object that includes both the data and pagination info
var pageData = JsonConvert.DeserializeObject<PersonioResponse<TimeOffPeriod>>(responseContent);
// Add the retrieved vacations to the list
if (pageData?.Data != null)
{
vacations.AddRange(pageData.Data); // Assuming Data is a list of objects representing vacations
}
// Check if all pages have been retrieved
if (pageData?.Metadata?.CurrentPage >= pageData?.Metadata?.TotalPages)
{
break; // Exit the loop if there are no more pages to fetch
}
// Prepare for the next request
offset += pageData.Metadata.CurrentPage; // Adjust the offset for the next page
}
else
{
throw new Exception($"Failed to fetch data: {responseContent}");
}
}
// Apply the filter predicate passed as a parameter
return vacations?.Where(filterPredicate).ToList();
}
Hier ist ein Beispiel, das zeigt, wie ein Urlaubsobjekt erstellt und an Personio gesendet wird:
var payload = new
{
employee_id = user.USR_PersonioId.ToString(),
time_off_type_id = user.USR_IsExternal ? "2567057" : "2228792",
start_date = tcVacation.APP_BeginTime.Value.ToString("yyyy-MM-dd"),
end_date = tcVacation.APP_EndTime.Value.ToString("yyyy-MM-dd"),
skip_approval = true,
half_day_start = !tcVacation.APP_IsWholeDay.Value && tcVacation.APP_BeginTime < tcVacation.APP_BeginTime.Value.Date.AddHours(12),
half_day_end = !tcVacation.APP_IsWholeDay.Value && tcVacation.APP_BeginTime >= tcVacation.APP_BeginTime.Value.Date.AddHours(12),
};
public async Task<HttpResponseMessage> PostAttendance(object payload)
{
var jsonPayload = JsonConvert.SerializeObject(payload);
var jsonContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"company/attendances", jsonContent);
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new Exception(content);
}
return response;
}
public async Task<HttpResponseMessage> DeleteTimeOffPeriod(string id)
{
var response = await httpClient.DeleteAsync($"company/time-offs/{id}");
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
this.logger.LogWarning($"Could not find and delete time off period with id {id} in personio.");
}
else
{
var content = await response.Content.ReadAsStringAsync();
throw new Exception(content);
}
}
return response;
}
API-Integration: TimeCockpitClient
Um sich bei time cockpit zu authentifizieren, wird ein Personal Access Token (PAT) benötigt. Wie ein solches Token erstellt wird, können Sie der offiziellen Anleitung entnehmen: time cockpit Web API - Authentication.
Sicherer Umgang mit dem PAT
Nach der Erstellung des PAT empfehlen wir dringend, es sicher abzulegen. Für den Produktbetrieb ist die Ablage im Azure Key Vault ratsam, um eine sichere und zentralisierte Verwaltung von Zugangsdaten zu gewährleisten.
Integration des PAT in den TimeCockpitClient
Der TimeCockpitClient
wird für die Dependency Injection in .NET Core konfiguriert. Da time cockpit ein PAT verwendet, das nicht in ein Token umgewandelt werden muss, kann das Token direkt im Authorization-Header als Bearer Token gesetzt werden. Dies vereinfacht die Integration und sorgt für eine direkte und unkomplizierte Authentifizierung bei der API.
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureAppConfiguration(builder =>
{
...
})
.ConfigureServices((hostContext, services) =>
{
services.AddHttpClient("TimeCockpit", httpClient =>
{
httpClient.BaseAddress = new Uri(config["TimeCockpitBaseUri"]);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue($"Bearer", $"{config["TimeCockpitDataApiPAT"]}");
});
})
.Build();
host.Run();
Das Personal Access Token (PAT) wird direkt im Authorization-Header als Bearer Token gesetzt, wodurch der Authentifizierungsprozess vereinfacht wird.
Nachdem wir den HttpClient
eingerichtet haben, um mit time cockpit zu interagieren, kapseln wir die gesamte Interaktion mit der API in der Klasse TimeCockpitClient
.
Aufgaben des TimeCockpitClient
Der TimeCockpitClient
dient als zentrale Schnittstelle zur time cockpit-API und ermöglicht eine klare Trennung der API-Kommunikation vom restlichen Anwendungscode. Die Hauptaufgaben sind:
- Abrufen von Daten: Der Client ermöglicht den Zugriff auf Zeitbuchungen und andere relevante Daten aus time cockpit.
- CRUD-Operationen: Unterstützt das Erstellen, Aktualisieren und Löschen von Einträgen, um eine vollständige Datenverwaltung zu gewährleisten.
Aufbau des Clients
Der TimeCockpitClient
ist modular gestaltet und nutzt HttpClient
. Durch die Verwendung von Constructor Dependency Injection in .NET Core wird sichergestellt, dass der Client leicht konfiguriert und getestet werden kann.
public TimeCockpitClient(IHttpClientFactory httpClientFactory, ILogger<TimeCockpitClient> logger, IOptions<Settings> configuration)
{
httpClient = httpClientFactory.CreateClient("TimeCockpit");
this.configuration = configuration;
baseUri = this.configuration.Value.TimeCockpitBaseUri;
this.logger = logger;
}
Der TimeCockpitClient
dient als zentrale Schnittstelle zur Time Cockpit API und bietet umfassende Funktionalitäten zur Interaktion mit der Plattform.
Nutzung des dynamischen Datenmodells
Da time cockpit ein dynamisches Datenmodell bietet (Details zum dynamischen Datenmodell), setzen wir für dieses Beispiel den Query-Endpoint der API ein (Details zur Query-Schnittstelle). Dieser Endpunkt erlaubt es, flexibel auf verschiedene Datenstrukturen zuzugreifen und komplexe Abfragen durchzuführen.
TCQL-Integration
Der TimeCockpitClient
unterstützt das Absetzen von TCQL-Queries (Time Cockpit Query Language). Diese speziell für time cockpit entwickelte Abfragesprache ermöglicht es, Datenabfragen auf das dynamische Datenmodell abzusetzen. Mithilfe folgender Methode können Entwickler TCQL-Queries ausführen, um Informationen aus der Plattform abzurufen.
Dieser Ansatz nutzt die volle Flexibilität des dynamischen Datenmodells von time cockpit, ohne die Anwendung unnötig komplex zu machen.
public async Task<List<T>> SelectAsync<T>(string query, List<Tuple<string, string>> parameters = null) where T : IJsonDeserializable<T>
{
JObject jsonRequest = new JObject(
new JProperty("query", query),
new JProperty("parameters", parameters?.ToDictionary(p => p.Item1, p => p.Item2)));
Func<JObject, List<T>> deserialize = json =>
json["value"].Select(item => T.FromJson(item)).ToList();
return await GetResult(jsonRequest, deserialize);
}
Die Methode SelectAsync
wird genutzt, um Abfragen an die time cockpit API zu stellen. Im folgenden Beispiel wird gezeigt, wie alle Zeitbuchungen abgefragt werden können, die seit einem bestimmten Datum erstellt oder geändert wurden.
Die Rückgabe erfolgt in Form von JSON-Daten, die mithilfe von Json.NET generisch in ein Plain Old CLR Object (POCO), wie z. B. TcTimesheet
, deserialisiert werden.
Die Kombination aus SelectAsync
und der generischen Deserialisierung bietet maximale Flexibilität und Wiederverwendbarkeit, da die Methode mit unterschiedlichen TCQL-Queries und Rückgabestrukturen arbeiten kann.
var createdOrUpdates = await this.timeCockpitClient.SelectAsync<TcTimesheet>(
"From V In Timesheet Where V.UserDetail.UserDetailUuid = @Id " +
"And (V.CreationDateUtc >= @LastExportedDate " +
"Or V.UpdateDateUtc >= @LastExportedDate) " +
"Select V",
new List<Tuple<string, string>> {
Tuple.Create("LastExportedDate",$"datetime'{lastExportedDate.Value:yyyy-MM-ddTHH:mm:ss}'"),
Tuple.Create("Id", $"guid'{user.APP_UserDetailUuid}'")
});
Überblick über den Synchronisationsprozess von Urlauben
Der Synchronisationsprozess erfolgt im Rahmen eines stündlichen Datenabgleichs. Dabei wird wie folgt vorgegangen:
Bestimmung des letzten Exports:
- Bei jedem Export prüft time cockpit, wann zuletzt ein erfolgreicher Export nach Personio durchgeführt wurde.
- Wurde noch nie ein Export durchgeführt, wird das
lastExportDate
initial auf den 1. Januar 1970 gesetzt. In diesem Fall werden alle Urlaube in time cockpit für den Export berücksichtigt.
Abruf der Urlaube aus Personio:
- Alle Urlaube eines bestimmten Benutzers werden aus Personio abgerufen und in einem Dictionary organisiert, basierend auf der Personio-ID der Urlaube.
Abgleich der Urlaube aus time cockpit:
- Für jeden Urlaub der nach
lastExportDate
angelegt oder geändert wurde wird geprüft, ob dieser bereits nach Personio exportiert wurde. - Diese Prüfung erfolgt anhand des USR_PersonioId-Feldes, das mithilfe des anpassbaren Datenmodells von time cockpit leicht zur Entität APP_Vacation hinzugefügt werden kann. Dieses Feld speichert die interne ID von Personio für jeden Urlaub.
- Für jeden Urlaub der nach
Handling von Änderungen:
- Falls ein geänderter Urlaub aus time cockpit bereits in Personio vorhanden ist, wird dieser in Personio gelöscht.
Aktuell (Stand: 20.1.2025) unterstützt Personio lediglich das Einfügen und Löschen von Urlaubseinträgen, nicht deren direkte Aktualisierung. Um diese Tatsache zu umgehen, wird der geänderte Eintrag in Personio gelöscht und anschließend neu angelegt. Die neue Personio-ID wird danach in time cockpit im Feld USR_PersonioId gespeichert.
- Anlegen neuer Urlaube:
- Jeder in time cockpit geänderte Urlaub wird nach Personio exportiert. Anschließend wird die neue Personio-ID zurück in time cockpit gespeichert.
private async Task UpsertVacationsInPersonio(DateTime? lastExportedDate, TcUserDetail user)
{
var tcVacations = await this.timeCockpitClient.SelectAsync<TcVacation>(
"From V In Vacation Where V.UserDetail.UserDetailUuid = @Id " +
"And (V.CreationDateUtc >= @LastExportedDate " +
"Or V.UpdateDateUtc >= @LastExportedDate) " +
"Select V",
new List<Tuple<string, string>>() {
Tuple.Create("LastExportedDate",$"datetime'{lastExportedDate.Value.ToString("yyyy-MM-dd")}'"),
Tuple.Create("Id", $"guid'{user.APP_UserDetailUuid.ToString()}'")
});
Func<TimeOffPeriod, bool> filter = x => x.Attributes.TimeOffType.Attributes.Category == "paid_vacation"
|| x.Attributes.TimeOffType.Attributes.Category == "unpaid_vacation";
var personioAbsences = (await this.personioClient.GetAbsencensOfUser(user.USR_PersonioId, filter)).ToDictionary(x => x.Attributes.Id.ToString());
foreach (var tcVacation in tcVacations)
{
if (tcVacation.USR_PersonioId != null && personioAbsences.TryGetValue(tcVacation.USR_PersonioId, out var timeOffPeriod))
{
// there is no update in personio
await this.personioClient.DeleteTimeOffPeriod(timeOffPeriod.Attributes.Id.ToString());
}
var newTimeOffPeriod = new
{
employee_id = user.USR_PersonioId.ToString(),
time_off_type_id = user.USR_IsExternal ? "2567057" : "2228792",
start_date = tcVacation.APP_BeginTime.Value.ToString("yyyy-MM-dd"),
end_date = tcVacation.APP_EndTime.Value.ToString("yyyy-MM-dd"),
skip_approval = true,
half_day_start = !tcVacation.APP_IsWholeDay.Value && tcVacation.APP_BeginTime < tcVacation.APP_BeginTime.Value.Date.AddHours(12),
half_day_end = !tcVacation.APP_IsWholeDay.Value && tcVacation.APP_BeginTime >= tcVacation.APP_BeginTime.Value.Date.AddHours(12),
};
var response = await this.personioClient.PostTimeOffPeriod(newTimeOffPeriod);
if (response.IsSuccessStatusCode)
{
var allJsonData = await response.Content.ReadAsStringAsync();
var timeOffPeriodJson = JsonConvert.DeserializeObject<TimeOffPeriod>(JObject.Parse(allJsonData)["data"].ToString());
tcVacation.USR_PersonioId = timeOffPeriodJson.Attributes.Id.ToString();
await this.timeCockpitClient.PatchAsync("APP_Vacation", tcVacation.APP_VacationUuid, tcVacation);
}
else
{
this.logger.LogError($"Could not create absence for vacation with id {tcVacation.APP_VacationUuid}");
}
}
}
Nützliche Tipps:
Integration mit Azure Key Vault
Diese Lösung nutzt Azure Key Vault zur sicheren Speicherung von Secrets und Konfigurationseinstellungen. Die Verwendung von Key Vault bietet sowohl im Produktivbetrieb als auch in der lokalen Entwicklung erhebliche Vorteile:
builder.AddAzureKeyVault(
new Uri(builtConfig["VaultUri"]),
new DefaultAzureCredential(
new DefaultAzureCredentialOptions
{
ExcludeManagedIdentityCredential = false,
ExcludeAzureCliCredential = false,
ExcludeInteractiveBrowserCredential = true,
}
)
);
Vorteile der Key Vault Integration
- Zentrale Verwaltung: Secrets und Konfigurationen werden zentral im Key Vault gespeichert, sodass Änderungen sofort für alle Umgebungen wirksam sind.
- Sicherheit: Es müssen keine Secrets lokal in Dateien wie
local.settings.json
oder den User Secrets gespeichert werden, wodurch das Risiko von Leaks minimiert wird. - Konsistenz: Lokale Entwicklung und produktive Umgebung greifen auf die gleichen Konfigurationseinstellungen zu, was Konfigurationsunterschiede vermeidet.
Einsatz im Produktivbetrieb
Im Produktivbetrieb erfolgt der Zugriff auf den Key Vault mittels Managed Identity. Dadurch entfällt die Notwendigkeit, Zugangsdaten in der Anwendung oder Umgebung zu speichern.
Nutzung in der lokalen Entwicklung
Auch für die lokale Entwicklung wird Key Vault verwendet. Der Zugriff ist einfach zu konfigurieren:
- Berechtigungen: Der Benutzer muss als Key Vault Secrets User berechtigt werden.
- Konfiguration: Die Vault URI wird in der Datei
local.settings.json
hinterlegt. Dies ist die einzige lokale Einstellung, die erforderlich ist.
💡Da Secrets nicht lokal gespeichert werden, ist es nicht möglich, versehentlich sensible Daten in Versionskontrollsysteme wie Git einzuchecken. Gleichzeitig bleiben die Entwicklungs- und Produktionsumgebungen synchron, da beide auf denselben Key Vault zugreifen.
Die Methode AddAzureKeyVault
registriert Azure Key Vault als Konfigurationsquelle in einer .NET-Anwendung. Dadurch können Secrets und Konfigurationseinstellungen direkt aus dem Key Vault abgerufen und in der Anwendung genutzt werden, ohne sie lokal speichern zu müssen.
Fazit
Die Integration von Personio und time cockpit bietet eine pragmatische Lösung, um den Herausforderungen moderner Arbeitszeit- und Projektzeiterfassung zu begegnen. Durch die Nutzung von Azure-basierten Technologien wie Azure Functions, Azure Key Vault und Application Insights können Unternehmen effiziente, sichere und skalierbare Prozesse implementieren.
Die beschriebenen Synchronisationsmechanismen zeigen, wie sich durch die Verbindung zweier spezialisierter Systeme Synergien schaffen lassen. Dabei ermöglicht die Personio-Schnittstelle die Verwaltung von Anwesenheits- und Abwesenheitszeiten in Bezug auf HR-Themen, während time cockpit mit seiner granularen Projektzeiterfassung tiefere Einblicke in Aufgaben und Budgets liefert. Die stündliche Synchronisation stellt sicher, dass beide Systeme stets konsistent bleiben, ohne manuellen Aufwand oder erhöhte Fehleranfälligkeit.