package models import ( "fmt" "odoo-go/pkg/orm" ) // initCRMLeadExtended adds activity, address, contact, and scoring fields // to crm.lead, plus business methods (merge, assign, schedule, duplicate, etc.). // Mirrors: odoo/addons/crm/models/crm_lead.py (extended CRM fields and methods) func initCRMLeadExtended() { m := orm.ExtendModel("crm.lead") // ──── Activity fields ──── // Mirrors: odoo/addons/crm/models/crm_lead.py activity fields m.AddFields( orm.Date("activity_date_deadline", orm.FieldOpts{ String: "Next Activity Deadline", Help: "Date by which the next activity should be completed.", }), orm.Char("activity_summary", orm.FieldOpts{ String: "Next Activity Summary", Help: "Short description of the next scheduled activity.", }), orm.Selection("activity_type", []orm.SelectionItem{ {Value: "email", Label: "Email"}, {Value: "call", Label: "Call"}, {Value: "meeting", Label: "Meeting"}, {Value: "todo", Label: "To-Do"}, }, orm.FieldOpts{ String: "Activity Type", Help: "Type of the next scheduled activity.", }), orm.Many2one("activity_user_id", "res.users", orm.FieldOpts{ String: "Activity Responsible", Help: "User responsible for the next scheduled activity.", }), // ──── Tracking / timing fields ──── // Mirrors: odoo/addons/crm/models/crm_lead.py day_open, day_close orm.Integer("day_open", orm.FieldOpts{ String: "Days to Assign", Help: "Number of days to assign this lead to a salesperson.", }), orm.Integer("day_close", orm.FieldOpts{ String: "Days to Close", Help: "Number of days to close this lead/opportunity.", }), // ──── Additional contact/address fields ──── // Fields not already on crm.lead base definition. // Mirrors: odoo/addons/crm/models/crm_lead.py address & contact fields orm.Char("referred", orm.FieldOpts{ String: "Referred By", Help: "Name or reference of who referred this lead.", }), orm.Char("title", orm.FieldOpts{ String: "Title", Help: "Contact title (Mr., Mrs., etc.).", }), orm.Many2one("state_id", "res.country.state", orm.FieldOpts{ String: "State", Help: "State/province of the lead address.", }), orm.Text("contact_name", orm.FieldOpts{ String: "Contact Name", Help: "Name of the primary contact person.", }), orm.Char("mobile", orm.FieldOpts{ String: "Mobile", Help: "Mobile phone number of the contact.", }), orm.Char("street2", orm.FieldOpts{ String: "Street2", Help: "Second line of the street address.", }), // ──── Revenue fields ──── // Mirrors: odoo/addons/crm/models/crm_lead.py prorated_revenue orm.Monetary("prorated_revenue", orm.FieldOpts{ String: "Prorated Revenue", CurrencyField: "company_currency", Compute: "_compute_prorated_revenue", Help: "Expected revenue weighted by probability.", }), orm.Many2one("company_currency", "res.currency", orm.FieldOpts{ String: "Company Currency", }), // ──── Scoring / automated fields ──── // Mirrors: odoo/addons/crm/models/crm_lead_scoring.py (simplified) orm.Float("automated_probability", orm.FieldOpts{ String: "Automated Probability (%)", Help: "Probability computed by the lead scoring engine.", }), orm.Boolean("is_automated_probability", orm.FieldOpts{ String: "Use Automated Probability", Default: true, Help: "If true, probability is set automatically by scoring.", }), // ──── Source / campaign tracking ──── // Mirrors: odoo/addons/crm/models/crm_lead.py utm fields orm.Many2one("source_id", "utm.source", orm.FieldOpts{ String: "Source", Help: "Source of the lead (e.g. search engine, social media).", }), orm.Many2one("medium_id", "utm.medium", orm.FieldOpts{ String: "Medium", Help: "Medium of the lead (e.g. email, banner).", }), orm.Many2one("campaign_id", "utm.campaign", orm.FieldOpts{ String: "Campaign", Help: "Marketing campaign that generated this lead.", }), // ──── Date tracking ──── // Mirrors: odoo/addons/crm/models/crm_lead.py date_conversion, date_closed orm.Datetime("date_conversion", orm.FieldOpts{ String: "Conversion Date", Help: "Date when the lead was converted to an opportunity.", }), orm.Datetime("date_closed", orm.FieldOpts{ String: "Closed Date", Help: "Date when the opportunity was won or lost.", }), ) // ──── Compute: prorated revenue ──── // Mirrors: odoo/addons/crm/models/crm_lead.py _compute_prorated_revenue m.RegisterCompute("prorated_revenue", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() leadID := rs.IDs()[0] var revenue float64 var probability float64 _ = env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(expected_revenue::float8, 0), COALESCE(probability, 0) FROM crm_lead WHERE id = $1`, leadID, ).Scan(&revenue, &probability) prorated := revenue * probability / 100.0 return orm.Values{"prorated_revenue": prorated}, nil }) // ──── Business Methods ──── // action_schedule_activity: return a window action to schedule an activity. // Mirrors: odoo/addons/crm/models/crm_lead.py action_schedule_activity m.RegisterMethod("action_schedule_activity", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { return map[string]interface{}{ "type": "ir.actions.act_window", "name": "Schedule Activity", "res_model": "crm.lead", "res_id": rs.IDs()[0], "view_mode": "form", "views": [][]interface{}{{nil, "form"}}, "target": "new", }, nil }) // action_merge: merge multiple leads into the first one. // Sums expected revenues from slave leads, deactivates them. // Mirrors: odoo/addons/crm/wizard/crm_merge_opportunities.py m.RegisterMethod("action_merge", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() ids := rs.IDs() if len(ids) < 2 { return nil, fmt.Errorf("need at least 2 leads to merge") } masterID := ids[0] for _, slaveID := range ids[1:] { // Sum revenues from slave into master _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET expected_revenue = COALESCE(expected_revenue, 0) + (SELECT COALESCE(expected_revenue, 0) FROM crm_lead WHERE id = $1) WHERE id = $2`, slaveID, masterID) // Copy partner info if master has none _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET partner_id = COALESCE( (SELECT partner_id FROM crm_lead WHERE id = $2), partner_id) WHERE id = $1 AND partner_id IS NULL`, masterID, slaveID) // Deactivate the slave lead _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET active = false WHERE id = $1`, slaveID) } return map[string]interface{}{ "type": "ir.actions.act_window", "res_model": "crm.lead", "res_id": masterID, "view_mode": "form", "views": [][]interface{}{{nil, "form"}}, "target": "current", }, nil }) // action_assign_salesperson: assign a salesperson to one or more leads. // Mirrors: odoo/addons/crm/models/crm_lead.py _handle_salesmen_assignment m.RegisterMethod("action_assign_salesperson", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("user_id required") } userID, ok := args[0].(float64) if !ok { return nil, fmt.Errorf("user_id must be a number") } env := rs.Env() for _, id := range rs.IDs() { _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET user_id = $1 WHERE id = $2`, int64(userID), id) } return true, nil }) // action_duplicate: duplicate a lead with a modified name. // Mirrors: odoo/addons/crm/models/crm_lead.py copy() m.RegisterMethod("action_duplicate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() leadID := rs.IDs()[0] var newID int64 err := env.Tx().QueryRow(env.Ctx(), `INSERT INTO crm_lead (name, type, partner_id, email_from, phone, stage_id, team_id, user_id, expected_revenue, probability, priority, company_id, currency_id, active, description) SELECT name || ' (copy)', type, partner_id, email_from, phone, stage_id, team_id, user_id, expected_revenue, probability, priority, company_id, currency_id, true, description FROM crm_lead WHERE id = $1 RETURNING id`, leadID, ).Scan(&newID) if err != nil { return nil, fmt.Errorf("action_duplicate: %w", err) } return map[string]interface{}{ "type": "ir.actions.act_window", "res_model": "crm.lead", "res_id": newID, "view_mode": "form", "views": [][]interface{}{{nil, "form"}}, "target": "current", }, nil }) // action_set_priority: set priority on selected leads. // Mirrors: odoo/addons/crm/models/crm_lead.py (bulk actions) m.RegisterMethod("action_set_priority", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("priority value required (0-3)") } priority, ok := args[0].(string) if !ok { return nil, fmt.Errorf("priority must be a string ('0','1','2','3')") } env := rs.Env() for _, id := range rs.IDs() { _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET priority = $1 WHERE id = $2`, priority, id) } return true, nil }) // action_archive: archive (deactivate) selected leads. // Mirrors: odoo/addons/crm/models/crm_lead.py toggle_active m.RegisterMethod("action_archive", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET active = false WHERE id = $1`, id) } return true, nil }) // action_unarchive: restore (reactivate) selected leads. // Mirrors: odoo/addons/crm/models/crm_lead.py toggle_active m.RegisterMethod("action_unarchive", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET active = true WHERE id = $1`, id) } return true, nil }) // action_set_stage: move leads to a specific stage. // Mirrors: odoo/addons/crm/models/crm_lead.py write() stage change logic m.RegisterMethod("action_set_stage", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stage_id required") } stageID, ok := args[0].(float64) if !ok { return nil, fmt.Errorf("stage_id must be a number") } env := rs.Env() for _, id := range rs.IDs() { _, _ = env.Tx().Exec(env.Ctx(), `UPDATE crm_lead SET stage_id = $1, date_last_stage_update = NOW() WHERE id = $2`, int64(stageID), id) } return true, nil }) // _get_lead_statistics: return summary statistics for a set of leads. // Internal helper used by dashboard views. m.RegisterMethod("_get_lead_statistics", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() var totalLeads, totalOpps, wonCount, lostCount int64 var totalRevenue, avgProbability float64 _ = env.Tx().QueryRow(env.Ctx(), ` SELECT COUNT(*) FILTER (WHERE type = 'lead'), COUNT(*) FILTER (WHERE type = 'opportunity'), COUNT(*) FILTER (WHERE state = 'won'), COUNT(*) FILTER (WHERE state = 'lost'), COALESCE(SUM(expected_revenue::float8), 0), COALESCE(AVG(probability), 0) FROM crm_lead WHERE active = true`, ).Scan(&totalLeads, &totalOpps, &wonCount, &lostCount, &totalRevenue, &avgProbability) return map[string]interface{}{ "total_leads": totalLeads, "total_opportunities": totalOpps, "won_count": wonCount, "lost_count": lostCount, "total_revenue": totalRevenue, "avg_probability": avgProbability, }, nil }) // Onchange: partner_id → populate contact/address fields from partner // Mirrors: odoo/addons/crm/models/crm_lead.py _onchange_partner_id m.RegisterOnchange("partner_id", func(env *orm.Environment, vals orm.Values) orm.Values { result := make(orm.Values) partnerID, ok := vals["partner_id"] if !ok || partnerID == nil { return result } pid, _ := partnerID.(float64) if pid == 0 { return result } var email, phone, street, city, zip, name string _ = env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(email,''), COALESCE(phone,''), COALESCE(street,''), COALESCE(city,''), COALESCE(zip,''), COALESCE(name,'') FROM res_partner WHERE id = $1`, int64(pid), ).Scan(&email, &phone, &street, &city, &zip, &name) if email != "" { result["email_from"] = email } if phone != "" { result["phone"] = phone } if street != "" { result["street"] = street } if city != "" { result["city"] = city } if zip != "" { result["zip"] = zip } if name != "" { result["partner_name"] = name } return result }) }