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 }