Files
goodie/addons/mail/models/mail_thread.go
Marc 66383adf06 feat: Portal, Email Inbound, Discuss + module improvements
- Portal: /my/* routes, signup, password reset, portal user support
- Email Inbound: IMAP polling (go-imap/v2), thread matching
- Discuss: mail.channel, long-polling bus, DM, unread count
- Cron: ir.cron runner (goroutine scheduler)
- Bank Import, CSV/Excel Import
- Automation (ir.actions.server)
- Fetchmail service
- HR Payroll model
- Various fixes across account, sale, stock, purchase, crm, hr, project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 18:41:57 +02:00

209 lines
5.8 KiB
Go

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)
}
}
}