Your Team Is Drowning in Email Threads Nobody Can Follow
Every ERP implementation we audit has the same pattern: critical business decisions buried in email chains that half the team was never CC'd on. A purchase order gets approved in an inbox nobody monitors. A customer complaint sits in a shared mailbox for three days because everyone assumed someone else replied. Knowledge lives in individual inboxes instead of on the records it belongs to.
Odoo 19's Discuss module solves this by moving all internal communication into a single hub that is deeply integrated with every other Odoo application. Messages are attached to records — sales orders, invoices, helpdesk tickets — so context is never lost. Channels replace mailing lists. Direct messages replace CC chains. And the mail gateway ensures that external emails are routed to the right Odoo record automatically, creating a single source of truth.
This guide covers the full Discuss stack: setting up channels (public, private, and group), configuring direct messages, wiring the incoming and outgoing mail gateway with catchall and bounce addresses, integrating mail.thread into custom models, configuring notification preferences (Inbox vs. Email), deploying the Discuss chatbot, enabling VoIP integration, and setting up channel moderation. Every section includes the exact menu paths, field names, and code you need.
Channels: Public, Private, and Group Conversations
Channels are the backbone of Discuss. They replace email distribution lists with persistent, searchable conversations that live inside Odoo. In Odoo 19, channels have been restructured under the discuss.channel model (replacing the older mail.channel alias) with three distinct types that control visibility and access.
Public Channels
Navigate to Discuss → Channels → New. Set the Privacy field to Everyone. Public channels are visible to all internal users in the channel directory. Use them for company-wide announcements, cross-department coordination, or general discussion topics. Any employee can join without an invitation.
Private Channels
Set Privacy to Invited People Only. Private channels are invisible in the directory — users can only join if explicitly added by a channel administrator. Use them for management discussions, HR-sensitive topics, or project teams that need a clean separation from the rest of the company.
Group Channels
Set Privacy to Selected Group of Users and pick a security group from the Authorized Group field (e.g., sales_team.group_sale_manager). Only members of that group can see and join the channel. This is ideal for role-based communication: all sales managers share a channel, all warehouse supervisors share another.
import xmlrpc.client
url = 'https://your-odoo.com'
db = 'production'
uid = 2 # admin
password = 'your_api_key'
models = xmlrpc.client.ServerProxy(f'{{url}}/xmlrpc/2/object')
# Create a public channel for company announcements
public_channel_id = models.execute_kw(db, uid, password,
'discuss.channel', 'create', [{
'name': 'Company Announcements',
'channel_type': 'channel',
'group_public_id': False, # Everyone (public)
'description': 'Official company-wide updates and news',
}])
# Create a private channel for the finance team
private_channel_id = models.execute_kw(db, uid, password,
'discuss.channel', 'create', [{
'name': 'Finance Team',
'channel_type': 'channel',
'group_public_id': None,
'description': 'Internal finance discussions',
}])
# Add specific members to the private channel
user_ids = models.execute_kw(db, uid, password,
'res.users', 'search', [[
('groups_id', 'in', [
models.execute_kw(db, uid, password,
'ir.model.data', 'check_object_reference',
['account', 'group_account_manager'])[1]
])
]])
for user_id in user_ids:
models.execute_kw(db, uid, password,
'discuss.channel', 'add_members',
[[private_channel_id], [user_id]]) Adopt a prefix system early: #dept-finance, #proj-website-redesign, #alert-inventory. When you have 40+ channels, searchability is everything. Odoo 19 supports the channel search bar in Discuss, but it only matches channel names — not descriptions. The name must be self-explanatory.
Direct Messages and Group DMs
Direct messages in Odoo 19 Discuss are one-to-one or small-group conversations that bypass channels entirely. They use the same discuss.channel model but with channel_type set to chat (1:1) or group (multi-party). Unlike channels, DMs cannot be moderated, do not appear in the channel directory, and cannot have a description or group-based access.
Starting a Direct Message
Open Discuss and click the + (New Message) icon next to "Direct Messages" in the sidebar. Type a colleague's name to start a 1:1 conversation. To create a group DM, add multiple users before sending the first message. The conversation is created with channel_type = 'group' and all participants are added as members automatically.
Direct messages are ideal for quick questions that don't need a permanent channel record. However, be mindful: if a conversation becomes recurring (e.g., daily sync between two managers), it should become a private channel instead. Channels have history, searchability, and the ability to add new members who can see past messages — DMs do not.
Presence and Status Indicators
Odoo 19 Discuss shows real-time presence indicators next to each user's avatar: a green dot for Online (active in Odoo within the last 30 seconds), an orange dot for Idle (authenticated but inactive for over 30 minutes), and no dot for Offline. These indicators appear in the DM sidebar, on @mention autocomplete dropdowns, and on partner cards throughout the system.
Presence data is managed by the bus.presence model and relies on the Odoo bus (longpolling or WebSocket). If presence indicators are not updating, check that the /websocket endpoint is reachable and that your reverse proxy forwards the Upgrade header correctly. The bus heartbeat interval is configurable via the bus_presence_period system parameter (default: 30000 ms).
Odoo 19 Discuss supports rich text formatting in all message types: bold, italic, code blocks, bullet lists, and file attachments up to the ir.config_parameter key mail.mail_limit_filesize (default 25 MB). Use @mention to notify specific users and #channel to cross-reference channels directly in messages.
Mail Gateway Configuration: Catchall, Bounce, and Incoming Routes
The mail gateway is the bridge between external email and Odoo. It ensures that replies to outgoing Odoo emails land on the correct record (invoice, helpdesk ticket, purchase order) instead of in someone's personal inbox. In Odoo 19, this is handled by the mail module through a combination of incoming mail servers, catchall aliases, and bounce handling.
Step 1: Configure the Outgoing Mail Server
Go to Settings → Technical → Email → Outgoing Mail Servers. Create a new SMTP server with your provider's credentials. The critical fields are:
- SMTP Server: e.g.,
smtp.gmail.comormail.yourdomain.com - SMTP Port:
587(TLS) or465(SSL) - Connection Security: TLS (STARTTLS) recommended
- Username / Password: Service account credentials (not a personal mailbox)
Step 2: Configure the Incoming Mail Server
Go to Settings → Technical → Email → Incoming Mail Servers. Create a new server using IMAP (preferred) or POP3. This server will poll the catchall mailbox for new emails and route them into Odoo.
# Configure incoming IMAP server for catchall
incoming_server = models.execute_kw(db, uid, password,
'fetchmail.server', 'create', [{
'name': 'Catchall Inbox',
'server_type': 'imap',
'server': 'imap.yourdomain.com',
'port': 993,
'is_ssl': True,
'user': 'catchall@yourdomain.com',
'password': 'app_specific_password',
'state': 'draft',
'configuration': 'automatic',
}])
# Activate the server
models.execute_kw(db, uid, password,
'fetchmail.server', 'button_confirm_login',
[[incoming_server]])
# The server will now poll every 5 minutes (default)
# Adjust interval via Settings → Technical → Automation →
# Scheduled Actions → "Mail: Fetchmail Service"Step 3: Set Catchall and Bounce Aliases
Navigate to Settings → General Settings → Discuss. Configure the two critical aliases:
- Catchall Email Alias:
catchall@yourdomain.com— receives replies to all outgoing Odoo emails. Themail.catchall.aliassystem parameter stores the local part (e.g.,catchall). - Bounce Email Alias:
bounce@yourdomain.com— receives delivery failure notifications. Stored inmail.bounce.alias. Odoo uses the bounce address as the SMTP envelope sender to separate delivery errors from real replies.
The mail.catchall.domain system parameter must match your MX domain exactly. When Odoo sends an email from a sales order, it sets the Reply-To header to something like catchall+SO-00042@yourdomain.com. The + alias trick (subaddressing) lets a single mailbox receive replies for every record in the system. When the fetchmail service picks up this email, Odoo parses the +SO-00042 suffix and routes the reply to the correct sale order chatter.
Step 4: Email Routing — How Odoo Matches Emails to Records
Understanding the email routing pipeline is critical for debugging. When a new email arrives, Odoo processes it through the mail.thread method message_route() in this order:
- Message-Id / In-Reply-To: If the email is a reply, Odoo matches the
In-Reply-ToorReferencesheader to an existingmail.messagerecord. This is the most reliable routing method. - Alias matching: Odoo checks the
ToandCCheaders for any registeredmail.aliasaddresses. Ifsupport@yourdomain.comis an alias for the helpdesk team, the email creates a new ticket. - Catchall fallback: If no alias matches, the email goes to the catchall route. Odoo attempts to extract a record reference from the email address (e.g.,
catchall+SO-00042) and routes the message to that record. - Default routing: If all else fails, the email is either discarded or forwarded to a designated fallback channel, depending on your
mail.default.fromand alias configuration.
To debug routing issues, enable Developer Mode and check the mail.mail model for failed entries. You can also increase the mail module's log level by adding --log-handler=odoo.addons.mail:DEBUG to your Odoo startup command. This logs every step of the routing pipeline, including which headers were checked and why a particular route was selected or rejected.
Your domain's MX record must point to a mail server that delivers to the catchall mailbox. Additionally, configure SPF, DKIM, and DMARC records for the outgoing server domain. Without these, emails sent from Odoo will land in spam folders — and replies will never reach the gateway. Run dig TXT yourdomain.com to verify SPF is set, and check DKIM with your mail provider's validation tool.
Integrating mail.thread into Custom Models
The mail.thread mixin is the engine behind Odoo's chatter — the message log visible at the bottom of every record. When a model inherits from mail.thread, it gains the ability to send and receive messages, track field changes, log notes, and receive external emails via aliases. In Odoo 19, every core model (sale order, invoice, project task) already inherits this mixin. For custom models, you must add it explicitly.
from odoo import models, fields, api
class MaintenanceRequest(models.Model):
_name = 'x.maintenance.request'
_description = 'Custom Maintenance Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_mail_post_access = 'read' # users with read access can post
name = fields.Char(
string='Request Title',
required=True,
tracking=True, # log changes in chatter
)
state = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted'),
('in_progress', 'In Progress'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], default='draft', tracking=True)
priority = fields.Selection([
('0', 'Normal'),
('1', 'Low'),
('2', 'High'),
('3', 'Urgent'),
], default='0', tracking=True)
assigned_to = fields.Many2one(
'res.users',
string='Assigned To',
tracking=True,
)
description = fields.Html(string='Description')
tag_ids = fields.Many2many(
'x.maintenance.tag',
string='Tags',
)
# Mail alias: allows creating records via email
_mail_alias_model = 'x.maintenance.request'
def _track_subtype(self, init_values):
"""Return subtype for tracking based on field changes."""
self.ensure_one()
if 'state' in init_values:
if self.state == 'submitted':
return self.env.ref(
'your_module.mt_request_submitted'
)
elif self.state == 'done':
return self.env.ref(
'your_module.mt_request_done'
)
return super()._track_subtype(init_values)Setting Up a Mail Alias for the Model
A mail alias lets users create records by sending an email to a specific address. For example, maintenance@yourdomain.com can automatically create maintenance requests. Configure this in the XML data file of your module:
<odoo>
<data noupdate="1">
<!-- Mail alias: maintenance@yourdomain.com creates records -->
<record id="mail_alias_maintenance" model="mail.alias">
<field name="alias_name">maintenance</field>
<field name="alias_model_id"
ref="your_module.model_x_maintenance_request"/>
<field name="alias_contact">everyone</field>
<field name="alias_defaults">
{{'state': 'submitted', 'priority': '1'}}
</field>
</record>
<!-- Subtypes for tracking notifications -->
<record id="mt_request_submitted" model="mail.message.subtype">
<field name="name">Request Submitted</field>
<field name="res_model">x.maintenance.request</field>
<field name="default" eval="True"/>
<field name="description">Request has been submitted</field>
</record>
<record id="mt_request_done" model="mail.message.subtype">
<field name="name">Request Completed</field>
<field name="res_model">x.maintenance.request</field>
<field name="default" eval="False"/>
<field name="description">Request has been completed</field>
</record>
</data>
</odoo> The alias_contact field controls who can email the alias: everyone (any sender), partners (only known contacts), or followers (only followers of the parent record). For public-facing aliases like helpdesk or maintenance, use everyone. For internal aliases like HR requests, use partners to prevent spam from unknown senders.
Fields with tracking=True generate automatic "Field changed from X to Y" messages in the chatter. Use this for status fields, assignments, and priorities — fields where you need an audit trail. Do not enable tracking on high-frequency fields like write_date or computed fields that recalculate on every save. The chatter will become noisy and unreadable.
Notification Preferences: Inbox vs. Email
Every Odoo user has a notification preference that determines how they receive messages: Handle by Emails or Handle in Odoo (Inbox). This setting lives under Preferences on the user's profile (res.users field notification_type).
Inbox Mode (Handle in Odoo)
Messages appear in the Discuss inbox as notifications. No emails are sent. The user must open Odoo to see new messages. This is the preferred mode for teams that work inside Odoo all day — warehouse operators, sales reps using the CRM, support agents on helpdesk. It eliminates email overload and keeps the conversation inside the ERP.
Email Mode (Handle by Emails)
Every Odoo notification generates an email. The user receives messages in their external inbox (Gmail, Outlook, etc.) and can reply directly — the mail gateway routes the reply back to the correct record. This mode is better for users who don't live in Odoo: executives who check email but rarely log into the ERP, external partners with portal access, or field workers who rely on their phone's email app.
The notification_type field is set per user, but you can enforce a default for new users via a server action or by overriding the _default_notification_type method on res.users. In Odoo 19, the Discuss module also respects subtype subscriptions — a user following a record can choose which subtypes trigger notifications (e.g., "only notify me on status changes, not on every internal note").
| Scenario | Recommended Mode | Reason |
|---|---|---|
| Warehouse operators | Inbox (Handle in Odoo) | Always in Odoo; email adds latency and noise |
| Sales reps (CRM) | Inbox (Handle in Odoo) | CRM pipeline is their primary workspace |
| C-suite executives | Email (Handle by Emails) | Rarely log into Odoo; need push notifications |
| External partners (portal) | Email (Handle by Emails) | No Odoo backend access; email is their interface |
| Field technicians | Email (Handle by Emails) | Mobile-first workflow; email push is more reliable |
Subtype Subscriptions and Follower Management
When a user follows a record (sale order, project task, helpdesk ticket), they subscribe to all default subtypes. But most users do not need every notification. On the record form, click the followers section, then the pencil icon next to a follower's name. A dialog shows checkboxes for each subtype: "Discussions," "Activities," "Note," and model-specific subtypes like "Stage Changed" or "Rating Received." Unchecking a subtype stops that category of notification for that follower on that record.
For bulk management, use the message_subscribe() method with the subtype_ids parameter. This is useful when you want to auto-subscribe users to new records with a limited set of subtypes — for example, subscribing all sales managers to new quotations but only for the "Quotation Confirmed" subtype.
We recommend Inbox mode for all internal users with one exception: configure email notifications for @mentions only. This way, routine tracking messages go to the Discuss inbox (low noise), but when someone explicitly tags you with @YourName, you also get an email (high urgency). This hybrid approach requires a custom override of the _notify_get_recipients method on mail.thread.
Chatbot in Discuss and VoIP Integration
Odoo 19 extends the chatbot beyond website live chat into the Discuss ecosystem. Internal chatbots can handle employee FAQs (IT support, HR policy questions, expense report status), while the VoIP integration adds voice calling directly from the Discuss interface.
Internal Chatbot Setup
Navigate to Live Chat → Configuration → Chatbot. Create a new chatbot script and link it to an internal channel. The chatbot responds to messages in that channel using the configured script steps: question_text, question_selection, question_email, forward_operator, and free_input_single. In Odoo 19, the chatbot.script.step model includes a triggering_answer_ids field for conditional branching.
VoIP Integration
Install the voip module from Apps. Go to Settings → General Settings → Integrations → VoIP and configure your SIP provider credentials:
- PBX Server IP: Your SIP server address (e.g.,
pbx.yourdomain.com) - WebSocket URL:
wss://pbx.yourdomain.com:8089/ws - STUN/TURN Server: Required for NAT traversal in WebRTC calls
Each user configures their SIP credentials under Preferences → VoIP: the voip_username, voip_secret, and optional external_device_number for call forwarding. Once configured, a phone icon appears in the Discuss sidebar and on every contact/partner form, enabling click-to-call functionality.
# Set global VoIP parameters
config_params = {
'voip.pbx_ip': 'pbx.yourdomain.com',
'voip.wsServer': 'wss://pbx.yourdomain.com:8089/ws',
'voip.mode': 'production',
}
for key, value in config_params.items():
models.execute_kw(db, uid, password,
'ir.config_parameter', 'set_param', [key, value])
# Configure VoIP credentials for a specific user
user_ids = models.execute_kw(db, uid, password,
'res.users', 'search', [[
('login', '=', 'john@yourdomain.com'),
]])
if user_ids:
models.execute_kw(db, uid, password,
'res.users', 'write', [user_ids, {
'voip_username': '1001',
'voip_secret': 'sip_password_here',
'external_device_number': '+1-555-0142',
}])
# VoIP call logs are stored in discuss.channel
# with channel_type = 'chat' and a voip_call_id linkCall Logging and CRM Integration
Every VoIP call made from Discuss is automatically logged as a mail.message on the associated record. If you click the phone icon on a CRM lead, the call log appears in that lead's chatter with the call duration, direction (inbound/outbound), and outcome. This creates an auditable communication history without requiring sales reps to manually log their calls.
For teams using the Next Activities workflow, a VoIP call can be scheduled as an activity type. Navigate to Settings → Technical → Email → Activity Types and create a new type with Category set to phonecall. The voip_phonecall_activity action triggers the VoIP dialer when the user clicks "Call" on the scheduled activity, automatically marking it as done when the call ends.
Browser-based VoIP through Odoo uses WebRTC, which is sensitive to network conditions. For reliable call quality, ensure your office network has QoS rules prioritizing UDP traffic on the SIP and RTP port ranges. If users report choppy audio, the issue is almost always the STUN/TURN server configuration or NAT traversal — not Odoo itself.
Channel Moderation and Message Control
For large organizations or public-facing channels, moderation prevents noise and off-topic messages from overwhelming the conversation. Odoo 19 includes built-in moderation controls on channels through the moderation field on discuss.channel.
Enabling Moderation
Open a channel, go to the Settings tab, and enable Moderation. Add moderators by selecting users in the Moderators field. When moderation is active, messages from non-moderator members are held in a pending queue until a moderator approves, rejects, or discards them.
Moderation Options
Moderators see a banner at the top of the channel showing the number of pending messages. For each message, the moderator can:
- Accept: Publish the message to all channel members
- Reject: Delete the message and optionally send a rejection explanation to the author
- Discard: Silently remove the message without notification
- Allow: Whitelist the author so their future messages bypass moderation
- Ban: Block the author from posting in the channel entirely
Moderation is particularly useful for channels linked to external email aliases. If a channel receives messages from an alias (e.g., info@yourdomain.com), spam emails will appear as pending messages instead of polluting the channel feed. Moderators review and approve only legitimate messages.
Automated Moderation with Scheduled Actions
For channels with high message volume, manual moderation is not sustainable. You can create a scheduled action (ir.cron) that automatically processes the moderation queue based on rules: auto-discard messages older than 48 hours, auto-accept messages from whitelisted domains, or auto-reject messages containing specific keywords.
from odoo import models, fields, api
from datetime import timedelta
class DiscussChannelModerationCron(models.Model):
_inherit = 'discuss.channel'
def _cron_auto_moderate_pending(self):
"""Auto-discard pending messages older than 48 hours
and auto-accept messages from trusted domains."""
cutoff = fields.Datetime.now() - timedelta(hours=48)
trusted_domains = ['@yourdomain.com', '@partner.com']
# Find all pending moderation messages
pending_msgs = self.env['mail.message'].search([
('moderation_status', '=', 'pending_moderation'),
])
for msg in pending_msgs:
# Auto-accept from trusted domains
if msg.email_from and any(
domain in msg.email_from
for domain in trusted_domains
):
msg._moderate_accept()
continue
# Auto-discard if older than 48 hours
if msg.create_date <= cutoff:
msg._moderate_discard()
# Log summary
accepted = len(pending_msgs.filtered(
lambda m: m.moderation_status == 'accepted'
))
discarded = len(pending_msgs.filtered(
lambda m: m.moderation_status == 'rejected'
))
_logger.info(
'Auto-moderation: %d accepted, %d discarded',
accepted, discarded,
) Register the cron job in your module's XML data file with interval_number="1" and interval_type="hours". This keeps the moderation queue lean without requiring moderators to check it constantly. For compliance-sensitive channels (e.g., finance or legal), disable auto-accept and only use auto-discard for clearly stale messages.
3 Discuss Mistakes That Break Communication
Catchall Alias Not Matching the MX Domain
The most common mail gateway failure. You configure mail.catchall.domain as yourdomain.com but your MX record points to mail.yourdomain.com, or your email provider uses a subdomain like company.mail.provider.com. Outgoing emails use a Reply-To with the wrong domain, and replies bounce or land in a mailbox nobody checks. Odoo silently drops these messages because the alias routing cannot match the incoming domain to any known alias.
Verify the full chain: mail.catchall.domain in Settings → Technical → Parameters → System Parameters must match the domain where your catchall mailbox receives email. Send a test email to catchall+test@yourdomain.com from an external account and confirm it appears in the Odoo fetchmail log under Settings → Technical → Email → Incoming Mail Servers → Fetch Now.
Tracking Fields on Computed or High-Frequency Fields
Adding tracking=True to computed fields or fields that update on every write() floods the chatter with hundreds of "Field changed" messages per day. We audited one client's system where the amount_total field on sale orders had tracking enabled — every time a sale order line was added, modified, or removed, the chatter logged the total change. The result: 15+ tracking messages per sale order, burying the actual human-written notes that mattered.
Only track fields that represent deliberate state transitions: status, assignment, priority, stage. Never track monetary totals, dates that auto-update, or any compute field. If you need an audit trail on computed values, use a separate mail.activity or a custom logging mechanism that writes one summary message per save instead of per-field changes.
Email Notification Mode for High-Volume Internal Users
Users with notification_type = 'email' who follow hundreds of records will receive hundreds of emails per day — one for every chatter update on every record they follow. A warehouse manager following 200 delivery orders gets 200+ emails daily for status changes, internal notes, and tracking updates. Their inbox becomes unusable, they stop reading Odoo emails entirely, and critical notifications (like a blocked shipment) get buried.
Switch all internal power users to Inbox mode. For users who insist on email, configure subtype subscriptions to limit which events generate notifications. On the record's followers list, click the pencil icon next to a follower to select only the subtypes they care about (e.g., "Status Change" yes, "Discussions" no, "Activities" no).
The Hidden Cost of Fragmented Communication
When internal communication lives outside your ERP — in Slack, email, WhatsApp, or text messages — knowledge becomes invisible. Decisions are made in threads that aren't linked to the records they affect. Onboarding new employees means forwarding 50 email chains. Here is what we see after consolidating communication into Discuss:
When every message lives on the record it belongs to, nothing falls through the cracks. A note on a purchase order stays on that purchase order — not in an email thread that gets archived after 30 days.
McKinsey reports that knowledge workers spend 28% of their week on email. Discuss eliminates the "search for that email from three months ago" problem by attaching conversations to records with full-text search.
New team members see the complete conversation history on every record from day one. No need to forward email chains or explain "what happened on this account." The chatter is the institutional memory.
For a 50-person company, 4.2 hours saved per employee per week equals 210 hours per week — the equivalent of 5.25 full-time employees. Even if the real savings are half that conservative estimate, the ROI of consolidating communication into Discuss pays for the entire Odoo subscription many times over.
Optimization Metadata
Complete guide to Odoo 19 Discuss: set up channels, configure the mail gateway with catchall and bounce aliases, integrate mail.thread into custom models, configure notifications, and enable chatbot and VoIP.
1. "Channels: Public, Private, and Group Conversations"
2. "Mail Gateway Configuration: Catchall, Bounce, and Incoming Routes"
3. "Integrating mail.thread into Custom Models"
4. "3 Discuss Mistakes That Break Communication"