Skip to Content

Odoo 19 Email Templates: How the 'From' Field Actually Works

Every Odoo project I've run eventually hits the same moment: a client asks why an invoice or a quote showed up in their customer's inbox from the wrong person. Not a bounce, not a spam filter. The email arrived fine, it just said "From: Dave" when Dave had nothing to do with it. Nobody configured that. Odoo just decided.

The reason is that Odoo doesn't treat "who sent this email" as one field you fill in. It's a fallback chain: a short list of candidates, checked top to bottom, and the first one with an actual email address wins. Untangling which candidate wins on which template is most of the work of getting Odoo's outbound email right for a client.

Example - Sales: Send Quotation

Here's the actual expression Odoo 19 ships on the standard "Sales: Send Quotation" template, the one that fires when someone clicks Send on a quote:

{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}

Read left to right, that's three checks, in order:

  1. object.user_id.email_formatted: is there a salesperson assigned to this quote, and do they have an email on file? If yes, they're the sender.
  2. object.company_id.email_formatted: if not, does the company itself have an email address configured? If yes, the customer sees the company brand as sender instead of a person.
  3. user.email_formatted: if neither of those resolved to anything, fall back to whoever is actually logged in and clicking Send.

The "or" here doesn't mean "any of these." It means first-match-wins. Odoo checks candidate one, and only moves to candidate two if candidate one comes back empty. That's the entire mental model for reading any of these expressions: read it as a waterfall, not a set.

That same shape (salesperson, then company, then logged-in user, with small variations) repeats across almost every customer-facing template Odoo ships. A few skip a step. One template runs the order backwards. None of it is arbitrary once you know to look for the chain.

Every Template's From Field, Broken Down

Everything below is stock Odoo 19, pulled directly off a fresh install, no 19 Prince customization, grouped by app: Sales, Accounting, CRM, Purchasing, Inventory, Helpdesk, Project, Documents, everything else, then Connectors (Amazon, Lazada, Shopee, TikTok, and Gelato, grouped together at the end).

Sales

Sales (sale)

