feat: Portal, Email Inbound, Discuss + module improvements
- Portal: /my/* routes, signup, password reset, portal user support - Email Inbound: IMAP polling (go-imap/v2), thread matching - Discuss: mail.channel, long-polling bus, DM, unread count - Cron: ir.cron runner (goroutine scheduler) - Bank Import, CSV/Excel Import - Automation (ir.actions.server) - Fetchmail service - HR Payroll model - Various fixes across account, sale, stock, purchase, crm, hr, project Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,94 @@ func initTimesheetReport() {
|
||||
}, nil
|
||||
})
|
||||
|
||||
// get_timesheet_pivot_data: Pivot data by employee x task with date breakdown.
|
||||
// Returns rows grouped by employee, columns grouped by task, cells contain hours per date period.
|
||||
// Mirrors: odoo/addons/hr_timesheet/report/hr_timesheet_report.py pivot view
|
||||
m.RegisterMethod("get_timesheet_pivot_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
|
||||
// Query: employee x task x week, with hours
|
||||
rows, err := env.Tx().Query(env.Ctx(), `
|
||||
SELECT COALESCE(he.name, 'Unknown') AS employee,
|
||||
COALESCE(pt.name, 'No Task') AS task,
|
||||
date_trunc('week', aal.date) AS week,
|
||||
SUM(aal.unit_amount) AS hours
|
||||
FROM account_analytic_line aal
|
||||
LEFT JOIN hr_employee he ON he.id = aal.employee_id
|
||||
LEFT JOIN project_task pt ON pt.id = aal.task_id
|
||||
WHERE aal.project_id IS NOT NULL
|
||||
GROUP BY he.name, pt.name, date_trunc('week', aal.date)
|
||||
ORDER BY he.name, pt.name, week
|
||||
LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("timesheet_report: pivot query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Build pivot structure: { rows: [{employee, task, dates: [{week, hours}]}] }
|
||||
type pivotCell struct {
|
||||
Week string `json:"week"`
|
||||
Hours float64 `json:"hours"`
|
||||
}
|
||||
type pivotRow struct {
|
||||
Employee string `json:"employee"`
|
||||
Task string `json:"task"`
|
||||
Dates []pivotCell `json:"dates"`
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
rowMap := make(map[string]*pivotRow) // key = "employee|task"
|
||||
var allWeeks []string
|
||||
weekSet := make(map[string]bool)
|
||||
|
||||
for rows.Next() {
|
||||
var employee, task string
|
||||
var week time.Time
|
||||
var hours float64
|
||||
if err := rows.Scan(&employee, &task, &week, &hours); err != nil {
|
||||
continue
|
||||
}
|
||||
weekStr := week.Format("2006-01-02")
|
||||
key := employee + "|" + task
|
||||
r, ok := rowMap[key]
|
||||
if !ok {
|
||||
r = &pivotRow{Employee: employee, Task: task}
|
||||
rowMap[key] = r
|
||||
}
|
||||
r.Dates = append(r.Dates, pivotCell{Week: weekStr, Hours: hours})
|
||||
r.Total += hours
|
||||
|
||||
if !weekSet[weekStr] {
|
||||
weekSet[weekStr] = true
|
||||
allWeeks = append(allWeeks, weekStr)
|
||||
}
|
||||
}
|
||||
|
||||
var pivotRows []pivotRow
|
||||
for _, r := range rowMap {
|
||||
pivotRows = append(pivotRows, *r)
|
||||
}
|
||||
|
||||
// Compute totals per employee
|
||||
empTotals := make(map[string]float64)
|
||||
for _, r := range pivotRows {
|
||||
empTotals[r.Employee] += r.Total
|
||||
}
|
||||
var empSummary []map[string]interface{}
|
||||
for emp, total := range empTotals {
|
||||
empSummary = append(empSummary, map[string]interface{}{
|
||||
"employee": emp,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"pivot_rows": pivotRows,
|
||||
"weeks": allWeeks,
|
||||
"employee_totals": empSummary,
|
||||
}, nil
|
||||
})
|
||||
|
||||
// get_timesheet_by_week: Weekly breakdown of timesheet hours.
|
||||
m.RegisterMethod("get_timesheet_by_week", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
|
||||
Reference in New Issue
Block a user