package models import ( "fmt" "time" "odoo-go/pkg/orm" ) // initCRMLead registers the crm.lead model. // Mirrors: odoo/addons/crm/models/crm_lead.py func initCRMLead() { m := orm.NewModel("crm.lead", orm.ModelOpts{ Description: "Lead/Opportunity", Order: "priority desc, id desc", RecName: "name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Opportunity", Required: true, Index: true}), orm.Selection("type", []orm.SelectionItem{ {Value: "lead", Label: "Lead"}, {Value: "opportunity", Label: "Opportunity"}, }, orm.FieldOpts{String: "Type", Required: true, Default: "lead", Index: true}), orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Customer", Index: true}), orm.Char("partner_name", orm.FieldOpts{String: "Company Name"}), orm.Char("email_from", orm.FieldOpts{String: "Email", Index: true}), orm.Char("phone", orm.FieldOpts{String: "Phone"}), orm.Char("website", orm.FieldOpts{String: "Website"}), orm.Char("function", orm.FieldOpts{String: "Job Position"}), orm.Selection("state", []orm.SelectionItem{ {Value: "open", Label: "Open"}, {Value: "won", Label: "Won"}, {Value: "lost", Label: "Lost"}, }, orm.FieldOpts{String: "Status", Default: "open"}), orm.Many2one("stage_id", "crm.stage", orm.FieldOpts{String: "Stage", Index: true}), orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Salesperson", Index: true}), orm.Many2one("team_id", "crm.team", orm.FieldOpts{String: "Sales Team", Index: true}), orm.Monetary("expected_revenue", orm.FieldOpts{ String: "Expected Revenue", CurrencyField: "currency_id", }), orm.Monetary("recurring_revenue", orm.FieldOpts{ String: "Recurring Revenue", CurrencyField: "currency_id", }), orm.Selection("recurring_plan", []orm.SelectionItem{ {Value: "monthly", Label: "Monthly"}, {Value: "quarterly", Label: "Quarterly"}, {Value: "yearly", Label: "Yearly"}, }, orm.FieldOpts{String: "Recurring Plan"}), orm.Date("date_deadline", orm.FieldOpts{String: "Expected Closing"}), orm.Datetime("date_last_stage_update", orm.FieldOpts{String: "Last Stage Update"}), orm.Selection("priority", []orm.SelectionItem{ {Value: "0", Label: "Normal"}, {Value: "1", Label: "Low"}, {Value: "2", Label: "High"}, {Value: "3", Label: "Very High"}, }, orm.FieldOpts{String: "Priority", Default: "0"}), orm.Integer("color", orm.FieldOpts{String: "Color Index"}), orm.Many2many("tag_ids", "crm.tag", orm.FieldOpts{String: "Tags"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", Index: true, }), orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}), orm.Float("probability", orm.FieldOpts{String: "Probability (%)"}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), orm.Text("description", orm.FieldOpts{String: "Notes"}), orm.Many2one("lost_reason_id", "crm.lost.reason", orm.FieldOpts{String: "Lost Reason"}), // Address fields orm.Char("city", orm.FieldOpts{String: "City"}), orm.Char("street", orm.FieldOpts{String: "Street"}), orm.Char("zip", orm.FieldOpts{String: "Zip"}), orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}), ) // Onchange: stage_id -> auto-update probability from stage. // Mirrors: odoo/addons/crm/models/crm_lead.py _onchange_stage_id m.RegisterOnchange("stage_id", func(env *orm.Environment, vals orm.Values) orm.Values { result := make(orm.Values) stageID, ok := vals["stage_id"] if !ok || stageID == nil { return result } var sid float64 switch v := stageID.(type) { case float64: sid = v case int64: sid = float64(v) case int: sid = float64(v) default: return result } if sid == 0 { return result } var probability float64 if err := env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(probability, 10) FROM crm_stage WHERE id = $1`, int64(sid), ).Scan(&probability); err != nil { return result } result["probability"] = probability result["date_last_stage_update"] = time.Now().Format("2006-01-02 15:04:05") return result }) // DefaultGet: set company_id, user_id, team_id, type from session/defaults. // Mirrors: odoo/addons/crm/models/crm_lead.py default_get m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values { vals := make(orm.Values) if env.CompanyID() > 0 { vals["company_id"] = env.CompanyID() } if env.UID() > 0 { vals["user_id"] = env.UID() } vals["type"] = "lead" // Try to find a default sales team for the user var teamID int64 if env.UID() > 0 { if err := env.Tx().QueryRow(env.Ctx(), `SELECT ct.id FROM crm_team ct JOIN crm_team_member ctm ON ctm.crm_team_id = ct.id WHERE ctm.user_id = $1 AND ct.active = true ORDER BY ct.sequence LIMIT 1`, env.UID()).Scan(&teamID); err != nil { // No team found for user — not an error, just no default teamID = 0 } } if teamID > 0 { vals["team_id"] = teamID } return vals } // action_set_won: mark lead as won, set date_closed, find won stage. // Mirrors: odoo/addons/crm/models/crm_lead.py action_set_won m.RegisterMethod("action_set_won", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() var wonStageID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM crm_stage WHERE is_won = true ORDER BY sequence LIMIT 1`).Scan(&wonStageID) for _, id := range rs.IDs() { var err error if wonStageID > 0 { _, err = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET state = 'won', probability = 100, automated_probability = 100, date_closed = NOW(), active = true, stage_id = $2 WHERE id = $1`, id, wonStageID) } else { _, err = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET state = 'won', probability = 100, automated_probability = 100, date_closed = NOW(), active = true WHERE id = $1`, id) } if err != nil { return nil, fmt.Errorf("crm.lead: set_won %d: %w", id, err) } } return true, nil }) // action_set_lost: mark lead as lost, accept lost_reason_id from kwargs. // Mirrors: odoo/addons/crm/models/crm_lead.py action_set_lost m.RegisterMethod("action_set_lost", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() // Extract lost_reason_id from kwargs if provided var lostReasonID int64 if len(args) > 0 { if kwargs, ok := args[0].(map[string]interface{}); ok { if rid, ok := kwargs["lost_reason_id"]; ok { switch v := rid.(type) { case float64: lostReasonID = int64(v) case int64: lostReasonID = v case int: lostReasonID = int64(v) } } } } for _, id := range rs.IDs() { var err error if lostReasonID > 0 { _, err = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET state = 'lost', probability = 0, automated_probability = 0, active = false, date_closed = NOW(), lost_reason_id = $2 WHERE id = $1`, id, lostReasonID) } else { _, err = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET state = 'lost', probability = 0, automated_probability = 0, active = false, date_closed = NOW() WHERE id = $1`, id) } if err != nil { return nil, fmt.Errorf("crm.lead: set_lost %d: %w", id, err) } } return true, nil }) // convert_to_opportunity: lead -> opportunity, set date_conversion. // Mirrors: odoo/addons/crm/models/crm_lead.py _convert_opportunity_data m.RegisterMethod("convert_to_opportunity", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { if _, err := env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET type = 'opportunity', date_conversion = NOW(), date_open = COALESCE(date_open, NOW()) WHERE id = $1 AND type = 'lead'`, id); err != nil { return nil, fmt.Errorf("crm.lead: convert_to_opportunity %d: %w", id, err) } } return true, nil }) // convert_opportunity: convert lead to opportunity with optional partner/team assignment. // Mirrors: odoo/addons/crm/models/crm_lead.py convert_opportunity m.RegisterMethod("convert_opportunity", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() // Optional partner_id from args var partnerID int64 if len(args) > 0 { if pid, ok := args[0].(float64); ok { partnerID = int64(pid) } } for _, id := range rs.IDs() { if partnerID > 0 { env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET type = 'opportunity', date_conversion = NOW(), date_open = COALESCE(date_open, NOW()), partner_id = $2 WHERE id = $1`, id, partnerID) } else { env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET type = 'opportunity', date_conversion = NOW(), date_open = COALESCE(date_open, NOW()) WHERE id = $1`, id) } } return true, nil }) // action_set_won_rainbowman: set won + rainbow effect. // Mirrors: odoo/addons/crm/models/crm_lead.py action_set_won_rainbowman m.RegisterMethod("action_set_won_rainbowman", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() // Find the first won stage var wonStageID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM crm_stage WHERE is_won = true ORDER BY sequence LIMIT 1`).Scan(&wonStageID) for _, id := range rs.IDs() { if wonStageID > 0 { env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET state = 'won', probability = 100, automated_probability = 100, date_closed = NOW(), active = true, stage_id = $2 WHERE id = $1`, id, wonStageID) } else { env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET state = 'won', probability = 100, automated_probability = 100, date_closed = NOW(), active = true WHERE id = $1`, id) } } return map[string]interface{}{ "effect": map[string]interface{}{ "type": "rainbow_man", "message": "Congrats, you won this opportunity!", "fadeout": "slow", }, }, nil }) } // initCRMStage registers the crm.stage model. // Mirrors: odoo/addons/crm/models/crm_stage.py func initCRMStage() { m := orm.NewModel("crm.stage", orm.ModelOpts{ Description: "CRM Stage", Order: "sequence, name, id", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Stage Name", Required: true, Translate: true}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}), orm.Boolean("fold", orm.FieldOpts{String: "Folded in Pipeline"}), orm.Boolean("is_won", orm.FieldOpts{String: "Is Won Stage"}), orm.Float("probability", orm.FieldOpts{ String: "Probability (%)", Help: "Default probability when a lead enters this stage.", Default: float64(10), }), orm.Many2many("team_ids", "crm.team", orm.FieldOpts{String: "Sales Teams"}), orm.Text("requirements", orm.FieldOpts{String: "Requirements"}), ) } // initCRMTeam registers the crm.team model. // Mirrors: odoo/addons/crm/models/crm_team.py func initCRMTeam() { m := orm.NewModel("crm.team", orm.ModelOpts{ Description: "Sales Team", Order: "sequence, name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Sales Team", Required: true, Translate: true}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", Index: true, }), orm.Many2many("member_ids", "res.users", orm.FieldOpts{String: "Channel Members"}), orm.Integer("color", orm.FieldOpts{String: "Color Index"}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), ) } // initCRMTag registers the crm.tag model. // Mirrors: odoo/addons/crm/models/crm_lead.py CrmTag func initCRMTag() { orm.NewModel("crm.tag", orm.ModelOpts{ Description: "CRM Tag", Order: "name", }).AddFields( orm.Char("name", orm.FieldOpts{String: "Tag Name", Required: true, Translate: true}), orm.Integer("color", orm.FieldOpts{String: "Color Index"}), ) } // initCRMLostReason registers the crm.lost.reason model. // Mirrors: odoo/addons/crm/models/crm_lost_reason.py func initCRMLostReason() { orm.NewModel("crm.lost.reason", orm.ModelOpts{ Description: "Opp. Lost Reason", Order: "name", }).AddFields( orm.Char("name", orm.FieldOpts{String: "Description", Required: true, Translate: true}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), ) }