From b485af2b8c0e4992380b509755b639ec107fdccd Mon Sep 17 00:00:00 2001 From: ccremer Date: Wed, 4 Jan 2023 11:44:08 +0100 Subject: [PATCH] WIP: Use timezone from attendance in report --- pkg/odoo/timezone.go | 15 ++++++++ pkg/odoo/timezone_test.go | 62 ++++++++++++++++++++++++++++++ pkg/timesheet/dailysummary.go | 5 +++ pkg/timesheet/dailysummary_test.go | 9 +++++ pkg/timesheet/report.go | 4 +- pkg/timesheet/report_test.go | 9 +++-- 6 files changed, 100 insertions(+), 4 deletions(-) diff --git a/pkg/odoo/timezone.go b/pkg/odoo/timezone.go index bddd5a0..473dcac 100644 --- a/pkg/odoo/timezone.go +++ b/pkg/odoo/timezone.go @@ -54,3 +54,18 @@ func (tz *TimeZone) IsEmpty() bool { } return false } + +// String returns the location name. +// Returns empty string if nil. +func (tz *TimeZone) String() string { + if tz == nil || tz.Location == nil { + return "" + } + return tz.Location.String() +} + +// IsEqualTo returns true if the given TimeZone is equal to other. +// If both are nil, it returns true. +func (tz *TimeZone) IsEqualTo(other *TimeZone) bool { + return tz.String() == other.String() +} diff --git a/pkg/odoo/timezone_test.go b/pkg/odoo/timezone_test.go index edf7ddc..53225c0 100644 --- a/pkg/odoo/timezone_test.go +++ b/pkg/odoo/timezone_test.go @@ -8,6 +8,24 @@ import ( "github.com/stretchr/testify/require" ) +var ( + zurichTZ *time.Location + vancouverTZ *time.Location +) + +func init() { + zue, err := time.LoadLocation("Europe/Zurich") + if err != nil { + panic(err) + } + zurichTZ = zue + van, err := time.LoadLocation("America/Vancouver") + if err != nil { + panic(err) + } + vancouverTZ = van +} + func TestTimeZone_UnmarshalJSON(t *testing.T) { tests := map[string]struct { givenInput string @@ -59,6 +77,50 @@ func TestTimeZone_MarshalJSON(t *testing.T) { } } +func TestTimeZone_IsEqualTo(t *testing.T) { + tests := map[string]struct { + givenTimeZoneA *TimeZone + givenTimeZoneB *TimeZone + expectedResult bool + }{ + "BothNil": { + givenTimeZoneA: nil, givenTimeZoneB: nil, + expectedResult: true, + }, + "BothNilNested": { + givenTimeZoneA: NewTimeZone(nil), + givenTimeZoneB: NewTimeZone(nil), + expectedResult: true, + }, + "A_IsNil": { + givenTimeZoneA: nil, + givenTimeZoneB: NewTimeZone(vancouverTZ), + expectedResult: false, + }, + "B_IsNil": { + givenTimeZoneA: NewTimeZone(vancouverTZ), + givenTimeZoneB: nil, + expectedResult: false, + }, + "BothSame": { + givenTimeZoneA: NewTimeZone(zurichTZ), + givenTimeZoneB: NewTimeZone(zurichTZ), + expectedResult: true, + }, + "A_NestedNil": { + givenTimeZoneA: NewTimeZone(nil), + givenTimeZoneB: NewTimeZone(zurichTZ), + expectedResult: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := tc.givenTimeZoneA.IsEqualTo(tc.givenTimeZoneB) + assert.Equal(t, tc.expectedResult, actual, "zone not equal: zone A: %s, zone B: %s", tc.givenTimeZoneA, tc.givenTimeZoneB) + }) + } +} + func mustLoadLocation(name string) *time.Location { loc, err := time.LoadLocation(name) if err != nil { diff --git a/pkg/timesheet/dailysummary.go b/pkg/timesheet/dailysummary.go index c1c1f49..9cda7f3 100644 --- a/pkg/timesheet/dailysummary.go +++ b/pkg/timesheet/dailysummary.go @@ -125,6 +125,11 @@ func (s *DailySummary) ValidateTimesheetEntries() error { return NewValidationError(s.Date, fmt.Errorf("the reasons for shift %s and %s should be equal: start %s (%s), end %s (%s)", model.ActionSignIn, model.ActionSignOut, shift.Start.DateTime.Format(odoo.TimeFormat), shift.Start.Reason, shift.End.DateTime.Format(odoo.TimeFormat), shift.End.Reason)) } + if !shift.Start.Timezone.IsEqualTo(shift.End.Timezone) { + return NewValidationError(s.Date, fmt.Errorf("if given, explicit timezones for attendances in a shift must be equal: start %s (%s), end: %s (%s)", + shift.Start.DateTime.Format(odoo.TimeFormat), shift.Start.Timezone, shift.End.DateTime.Format(odoo.TimeFormat), shift.End.Timezone, + )) + } totalDuration += shiftDuration } if totalDuration > 24*time.Hour { diff --git a/pkg/timesheet/dailysummary_test.go b/pkg/timesheet/dailysummary_test.go index 886e034..45ca4ad 100644 --- a/pkg/timesheet/dailysummary_test.go +++ b/pkg/timesheet/dailysummary_test.go @@ -350,6 +350,15 @@ func TestDailySummary_ValidateTimesheetEntries(t *testing.T) { }, expectedError: "the reasons for shift sign_in and sign_out should be equal: start 08:00:00 (), end 10:00:00 (Sick / Medical Consultation)", }, + "ExplicitTimezoneDifferent": { + givenShifts: []AttendanceShift{ + { + Start: model.Attendance{DateTime: odoo.NewDate(2021, 01, 02, 8, 0, 0, time.UTC), Action: model.ActionSignIn}, + End: model.Attendance{DateTime: odoo.NewDate(2021, 01, 02, 18, 0, 0, time.UTC), Action: model.ActionSignIn, Timezone: odoo.NewTimeZone(vancouverTZ)}, + }, + }, + expectedError: "if given, explicit timezones for attendances in a shift must be equal: start 08:00:00 (), end: 18:00:00 (America/Vancouver)", + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { diff --git a/pkg/timesheet/report.go b/pkg/timesheet/report.go index 414e1c1..aa44ae7 100644 --- a/pkg/timesheet/report.go +++ b/pkg/timesheet/report.go @@ -150,18 +150,20 @@ func (r *ReportBuilder) getTimeZone() *time.Location { } func (r *ReportBuilder) addAttendancesToDailyShifts(attendances model.AttendanceList, dailies []*DailySummary) { - tz := r.getTimeZone() + monthTz := r.getTimeZone() dailyMap := make(map[string]*DailySummary, len(dailies)) for _, dailySummary := range dailies { dailyMap[dailySummary.Date.Format(odoo.DateFormat)] = dailySummary } for _, attendance := range attendances.Items { + tz := attendance.Timezone.LocationOrDefault(monthTz) date := attendance.DateTime.In(tz) daily, exists := dailyMap[date.Format(odoo.DateFormat)] if !exists { continue // irrelevant attendance } + daily.Date = daily.Date.In(tz) // Update the timezone of the day var shift AttendanceShift shiftCount := len(daily.Shifts) newShift := false diff --git a/pkg/timesheet/report_test.go b/pkg/timesheet/report_test.go index 72ad3f0..00c8bed 100644 --- a/pkg/timesheet/report_test.go +++ b/pkg/timesheet/report_test.go @@ -442,8 +442,11 @@ func TestReportBuilder_CalculateReport(t *testing.T) { {DateTime: odoo.NewDate(2021, 1, 5, 10, 0, 0, zurichTZ), Action: model.ActionSignIn}, {DateTime: odoo.NewDate(2021, 1, 5, 17, 0, 0, zurichTZ), Action: model.ActionSignOut}, // 7h worked - {DateTime: odoo.NewDate(2021, 1, 7, 8, 0, 0, zurichTZ), Action: model.ActionSignIn}, - {DateTime: odoo.NewDate(2021, 1, 7, 17, 5, 0, zurichTZ), Action: model.ActionSignOut}, // faked signed out, still working though + {DateTime: odoo.NewDate(2021, 1, 7, 10, 0, 0, vancouverTZ), Action: model.ActionSignIn, Timezone: odoo.NewTimeZone(vancouverTZ)}, + {DateTime: odoo.NewDate(2021, 1, 7, 18, 0, 0, vancouverTZ), Action: model.ActionSignIn, Timezone: odoo.NewTimeZone(vancouverTZ)}, // 8h worked + + {DateTime: odoo.NewDate(2021, 1, 8, 8, 0, 0, zurichTZ), Action: model.ActionSignIn}, + {DateTime: odoo.NewDate(2021, 1, 8, 17, 5, 0, zurichTZ), Action: model.ActionSignOut}, // faked signed out, still working though }} givenLeaves := odoo.List[model.Leave]{Items: []model.Leave{ {DateFrom: odoo.NewDate(2021, 01, 06, 0, 0, 0, zurichTZ), DateTo: odoo.NewDate(2021, 01, 06, 23, 59, 0, zurichTZ), Type: &model.LeaveType{Name: TypeLegalLeavesPrefix}, State: StateApproved}, @@ -463,7 +466,7 @@ func TestReportBuilder_CalculateReport(t *testing.T) { report, err := b.CalculateReport(start, end) assert.NoError(t, err) assert.Equal(t, report.Employee.Name, givenEmployee.Name, "employee name") - assert.Equal(t, ((9+3+7+9)*time.Hour)+(5*time.Minute), report.Summary.TotalWorkedTime, "total worked time") + assert.Equal(t, ((9+3+7+9+8)*time.Hour)+(5*time.Minute), report.Summary.TotalWorkedTime, "total worked time") assert.Equal(t, ((1+3+1)*time.Hour)+(5*time.Minute), report.Summary.TotalOvertime, "total over time") assert.Equal(t, 1.0, report.Summary.TotalLeave, "total leave") assert.Equal(t, (8+2)*time.Hour, report.Summary.TotalExcusedTime, "total excused time")