From 68fdd7356264d93c8293a55364c4a959f857a97d Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 22 Nov 2021 12:06:39 +0100 Subject: [PATCH 1/3] Query the workload percentage from Odoo --- pkg/odoo/contract.go | 67 +++++++++++++++ pkg/odoo/contract_test.go | 75 +++++++++++++++++ pkg/odoo/date.go | 21 ++++- pkg/odoo/date_test.go | 42 +++++++++- pkg/odoo/json_rpc.go | 3 + pkg/odoo/leave_test.go | 10 +-- pkg/odoo/workingschedule.go | 65 ++++++++++++++ pkg/odoo/workingschedule_test.go | 50 +++++++++++ pkg/timesheet/report.go | 12 ++- ...tendance_handlers.go => report_handler.go} | 84 ++++++++++++------- 10 files changed, 390 insertions(+), 39 deletions(-) create mode 100644 pkg/odoo/contract.go create mode 100644 pkg/odoo/contract_test.go create mode 100644 pkg/odoo/workingschedule.go create mode 100644 pkg/odoo/workingschedule_test.go rename pkg/web/{attendance_handlers.go => report_handler.go} (52%) diff --git a/pkg/odoo/contract.go b/pkg/odoo/contract.go new file mode 100644 index 0000000..b573b9f --- /dev/null +++ b/pkg/odoo/contract.go @@ -0,0 +1,67 @@ +package odoo + +import ( + "fmt" + "time" +) + +type ContractList []Contract + +type Contract struct { + ID float64 `json:"id"` + Start *Date `json:"date_start"` + End *Date `json:"date_end"` + WorkingSchedule *WorkingSchedule `json:"working_hours"` +} + +func (l ContractList) GetFTERatioForDay(day Date) (float64, error) { + date := day.ToTime() + for _, contract := range l { + if contract.End.IsZero() { + // current contract + if contract.Start.ToTime().Add(-1 * time.Second).Before(date) { + return contract.WorkingSchedule.GetFTERatio() + } + continue + } + start := contract.Start.ToTime().Add(-1 * time.Second) + end := contract.End.ToTime().Add(1 * time.Second) + if start.Before(date) && end.After(date) { + return contract.WorkingSchedule.GetFTERatio() + } + } + return 0, fmt.Errorf("no contract found that covers date: %s", day.String()) +} + +func (c Client) FetchAllContracts(sid string, employeeID int) (ContractList, error) { + return c.readContracts(sid, []Filter{ + []interface{}{"employee_id", "=", employeeID}, + }) +} + +func (c Client) readContracts(sid string, domainFilters []Filter) (ContractList, error) { + // Prepare "search contract" request + body, err := NewJsonRpcRequest(&ReadModelRequest{ + Model: "hr.contract", + Domain: domainFilters, + Fields: []string{"date_start", "date_end", "working_hours"}, + }).Encode() + if err != nil { + return nil, fmt.Errorf("encoding request: %w", err) + } + + res, err := c.makeRequest(sid, body) + if err != nil { + return nil, err + } + + type readResult struct { + Length int `json:"length,omitempty"` + Records []Contract `json:"records,omitempty"` + } + result := &readResult{} + if err := c.unmarshalResponse(res.Body, result); err != nil { + return nil, err + } + return result.Records, nil +} diff --git a/pkg/odoo/contract_test.go b/pkg/odoo/contract_test.go new file mode 100644 index 0000000..a2a658b --- /dev/null +++ b/pkg/odoo/contract_test.go @@ -0,0 +1,75 @@ +package odoo + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newFTESchedule(ratioPercentage int) *WorkingSchedule { + return &WorkingSchedule{ + ID: 0, + Name: strconv.Itoa(ratioPercentage) + "%", + } +} + +func TestContractList_GetFTERatioForDay(t *testing.T) { + tests := map[string]struct { + givenList ContractList + givenDay Date + expectedRatio float64 + expectErr bool + }{ + "GivenEmptyList_WhenNil_ThenReturnErr": { + givenList: nil, + expectErr: true, + }, + "GivenEmptyList_WhenNoContracts_ThenReturnErr": { + givenList: []Contract{}, + expectErr: true, + }, + "GivenListWith1Contract_WhenOpenEnd_ThenReturnRatio": { + givenDay: *newDate(t, "2021-12-04"), + givenList: []Contract{ + {Start: newDate(t, "2021-02-01"), WorkingSchedule: newFTESchedule(100)}, + }, + expectedRatio: 1, + }, + "GivenListWith1Contract_WhenDayBeforeStart_ThenReturnErr": { + givenDay: *newDate(t, "2021-02-01"), + givenList: []Contract{ + {Start: newDate(t, "2021-02-02"), WorkingSchedule: newFTESchedule(100)}, + }, + expectErr: true, + }, + "GivenListWith2Contract_WhenDayBetweenContract_ThenReturnRatioFromTerminatedContract": { + givenDay: *newDate(t, "2021-03-31"), + givenList: []Contract{ + {Start: newDate(t, "2021-02-02"), End: newDate(t, "2021-03-31"), WorkingSchedule: newFTESchedule(90)}, + {Start: newDate(t, "2021-04-01"), WorkingSchedule: newFTESchedule(80)}, + }, + expectedRatio: 0.9, + }, + "GivenListWith2Contract_WhenDayInOpenContract_ThenReturnRatioFromOpenContract": { + givenDay: *newDate(t, "2021-04-01"), + givenList: []Contract{ + {Start: newDate(t, "2021-02-02"), End: newDate(t, "2021-03-31"), WorkingSchedule: newFTESchedule(90)}, + {Start: newDate(t, "2021-04-01"), WorkingSchedule: newFTESchedule(80)}, + }, + expectedRatio: 0.8, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + result, err := tt.givenList.GetFTERatioForDay(tt.givenDay) + if tt.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectedRatio, result) + }) + } +} diff --git a/pkg/odoo/date.go b/pkg/odoo/date.go index 1b889b9..113a6bf 100644 --- a/pkg/odoo/date.go +++ b/pkg/odoo/date.go @@ -2,6 +2,7 @@ package odoo import ( "bytes" + "encoding/json" "fmt" "time" ) @@ -26,15 +27,29 @@ func (d Date) MarshalJSON() ([]byte, error) { func (d *Date) UnmarshalJSON(b []byte) error { ts := bytes.Trim(b, `"`) - t, err := time.Parse(DateTimeFormat, string(ts)) - if err != nil { - return err + var f bool + if err := json.Unmarshal(b, &f); err == nil || string(b) == "false" { + return nil + } + // try parsing date + time + t, dateTimeErr := time.Parse(DateTimeFormat, string(ts)) + if dateTimeErr != nil { + // second attempt parsing date only + t, dateTimeErr = time.Parse(DateFormat, string(ts)) + if dateTimeErr != nil { + return dateTimeErr + } } *d = Date(t) return nil } +// IsZero returns true if Date is nil or Time.IsZero() +func (d *Date) IsZero() bool { + return d == nil || d.ToTime().IsZero() +} + func (d *Date) ToTime() time.Time { return time.Time(*d) } diff --git a/pkg/odoo/date_test.go b/pkg/odoo/date_test.go index 125da21..301ba25 100644 --- a/pkg/odoo/date_test.go +++ b/pkg/odoo/date_test.go @@ -5,12 +5,52 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func newDate(t *testing.T, value string) *Date { +func TestDate_UnmarshalJSON(t *testing.T) { + tests := map[string]struct { + givenInput string + expectedDate *Date + }{ + "GivenFalse_ThenExpectZeroDate": { + givenInput: "false", + expectedDate: nil, + }, + "GivenValidInput_WhenFormatIsDate_ThenExpectDate": { + givenInput: "2021-02-03", + expectedDate: newDate(t, "2021-02-03"), + }, + "GivenValidInput_WhenFormatIsDateTime_ThenExpectDateTime": { + givenInput: "2021-02-03 15:34:00", + expectedDate: newDateTime(t, "2021-02-03 15:34"), + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + subject := &Date{} + err := subject.UnmarshalJSON([]byte(tt.givenInput)) + require.NoError(t, err) + if tt.expectedDate == nil { + assert.True(t, subject.IsZero()) + return + } + assert.Equal(t, tt.expectedDate, subject) + }) + } +} + +func newDateTime(t *testing.T, value string) *Date { tm, err := time.Parse(DateTimeFormat, fmt.Sprintf("%s:00", value)) require.NoError(t, err) ptr := Date(tm) return &ptr } + +func newDate(t *testing.T, value string) *Date { + tm, err := time.Parse(DateFormat, value) + require.NoError(t, err) + ptr := Date(tm) + return &ptr +} diff --git a/pkg/odoo/json_rpc.go b/pkg/odoo/json_rpc.go index 68e75db..2c69952 100644 --- a/pkg/odoo/json_rpc.go +++ b/pkg/odoo/json_rpc.go @@ -76,6 +76,9 @@ func DecodeResult(buf io.Reader, result interface{}) error { if err := json.NewDecoder(buf).Decode(&res); err != nil { return fmt.Errorf("decode intermediate: %w", err) } + if res.Error != nil { + return fmt.Errorf("%s: %s", res.Error.Message, res.Error.Data["message"]) + } return json.Unmarshal(*res.Result, result) } diff --git a/pkg/odoo/leave_test.go b/pkg/odoo/leave_test.go index e24a8e9..8c9c77a 100644 --- a/pkg/odoo/leave_test.go +++ b/pkg/odoo/leave_test.go @@ -11,8 +11,8 @@ func TestLeave_SplitByDay(t *testing.T) { t.Run("GivenLeaveWithSingleDate_ThenExpectSameLeave", func(t *testing.T) { givenLeave := Leave{ ID: 1, - DateFrom: newDate(t, "2021-02-03 07:00"), - DateTo: newDate(t, "2021-02-03 19:00"), + DateFrom: newDateTime(t, "2021-02-03 07:00"), + DateTo: newDateTime(t, "2021-02-03 19:00"), Type: &LeaveType{ID: 1, Name: "SomeType"}, State: "validated", } @@ -27,11 +27,11 @@ func TestLeave_SplitByDay(t *testing.T) { }{ "GivenLeave_WhenDurationGoesIntoNextDay_ThenExpectSplit": { givenLeave: Leave{ - DateFrom: newDate(t, "2021-02-03 07:00"), DateTo: newDate(t, "2021-02-04 19:00"), + DateFrom: newDateTime(t, "2021-02-03 07:00"), DateTo: newDateTime(t, "2021-02-04 19:00"), }, expectedLeaves: []Leave{ - {DateFrom: newDate(t, "2021-02-03 07:00"), DateTo: newDate(t, "2021-02-03 15:00")}, - {DateFrom: newDate(t, "2021-02-04 07:00"), DateTo: newDate(t, "2021-02-04 15:00")}, + {DateFrom: newDateTime(t, "2021-02-03 07:00"), DateTo: newDateTime(t, "2021-02-03 15:00")}, + {DateFrom: newDateTime(t, "2021-02-04 07:00"), DateTo: newDateTime(t, "2021-02-04 15:00")}, }, }, } diff --git a/pkg/odoo/workingschedule.go b/pkg/odoo/workingschedule.go new file mode 100644 index 0000000..c12d4d2 --- /dev/null +++ b/pkg/odoo/workingschedule.go @@ -0,0 +1,65 @@ +package odoo + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" +) + +var workingScheduleRegex = regexp.MustCompile("(?P[0-9]+\\s*%)") + +type WorkingSchedule struct { + ID float64 + Name string +} + +func (s *WorkingSchedule) String() string { + if s == nil { + return "" + } + return s.Name +} + +func (s WorkingSchedule) MarshalJSON() ([]byte, error) { + if s.Name == "" { + return []byte("false"), nil + } + arr := []interface{}{s.ID, s.Name} + return json.Marshal(arr) +} +func (s *WorkingSchedule) UnmarshalJSON(b []byte) error { + var f bool + if err := json.Unmarshal(b, &f); err == nil || string(b) == "false" { + return nil + } + var arr []interface{} + if err := json.Unmarshal(b, &arr); err != nil { + return err + } + if len(arr) >= 2 { + if v, ok := arr[1].(string); ok { + *s = WorkingSchedule{ + ID: arr[0].(float64), + Name: v, + } + } + } + return nil +} + +// GetFTERatio tries to extract the FTE ratio from the name of the schedule. +// It returns an error if it could not find a match +func (s *WorkingSchedule) GetFTERatio() (float64, error) { + match := workingScheduleRegex.FindStringSubmatch(s.Name) + if len(match) > 0 { + v := match[0] + v = strings.TrimSpace(v) + v = strings.ReplaceAll(v, " ", "") // there might be spaces in between + v = strings.ReplaceAll(v, "%", "") + ratio, err := strconv.Atoi(v) + return float64(ratio) / 100, err + } + return 0, fmt.Errorf("could not find FTE ratio in name: '%s'", s.Name) +} diff --git a/pkg/odoo/workingschedule_test.go b/pkg/odoo/workingschedule_test.go new file mode 100644 index 0000000..a267a32 --- /dev/null +++ b/pkg/odoo/workingschedule_test.go @@ -0,0 +1,50 @@ +package odoo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWorkingSchedule_GetFTERatio(t *testing.T) { + tests := map[string]struct { + givenNamePattern string + expectedRatio float64 + expectErr bool + }{ + "GivenWorkloadPrefix_When100%_ThenExpect_1": { + givenNamePattern: "Workload: 100% (40:00)", + expectedRatio: 1, + }, + "GivenWorkWeekSuffix_When80%_ThenExpect_0.8": { + givenNamePattern: "80% Work Week", + expectedRatio: 0.8, + }, + "GivenStandardWorkload_ThenExpect_1": { + givenNamePattern: "Standard 100% Work Week", + expectedRatio: 1, + }, + "GivenEmptyName_ThenExpectErr": { + givenNamePattern: "", + expectErr: true, + }, + "GivenNameWithoutPercentage_ThenExpectErr": { + givenNamePattern: "Some Work Week", + expectErr: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + subject := WorkingSchedule{ + Name: tt.givenNamePattern, + } + result, err := subject.GetFTERatio() + if tt.expectErr { + require.Error(t, err) + return + } + assert.Equal(t, tt.expectedRatio, result) + }) + } +} diff --git a/pkg/timesheet/report.go b/pkg/timesheet/report.go index b9369b2..68d69e6 100644 --- a/pkg/timesheet/report.go +++ b/pkg/timesheet/report.go @@ -1,6 +1,7 @@ package timesheet import ( + "fmt" "sort" "time" @@ -56,9 +57,10 @@ type Reporter struct { year int month int fteRatio float64 + contracts odoo.ContractList } -func NewReporter(attendances []odoo.Attendance, leaves []odoo.Leave, employee *odoo.Employee) *Reporter { +func NewReporter(attendances []odoo.Attendance, leaves []odoo.Leave, employee *odoo.Employee, contracts []odoo.Contract) *Reporter { return &Reporter{ attendances: attendances, leaves: leaves, @@ -66,6 +68,7 @@ func NewReporter(attendances []odoo.Attendance, leaves []odoo.Leave, employee *o year: now().UTC().Year(), month: int(now().UTC().Month()), fteRatio: float64(1), + contracts: contracts, } } @@ -146,7 +149,12 @@ func (r *Reporter) prepareDays() []*DailySummary { } for currentDay := firstDay; currentDay.Before(lastDay); currentDay = currentDay.AddDate(0, 0, 1) { - days = append(days, NewDailySummary(r.fteRatio, currentDay)) + currentRatio, err := r.contracts.GetFTERatioForDay(odoo.Date(currentDay)) + if err != nil { + fmt.Println(err) + currentRatio = r.fteRatio + } + days = append(days, NewDailySummary(currentRatio, currentDay)) } return days diff --git a/pkg/web/attendance_handlers.go b/pkg/web/report_handler.go similarity index 52% rename from pkg/web/attendance_handlers.go rename to pkg/web/report_handler.go index 6fe72a8..94c2bee 100644 --- a/pkg/web/attendance_handlers.go +++ b/pkg/web/report_handler.go @@ -12,6 +12,23 @@ import ( "github.com/vshn/odootools/pkg/web/views" ) +type OvertimeInput struct { + Year int + Month int + SearchUser string + SearchUserEnabled bool + FTERatio float64 +} + +func (i *OvertimeInput) FromForm(r *http.Request) { + i.SearchUserEnabled = r.FormValue("userscope") == "user-foreign-radio" + i.SearchUser = html.EscapeString(r.FormValue("username")) + + i.Year = parseIntOrDefault(r.FormValue("year"), time.Now().Year()) + i.Month = parseIntOrDefault(r.FormValue("month"), int(time.Now().Month())) + i.FTERatio = parseFloatOrDefault(r.FormValue("ftepercentage"), 100) +} + // OvertimeReport GET /report func (s Server) OvertimeReport() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -23,37 +40,24 @@ func (s Server) OvertimeReport() http.Handler { } view := views.NewOvertimeReportView(s.html) - forAnotherUser := r.FormValue("userscope") == "user-foreign-radio" - searchUser := html.EscapeString(r.FormValue("username")) - - year := parseIntOrDefault(r.FormValue("year"), time.Now().Year()) - month := parseIntOrDefault(r.FormValue("month"), int(time.Now().Month())) - fte := parseFloatOrDefault(r.FormValue("ftepercentage"), 100) - - var employee *odoo.Employee - if forAnotherUser { - e, err := s.odoo.SearchEmployee(searchUser, session.ID) - if err != nil { - view.ShowError(w, err) - return - } - if e == nil { - view.ShowError(w, fmt.Errorf("no user matching '%s' found", searchUser)) - return - } - employee = e - } else { - e, err := s.odoo.FetchEmployee(session.ID, session.UID) - if err != nil { - view.ShowError(w, err) - return - } - employee = e + input := OvertimeInput{} + input.FromForm(r) + + employee := getEmployee(w, input, s, session, view) + if employee == nil { + // error already shown in view + return } - begin := odoo.Date(time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)) + begin := odoo.Date(time.Date(input.Year, time.Month(input.Month), 1, 0, 0, 0, 0, time.UTC)) end := odoo.Date(begin.ToTime().AddDate(0, 1, 0)) + contracts, err := s.odoo.FetchAllContracts(session.ID, employee.ID) + if err != nil { + view.ShowError(w, err) + return + } + attendances, err := s.odoo.FetchAttendancesBetweenDates(session.ID, employee.ID, begin, end) if err != nil { view.ShowError(w, err) @@ -66,12 +70,36 @@ func (s Server) OvertimeReport() http.Handler { return } - reporter := timesheet.NewReporter(attendances, leaves, employee).SetFteRatio(fte/100).SetMonth(year, month) + reporter := timesheet.NewReporter(attendances, leaves, employee, contracts).SetFteRatio(input.FTERatio/100).SetMonth(input.Year, input.Month) report := reporter.CalculateReport() view.ShowAttendanceReport(w, report) }) } +func getEmployee(w http.ResponseWriter, input OvertimeInput, s Server, session *odoo.Session, view *views.OvertimeReportView) *odoo.Employee { + var employee *odoo.Employee + if input.SearchUserEnabled { + e, err := s.odoo.SearchEmployee(input.SearchUser, session.ID) + if err != nil { + view.ShowError(w, err) + return nil + } + if e == nil { + view.ShowError(w, fmt.Errorf("no user matching '%s' found", input.SearchUser)) + return nil + } + employee = e + return employee + } + e, err := s.odoo.FetchEmployee(session.ID, session.UID) + if err != nil { + view.ShowError(w, err) + return nil + } + employee = e + return employee +} + func parseIntOrDefault(toParse string, def int) int { if toParse == "" { return def From 71341297a5dd78cec753d300c49dfc7feea36353 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 22 Nov 2021 12:21:42 +0100 Subject: [PATCH 2/3] Remove FTE ratio input field --- pkg/odoo/contract.go | 4 ++-- pkg/timesheet/report.go | 9 +-------- pkg/web/report_handler.go | 4 +--- pkg/web/views/overtimereport_view.go | 1 + templates/createreport.html | 4 ---- templates/overtimereport.html | 4 ++++ 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/pkg/odoo/contract.go b/pkg/odoo/contract.go index b573b9f..26f19a3 100644 --- a/pkg/odoo/contract.go +++ b/pkg/odoo/contract.go @@ -17,14 +17,14 @@ type Contract struct { func (l ContractList) GetFTERatioForDay(day Date) (float64, error) { date := day.ToTime() for _, contract := range l { + start := contract.Start.ToTime().Add(-1 * time.Second) if contract.End.IsZero() { // current contract - if contract.Start.ToTime().Add(-1 * time.Second).Before(date) { + if start.Before(date) { return contract.WorkingSchedule.GetFTERatio() } continue } - start := contract.Start.ToTime().Add(-1 * time.Second) end := contract.End.ToTime().Add(1 * time.Second) if start.Before(date) && end.After(date) { return contract.WorkingSchedule.GetFTERatio() diff --git a/pkg/timesheet/report.go b/pkg/timesheet/report.go index 68d69e6..662ca47 100644 --- a/pkg/timesheet/report.go +++ b/pkg/timesheet/report.go @@ -56,7 +56,6 @@ type Reporter struct { employee *odoo.Employee year int month int - fteRatio float64 contracts odoo.ContractList } @@ -67,7 +66,6 @@ func NewReporter(attendances []odoo.Attendance, leaves []odoo.Leave, employee *o employee: employee, year: now().UTC().Year(), month: int(now().UTC().Month()), - fteRatio: float64(1), contracts: contracts, } } @@ -78,11 +76,6 @@ func (r *Reporter) SetMonth(year, month int) *Reporter { return r } -func (r *Reporter) SetFteRatio(fteRatio float64) *Reporter { - r.fteRatio = fteRatio - return r -} - func (r *Reporter) CalculateReport() Report { filteredAttendances := r.filterAttendancesInMonth() blocks := reduceAttendancesToBlocks(filteredAttendances) @@ -152,7 +145,7 @@ func (r *Reporter) prepareDays() []*DailySummary { currentRatio, err := r.contracts.GetFTERatioForDay(odoo.Date(currentDay)) if err != nil { fmt.Println(err) - currentRatio = r.fteRatio + currentRatio = 0 } days = append(days, NewDailySummary(currentRatio, currentDay)) } diff --git a/pkg/web/report_handler.go b/pkg/web/report_handler.go index 94c2bee..03840ab 100644 --- a/pkg/web/report_handler.go +++ b/pkg/web/report_handler.go @@ -17,7 +17,6 @@ type OvertimeInput struct { Month int SearchUser string SearchUserEnabled bool - FTERatio float64 } func (i *OvertimeInput) FromForm(r *http.Request) { @@ -26,7 +25,6 @@ func (i *OvertimeInput) FromForm(r *http.Request) { i.Year = parseIntOrDefault(r.FormValue("year"), time.Now().Year()) i.Month = parseIntOrDefault(r.FormValue("month"), int(time.Now().Month())) - i.FTERatio = parseFloatOrDefault(r.FormValue("ftepercentage"), 100) } // OvertimeReport GET /report @@ -70,7 +68,7 @@ func (s Server) OvertimeReport() http.Handler { return } - reporter := timesheet.NewReporter(attendances, leaves, employee, contracts).SetFteRatio(input.FTERatio/100).SetMonth(input.Year, input.Month) + reporter := timesheet.NewReporter(attendances, leaves, employee, contracts).SetMonth(input.Year, input.Month) report := reporter.CalculateReport() view.ShowAttendanceReport(w, report) }) diff --git a/pkg/web/views/overtimereport_view.go b/pkg/web/views/overtimereport_view.go index 91066de..df4af2a 100644 --- a/pkg/web/views/overtimereport_view.go +++ b/pkg/web/views/overtimereport_view.go @@ -26,6 +26,7 @@ func (v *OvertimeReportView) formatDailySummary(daily *timesheet.DailySummary) V basic := Values{ "Weekday": daily.Date.Weekday(), "Date": daily.Date.Format(odoo.DateFormat), + "Workload": daily.FTERatio * 100, "ExcusedHours": formatDurationInHours(timesheet.ToDuration(daily.CalculateExcusedHours())), "WorkedHours": formatDurationInHours(timesheet.ToDuration(daily.CalculateWorkingHours())), "OvertimeHours": formatDurationInHours(daily.CalculateOvertime()), diff --git a/templates/createreport.html b/templates/createreport.html index e9ceaab..7f332f3 100644 --- a/templates/createreport.html +++ b/templates/createreport.html @@ -19,10 +19,6 @@

Create Report

-
- - -
diff --git a/templates/overtimereport.html b/templates/overtimereport.html index 4e0dd9b..09ea3c1 100644 --- a/templates/overtimereport.html +++ b/templates/overtimereport.html @@ -8,6 +8,7 @@

Attendance for {{ .Username }}

Weekday Date + Workload Leaves Excused hours Worked hours @@ -19,6 +20,7 @@

Attendance for {{ .Username }}

{{ .Weekday }} {{ .Date }} + {{ .Workload }}% {{ .LeaveType }} {{ .ExcusedHours }} {{ .WorkedHours }} @@ -28,6 +30,7 @@

Attendance for {{ .Username }}

+ Total Leaves @@ -36,6 +39,7 @@

Attendance for {{ .Username }}

Total Overtime + {{ .Summary.TotalLeaves }} From 9c54e135cdb611ef78257358bdea22c34dc246b9 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 22 Nov 2021 14:11:58 +0100 Subject: [PATCH 3/3] Add docs --- doc/design.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/design.md b/doc/design.md index 8c6ad98..b53ca31 100644 --- a/doc/design.md +++ b/doc/design.md @@ -16,6 +16,11 @@ In the end it doesn't matter: - Not using a leave day, but only work half day results in undertime, but still having an additional holiday. At the end of the year, excess holidays are transformed into overtime. +## Workload ratio + +The workload ratio can be retrieved from Odoo via the "Contracts" model. +However, special read access needs to be granted so that they can be queried for their own user. + ## FAQs ### Why not an SPA and offload all work into the user's browser? @@ -27,11 +32,6 @@ CORS. Browsers won't connect to Odoo from somewhere else (which is fine). Payslips can be queried and updated - just not with the normal VSHNeer access levels. Easiest solution would be to enable at least read access so that odootools can accumulate the delta. -### Get FTE ratio instead of manually entering it? - -FTE ratio can also be queried, but not with current VSHNeer access levels. -The contracts and the ratio are available as fields - one just needs access to it. - # Backlog ## Caching