- 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>
161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"odoo-go/pkg/orm"
|
|
"odoo-go/pkg/tools"
|
|
)
|
|
|
|
// RunAutomatedActions checks and executes server actions triggered by Create/Write/Unlink.
|
|
// Called from the ORM after successful Create/Write/Unlink operations.
|
|
// Mirrors: odoo/addons/base_automation/models/base_automation.py
|
|
func RunAutomatedActions(env *orm.Environment, modelName, trigger string, recordIDs []int64) {
|
|
if len(recordIDs) == 0 {
|
|
return
|
|
}
|
|
|
|
// Look up the ir_model ID for this model
|
|
var modelID int64
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT id FROM ir_model WHERE model = $1`, modelName).Scan(&modelID)
|
|
if err != nil {
|
|
return // Model not in ir_model — no actions possible
|
|
}
|
|
|
|
// Find matching automated actions
|
|
rows, err := env.Tx().Query(env.Ctx(),
|
|
`SELECT id, state, COALESCE(update_field_id, ''), COALESCE(update_value, ''),
|
|
COALESCE(email_to, ''), COALESCE(email_subject, ''), COALESCE(email_body, ''),
|
|
COALESCE(filter_domain, '')
|
|
FROM ir_act_server
|
|
WHERE model_id = $1
|
|
AND active = true
|
|
AND trigger = $2
|
|
ORDER BY sequence, id`, modelID, trigger)
|
|
if err != nil {
|
|
log.Printf("automation: query error for %s/%s: %v", modelName, trigger, err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type action struct {
|
|
id int64
|
|
state string
|
|
updateField string
|
|
updateValue string
|
|
emailTo string
|
|
emailSubject string
|
|
emailBody string
|
|
filterDomain string
|
|
}
|
|
|
|
var actions []action
|
|
for rows.Next() {
|
|
var a action
|
|
if err := rows.Scan(&a.id, &a.state, &a.updateField, &a.updateValue,
|
|
&a.emailTo, &a.emailSubject, &a.emailBody, &a.filterDomain); err != nil {
|
|
continue
|
|
}
|
|
actions = append(actions, a)
|
|
}
|
|
|
|
if len(actions) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, a := range actions {
|
|
switch a.state {
|
|
case "object_write":
|
|
executeObjectWrite(env, modelName, recordIDs, a.updateField, a.updateValue)
|
|
case "email":
|
|
executeEmailAction(env, modelName, recordIDs, a.emailTo, a.emailSubject, a.emailBody)
|
|
}
|
|
}
|
|
}
|
|
|
|
// executeObjectWrite updates a field on the triggered records.
|
|
func executeObjectWrite(env *orm.Environment, modelName string, recordIDs []int64, fieldName, value string) {
|
|
if fieldName == "" {
|
|
return
|
|
}
|
|
tableName := strings.ReplaceAll(modelName, ".", "_")
|
|
for _, id := range recordIDs {
|
|
_, err := env.Tx().Exec(env.Ctx(),
|
|
fmt.Sprintf(`UPDATE %s SET %s = $1 WHERE id = $2`,
|
|
pgx.Identifier{tableName}.Sanitize(),
|
|
pgx.Identifier{fieldName}.Sanitize()),
|
|
value, id)
|
|
if err != nil {
|
|
log.Printf("automation: object_write error %s.%s on %d: %v", modelName, fieldName, id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// executeEmailAction sends an email for each triggered record.
|
|
func executeEmailAction(env *orm.Environment, modelName string, recordIDs []int64, emailToField, subject, bodyTemplate string) {
|
|
if emailToField == "" {
|
|
return
|
|
}
|
|
|
|
cfg := tools.LoadSMTPConfig()
|
|
if cfg.Host == "" {
|
|
return
|
|
}
|
|
|
|
tableName := strings.ReplaceAll(modelName, ".", "_")
|
|
|
|
for _, id := range recordIDs {
|
|
// Resolve email address from the record
|
|
var email string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
fmt.Sprintf(`SELECT COALESCE(%s, '') FROM %s WHERE id = $1`,
|
|
pgx.Identifier{emailToField}.Sanitize(),
|
|
pgx.Identifier{tableName}.Sanitize()),
|
|
id).Scan(&email)
|
|
if err != nil || email == "" {
|
|
continue
|
|
}
|
|
|
|
// Simple template: replace {{field}} with record values
|
|
body := bodyTemplate
|
|
if strings.Contains(body, "{{") {
|
|
body = resolveTemplate(env, tableName, id, body)
|
|
}
|
|
|
|
if err := tools.SendEmail(cfg, email, subject, body); err != nil {
|
|
log.Printf("automation: email error to %s for %s/%d: %v", email, modelName, id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolveTemplate replaces {{field_name}} placeholders with actual record values.
|
|
func resolveTemplate(env *orm.Environment, tableName string, recordID int64, template string) string {
|
|
result := template
|
|
for {
|
|
start := strings.Index(result, "{{")
|
|
if start == -1 {
|
|
break
|
|
}
|
|
end := strings.Index(result[start:], "}}")
|
|
if end == -1 {
|
|
break
|
|
}
|
|
fieldName := strings.TrimSpace(result[start+2 : start+end])
|
|
var val string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
fmt.Sprintf(`SELECT COALESCE(CAST(%s AS TEXT), '') FROM %s WHERE id = $1`,
|
|
pgx.Identifier{fieldName}.Sanitize(),
|
|
pgx.Identifier{tableName}.Sanitize()),
|
|
recordID).Scan(&val)
|
|
if err != nil {
|
|
val = ""
|
|
}
|
|
result = result[:start] + val + result[start+end+2:]
|
|
}
|
|
return result
|
|
}
|