How to Transfer Chatter History from Sales to Delivery in Odoo 18: Step by Step Guide
By Braincuber Team
Published on March 7, 2026
Your warehouse team is flying blind because the customer instructions are stuck on the Sales Order's Chatter. The delivery guy doesn't see the note about "fragile items" or "deliver after 3 PM." By the time he realizes, the customer is already furious. Odoo 18's Chatter keeps conversations tied to individual records. But it does *not* carry those messages forward to related Delivery Orders. This tutorial shows you how to fix that with a custom button and 13 lines of Python.
What You'll Learn:
- How Odoo 18 Chatter works and its 6 core features
- How to add a custom "Copy Chatter" button to the Sales Order form
- How to write the Python action that copies mail.message records
- How to filter and copy only specific message types (Log Notes, Activities)
- How to adapt this pattern for any model-to-model chatter transfer
The Chatter Problem Nobody Talks About
Chatter is Odoo's built-in communication hub. Every record — Sales Order, Invoice, Delivery — gets its own Chatter thread. Your sales team logs customer preferences, internal notes, and file attachments right on the Sales Order. That's great.
But when that Sales Order creates a Delivery Order, none of those messages transfer. The warehouse team opens the Delivery Order and sees a blank Chatter. Zero context. They don't know the customer asked for separate packaging, or that the CEO left a note saying "priority client — handle with care."
We've seen this cause $3,700 in re-delivery costs at a single D2C brand in one quarter. All because the delivery team couldn't see 4 lines of text.
Chatter's 6 Core Features You Need to Know
Before building the transfer mechanism, understand what Chatter actually stores. Each of these generates a mail.message record in the database — and that's what we'll be copying.
Send Message
Sends emails directly from the record to all followers. Supports email templates. Customer replies get auto-logged back into the thread.
Log Note
Internal-only notes visible to team members. Perfect for documenting decisions, instructions, or updates without notifying customers.
Schedule Activity
Assigns tasks, sets deadlines, and schedules follow-ups. Activities appear in both the Chatter thread and the user's activity dashboard.
Attachments
Upload and access files linked to a specific record. The attachment count is always visible, keeping invoices, contracts, and reference files at hand.
Follow / Following Toggle
Follow or unfollow any document. Following a record means you get notifications about every change, message, or activity posted on it.
Followers Management
View all current followers. Add or remove followers manually and define what types of notifications they receive — emails, notes, or activities.
How Chatter Messages Are Stored
Every Chatter message is a row in the mail.message table. Two fields tell Odoo which record the message belongs to:
| Field | Type | What It Does |
|---|---|---|
| model | Char | The technical model name (e.g., sale.order, stock.picking) |
| res_id | Integer | The database ID of the specific record |
| subtype_id | Many2one | Message subtype — Note, Discussions, Activities, etc. |
| body | Html | The actual message content (HTML formatted) |
The trick is simple: copy the mail.message record and change the model and res_id to point at the Delivery Order. That's the entire concept. Now let's build it.
Step 1: Add a "Copy Chatter" Button to the Sales Form
First, create an inherited view that adds a Copy Chatter button to the Sales Order form. This button will sit next to the Cancel button, giving your sales team a one-click way to push all chatter messages to the linked delivery.
Create the XML View File
In your custom module, create a view XML file (e.g., views/sale_order_views.xml) to inherit the default Sales Order form and inject the button.
Use XPath to Position the Button
Target the action_draft button using an xpath expression with position="after". This places your new button right next to the existing Cancel button.
Define the Button Attributes
Set name="action_chatter_copy" (the Python method name), type="object", and class="btn-primary" to make it a blue action button on the form.
<record id="view_order_form_inherit_chatter_copy_in_sale"
model="ir.ui.view">
<field name="name">sale.order.form.inherit.copy.chatter.sale</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_draft']"
position="after">
<button name="action_chatter_copy"
string="Copy Chatter"
class="btn-primary"
type="object"/>
</xpath>
</field>
</record>
XPath Target Matters
The xpath expression //button[@name='action_draft'] targets the Cancel button. If your Sales form has been customized by another module and that button is missing, the view will fail to load. Always verify the target element exists in your specific Odoo 18 installation before deploying.
Step 2: Write the Python Action to Copy Messages
This is where the real work happens. The Python method searches for all mail.message records linked to the current Sales Order, then copies each one with the model and res_id changed to point at the first non-cancelled Delivery Order.
Search mail.message by Model and Record ID
Use self.env["mail.message"].search() with domain filters: model = "sale.order" and res_id = self.id. Order by create_date asc to preserve chronological order.
Get the First Active Delivery Order
Filter self.picking_ids using a lambda to exclude cancelled deliveries (state != 'cancel'). Take the first ID from the result list.
Copy Each Message with New Target
Loop through each message and call chat.copy() with a dictionary overriding model to "stock.picking" and res_id to the delivery order's ID.
def action_chatter_copy(self):
"""
Copies Chatter messages from the sale order to the first
non-cancelled delivery order.
"""
messages_sale = self.env["mail.message"].search(
["&", ("res_id", "=", self.id),
("model", "=", "sale.order")],
order='create_date asc')
for chat in messages_sale:
delivery_id = [x.id for x in
self.picking_ids.filtered(
lambda l: l.state != 'cancel')]
chat.copy({"model": "stock.picking",
"res_id": delivery_id[0]})
Click the Copy Chatter button on the Sales Order. Open the linked Delivery Order. Every message, note, and log entry from the Sales Order's Chatter is now visible in the Delivery Order's Chatter thread. Same content, same chronological order.
Step 3: Filter Messages — Copy Only What Matters
Copying everything is sometimes overkill. Your warehouse team doesn't need to see the email thread where sales negotiated pricing. They need the internal notes — the "ship in 2 boxes" kind of stuff.
The .filtered() method lets you selectively copy only specific message subtypes. Here's the modified code that copies only Log Notes:
Get Delivery IDs First (Outside the Loop)
Move the delivery_id lookup outside the for-loop. No reason to recalculate it for every single message. Cleaner code, slightly faster execution.
Apply .filtered() on the Message Recordset
Use messages_sale.filtered(lambda m: m.subtype_id.name == 'Note') to extract only Log Note messages. Change 'Note' to 'Discussions' for customer-facing emails, or any other subtype.
def action_chatter_copy(self):
"""
Copies only Log Note messages from the Sales Order
to the first non-canceled Delivery Order.
"""
messages_sale = self.env["mail.message"].search(
["&", ("res_id", "=", self.id),
("model", "=", "sale.order")],
order='create_date asc')
delivery_id = [x.id for x in
self.picking_ids.filtered(
lambda l: l.state != 'cancel')]
for chat in messages_sale.filtered(
lambda m: m.subtype_id.name == 'Note'):
chat.copy({"model": "stock.picking",
"res_id": delivery_id[0]})
Message Subtype Reference
When building your filter, you need to know which subtype_id.name values to target. Here's a reference of the most common Odoo 18 message subtypes:
| Subtype Name | What It Captures | Visible To |
|---|---|---|
| Note | Internal log notes created via "Log Note" button | Internal users only |
| Discussions | Customer-facing emails sent via "Send Message" | All followers (including external) |
| Activities | Scheduled tasks, follow-ups, and reminders | Assigned users |
| Sales Order Confirmation | Auto-generated when SO is confirmed | All followers |
Adapting This for Other Models
This pattern is not limited to Sales-to-Delivery. You can copy Chatter messages between any two models in Odoo 18. The logic stays identical — only the model names and the relationship field change.
Purchase Order -> Incoming Shipment
model: "purchase.order" -> "stock.picking" via self.picking_ids
Sales Order -> Invoice
model: "sale.order" -> "account.move" via self.invoice_ids
Project Task -> Timesheet Entry
model: "project.task" -> "account.analytic.line" via self.timesheet_ids
Watch Out for Duplicate Copies
Every click of the Copy Chatter button copies all matching messages again. If someone clicks it 3 times, you'll have 3 copies of every message on the Delivery Order. Consider adding a flag field (e.g., chatter_copied = fields.Boolean()) and checking it before running the copy to prevent duplicates.
Frequently Asked Questions
Does copying Chatter messages also copy file attachments?
The chat.copy() method in Odoo 18 duplicates the mail.message record, including the message body and metadata. Attachments linked via attachment_ids on the message are referenced, not duplicated — so they appear on the Delivery Order without consuming extra storage.
Can I automate this so it runs without clicking a button?
Yes. You can trigger the same Python method via a server action or by overriding the action_confirm() method on sale.order. When the SO is confirmed and a delivery is created, the chatter copy runs automatically.
What happens if the Sales Order has no Delivery Order yet?
The code will throw an IndexError because delivery_id[0] won't exist. Add a guard clause: if not delivery_id: raise UserError("No active delivery orders found.") before the copy loop.
Will this work with Odoo 17 or only Odoo 18?
The mail.message model structure and copy() method have been consistent across Odoo 16, 17, and 18. The same code works on Odoo 17 with no modifications. Minor view XML adjustments may be needed for older versions.
Can I copy Chatter to multiple Delivery Orders instead of just the first one?
Yes. Instead of using delivery_id[0], loop through all IDs in the delivery_id list and call chat.copy() for each one. This copies the full chatter thread to every active delivery order linked to that Sales Order.
Need Help with Odoo 18 Custom Development?
Our Odoo Gold Partner team can build custom chatter integrations, server actions, and workflow automations for your specific business processes. One call. No contract. We'll tell you if it's worth building.
