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:
Marc
2026-04-12 18:41:57 +02:00
parent 2c7c1e6c88
commit 66383adf06
87 changed files with 14696 additions and 654 deletions

View File

@@ -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()