- 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>
576 lines
20 KiB
Go
576 lines
20 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"odoo-go/pkg/orm"
|
|
)
|
|
|
|
// initHrLeaveType registers the hr.leave.type model.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave_type.py
|
|
func initHrLeaveType() {
|
|
orm.NewModel("hr.leave.type", orm.ModelOpts{
|
|
Description: "Time Off Type",
|
|
Order: "sequence, id",
|
|
}).AddFields(
|
|
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
|
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 100}),
|
|
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
|
orm.Selection("leave_validation_type", []orm.SelectionItem{
|
|
{Value: "no_validation", Label: "No Validation"},
|
|
{Value: "hr", Label: "By Time Off Officer"},
|
|
{Value: "manager", Label: "By Employee's Approver"},
|
|
{Value: "both", Label: "By Employee's Approver and Time Off Officer"},
|
|
}, orm.FieldOpts{String: "Approval", Default: "hr"}),
|
|
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
|
orm.Integer("color", orm.FieldOpts{String: "Color"}),
|
|
orm.Boolean("requires_allocation", orm.FieldOpts{String: "Requires Allocation", Default: true}),
|
|
orm.Float("max_allowed", orm.FieldOpts{String: "Max Days Allowed"}),
|
|
)
|
|
}
|
|
|
|
// initHrLeave registers the hr.leave model.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py
|
|
func initHrLeave() {
|
|
m := orm.NewModel("hr.leave", orm.ModelOpts{
|
|
Description: "Time Off",
|
|
Order: "date_from desc",
|
|
})
|
|
m.AddFields(
|
|
orm.Char("name", orm.FieldOpts{String: "Description"}),
|
|
orm.Many2one("employee_id", "hr.employee", orm.FieldOpts{String: "Employee", Required: true}),
|
|
orm.Many2one("holiday_status_id", "hr.leave.type", orm.FieldOpts{String: "Time Off Type", Required: true}),
|
|
orm.Many2one("department_id", "hr.department", orm.FieldOpts{String: "Department"}),
|
|
orm.Many2one("manager_id", "hr.employee", orm.FieldOpts{String: "Manager"}),
|
|
orm.Datetime("date_from", orm.FieldOpts{String: "Start Date", Required: true}),
|
|
orm.Datetime("date_to", orm.FieldOpts{String: "End Date", Required: true}),
|
|
orm.Float("number_of_days", orm.FieldOpts{String: "Duration (Days)"}),
|
|
orm.Selection("state", []orm.SelectionItem{
|
|
{Value: "draft", Label: "To Submit"},
|
|
{Value: "confirm", Label: "To Approve"},
|
|
{Value: "validate1", Label: "Second Approval"},
|
|
{Value: "validate", Label: "Approved"},
|
|
{Value: "refuse", Label: "Refused"},
|
|
}, orm.FieldOpts{String: "Status", Default: "draft"}),
|
|
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
|
orm.Text("notes", orm.FieldOpts{String: "Reasons"}),
|
|
)
|
|
|
|
// action_approve: Manager approves leave request (first approval).
|
|
// For leave types with 'both' validation, moves to validate1 (second approval needed).
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py HrLeave.action_approve()
|
|
m.RegisterMethod("action_approve", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: read state for %d: %w", id, err)
|
|
}
|
|
if state != "confirm" && state != "validate1" {
|
|
return nil, fmt.Errorf("hr.leave: can only approve leaves in 'To Approve' or 'Second Approval' state (leave %d is %q)", id, state)
|
|
}
|
|
|
|
// Check if second approval is needed
|
|
var validationType string
|
|
if err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(lt.leave_validation_type, 'hr')
|
|
FROM hr_leave l
|
|
JOIN hr_leave_type lt ON lt.id = l.holiday_status_id
|
|
WHERE l.id = $1`, id,
|
|
).Scan(&validationType); err != nil {
|
|
validationType = "hr" // safe default
|
|
}
|
|
|
|
newState := "validate"
|
|
if validationType == "both" && state == "confirm" {
|
|
newState = "validate1"
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave SET state = $1 WHERE id = $2`, newState, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: approve leave %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// action_validate: Final validation (second approval if needed).
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py HrLeave.action_validate()
|
|
m.RegisterMethod("action_validate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: read state for %d: %w", id, err)
|
|
}
|
|
if state != "confirm" && state != "validate1" {
|
|
return nil, fmt.Errorf("hr.leave: can only validate leaves in 'To Approve' or 'Second Approval' state (leave %d is %q)", id, state)
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave SET state = 'validate' WHERE id = $1`, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: validate leave %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// action_refuse: Manager refuses leave request.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py HrLeave.action_refuse()
|
|
m.RegisterMethod("action_refuse", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: read state for %d: %w", id, err)
|
|
}
|
|
if state == "draft" {
|
|
return nil, fmt.Errorf("hr.leave: cannot refuse a draft leave (leave %d). Submit it first", id)
|
|
}
|
|
if state == "validate" {
|
|
return nil, fmt.Errorf("hr.leave: cannot refuse an already approved leave (leave %d). Reset to draft first", id)
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave SET state = 'refuse' WHERE id = $1`, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: refuse leave %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// action_draft: Reset leave to draft state.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py HrLeave.action_draft()
|
|
m.RegisterMethod("action_draft", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: read state for %d: %w", id, err)
|
|
}
|
|
if state != "confirm" && state != "refuse" {
|
|
return nil, fmt.Errorf("hr.leave: can only reset to draft from 'To Approve' or 'Refused' state (leave %d is %q)", id, state)
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave SET state = 'draft' WHERE id = $1`, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: reset to draft leave %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// action_confirm: Submit leave for approval (draft -> confirm).
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py HrLeave.action_confirm()
|
|
m.RegisterMethod("action_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: read state for %d: %w", id, err)
|
|
}
|
|
if state != "draft" {
|
|
return nil, fmt.Errorf("hr.leave: can only confirm draft leaves (leave %d is %q)", id, state)
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave SET state = 'confirm' WHERE id = $1`, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: confirm leave %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
}
|
|
|
|
// initHrLeaveExtensions adds additional leave methods.
|
|
func initHrLeaveExtensions() {
|
|
leave := orm.ExtendModel("hr.leave")
|
|
|
|
// action_approve_batch: Approve multiple leave requests at once (manager workflow).
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave.py action_approve (multi)
|
|
leave.RegisterMethod("action_approve_batch", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
ids := rs.IDs()
|
|
if len(ids) == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// Leaves with 'both' validation: confirm → validate1 (not directly to validate)
|
|
r1, err := env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave l SET state = 'validate1'
|
|
WHERE l.id = ANY($1) AND l.state = 'confirm'
|
|
AND EXISTS (
|
|
SELECT 1 FROM hr_leave_type lt
|
|
WHERE lt.id = l.holiday_status_id AND lt.leave_validation_type = 'both'
|
|
)`, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: batch approve (first step): %w", err)
|
|
}
|
|
|
|
// Non-'both' confirm → validate, and existing validate1 → validate
|
|
// Exclude IDs that were just set to validate1 above
|
|
r2, err := env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave l SET state = 'validate'
|
|
WHERE l.id = ANY($1)
|
|
AND (l.state = 'validate1'
|
|
OR (l.state = 'confirm' AND NOT EXISTS (
|
|
SELECT 1 FROM hr_leave_type lt
|
|
WHERE lt.id = l.holiday_status_id AND lt.leave_validation_type = 'both'
|
|
)))`, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave: batch approve: %w", err)
|
|
}
|
|
count := r1.RowsAffected() + r2.RowsAffected()
|
|
return map[string]interface{}{"approved": count}, nil
|
|
})
|
|
|
|
// _compute_number_of_days: Auto-compute duration from date_from/date_to.
|
|
leave.RegisterCompute("number_of_days", func(rs *orm.Recordset) (orm.Values, error) {
|
|
env := rs.Env()
|
|
id := rs.IDs()[0]
|
|
var days float64
|
|
if err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(EXTRACT(EPOCH FROM (date_to - date_from)) / 86400.0, 0)
|
|
FROM hr_leave WHERE id = $1`, id).Scan(&days); err != nil {
|
|
return orm.Values{"number_of_days": float64(0)}, nil
|
|
}
|
|
if days < 0 {
|
|
days = 0
|
|
}
|
|
return orm.Values{"number_of_days": days}, nil
|
|
})
|
|
}
|
|
|
|
// initHrLeaveReport registers the hr.leave.report transient model.
|
|
// Mirrors: odoo/addons/hr_holidays/report/hr_leave_report.py
|
|
func initHrLeaveReport() {
|
|
m := orm.NewModel("hr.leave.report", orm.ModelOpts{
|
|
Description: "Time Off Summary Report",
|
|
Type: orm.ModelTransient,
|
|
})
|
|
m.AddFields(
|
|
orm.Many2one("employee_id", "hr.employee", orm.FieldOpts{String: "Employee"}),
|
|
orm.Date("date_from", orm.FieldOpts{String: "From"}),
|
|
orm.Date("date_to", orm.FieldOpts{String: "To"}),
|
|
)
|
|
|
|
// get_leave_summary: Returns leave days grouped by leave type for an employee in a date range.
|
|
// Mirrors: odoo/addons/hr_holidays/report/hr_leave_report.py
|
|
m.RegisterMethod("get_leave_summary", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
|
|
// Parse kwargs
|
|
var employeeID int64
|
|
dateFrom := "2000-01-01"
|
|
dateTo := "2099-12-31"
|
|
if len(args) > 0 {
|
|
if kw, ok := args[0].(map[string]interface{}); ok {
|
|
if v, ok := kw["employee_id"]; ok {
|
|
switch vid := v.(type) {
|
|
case int64:
|
|
employeeID = vid
|
|
case float64:
|
|
employeeID = int64(vid)
|
|
case int:
|
|
employeeID = int64(vid)
|
|
}
|
|
}
|
|
if v, ok := kw["date_from"].(string); ok && v != "" {
|
|
dateFrom = v
|
|
}
|
|
if v, ok := kw["date_to"].(string); ok && v != "" {
|
|
dateTo = v
|
|
}
|
|
}
|
|
}
|
|
if employeeID == 0 {
|
|
return nil, fmt.Errorf("hr.leave.report: employee_id is required")
|
|
}
|
|
|
|
// Approved leaves grouped by type
|
|
rows, err := env.Tx().Query(env.Ctx(),
|
|
`SELECT lt.id, lt.name, COALESCE(SUM(l.number_of_days), 0) AS total_days,
|
|
COUNT(l.id) AS leave_count
|
|
FROM hr_leave l
|
|
JOIN hr_leave_type lt ON lt.id = l.holiday_status_id
|
|
WHERE l.employee_id = $1
|
|
AND l.state = 'validate'
|
|
AND l.date_from::date >= $2
|
|
AND l.date_to::date <= $3
|
|
GROUP BY lt.id, lt.name
|
|
ORDER BY lt.name`, employeeID, dateFrom, dateTo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.report: query leaves: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var summary []map[string]interface{}
|
|
var totalDays float64
|
|
for rows.Next() {
|
|
var typeID int64
|
|
var typeName string
|
|
var days float64
|
|
var count int
|
|
if err := rows.Scan(&typeID, &typeName, &days, &count); err != nil {
|
|
continue
|
|
}
|
|
summary = append(summary, map[string]interface{}{
|
|
"leave_type_id": typeID,
|
|
"leave_type_name": typeName,
|
|
"total_days": days,
|
|
"leave_count": count,
|
|
})
|
|
totalDays += days
|
|
}
|
|
|
|
// Remaining allocation per type
|
|
allocRows, err := env.Tx().Query(env.Ctx(),
|
|
`SELECT lt.id, lt.name,
|
|
COALESCE(SUM(a.number_of_days), 0) AS allocated
|
|
FROM hr_leave_allocation a
|
|
JOIN hr_leave_type lt ON lt.id = a.holiday_status_id
|
|
WHERE a.employee_id = $1 AND a.state = 'validate'
|
|
GROUP BY lt.id, lt.name
|
|
ORDER BY lt.name`, employeeID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.report: query allocations: %w", err)
|
|
}
|
|
defer allocRows.Close()
|
|
|
|
var allocations []map[string]interface{}
|
|
for allocRows.Next() {
|
|
var typeID int64
|
|
var typeName string
|
|
var allocated float64
|
|
if err := allocRows.Scan(&typeID, &typeName, &allocated); err != nil {
|
|
continue
|
|
}
|
|
allocations = append(allocations, map[string]interface{}{
|
|
"leave_type_id": typeID,
|
|
"leave_type_name": typeName,
|
|
"allocated_days": allocated,
|
|
})
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"employee_id": employeeID,
|
|
"date_from": dateFrom,
|
|
"date_to": dateTo,
|
|
"leaves": summary,
|
|
"allocations": allocations,
|
|
"total_days": totalDays,
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
// initHrLeaveTypeExtensions adds remaining quota computation to leave types.
|
|
func initHrLeaveTypeExtensions() {
|
|
lt := orm.ExtendModel("hr.leave.type")
|
|
|
|
lt.AddFields(
|
|
orm.Float("remaining_leaves", orm.FieldOpts{
|
|
String: "Remaining Leaves", Compute: "_compute_remaining_leaves",
|
|
Help: "Remaining leaves for current employee (allocated - taken)",
|
|
}),
|
|
)
|
|
|
|
// _compute_remaining_quota: Calculate remaining leaves per type for current user's employee.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave_type.py _compute_leaves()
|
|
lt.RegisterCompute("remaining_leaves", func(rs *orm.Recordset) (orm.Values, error) {
|
|
env := rs.Env()
|
|
typeID := rs.IDs()[0]
|
|
|
|
// Get current user's employee
|
|
var employeeID int64
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT id FROM hr_employee WHERE user_id = $1 LIMIT 1`, env.UID(),
|
|
).Scan(&employeeID)
|
|
if employeeID == 0 {
|
|
return orm.Values{"remaining_leaves": float64(0)}, nil
|
|
}
|
|
|
|
// Allocated days for this type (approved allocations)
|
|
var allocated float64
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(SUM(number_of_days), 0) FROM hr_leave_allocation
|
|
WHERE employee_id = $1 AND holiday_status_id = $2 AND state = 'validate'`,
|
|
employeeID, typeID).Scan(&allocated)
|
|
|
|
// Used days for this type (approved leaves, current fiscal year)
|
|
var used float64
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(SUM(number_of_days), 0) FROM hr_leave
|
|
WHERE employee_id = $1 AND holiday_status_id = $2 AND state = 'validate'
|
|
AND date_from >= date_trunc('year', CURRENT_DATE)`,
|
|
employeeID, typeID).Scan(&used)
|
|
|
|
return orm.Values{"remaining_leaves": allocated - used}, nil
|
|
})
|
|
}
|
|
|
|
// initHrLeaveAllocation registers the hr.leave.allocation model.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave_allocation.py
|
|
func initHrLeaveAllocation() {
|
|
m := orm.NewModel("hr.leave.allocation", orm.ModelOpts{
|
|
Description: "Time Off Allocation",
|
|
Order: "create_date desc",
|
|
})
|
|
m.AddFields(
|
|
orm.Char("name", orm.FieldOpts{String: "Description"}),
|
|
orm.Many2one("employee_id", "hr.employee", orm.FieldOpts{String: "Employee", Required: true}),
|
|
orm.Many2one("holiday_status_id", "hr.leave.type", orm.FieldOpts{String: "Time Off Type", Required: true}),
|
|
orm.Float("number_of_days", orm.FieldOpts{String: "Duration (Days)", Required: true}),
|
|
orm.Selection("state", []orm.SelectionItem{
|
|
{Value: "draft", Label: "To Submit"},
|
|
{Value: "confirm", Label: "To Approve"},
|
|
{Value: "validate", Label: "Approved"},
|
|
{Value: "refuse", Label: "Refused"},
|
|
}, orm.FieldOpts{String: "Status", Default: "draft"}),
|
|
orm.Many2one("department_id", "hr.department", orm.FieldOpts{String: "Department"}),
|
|
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
|
orm.Selection("allocation_type", []orm.SelectionItem{
|
|
{Value: "regular", Label: "Regular Allocation"},
|
|
{Value: "accrual", Label: "Accrual Allocation"},
|
|
}, orm.FieldOpts{String: "Allocation Type", Default: "regular"}),
|
|
orm.Float("accrual_increment", orm.FieldOpts{
|
|
String: "Monthly Accrual Increment",
|
|
Help: "Number of days added each month for accrual allocations",
|
|
}),
|
|
orm.Date("last_accrual_date", orm.FieldOpts{
|
|
String: "Last Accrual Date",
|
|
Help: "Date when the last accrual increment was applied",
|
|
}),
|
|
)
|
|
|
|
// action_approve: Approve allocation request.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave_allocation.py HrLeaveAllocation.action_approve()
|
|
m.RegisterMethod("action_approve", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave_allocation WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.allocation: read state for %d: %w", id, err)
|
|
}
|
|
if state != "confirm" && state != "draft" {
|
|
return nil, fmt.Errorf("hr.leave.allocation: can only approve allocations in 'To Submit' or 'To Approve' state (allocation %d is %q)", id, state)
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave_allocation SET state = 'validate' WHERE id = $1`, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.allocation: approve allocation %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// action_refuse: Refuse allocation request.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave_allocation.py HrLeaveAllocation.action_refuse()
|
|
m.RegisterMethod("action_refuse", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM hr_leave_allocation WHERE id = $1`, id,
|
|
).Scan(&state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.allocation: read state for %d: %w", id, err)
|
|
}
|
|
if state == "validate" {
|
|
return nil, fmt.Errorf("hr.leave.allocation: cannot refuse an already approved allocation (allocation %d)", id)
|
|
}
|
|
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave_allocation SET state = 'refuse' WHERE id = $1`, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.allocation: refuse allocation %d: %w", id, err)
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
}
|
|
|
|
// initHrLeaveAccrualCron registers the accrual allocation cron method.
|
|
// Mirrors: odoo/addons/hr_holidays/models/hr_leave_allocation.py _cron_accrual_allocation()
|
|
func initHrLeaveAccrualCron() {
|
|
alloc := orm.ExtendModel("hr.leave.allocation")
|
|
|
|
// _cron_accrual_allocation: Auto-increment approved accrual-type allocations monthly.
|
|
// For each approved accrual allocation whose last_accrual_date is more than a month ago
|
|
// (or NULL), add accrual_increment days to number_of_days and update last_accrual_date.
|
|
alloc.RegisterMethod("_cron_accrual_allocation", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
today := time.Now().Format("2006-01-02")
|
|
|
|
// Find all approved accrual allocations due for increment
|
|
// Due = last_accrual_date is NULL or more than 30 days ago
|
|
rows, err := env.Tx().Query(env.Ctx(),
|
|
`SELECT id, COALESCE(number_of_days, 0), COALESCE(accrual_increment, 0)
|
|
FROM hr_leave_allocation
|
|
WHERE state = 'validate'
|
|
AND allocation_type = 'accrual'
|
|
AND COALESCE(accrual_increment, 0) > 0
|
|
AND (last_accrual_date IS NULL
|
|
OR last_accrual_date <= CURRENT_DATE - INTERVAL '30 days')`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hr.leave.allocation: accrual cron query: %w", err)
|
|
}
|
|
|
|
type accrualRow struct {
|
|
id int64
|
|
days float64
|
|
increment float64
|
|
}
|
|
var pending []accrualRow
|
|
for rows.Next() {
|
|
var r accrualRow
|
|
if err := rows.Scan(&r.id, &r.days, &r.increment); err != nil {
|
|
continue
|
|
}
|
|
pending = append(pending, r)
|
|
}
|
|
rows.Close()
|
|
|
|
var updated int64
|
|
for _, r := range pending {
|
|
newDays := r.days + r.increment
|
|
if _, err := env.Tx().Exec(env.Ctx(),
|
|
`UPDATE hr_leave_allocation
|
|
SET number_of_days = $1, last_accrual_date = $2
|
|
WHERE id = $3`, newDays, today, r.id); err != nil {
|
|
return nil, fmt.Errorf("hr.leave.allocation: accrual update %d: %w", r.id, err)
|
|
}
|
|
updated++
|
|
}
|
|
|
|
if updated > 0 {
|
|
fmt.Printf("hr.leave.allocation accrual cron: incremented %d allocations\n", updated)
|
|
}
|
|
return map[string]interface{}{"updated": updated}, nil
|
|
})
|
|
}
|