Skip to content

Commit

Permalink
Merge pull request #92 from vshn/refactor-summary
Browse files Browse the repository at this point in the history
Add Out-of-office hours column to employee report
  • Loading branch information
ccremer authored May 27, 2022
2 parents b123c82 + 166147a commit f3c3092
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 44 deletions.
84 changes: 54 additions & 30 deletions pkg/timesheet/dailysummary.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ type DailySummary struct {
FTERatio float64
}

type OvertimeSummary struct {
RegularWorkingTime time.Duration
SickLeaveTime time.Duration
AuthoritiesTime time.Duration
OutOfOfficeTime time.Duration
DailyMax time.Duration
PublicServiceTime time.Duration
}

// NewDailySummary creates a new instance.
// The fteRatio is the percentage (input a value between 0..1) of the employee and is used to calculate the daily maximum hours an employee should work.
// Date is expected to be in a localized timezone.
Expand Down Expand Up @@ -40,7 +49,7 @@ func (s *DailySummary) addAbsenceBlock(block AbsenceBlock) {
s.Absences = append(s.Absences, block)
}

// CalculateOvertime returns the duration of overtime.
// CalculateOvertimeSummary returns the duration of overtime.
// If returned duration is positive, then the employee did overtime and undertime if duration is negative.
//
// The overtime is then calculated according to these business rules:
Expand All @@ -49,28 +58,45 @@ func (s *DailySummary) addAbsenceBlock(block AbsenceBlock) {
// However, there's no overtime possible using excused hours
// * If the working hours exceed the theoretical daily maximum, then the excused hours are basically ignored.
// Example: it's not possible to work 9 hours, have 1 hour sick leave and expect 2 hours overtime for an 8 hours daily maximum, the overtime here is 1 hour.
func (s *DailySummary) CalculateOvertime() time.Duration {
workingTime := s.CalculateWorkingTime()
excusedTime := s.CalculateExcusedTime()
func (s *DailySummary) CalculateOvertimeSummary() OvertimeSummary {
os := OvertimeSummary{}
dailyMax := s.calculateDailyMax() - s.CalculateAbsenceTime()
os.DailyMax = dailyMax
s.calculateWorkingTime(&os)
s.calculateExcusedTime(&os)
return os
}

dailyMax := s.CalculateDailyMax() - s.CalculateAbsenceTime()
if workingTime >= dailyMax {
// Can't be on sick leave etc. if working overtime.
// Overtime returns the total overtime with all business rules respected.
func (s OvertimeSummary) Overtime() time.Duration {
excusedTime := s.ExcusedTime()
workingTime := s.WorkingTime()
if workingTime >= s.DailyMax {
// Can't be on excused hours. if working overtime.
excusedTime = 0
} else if workingTime+excusedTime > dailyMax {
} else if workingTime+excusedTime > s.DailyMax {
// There is overlap: Not enough workHours, but having excused hours = Cap at daily max, no overtime
excusedTime = dailyMax - workingTime
excusedTime = s.DailyMax - workingTime
}
overtime := workingTime + excusedTime - dailyMax
return workingTime + excusedTime - s.DailyMax
}

return overtime
// WorkingTime is the sum of OutOfOfficeTime (multiplied with 1.5) and RegularWorkingTime.
func (s OvertimeSummary) WorkingTime() time.Duration {
overtime := 1.5 * float64(s.OutOfOfficeTime)
return s.RegularWorkingTime + time.Duration(overtime)
}

// CalculateDailyMax returns the theoretical amount of hours that an employee should work on this day.
// ExcusedTime returns the sum of AuthoritiesTime, PublicServiceTime and SickLeaveTime.
func (s OvertimeSummary) ExcusedTime() time.Duration {
return s.AuthoritiesTime + s.PublicServiceTime + s.SickLeaveTime
}

// calculateDailyMax returns the theoretical amount of hours that an employee should work on this day.
// * It returns 0 for weekend days.
// * It returns 8.5 hours multiplied by FTE ratio for days in 2020 and earlier.
// * It returns 8.0 hours multiplied by FTE ratio for days in 2021 and later.
func (s *DailySummary) CalculateDailyMax() time.Duration {
func (s *DailySummary) calculateDailyMax() time.Duration {
if s.IsWeekend() {
return 0
}
Expand All @@ -81,34 +107,32 @@ func (s *DailySummary) CalculateDailyMax() time.Duration {
return time.Duration(8 * s.FTERatio * float64(time.Hour))
}

// CalculateWorkingTime accumulates all working hours from that day.
// The outside office hours are multiplied with 1.5.
func (s *DailySummary) CalculateWorkingTime() time.Duration {
workTime := time.Duration(0)
// calculateWorkingTime accumulates all working hours from that day.
func (s *DailySummary) calculateWorkingTime(o *OvertimeSummary) {
for _, shift := range s.Shifts {
diff := shift.End.Sub(shift.Start)
switch shift.Reason {
case "":
diff := shift.End.Sub(shift.Start)
workTime += diff
o.RegularWorkingTime += diff
case ReasonOutsideOfficeHours:
diff := 1.5 * float64(shift.End.Sub(shift.Start))
workTime += time.Duration(diff)
o.OutOfOfficeTime += diff
}
}
return workTime
}

// CalculateExcusedTime accumulates all hours that are excused in some way (sick leave etc) from that day.
func (s *DailySummary) CalculateExcusedTime() time.Duration {
total := time.Duration(0)
// calculateExcusedTime accumulates all hours that are excused in some way (sick leave etc) from that day.
func (s *DailySummary) calculateExcusedTime(o *OvertimeSummary) {
for _, shift := range s.Shifts {
diff := shift.End.Sub(shift.Start)
switch shift.Reason {
case ReasonSickLeave, ReasonAuthorities, ReasonPublicService:
diff := shift.End.Sub(shift.Start)
total += diff
case ReasonSickLeave:
o.SickLeaveTime += diff
case ReasonAuthorities:
o.AuthoritiesTime += diff
case ReasonPublicService:
o.PublicServiceTime += diff
}
}
return total
}

// CalculateAbsenceTime accumulates all absence hours from that day.
Expand All @@ -119,7 +143,7 @@ func (s *DailySummary) CalculateAbsenceTime() time.Duration {
// VSHN specific: Odoo treats "Unpaid" as normal leave, but for VSHN it's informational-only, meaning one still has to work.
// For every other type of absence, we add the daily max equivalent.

total += s.CalculateDailyMax()
total += s.calculateDailyMax()
}
}
return total
Expand Down
4 changes: 2 additions & 2 deletions pkg/timesheet/dailysummary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestDailySummary_CalculateOvertime(t *testing.T) {
Shifts: tt.givenShifts,
FTERatio: 1,
}
result := s.CalculateOvertime()
result := s.CalculateOvertimeSummary().Overtime()
assert.Equal(t, tt.expectedOvertime, result)
})
}
Expand Down Expand Up @@ -168,7 +168,7 @@ func TestDailySummary_CalculateDailyMaxHours(t *testing.T) {
FTERatio: tt.givenFteRatio,
Absences: tt.givenAbsences,
}
result := s.CalculateDailyMax()
result := s.calculateDailyMax()
assert.Equal(t, time.Duration(tt.expectedHours*float64(time.Hour)), result)
})
}
Expand Down
15 changes: 9 additions & 6 deletions pkg/timesheet/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ type AbsenceBlock struct {
}

