Quick Answer
Processing 500+ orders daily? Stop manual work. Use Scheduled Actions (cron jobs) for time-based tasks, Server Actions for custom Python logic, and Automated Actions for event triggers. Properly automated systems save staff 40+ hours/month = $45,000-$120,000/year in productivity vs. manual data entry.
The Automation Problem
Your D2C brand processes 500+ orders daily. You need to:
❏ Automatically assign new orders to the correct sales rep (based on customer territory)
❏ Send shipping notifications when orders are marked "shipped"
❏ Archive old completed orders (over 1 year old) monthly
❏ Update inventory when a purchase order is received
❏ Create follow-up activities when a high-value customer places an order
Doing this manually is impossible. You need automation.
We've implemented 150+ Odoo systems. The ones with proper automation? Their staff spends 40+ hours/month on actual work instead of manual data entry. The ones without? Staff is drowning in repetitive tasks, making mistakes, and burning out. That's $45,000-$120,000/year in lost productivity.
Type 1: Scheduled Actions (Cron Jobs)
What it is: Task that runs automatically at a specific time interval.
Real D2C example: Archive orders older than 1 year
Step 1: Create the Python Logic
from odoo import models, fields, api
from datetime import timedelta
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_auto_archive_old_orders(self):
"""
Cron job: Archive orders completed more than 1 year ago.
Called automatically every week.
"""
# Calculate cutoff date (1 year ago)
cutoff_date = fields.Date.today() - timedelta(days=365)
# Find orders that are:
# - Completed (state = 'done')
# - AND completed before cutoff date
old_orders = self.search([
('state', '=', 'done'),
('date_order', '<', cutoff_date),
('active', '=', True), # Not already archived
])
# Archive them
old_orders.write({'active': False})
# Log for debugging
self.env['ir.logging'].create({
'name': 'Auto Archive Orders',
'type': 'server',
'level': 'info',
'message': f'Archived {len(old_orders)} old orders',
})
Step 2: Create the Scheduled Action (XML)
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- SCHEDULED ACTION (Cron Job) -->
<record id="ir_cron_archive_old_orders" model="ir.cron">
<!-- Name shown in UI -->
<field name="name">Auto Archive Old Orders</field>
<!-- Which model this runs on -->
<field name="model_id" ref="sale.model_sale_order"/>
<!-- State = code means execute Python code -->
<field name="state">code</field>
<!-- Python code to execute -->
<field name="code">model.action_auto_archive_old_orders()</field>
<!-- User to run as (root = system user) -->
<field name="user_id" ref="base.user_root"/>
<!-- RUN SCHEDULE -->
<!-- How often? Every X units -->
<field name="interval_number">1</field>
<!-- X = days, hours, minutes, seconds, weeks, months -->
<field name="interval_type">weeks</field>
<!-- How many times to run? -1 = unlimited -->
<field name="numbercall">-1</field>
<!-- Is this active? -->
<field name="active">True</field>
<!-- Do all at once? (True = yes) -->
<field name="doall">True</field>
</record>
</odoo>
Cron Schedule Options
| Interval Type | Example | Use Case |
|---|---|---|
| minutes | Every 30 minutes | High-frequency stock checks |
| hours | Every 2 hours | Sync inventory from Shopify |
| days | Every 1 day (daily) | Send daily sales reports |
| weeks | Every 1 week (weekly) | Archive old orders |
| months | Every 1 month (monthly) | Monthly reporting cleanup |
Type 2: Server Actions (Custom Logic)
What it is: Reusable action containing custom Python code that executes when triggered.
Real D2C example: Assign new orders to sales rep based on territory
Step 1: Create the Python Method
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_auto_assign_sales_rep(self):
"""
Server action: Assign order to sales rep based on customer's territory.
"""
for order in self:
if not order.user_id: # Only assign if not already assigned
# Get customer's country
customer_country = order.partner_id.country_id
# Find sales rep who covers this country
sales_rep = self.env['res.users'].search([
('x_territory_country_ids', '=', customer_country.id),
('groups_id', '=', self.env.ref('sales_team.group_sale_salesman').id),
], limit=1)
if sales_rep:
order.user_id = sales_rep
# Send email to notify them
self.env['mail.mail'].create({
'subject': f'New Order Assigned: {order.name}',
'email_to': sales_rep.email,
'body_html': f'<p>You have been assigned order {order.name} from {order.partner_id.name}</p>',
}).send()
Step 2: Create the Server Action (XML)
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- SERVER ACTION -->
<record id="action_auto_assign_sales_rep" model="ir.actions.server">
<!-- Name shown in UI -->
<field name="name">Auto Assign Sales Rep</field>
<!-- Which model this operates on -->
<field name="model_id" ref="sale.model_sale_order"/>
<!-- State = code means execute Python code -->
<field name="state">code</field>
<!-- Python code to execute -->
<field name="code">records.action_auto_assign_sales_rep()</field>
<!-- Type of action -->
<field name="type">ir.actions.server</field>
</record>
</odoo>
Step 3: Trigger Automatically with Automated Action
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AUTOMATED ACTION = trigger the server action -->
<record id="action_rule_assign_sales_rep" model="base.automation">
<!-- Name -->
<field name="name">Assign Sales Rep on Order Creation</field>
<!-- Which model -->
<field name="model_id" ref="sale.model_sale_order"/>
<!-- When to trigger? on_create = when record is created -->
<field name="trigger">on_create</field>
<!-- Only trigger if... (empty = always) -->
<field name="filter_pre_domain">[]</field>
<!-- Which server action to run -->
<field name="action_server_ids" eval="[(6, 0, [ref('action_auto_assign_sales_rep')])]"/>
<!-- Is this active? -->
<field name="active">True</field>
</record>
</odoo>
Type 3: Automated Actions (Event-Triggered)
What it is: Trigger actions when records are created, updated, or deleted.
Real D2C example: Send shipping notification when order status changes to "shipped"
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AUTOMATED ACTION = Send email when order ships -->
<record id="action_rule_send_shipping_notification" model="base.automation">
<!-- Name -->
<field name="name">Send Shipping Notification</field>
<!-- Which model -->
<field name="model_id" ref="sale.model_sale_order"/>
<!-- When to trigger? on_write = when record is updated -->
<field name="trigger">on_write</field>
<!-- Trigger only if specific field changed -->
<field name="trigger_fields" eval="[ref('sale.field_sale_order__state')]"/>
<!-- Only trigger if BEFORE state was NOT shipped -->
<field name="filter_pre_domain">[('state', '!=', 'shipped')]</field>
<!-- Only trigger if AFTER state IS shipped -->
<field name="filter_post_domain">[('state', '=', 'shipped')]</field>
<!-- ACTION: Create activity (to-do) for order processor -->
<field name="action" type="xml">
<action>
<field name="activity_type">test</field>
<field name="activity_user">user_id</field>
<field name="summary">Send shipping notification to customer</field>
</action>
</field>
<!-- Is this active? -->
<field name="active">True</field>
</record>
</odoo>
Automated Action Types
<!-- UPDATE FIELD -->
<action>
<field name="type">update</field>
<field name="field">x_notified_customer</field>
<field name="value">True</field>
</action>
<!-- CREATE NEW RECORD -->
<action>
<field name="type">create</field>
<field name="model">sale.order</field>
<field name="fields">
<field name="name">Reminder for <t t-esc="record.name"/></field>
<field name="partner_id" t-value="record.partner_id.id"/>
</field>
</action>
<!-- SEND EMAIL -->
<action>
<field name="type">send_email</field>
<field name="email_to">record.partner_id.email</field>
<field name="email_from">info@company.com</field>
<field name="subject">Your Order #<t t-esc="record.name"/> Has Shipped!</field>
</action>
<!-- CREATE ACTIVITY (To-Do) -->
<action>
<field name="type">activity</field>
<field name="activity_type">test</field>
<field name="activity_user">record.user_id</field>
<field name="activity_summary">Follow up with customer</field>
</action>
Real D2C Workflow: Complete Order Automation
When order is confirmed, automatically: assign to sales rep, send confirmation email, create shipping activity, update inventory.
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
"""Override to add automation on order confirmation."""
result = super().action_confirm()
# Automation 1: Assign sales rep
self.action_auto_assign_sales_rep()
# Automation 2: Send confirmation email
self.action_send_confirmation_email()
# Automation 3: Create shipping activity
self.action_create_shipping_activity()
return result
def action_auto_assign_sales_rep(self):
"""Assign to sales rep by territory."""
for order in self:
if not order.user_id:
sales_rep = self.env['res.users'].search([
('x_territory_country_ids', '=', order.partner_id.country_id.id),
], limit=1)
if sales_rep:
order.user_id = sales_rep
def action_send_confirmation_email(self):
"""Send order confirmation email to customer."""
for order in self:
template = self.env.ref('sale.email_template_edi_sale')
template.send_mail(order.id, force_send=True)
def action_create_shipping_activity(self):
"""Create shipping prep activity for warehouse."""
for order in self:
self.env['mail.activity'].create({
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'summary': f'Prepare shipment for {order.name}',
'user_id': self.env.ref('stock.group_stock_user').id,
'res_model_id': self.env.ref('sale.model_sale_order').id,
'res_id': order.id,
})
Automation Types Comparison
| Type | Trigger | Use Case | Example |
|---|---|---|---|
| Scheduled Action | Time-based (every hour/day/week) | Archive old orders, send reports | Weekly cleanup |
| Server Action | Manual or automated trigger | Reusable custom Python logic | Assign sales rep |
| Automated Action | Event-based (create/update/delete) | React to record changes | Send email on ship |
Action Items: Implement Automation
For Scheduled Actions
❏ Identify time-based tasks (archive old data, send reports)
❏ Write Python method in model
❏ Create ir.cron record in XML
❏ Add to manifest 'data' field
❏ Test by running manually in developer menu
For Server Actions
❏ Write Python method with custom logic
❏ Create ir.actions.server record in XML
❏ Specify state='code' and code field
❏ Test by running manually (More menu)
For Automated Actions
❏ Identify event triggers (on_create, on_write, on_delete)
❏ Create base.automation record
❏ Set filter_pre_domain and filter_post_domain
❏ Link to server action or define inline action
Frequently Asked Questions
When should I use Scheduled Actions vs. Automated Actions in Odoo?
Use Scheduled Actions (cron) for time-based tasks (archive old data every week, send reports daily). Use Automated Actions for event-based triggers (send email when order ships, assign rep when order created).
What's the difference between Server Actions and Automated Actions?
Server Actions are reusable Python code blocks. Automated Actions are event triggers that call server actions (or execute inline actions). Think of server actions as functions, automated actions as event listeners.
How do I debug scheduled actions that aren't running?
Go to Settings → Technical → Automation → Scheduled Actions. Find your cron job, check active=True, click "Run Manually" to test. Check logs in Settings → Technical → Logging for errors.
How much can automation save vs. manual work?
Properly automated systems save staff 40+ hours/month on manual data entry and repetitive tasks. At $50/hour loaded cost, that's $45,000-$120,000/year in productivity vs. manual work.
Free Workflow Automation Design Session
Stop doing manual, repetitive tasks. We'll map your repetitive manual tasks, identify which are candidates for automation, choose the right automation type (cron, server action, automated action), build a complete automation strategy, and implement your first 3-5 automations. Most D2C brands waste 40+ hours/month on manual work that could be automated in 1 day. The ROI is immediate and massive.
