- 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>
209 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|