TemplateSend FromPlain English
Sales: Send Quotation{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Assigned salesperson first, then company brand, then logged-in user.
Sales: Send Proforma{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Assigned salesperson first, then company brand, then logged-in user.
Sales: Order Confirmation{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Assigned salesperson first, then company brand, then logged-in user.
Sales: Payment Done{{ (object.user_id.email_formatted or user.email_formatted) }}Assigned salesperson first, then logged-in user, skips the company-brand fallback.

Subscriptions (sale_subscription)

TemplateSend FromPlain English
Subscription: Default Email Alert{{ (object.user_id.email_formatted or user.email_formatted) }}Assigned salesperson first, then logged-in user.
Subscription: Payment Failure{{ (object.user_id.email_formatted or object.company_id.email or user.email_formatted) }}Assigned salesperson first, then the company's raw email field (not the formatted display version), then logged-in user.
Subscription: Payment Reminder{{ (object.user_id.email_formatted or object.company_id.email or user.email_formatted) }}Same as Payment Failure.
Subscription: Rating Request{{ (object._rating_get_operator().email_formatted or user.email_formatted) }}Whoever's computed as the record's rating "operator" (the rep tied to it), else logged-in user.

eCommerce (website_sale)

TemplateSend FromPlain English
Ecommerce: Cart Recovery{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted or '') }}Assigned salesperson first, then company, then logged-in user, else blank.

Accounting

Accounting (account)

TemplateSend FromPlain English
Invoice: Sending{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Invoice's salesperson first, then company brand, then logged-in user.
Credit Note: Sending{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Same.
Self-billing invoice: Sending{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Same.
Self-billing credit note: Sending{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}Same.
Journal Notification{{ object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted }}Same 3-way fallback, just written without the outer parentheses.
New eInvoices Notification{{ object.company_id.email_formatted }}Always the company brand identity, no fallback.
Payment: Payment Receipt(blank)No From set at all.

Follow-up (account_followup)

TemplateSend FromPlain English
Payment Reminder{{ object._get_followup_responsible().email_formatted }}Whoever is assigned as this customer's collections/follow-up contact.
Second reminder followup{{ object._get_followup_responsible().email_formatted }}Same.

Worked example: unlike the field chains above, the Follow-up templates use a method call instead of a simple field lookup — {{ object._get_followup_responsible().email_formatted }}. Three pieces, left to right: object is the customer record the reminder is about; _get_followup_responsible() is a method, not a stored field, so Odoo runs actual logic to work out who owns collections for that customer; .email_formatted formats whatever comes back as "Name <email>", the same as every other From field on this page. Notably, there's no or fallback chain here — if the method can't resolve anyone, the From field just comes back empty.

Bank Synchronization (account_online_synchronization)

TemplateSend FromPlain English
Bank connection expiration reminder{{ object.company_id.email_formatted or user.email_formatted }}Company brand first, then whoever's logged in.

Peppol E-Invoicing (account_peppol)

TemplateSend FromPlain English
Peppol: Registration update{{ object.email_formatted }}The record here is the company itself, so this is just the company's own address, no fallback.

Accounting Reports (account_reports)

TemplateSend FromPlain English
Customer Statement{{ object._get_followup_responsible().email_formatted }}The customer's collections/follow-up contact.
Follow Up Report{{ object._get_followup_responsible().email_formatted }}Same.
Tax payment{{ (user.email_formatted or object.company_id.email_formatted) }}Logged-in user first, then company brand, the one template in the whole database that reverses the usual priority.
Tax Return Deadline(blank)No From set at all.

CRM

Lead Mining (crm_iap_mine)

TemplateSend FromPlain English
IAP Lead Generation Notification[email protected]Hardcoded Odoo address, not client-branded at all.

Resellers / Partner Assignment (website_crm_partner_assign)

TemplateSend FromPlain English
Lead Forward: Send to partner{{ user.email_formatted }}Whoever is logged in and forwarding the lead, no fallback at all. Notably, this is the exact "quote creator is the sender" pattern proposed as a custom fix for a client's sender-attribution complaint, Odoo already ships it here, just on this one CRM lead-forwarding template rather than on the main Sales quotation flow.

Purchasing

Purchase (purchase)

TemplateSend FromPlain English
Purchase: Vendor Reminder{{ (object.user_id.email_formatted or user.email_formatted) }}Assigned buyer first, then logged-in user.
Purchase: Purchase Order(blank)No From set at all.
Purchase: Request For Quotation(blank)No From set at all.

Inventory

Inventory (stock)

TemplateSend FromPlain English
Shipping: Send by Email(blank)No From set at all.

Email Marketing

No `mail.template` records. Mass mailings compose their body/sender directly on `mailing.mailing`.

Marketing Automation

No `mail.template` records. Campaign activities compose their content directly, not through templates.

Helpdesk

Helpdesk (helpdesk)

TemplateSend FromPlain English
Helpdesk: Ticket Received{{ (object.team_id.alias_email_from or object.company_id.email_formatted or object.user_id.email_formatted or user.email_formatted) }}The helpdesk team's alias address first, then company brand, then the assigned agent, then logged-in user. Four-deep, the longest fallback chain in the database.
Helpdesk: Ticket Closed{{ (object.team_id.alias_email_from or object.company_id.email_formatted or object.user_id.email_formatted or user.email_formatted) }}Same 4-way chain.
Helpdesk: Ticket Rating Request{{ (object.team_id.alias_email_from or object.company_id.email_formatted or object._rating_get_operator().email_formatted or user.email_formatted) }}Same shape, but the third step is a computed rating-operator lookup instead of the assigned agent field directly.

Project

Project (project)

TemplateSend FromPlain English
Project: Request Acknowledgment(blank)No From set at all.
Project: Task Rating Request{{ (object._rating_get_operator().email_formatted if object._rating_get_operator() else user.email_formatted) }}Whoever's computed as the task's rating operator, if one exists, else logged-in user.

Field Service (industry_fsm)

TemplateSend FromPlain English
Field Service: Field Service Report(blank)No From set at all.
Field Service: Intervention Scheduled(blank)No From set at all.

Documents

Documents (documents)

TemplateSend FromPlain English
Document Request: Reminder(blank)No From set at all.
Document: Document Request(blank)No From set at all.
Document: Document Share(blank)No From set at all.

Payroll Documents (documents_hr_payroll)

TemplateSend FromPlain English
Payroll: New Declaration{{ user.email_formatted }}Whoever is logged in, no fallback, the HR staffer processing payroll.
Payroll: New Payslip Document{{ user.email_formatted }}Same.

Everything Else

Appointments (appointment)

TemplateSend FromPlain English
Appointment: Appointment Booked{{ (user.email_formatted or object.user_id.email_formatted) }}Logged-in user first, then the event's assigned user, a reversed order compared to most other templates.
Appointment: Appointment Canceled{{ (user.email_formatted or object.user_id.email_formatted) }}Same reversed order.
Appointment: Attendee Invitation{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}The calendar event's organizer first, then logged-in user, else blank.

Appointments - Recruitment Scheduling (appointment_hr_recruitment)

TemplateSend FromPlain English
Recruitment: Schedule interview(blank)No From set at all.

Appraisals (hr_appraisal)

TemplateSend FromPlain English
HR: Appraisal Request By Employee(blank)No From set at all.
HR: Appraisal reminder(blank)No From set at all.
HR: General Appraisal Confirmation(blank)No From set at all.

Appraisals - 360 Feedback (hr_appraisal_survey)

TemplateSend FromPlain English
HR: Ask Feedback 360(blank)No From set at all.

Calendar (calendar)

TemplateSend FromPlain English
Calendar: Meeting Invitation{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}The event's organizer first, then logged-in user, else blank.
Calendar: Reminder{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}Same.
Calendar: Date Updated{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}Same.
Calendar: Event Update{{ (object.user_id.email_formatted or user.email_formatted or '') }}Same shape, but operates directly on the event record instead of an attendee record.
Calendar: Event Deleted{{ (object.user_id.email_formatted or user.email_formatted or '') }}Same.

eLearning (website_slides)

TemplateSend FromPlain English
Channel Shared{{ user.email_formatted }}Logged-in user, no fallback.
Elearning: Add Attendees to Course{{ user.email_formatted }}Same.
Elearning: Course Share{{ user.email_formatted }}Same.
Elearning: Promotional Course Invitation{{ user.email_formatted }}Same.
Elearning: Completed Course{{ (object.channel_id.user_id.email_formatted or object.channel_id.user_id.company_id.catchall_formatted) }}The course's instructor/owner first, then that instructor's company catchall address, no logged-in-user fallback at all.
Elearning: New Course Content Notification(blank)No From set at all.

eLearning - Certification (website_slides_survey)

TemplateSend FromPlain English
Survey: Certification Failure(blank)No From set at all.

Equity (equity)

TemplateSend FromPlain English
Equity Shareholder Email(blank)No From set at all.
Equity UBO Form Email(blank)No From set at all.

Events (event)

TemplateSend FromPlain English
Event: Registration Badge{{ (object.event_id.organizer_id.email_formatted or object.event_id.company_id.email_formatted or user.email_formatted or '') }}Event organizer first, then the event's company brand, then logged-in user, else blank.
Event: Registration Confirmation{{ (object.event_id.organizer_id.email_formatted or object.event_id.company_id.email_formatted or user.email_formatted or '') }}Same.
Event: Reminder{{ (object.event_id.organizer_id.email_formatted or object.event_id.company_id.email_formatted or user.email_formatted or '') }}Same.

Events - Website Track (website_event_track)

TemplateSend FromPlain English
Event: Track Confirmation{{ (object.event_id.organizer_id.email_formatted or object.event_id.company_id.email_formatted or user.email_formatted or '') }}Same organizer/company/logged-in chain as the core Events app.
Add reminder via email{{ (object.event_id.organizer_id.email_formatted or object.event_id.company_id.email_formatted or user.email_formatted or '') }}Same.

Gamification (gamification)

TemplateSend FromPlain English
Gamification: Badge Received(blank)No From set at all.
Gamification: Challenge Report(blank)No From set at all.
Gamification: New Rank Reached(blank)No From set at all.
Gamification: Reminder For Goal Update(blank)No From set at all.

Loyalty & Gift Cards (loyalty)

TemplateSend FromPlain English
Coupon: Coupon Information{{ object.program_id.company_id.email }}The loyalty program's owning company, its raw email field, not the formatted display version.
Gift Card: Gift Card Information(blank)No From set at all.

Lunch (lunch)

TemplateSend FromPlain English
Lunch: Supplier Order{{ ctx.get('order', {}).get('email_from') }}Pulled entirely from context at send time (whoever placed the order), not from any field on the record.

Payroll (hr_payroll)

TemplateSend FromPlain English
Payroll: New Payslip{{ user.email_formatted }}Logged-in user, no fallback.

Planning (planning)

TemplateSend FromPlain English
Planning: New Schedule{{ object.company_id.email_formatted }}Always the company brand, no fallback.
Planning: New Shift{{ object.company_id.email_formatted }}Same.
Planning: Shift Re-assigned{{ object.company_id.email_formatted }}Same.

Point of Sale (point_of_sale)

TemplateSend FromPlain English
Point of Sale: Receipt{{ (object.company_id.email_formatted or object.user_id.email_formatted)}}Company brand first, then the assigned cashier/user.

Point of Sale - Self-Order (pos_self_order)

TemplateSend FromPlain English
Takeout: Confirmation order for self{{ (object.company_id.email_formatted or object.user_id.email_formatted)}}Same as the core Point of Sale receipt.
Delivery: Confirmation order for self{{ (object.company_id.email_formatted or object.user_id.email_formatted)}}Same.

Recruitment (hr_recruitment)

TemplateSend FromPlain English
Recruitment: Interest(blank)No From set at all.
Recruitment: Application Acknowledgement(blank)No From set at all.
Recruitment: Not interested anymore(blank)No From set at all.
Recruitment: Refuse(blank)No From set at all.

Salary Configurator (hr_contract_salary)

TemplateSend FromPlain English
Employee: Contract And Salary Package(blank)No From set at all.
Recruitment: Your Salary Package(blank)No From set at all.

Settings - Module Install Requests (base_install_request)

TemplateSend FromPlain English
Mail: Install Request{{ object.user_id.email_formatted or user.email_formatted }}The person who requested the module install, else whoever's logged in.

Settings - Portal & Sign-Up (auth_signup)

TemplateSend FromPlain English
Settings: New User Invite{{ (object.company_id.email_formatted or user.email_formatted) }}Company brand first, then whoever's logged in.
Settings: New Portal Sign Up{{ (object.company_id.email_formatted or user.email_formatted) }}Same.
Settings: New Portal User Invite{{ (object.company_id.email_formatted or user.email_formatted) }}Same.
Settings: Unregistered User Reminder{{ (object.company_id.email_formatted or user.email_formatted) }}Same.

Settings - Two-Factor Authentication (auth_totp_mail)

TemplateSend FromPlain English
Settings: 2Fa Invitation{{ (object.company_id.email_formatted or user.email_formatted) }}Company brand first, then whoever's logged in.
Settings: 2Fa New Login{{ (object.company_id.email_formatted or user.email_formatted) }}Same.

Settings - IAP Extract Notification (iap_extract)

TemplateSend FromPlain English
IAP Extract Notification[email protected]Hardcoded Odoo address, not client-branded at all.

Surveys (survey)

TemplateSend FromPlain English
Survey: Invite{{ user.email_formatted }}Logged-in user, no fallback.
Survey: Certification Success{{ (object.survey_id.create_uid.email_formatted or user.email_formatted or user.company_id.catchall_formatted) }}The survey's creator first, then logged-in user, then the company's catchall address as a last resort, three deep, ending in a mailbox instead of a person.

Sustainability Reporting / CSRD (esg_csrd)

TemplateSend FromPlain English
CSRD Metric: Ask Stakeholder Reviews(blank)No From, and no Subject either, this template is essentially an unconfigured placeholder as shipped.

Timesheets (timesheet_grid)

TemplateSend FromPlain English
Timesheets: Approver Reminder{{ (object.user_id.company_id.partner_id.email_formatted or user.email_formatted) }}The employee's own company contact record first, then logged-in user.
Timesheets: Employee Reminder{{ (object.user_id.company_id.partner_id.email_formatted or user.email_formatted) }}Same.

Website - AI Site Generator (website_generator)

TemplateSend FromPlain English
Website Scraped{{ ctx.get('email_to') }}Pulled from context at send time, notably reusing the same context key normally meant for the recipient address as the sender too.

Website - Forum Profile (website_profile)

TemplateSend FromPlain English
Forum: Email Verification{{ object.company_id.email_formatted or object.email_formatted }}Company brand first, then falls back to the record's own address, since the record here is the user being verified, not the more usual "logged-in user" fallback.

Developer/QA - Mail Test (test_mail)

TemplateSend FromPlain English
Mail Test Full: Tracking Template(blank)No From set. These two are Odoo's own internal test-suite fixtures, not real business templates.
Mail Test: Template(blank)Same.

Connectors

Amazon Connector (sale_amazon)

TemplateSend FromPlain English
Amazon: Order Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Company's raw email field first, then the assigned user's email, then logged-in user.
Amazon: Delivery Order Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same fallback order.
Amazon: Available Inventory for Offers Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same fallback order.

Gelato Connector (sale_gelato)

TemplateSend FromPlain English
Gelato: Order status update(blank)No From set at all, Odoo falls back to the system/alias-domain default.

Lazada Connector (sale_lazada)

TemplateSend FromPlain English
Lazada: Order Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same 3-way pattern as the Amazon connector.
Lazada: Inventory Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same.

Shopee Connector (sale_shopee)

TemplateSend FromPlain English
Shopee: Order Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same 3-way pattern as the other connectors.
Shopee: Inventory Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same.
Shopee: Shipping Label Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same.

TikTok Connector (sale_tiktok)

TemplateSend FromPlain English
TikTok: Order Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same 3-way pattern as the other connectors.
TikTok: Inventory Synchronization Failure{{ (object.company_id.email or object.user_id.email_formatted or user.email_formatted) }}Same.

One row in the CRM section is worth calling out on its own. The "Lead Forward: Send to partner" template sends from whoever is logged in and forwarding the lead, with no fallback to the company or anyone else. That's the exact "quote creator is the sender" pattern that comes up whenever a client complains about the wrong name showing up in a customer's inbox. Odoo already ships that fix, it just lives on one CRM lead-forwarding template, not on the Sales quotation flow where most people go looking for it.

If your customers have ever asked "why did this come from someone who doesn't work here anymore," the answer is almost always sitting in one of these rows. Fixing it isn't a rebuild. It's usually a one-line change to the right template, once you know which candidate in the chain is actually winning.

— Darren from 19 Prince