Abrechnungen werden bei avocado software engineering mit einer eigens dafür erstellten Web Application erstellt. Auch intern vereinfachen wir Abläufe stetig und gestalten sie somit effizienter und smarter. Das bereits bestehende Abrechnungstool sollte durch eine neuere Version abgelöst werden.
Das Softwaretool deckt die folgenden drei Hauptbereiche ab:
- Abrechnungen für Kunden sollen im Tool erstellt werden.
- Einzelne Zeiteinträge von Redmine (unser Ticket-Managementsystem) sollen neuen Abrechnungen zugeordnet werden.
- Die noch nicht abgerechneten Zeiten und somit offenen Zeiten sollen eingesehen und verwaltet werden.
Oberfläche des Tools
Das Dashboard
Über das Dashboard können bequem neue Abrechnungen erstellt werden, oder vorhandene Abrechnungen bearbeitet werden. In dieser Ansicht ist es auch möglich sich eine Excel-Datei für eine vorhandene Abrechnung erzeugen zu lassen. Diese Excel-Liste mit allen Arbeitszeiteinträgen im gewünschten Zeitraum geht schlussendlich an unsere Kunden zusammen mit einer Rechnung heraus.
Erstellen einer neuen Abrechnung
Durch Auswahl des gewünschten Projekts, sowie des gewünschten Zeitraums, werden alle der Auswahl entsprechenden Zeiteinträge von Redmine in die Ansicht geladen. Mithilfe der Checkboxen auf der linken Seite können die einzelnen Zeiteinträge der Abrechnung hinzugefügt oder von der Abrechnung entfernt werden.
Die offenen Zeiten
Alle Zeiten, die noch nicht abgerechnet wurden, gehören zu den offenen Zeiten. Die offenen Zeiten werden nach Auswahl des gewünschten Zeitraums in einer Tabelle nach Projekt sortiert angezeigt. Über einen Button kann eine neue Abrechnung mit den noch offenen Zeiten des Projekts erstellt werden.
Technische Ausführung
Zeiteinträge von Redmine laden
Die Zeiten eines Projekts werden in der frei verfügbaren Software Redmine gespeichert. Wenn ein Zeitraum angegeben wird, und das abzurechnende Projekt ausgewählt ist, werden mit einem Aufruf an die Redmine-API die Zeiteinträge im JSON-Format geladen und in der Tabelle angezeigt. Alternativ zu der Wahl eines Zeitraums kann auch die gewünschte Anzahl an Projekttagen ausgewählt werden. Dafür werden ab dem jüngsten Zeiteintrag der Datenbank bis zum Tag der Erstellung der Abrechnung die Zeiteinträge geladen und danach nach Projekttagen gefiltert.
public getTimeEntriesWithDate(startDate: Date, endDate: Date, projectId: number, offset: number): Observable<any> {
// +1 Weil -1 Tag ausgewählt wird wegen ISO
const sDate: Date = new Date(startDate);
this.addDays(sDate, 1);
const eDate: Date = new Date(endDate);
this.addDays(eDate, 1);
const startDateFormat = sDate.toISOString().substr(0, 10);
const endDateFormat = eDate.toISOString().substr(0, 10);
return this.http.get(this.url + '/time_entries.json?' + 'project_id=' + projectId + '&from=' + startDateFormat +
'&to=' + endDateFormat + '&offset=' + offset + '&limit=100', this.getHttpOptions());
}
Die Zeiteinträge von Redmine sind folgendermaßen aufgebaut.
{
activity: Object { id: 9, name: "Entwicklung" }
comments: "Es wurde eine Liste von Heroes hinzugefügt, wobei bei einem ausgewählten Hero nähere Informationen angezeigt werden."
created_on: "2021-09-01T15:36:00Z"
custom_fields: Array [ {…} ]
hours: 2.2
id: 63056
issue: Object { id: 18478 }
project: Object { id: 168, name: "Heroes" }
spent_on: "2021-09-01"
updated_on: "2021-09-02T07:40:33Z"
user: Object { id: 39, name: "Uwe Sänger" }
}
comments: Die Beschreibung der Tätigkeit spent_on: Datum, an welchem die Zeit aufgewendet wurde hours: Die aufgewendete Zeit in Stunden project: Welchem Projekt die Zeit zugeordnet ist user: Wer den Zeiteintrag erstellt hat
Speichern einer Abrechnung
Wenn im Frontend vom User eine neue Abrechnung mit Zeiteinträgen erstellt wird, muss diese in der Datenbank abgespeichert werden. Hierfür gibt es ein Model, das wiederum ein Model für die Abrechnung und eine Liste von Models für die Zeiteinträge beinhaltet. Die Daten der Abrechnung werden im JSON-Format an das auf Java (Spring) basierende Backend versendet und dort in der Datenbank gespeichert. Die Datenbank enthält zwei für das Abrechnungstool wichtige Tabellen. Die Tabelle invoices für die Abrechnungen und die Tabelle time_entries für die Zeiteinträge, die einen Fremdschlüssel auf die Tabelle invoices besitzt, sodass die Zeiteinträge eindeutig einer Abrechnung zuordenbar sind.
public Boolean add(InvoiceWithTimeEntries invoiceWithTimeEntries) {
LocalDateTime today = LocalDateTime.now();
java.sql.Date todaySql = java.sql.Date.valueOf(today.toLocalDate());
boolean successfulInsert = dslContext.insertInto(INVOICES)
.set(INVOICES.CREATED_AT, todaySql)
.set(INVOICES.ASSIGNED_TIME, invoiceWithTimeEntries.get_entries().length)
.set(INVOICES.NAME, invoiceWithTimeEntries.get_invoice().getName())
.set(INVOICES.PROJECT_ID, invoiceWithTimeEntries.get_invoice().getProjectId())
.set(INVOICES.SUM_HOURS, (double) invoiceWithTimeEntries.get_invoice().getSumHours())
.set(INVOICES.STATUS, invoiceWithTimeEntries.get_invoice().getStatus())
.execute() > 0;
if (!successfulInsert) {
return false;
}
int id = dslContext.lastID().intValue();
List<UpdatableRecord<?>> recordList = new ArrayList<>();
for (TimeEntries timeEntries : invoiceWithTimeEntries.get_entries()) {
LocalDate createdOnLocal = this.convertToLocalDateViaInstant(timeEntries.getSpent_on());
java.sql.Date createdOn = java.sql.Date.valueOf(createdOnLocal);
UpdatableRecord<?> newRecord = (UpdatableRecord<?>) dslContext.newRecord(TIME_ENTRIES);
newRecord.set(TIME_ENTRIES.USERNAME, timeEntries.getUser().getName());
newRecord.set(TIME_ENTRIES.HOURS, timeEntries.getHours());
newRecord.set(TIME_ENTRIES.ACTIVITY, timeEntries.getActivity().getName());
newRecord.set(TIME_ENTRIES.COMMENTS, timeEntries.getComments());
newRecord.set(TIME_ENTRIES.DATE, createdOn);
newRecord.set(TIME_ENTRIES.INVOICES_ID, id);
newRecord.set(TIME_ENTRIES.ID_REDMINE, timeEntries.getId());
recordList.add(newRecord);
}
dslContext.batchStore(recordList).execute();
return true;
}
Filtern der offenen Zeiten
Um für jedes Projekt die noch offenen / noch nicht abgerechneten Zeiten anzeigen zu können, muss die Software für jedes Projekt die schon abgerechneten Zeiteinträge aus der Datenbank laden und alle Zeiteinträge von Redmine laden. Anschließend werden für jedes Projekt die Einträge von Redmine und die Einträge aus der Datenbank verglichen und nur die nicht in der Datenbank vorkommenden Zeiteinträge werden für die offenen Zeiten verwendet.
public filterOpenEntries(): void {
this.dbEntries.forEach((tupel, index) => {
tupel[1].forEach(entry => {
const tmpindex = this.redmineEntries[index][1].findIndex(timeEntry => (timeEntry.id === entry.redmineId));
if (tmpindex >= 0) {
this.redmineEntries[index][1].splice(tmpindex, 1);
}
});
});
this.setDaysAndHours();
}