type Summary struct {
TotalOvertime time.Duration
TotalExcusedTime time.Duration
TotalWorkedTime time.Duration
TotalOvertime time.Duration
TotalExcusedTime time.Duration
TotalWorkedTime time.Duration
TotalOutOfOfficeTime time.Duration
// TotalLeave is the amount of paid leave days.
// This value respects FTE ratio, e.g. in a 50% ratio a public holiday is still counted as '1d'.
TotalLeave float64
Expand Down Expand Up @@ -129,9 +130,11 @@ func (r *ReportBuilder) CalculateReport() (Report, error) {

summary := Summary{}
for _, dailySummary := range dailySummaries {
summary.TotalOvertime += dailySummary.CalculateOvertime()
summary.TotalExcusedTime += dailySummary.CalculateExcusedTime()
summary.TotalWorkedTime += dailySummary.CalculateWorkingTime()
overtimeSummary := dailySummary.CalculateOvertimeSummary()
summary.TotalOvertime += overtimeSummary.Overtime()
summary.TotalExcusedTime += overtimeSummary.ExcusedTime()
summary.TotalWorkedTime += overtimeSummary.WorkingTime()
summary.TotalOutOfOfficeTime += overtimeSummary.OutOfOfficeTime
if dailySummary.IsHoliday() {
summary.TotalLeave += 1
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/web/controller/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@ func (v BaseView) GetPreviousMonth(year, month int) (int, int) {

// FormatDailySummary returns Values with sensible format.
func (v BaseView) FormatDailySummary(daily *timesheet.DailySummary) Values {
overtimeSummary := daily.CalculateOvertimeSummary()
basic := Values{
"Weekday": daily.Date.Weekday(),
"Date": daily.Date.Format(odoo.DateFormat),
"Workload": daily.FTERatio * 100,
"ExcusedHours": v.FormatDurationInHours(daily.CalculateExcusedTime()),
"WorkedHours": v.FormatDurationInHours(daily.CalculateWorkingTime()),
"OvertimeHours": v.FormatDurationInHours(daily.CalculateOvertime()),
"ExcusedHours": v.FormatDurationInHours(overtimeSummary.ExcusedTime()),
"WorkedHours": v.FormatDurationInHours(overtimeSummary.WorkingTime()),
"OvertimeHours": v.FormatDurationInHours(overtimeSummary.Overtime()),
"LeaveType": "",
}
if daily.HasAbsences() {
Expand Down
1 change: 1 addition & 0 deletions pkg/web/employeereport/employeereport_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (v *reportView) getValuesForReport(report timesheet.Report, previousPayslip
"Leaves": report.Summary.TotalLeave,
"ExcusedHours": v.FormatDurationInHours(report.Summary.TotalExcusedTime),
"WorkedHours": v.FormatDurationInHours(report.Summary.TotalWorkedTime),
"OutOfOfficeHours": v.FormatDurationInHours(report.Summary.TotalOutOfOfficeTime),
"OvertimeHours": v.FormatDurationInHours(report.Summary.TotalOvertime),
"PreviousBalance": previousBalanceCellText,
"NextBalance": nextBalanceCellText,
Expand Down
2 changes: 1 addition & 1 deletion pkg/web/overtimereport/monthlyreport_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (v *reportView) formatMonthlySummary(s timesheet.Summary, payslip *model.Pa
func (v *reportView) GetValuesForMonthlyReport(report timesheet.Report, payslip *model.Payslip) controller.Values {
formatted := make([]controller.Values, 0)
for _, summary := range report.DailySummaries {
if summary.IsWeekend() && summary.CalculateWorkingTime() == 0 {
if summary.IsWeekend() && summary.CalculateOvertimeSummary().WorkingTime() == 0 {
continue
}
formatted = append(formatted, v.FormatDailySummary(summary))
Expand Down
2 changes: 1 addition & 1 deletion pkg/web/reportconfig/config_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type ConfigView struct {
func (v *ConfigView) GetConfigurationValues(report timesheet.Report) controller.Values {
formatted := make([]controller.Values, 0)
for _, summary := range report.DailySummaries {
if summary.IsWeekend() && summary.CalculateWorkingTime() == 0 {
if summary.IsWeekend() && summary.CalculateOvertimeSummary().WorkingTime() == 0 {
continue
}
formatted = append(formatted, v.FormatDailySummary(summary))
Expand Down
2 changes: 2 additions & 0 deletions templates/employeereport.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ <h1>Attendances for {{ .Month }} {{ .Year }}</h1>
<th scope="col">Leaves</th>
<th scope="col">Excused hours</th>
<th scope="col">Worked hours</th>
<th scope="col">(Out of office hours, real)</th>
<th scope="col">{{ .LastMonth }} Payslip</th>
<th scope="col">Overtime delta</th>
<th scope="col">Proposed balance</th>
Expand All @@ -89,6 +90,7 @@ <h1>Attendances for {{ .Month }} {{ .Year }}</h1>
<td>{{ .Leaves }}d</td>
<td>{{ .ExcusedHours }}</td>
<td>{{ .WorkedHours }}</td>
<td>{{ .OutOfOfficeHours }}</td>
<td>{{ .PreviousBalance }}</td>
<td>{{ .OvertimeHours }}</td>
<td>{{ .ProposedBalance }}</td>
Expand Down
8 changes: 7 additions & 1 deletion templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
<link href="/static/bootstrap.min.css" rel="stylesheet">
<script src="/static/bootstrap.min.js" rel="script"></script>
</head>

<style>
@media (min-width: 2000px) {
.container{
max-width: 2000px;
}
}
</style>
<body>
<main class="container">
{{ template "nav" . }}
Expand Down

0 comments on commit f3c3092

Please sign in to comment.