package models import ( "fmt" "log" "odoo-go/pkg/orm" ) // initCrmAnalysis registers the crm.lead.analysis transient model // for pipeline reporting and dashboard data. // Mirrors: odoo/addons/crm/report/crm_activity_report.py (simplified) func initCrmAnalysis() { m := orm.NewModel("crm.lead.analysis", orm.ModelOpts{ Description: "Pipeline Analysis", Type: orm.ModelTransient, }) m.AddFields( orm.Many2one("team_id", "crm.team", orm.FieldOpts{ String: "Sales Team", Help: "Filter analysis by sales team.", }), orm.Many2one("user_id", "res.users", orm.FieldOpts{ String: "Salesperson", Help: "Filter analysis by salesperson.", }), orm.Date("date_from", orm.FieldOpts{ String: "From", Help: "Start date for the analysis period.", }), orm.Date("date_to", orm.FieldOpts{ String: "To", Help: "End date for the analysis period.", }), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", Help: "Filter analysis by company.", }), ) // get_pipeline_data: return pipeline statistics grouped by stage. // Mirrors: odoo/addons/crm/report/crm_activity_report.py read_group m.RegisterMethod("get_pipeline_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() // Pipeline by stage rows, err := env.Tx().Query(env.Ctx(), ` SELECT s.name, COUNT(l.id), COALESCE(SUM(l.expected_revenue::float8), 0) FROM crm_lead l JOIN crm_stage s ON s.id = l.stage_id WHERE l.active = true AND l.type = 'opportunity' GROUP BY s.id, s.name, s.sequence ORDER BY s.sequence`) if err != nil { return nil, fmt.Errorf("get_pipeline_data: stages query: %w", err) } defer rows.Close() var stages []map[string]interface{} for rows.Next() { var name string var count int64 var revenue float64 if err := rows.Scan(&name, &count, &revenue); err != nil { return nil, fmt.Errorf("get_pipeline_data: scan stage: %w", err) } stages = append(stages, map[string]interface{}{ "stage": name, "count": count, "revenue": revenue, }) } // Win rate var total, won int64 if err := env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*), COALESCE(SUM(CASE WHEN s.is_won THEN 1 ELSE 0 END), 0) FROM crm_lead l JOIN crm_stage s ON s.id = l.stage_id WHERE l.type = 'opportunity'`, ).Scan(&total, &won); err != nil { log.Printf("warning: crm win rate query failed: %v", err) } winRate := float64(0) if total > 0 { winRate = float64(won) / float64(total) * 100 } return map[string]interface{}{ "stages": stages, "total": total, "won": won, "win_rate": winRate, }, nil }) // get_conversion_data: return lead-to-opportunity conversion statistics. // Mirrors: odoo/addons/crm/report/crm_activity_report.py (conversion metrics) m.RegisterMethod("get_conversion_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() var totalLeads, convertedLeads int64 if err := env.Tx().QueryRow(env.Ctx(), ` SELECT COUNT(*) FILTER (WHERE type = 'lead'), COUNT(*) FILTER (WHERE type = 'opportunity' AND date_conversion IS NOT NULL) FROM crm_lead WHERE active = true`, ).Scan(&totalLeads, &convertedLeads); err != nil { log.Printf("warning: crm conversion data query failed: %v", err) } conversionRate := float64(0) if totalLeads > 0 { conversionRate = float64(convertedLeads) / float64(totalLeads) * 100 } // Average days to convert var avgDaysConvert float64 if err := env.Tx().QueryRow(env.Ctx(), ` SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (date_conversion - create_date)) / 86400), 0) FROM crm_lead WHERE type = 'opportunity' AND date_conversion IS NOT NULL AND active = true`, ).Scan(&avgDaysConvert); err != nil { log.Printf("warning: crm avg days to convert query failed: %v", err) } // Average days to close (won) var avgDaysClose float64 if err := env.Tx().QueryRow(env.Ctx(), ` SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (date_closed - create_date)) / 86400), 0) FROM crm_lead WHERE state = 'won' AND date_closed IS NOT NULL`, ).Scan(&avgDaysClose); err != nil { log.Printf("warning: crm avg days to close query failed: %v", err) } return map[string]interface{}{ "total_leads": totalLeads, "converted_leads": convertedLeads, "conversion_rate": conversionRate, "avg_days_convert": avgDaysConvert, "avg_days_close": avgDaysClose, }, nil }) // get_team_performance: return per-team performance comparison. // Mirrors: odoo/addons/crm/report/crm_activity_report.py (team grouping) m.RegisterMethod("get_team_performance", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), ` SELECT t.name, COUNT(l.id) AS opp_count, COALESCE(SUM(l.expected_revenue::float8), 0) AS total_revenue, COALESCE(AVG(l.probability), 0) AS avg_probability, COUNT(l.id) FILTER (WHERE l.state = 'won') AS won_count FROM crm_lead l JOIN crm_team t ON t.id = l.team_id WHERE l.active = true AND l.type = 'opportunity' GROUP BY t.id, t.name ORDER BY total_revenue DESC`) if err != nil { return nil, fmt.Errorf("get_team_performance: %w", err) } defer rows.Close() var teams []map[string]interface{} for rows.Next() { var name string var oppCount, wonCount int64 var totalRevenue, avgProb float64 if err := rows.Scan(&name, &oppCount, &totalRevenue, &avgProb, &wonCount); err != nil { return nil, fmt.Errorf("get_team_performance: scan: %w", err) } winRate := float64(0) if oppCount > 0 { winRate = float64(wonCount) / float64(oppCount) * 100 } teams = append(teams, map[string]interface{}{ "team": name, "opportunities": oppCount, "revenue": totalRevenue, "avg_probability": avgProb, "won": wonCount, "win_rate": winRate, }) } return map[string]interface{}{"teams": teams}, nil }) // get_salesperson_performance: return per-salesperson performance data. // Mirrors: odoo/addons/crm/report/crm_activity_report.py (user grouping) m.RegisterMethod("get_salesperson_performance", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), ` SELECT u.login, COUNT(l.id) AS opp_count, COALESCE(SUM(l.expected_revenue::float8), 0) AS total_revenue, COUNT(l.id) FILTER (WHERE l.state = 'won') AS won_count, COUNT(l.id) FILTER (WHERE l.state = 'lost') AS lost_count FROM crm_lead l JOIN res_users u ON u.id = l.user_id WHERE l.active = true AND l.type = 'opportunity' GROUP BY u.id, u.login ORDER BY total_revenue DESC`) if err != nil { return nil, fmt.Errorf("get_salesperson_performance: %w", err) } defer rows.Close() var users []map[string]interface{} for rows.Next() { var login string var oppCount, wonCount, lostCount int64 var totalRevenue float64 if err := rows.Scan(&login, &oppCount, &totalRevenue, &wonCount, &lostCount); err != nil { return nil, fmt.Errorf("get_salesperson_performance: scan: %w", err) } winRate := float64(0) if oppCount > 0 { winRate = float64(wonCount) / float64(oppCount) * 100 } users = append(users, map[string]interface{}{ "salesperson": login, "opportunities": oppCount, "revenue": totalRevenue, "won": wonCount, "lost": lostCount, "win_rate": winRate, }) } return map[string]interface{}{"salespersons": users}, nil }) }