package models import ( "fmt" "odoo-go/pkg/orm" ) // initCrmTeamExpanded extends the basic crm.team with CRM-specific fields // (pipeline dashboard, lead assignment, team configuration). // Mirrors: odoo/addons/crm/models/crm_team.py CrmTeam func initCrmTeamExpanded() { m := orm.ExtendModel("crm.team") m.AddFields( // Team configuration orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Team Leader"}), orm.One2many("crm_team_member_ids", "crm.team.member", "crm_team_id", orm.FieldOpts{ String: "Sales Team Members", }), orm.Boolean("use_leads", orm.FieldOpts{ String: "Leads", Default: true, Help: "Check this box to filter and qualify leads.", }), orm.Boolean("use_opportunities", orm.FieldOpts{ String: "Pipeline", Default: true, Help: "Check this box to manage a pipeline of opportunities.", }), orm.Many2one("alias_id", "mail.alias", orm.FieldOpts{ String: "Email Alias", Help: "Incoming emails on this alias will create leads/opportunities.", }), // Dashboard computed fields // Mirrors: _compute_opportunities_data in crm.team orm.Integer("opportunities_count", orm.FieldOpts{ String: "Number of Opportunities", Compute: "_compute_counts", Help: "Number of active opportunities in this team's pipeline.", }), orm.Monetary("opportunities_amount", orm.FieldOpts{ String: "Opportunities Revenue", Compute: "_compute_counts", CurrencyField: "currency_id", Help: "Total expected revenue of active opportunities.", }), orm.Many2one("currency_id", "res.currency", orm.FieldOpts{ String: "Currency", }), orm.Integer("unassigned_leads_count", orm.FieldOpts{ String: "Unassigned Leads", Compute: "_compute_counts", Help: "Number of leads without a salesperson assigned.", }), // Assignment settings // Mirrors: odoo/addons/crm/models/crm_team.py assignment fields orm.Selection("assignment_domain", []orm.SelectionItem{ {Value: "manual", Label: "Manual"}, {Value: "auto", Label: "Auto Assignment"}, }, orm.FieldOpts{ String: "Assignment Method", Default: "manual", Help: "How new leads are distributed among team members.", }), orm.Integer("assignment_max", orm.FieldOpts{ String: "Max Leads per Cycle", Default: 30, Help: "Maximum leads assigned to a member per assignment cycle.", }), orm.Boolean("assignment_enabled", orm.FieldOpts{ String: "Auto-Assignment Active", Help: "Enable automatic lead assignment for this team.", }), orm.Integer("assignment_optout_count", orm.FieldOpts{ String: "Members Opted-Out", Compute: "_compute_assignment_optout", Help: "Number of members who opted out of automatic assignment.", }), ) // _compute_counts: compute dashboard KPIs for the sales team. // Mirrors: odoo/addons/crm/models/crm_team.py _compute_opportunities_data m.RegisterCompute("opportunities_count", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() teamID := rs.IDs()[0] var count int64 var amount float64 _ = env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*), COALESCE(SUM(expected_revenue::float8), 0) FROM crm_lead WHERE team_id = $1 AND active = true AND type = 'opportunity'`, teamID, ).Scan(&count, &amount) var unassigned int64 _ = env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM crm_lead WHERE team_id = $1 AND active = true AND user_id IS NULL`, teamID, ).Scan(&unassigned) return orm.Values{ "opportunities_count": count, "opportunities_amount": amount, "unassigned_leads_count": unassigned, }, nil }) // action_assign_leads: trigger automatic lead assignment. // Mirrors: odoo/addons/crm/models/crm_team.py action_assign_leads m.RegisterMethod("action_assign_leads", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() teamID := rs.IDs()[0] // Fetch active members with remaining capacity rows, err := env.Tx().Query(env.Ctx(), ` SELECT m.user_id, COALESCE(m.assignment_max, 30) AS max_leads, COALESCE((SELECT COUNT(*) FROM crm_lead l WHERE l.user_id = m.user_id AND l.team_id = $1 AND l.active = true AND l.create_date >= date_trunc('month', CURRENT_DATE)), 0) AS current_count FROM crm_team_member m WHERE m.crm_team_id = $1 AND m.active = true ORDER BY current_count ASC`, teamID) if err != nil { return nil, fmt.Errorf("action_assign_leads: query members: %w", err) } defer rows.Close() type memberCap struct { userID int64 capacity int64 } var members []memberCap for rows.Next() { var uid int64 var maxLeads, current int64 if err := rows.Scan(&uid, &maxLeads, ¤t); err != nil { return nil, err } if remaining := maxLeads - current; remaining > 0 { members = append(members, memberCap{userID: uid, capacity: remaining}) } } if len(members) == 0 { return map[string]interface{}{"assigned": 0}, nil } // Fetch unassigned leads leadRows, err := env.Tx().Query(env.Ctx(), ` SELECT id FROM crm_lead WHERE team_id = $1 AND active = true AND user_id IS NULL ORDER BY priority DESC, id ASC`, teamID) if err != nil { return nil, fmt.Errorf("action_assign_leads: query leads: %w", err) } defer leadRows.Close() var assigned int64 memberIdx := 0 for leadRows.Next() { var leadID int64 if err := leadRows.Scan(&leadID); err != nil { return nil, err } if memberIdx >= len(members) { break } mc := &members[memberIdx] _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET user_id = $1 WHERE id = $2`, mc.userID, leadID) assigned++ mc.capacity-- if mc.capacity <= 0 { memberIdx++ } } return map[string]interface{}{"assigned": assigned}, nil }) // action_open_opportunities: return window action for team's opportunities. // Mirrors: odoo/addons/crm/models/crm_team.py action_open_opportunities m.RegisterMethod("action_open_opportunities", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { return map[string]interface{}{ "type": "ir.actions.act_window", "name": "Opportunities", "res_model": "crm.lead", "view_mode": "kanban,tree,form", "domain": fmt.Sprintf("[('team_id','=',%d),('type','=','opportunity')]", rs.IDs()[0]), "context": map[string]interface{}{"default_team_id": rs.IDs()[0], "default_type": "opportunity"}, }, nil }) // action_open_unassigned: return window action for unassigned leads. // Mirrors: odoo/addons/crm/models/crm_team.py (dashboard actions) m.RegisterMethod("action_open_unassigned", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { return map[string]interface{}{ "type": "ir.actions.act_window", "name": "Unassigned Leads", "res_model": "crm.lead", "view_mode": "tree,form", "domain": fmt.Sprintf("[('team_id','=',%d),('user_id','=',False)]", rs.IDs()[0]), "context": map[string]interface{}{"default_team_id": rs.IDs()[0]}, }, nil }) } // initCrmTeamMember registers the crm.team.member model. // Mirrors: odoo/addons/crm/models/crm_team_member.py CrmTeamMember func initCrmTeamMember() { m := orm.NewModel("crm.team.member", orm.ModelOpts{ Description: "Sales Team Member", }) m.AddFields( orm.Many2one("crm_team_id", "crm.team", orm.FieldOpts{ String: "Sales Team", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Many2one("user_id", "res.users", orm.FieldOpts{ String: "Salesperson", Required: true, Index: true, }), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), orm.Float("assignment_max", orm.FieldOpts{ String: "Max Leads", Help: "Maximum number of leads this member should be assigned per month.", Default: float64(30), }), orm.Integer("lead_month_count", orm.FieldOpts{ String: "Leads This Month", Compute: "_compute_lead_count", Help: "Number of leads assigned to this member in the current month.", }), orm.Boolean("assignment_optout", orm.FieldOpts{ String: "Opt-Out", Help: "If checked, this member will not receive automatically assigned leads.", }), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", Help: "Company of the sales team.", }), ) // _compute_lead_count: count leads assigned to this member this month. // Mirrors: odoo/addons/crm/models/crm_team_member.py _compute_lead_month_count m.RegisterCompute("lead_month_count", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() memberID := rs.IDs()[0] var userID, teamID int64 _ = env.Tx().QueryRow(env.Ctx(), `SELECT user_id, crm_team_id FROM crm_team_member WHERE id = $1`, memberID, ).Scan(&userID, &teamID) var count int64 _ = env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM crm_lead WHERE user_id = $1 AND team_id = $2 AND active = true AND create_date >= date_trunc('month', CURRENT_DATE)`, userID, teamID, ).Scan(&count) return orm.Values{"lead_month_count": count}, nil }) // SQL constraint: one member per team m.AddSQLConstraint( "unique_member_per_team", "UNIQUE(crm_team_id, user_id)", "A user can only be a member of a team once.", ) }