package models import ( "fmt" "log" "odoo-go/pkg/orm" "odoo-go/pkg/tools" ) // initMailThread extends existing models with mail.thread functionality. // In Python Odoo, models inherit from mail.thread to get chatter support. // Here we use ExtendModel to add the message fields and methods. // Mirrors: odoo/addons/mail/models/mail_thread.py func initMailThread() { // Models that support mail.thread chatter threadModels := []string{ "res.partner", "sale.order", "purchase.order", "account.move", "stock.picking", "crm.lead", "project.task", } for _, modelName := range threadModels { // Check if the model is registered (module may not be loaded) if orm.Registry.Get(modelName) == nil { continue } m := orm.ExtendModel(modelName) m.AddFields( orm.Integer("message_partner_ids_count", orm.FieldOpts{ String: "Followers Count", Help: "Number of partners following this document.", }), ) // message_post: post a new message on the record's chatter. // Mirrors: odoo/addons/mail/models/mail_thread.py message_post() m.RegisterMethod("message_post", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() ids := rs.IDs() if len(ids) == 0 { return false, nil } // Parse kwargs from args body := "" messageType := "comment" subject := "" var attachmentIDs []int64 if len(args) > 0 { if kw, ok := args[0].(map[string]interface{}); ok { if v, ok := kw["body"].(string); ok { body = v } if v, ok := kw["message_type"].(string); ok { messageType = v } if v, ok := kw["subject"].(string); ok { subject = v } if v, ok := kw["attachment_ids"].([]interface{}); ok { for _, aid := range v { switch id := aid.(type) { case float64: attachmentIDs = append(attachmentIDs, int64(id)) case int64: attachmentIDs = append(attachmentIDs, id) } } } } } // Get author from current user's partner_id var authorID int64 if err := env.Tx().QueryRow(env.Ctx(), `SELECT partner_id FROM res_users WHERE id = $1`, env.UID(), ).Scan(&authorID); err != nil { log.Printf("warning: mail_thread message_post author lookup failed: %v", err) } // Create mail.message var msgID int64 err := env.Tx().QueryRow(env.Ctx(), `INSERT INTO mail_message (model, res_id, body, message_type, author_id, subject, date, create_uid, write_uid, create_date, write_date) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $7, NOW(), NOW()) RETURNING id`, rs.ModelDef().Name(), ids[0], body, messageType, authorID, subject, env.UID(), ).Scan(&msgID) if err != nil { return nil, err } // Link attachments to the message via M2M for _, aid := range attachmentIDs { env.Tx().Exec(env.Ctx(), `INSERT INTO mail_message_ir_attachment_rel (mail_message_id, ir_attachment_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, msgID, aid) } // Notify followers via email notifyFollowers(env, rs.ModelDef().Name(), ids[0], authorID, subject, body) return msgID, nil }) // _message_get_thread: get messages for the record's chatter. // Mirrors: odoo/addons/mail/models/mail_thread.py _notify_thread() m.RegisterMethod("_message_get_thread", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() ids := rs.IDs() if len(ids) == 0 { return []interface{}{}, nil } rows, err := env.Tx().Query(env.Ctx(), `SELECT m.id, m.body, m.message_type, m.date, m.author_id, COALESCE(p.name, ''), COALESCE(m.subject, ''), COALESCE(m.email_from, '') FROM mail_message m LEFT JOIN res_partner p ON p.id = m.author_id WHERE m.model = $1 AND m.res_id = $2 ORDER BY m.id DESC`, rs.ModelDef().Name(), ids[0], ) if err != nil { return nil, err } defer rows.Close() var messages []map[string]interface{} for rows.Next() { var id int64 var body, msgType, subject, emailFrom string var date interface{} var authorID int64 var authorName string if err := rows.Scan(&id, &body, &msgType, &date, &authorID, &authorName, &subject, &emailFrom); err != nil { continue } msg := map[string]interface{}{ "id": id, "body": body, "message_type": msgType, "date": date, "subject": subject, "email_from": emailFrom, } if authorID > 0 { msg["author_id"] = []interface{}{authorID, authorName} } else { msg["author_id"] = false } messages = append(messages, msg) } if messages == nil { messages = []map[string]interface{}{} } return messages, nil }) } } // notifyFollowers sends email notifications to followers of a document. // Skips the message author to avoid self-notifications. // Mirrors: odoo/addons/mail/models/mail_thread.py _notify_thread() func notifyFollowers(env *orm.Environment, modelName string, resID, authorID int64, subject, body string) { rows, err := env.Tx().Query(env.Ctx(), `SELECT DISTINCT p.email, p.name FROM mail_followers f JOIN res_partner p ON p.id = f.partner_id WHERE f.res_model = $1 AND f.res_id = $2 AND f.partner_id != $3 AND p.email IS NOT NULL AND p.email != ''`, modelName, resID, authorID) if err != nil { log.Printf("mail: follower lookup failed for %s/%d: %v", modelName, resID, err) return } defer rows.Close() cfg := tools.LoadSMTPConfig() if cfg.Host == "" { return // SMTP not configured — skip silently } emailSubject := subject if emailSubject == "" { emailSubject = fmt.Sprintf("New message on %s", modelName) } for rows.Next() { var email, name string if err := rows.Scan(&email, &name); err != nil { continue } if err := tools.SendEmail(cfg, email, emailSubject, body); err != nil { log.Printf("mail: failed to notify %s (%s): %v", name, email, err) } } }