Welcome, Odoo developers! Today, we’re diving deep into a truly powerful Odoo 18 feature that can elevate your code quality, improve integration reliability, and make your development workflow significantly smoother: the @api.returns() decorator. If you’ve ever wrestled with inconsistent data formats between internal Odoo calls and external API requests, then understanding Odoo 18 API returns is absolutely crucial.
This comprehensive guide, inspired by the “Odoo 18 Pro Tip: Mastering @api.returns()” insights and supplementary code examples, will walk you through everything you need to know. We’ll cover its core functionality, practical implementation with step-by-step tutorials, advanced custom conversions, and even touch upon related best practices for method overriding.
Before we begin, for a quick visual overview of this powerful decorator, check out this valuable resource: Odoo 18 Development: Auto-Generate Product Internal Reference Based on Product Category. While the video focuses on product internal references, it highlights the importance of consistent development practices in Odoo 18, a principle deeply connected to mastering Odoo 18 API returns.
The Undeniable Power of Odoo 18 API Returns
The @api.returns() decorator is a cornerstone for writing robust and predictable Odoo methods. It’s specifically designed for methods that return records (like res.partner, sale.order, stock.picking) and ensures consistent output regardless of how the method is called. This consistency is vital for maintaining data integrity and simplifying both internal Odoo logic and external integrations.
What exactly does @api.returns() achieve?
- Declares the Return Model: It explicitly tells other developers and automated tools what kind of record or model your method is expected to return. This self-documenting aspect improves code readability and maintainability.
- Auto-Adapts the Output: This is where the magic of
Odoo 18 API returnstruly shines.- Internal Odoo Calls (Recordset Style): When your method is called from within Odoo (e.g., from another server action or a button click), it will return a recordset (e.g.,
env['res.partner'](42)). This allows you to directly access fields likepartner.nameor call record-specific methods likepartner.write(...). - Traditional/Legacy Style (ID Style): If the same method is invoked via a traditional RPC call or some older integration patterns,
Odoo 18 API returnsautomatically converts the recordset into its corresponding ID or a list of IDs (e.g.,42or[42, 43]). If no record is found, it returnsFalse. You don’t need to write any manual conversion code!
- Internal Odoo Calls (Recordset Style): When your method is called from within Odoo (e.g., from another server action or a button click), it will return a recordset (e.g.,
- Inherits Across Overrides: When you override an existing Odoo method that uses
@api.returns(), your child method automatically inherits the same return contract. This ensures that the expected output type remains consistent throughout your module’s hierarchy, making overrides safer and more predictable.
When is mastering Odoo 18 API Returns most beneficial?
- Whenever your method is designed to return one or more Odoo records.
- For methods that need to be accessible and provide consistent data both internally and to external systems (e.g., portal APIs, third-party integrations).
When is it not necessary?
- If your method returns primitive data types such as strings (
str), dictionaries (dict), booleans (bool), integers (int), or floats (float). In these cases, the native Python type is sufficient.
Essential Pro Tips for using @api.returns():
@api.returns('self'): If your method returns a record of the current model on which the method is defined, use'self'. This is a clean and explicit way to declare the return type.- Embrace Record-Style Methods: Avoid the deprecated
@api.oneand@api.multidecorators. Write your methods in the modern record-style, operating on recordsets. @api.modelUsage: Only use@api.modelwhen the method truly operates on the model itself, andself(the recordset on which the method is called) does not represent specific records, but rather the model as a whole (e.g., forsearch()orcreate()operations).
Odoo 18 API Returns in Action: A Minimal Example
Let’s start with a foundational example that demonstrates how to implement Odoo 18 API returns in a simple, yet practical, scenario.
Scenario: We need a method to either find an existing partner by email or create a new one if no match is found. This is a very common requirement for data synchronization and user management.
Steps:
- Create a Custom Module: If you don’t already have one, create a new Odoo module (e.g.,
my_custom_partners). This module will house our extensions to core Odoo models. - Create
res_partner.py: Inside your custom module’smodelsdirectory, create a Python file namedres_partner.py. This file will contain the code that extends theres.partnermodel. - Add the Following Code to
res_partner.py:
from odoo import models, api
class ResPartner(models.Model):
_inherit = "res.partner"
@api.model
@api.returns('res.partner')
def get_or_create_by_email(self, email, name=None):
"""
Finds a partner by email or creates a new one if not found.
Returns a res.partner recordset (internal) or ID (external API).
"""
partner = self.search([('email', '=', email)], limit=1)
if not partner:
# If no partner exists, create a new one.
partner = self.create({
'name': name or email.split('@')[0], # Use provided name or derive from email
'email': email
})
return partner
- Explanation of the Code:
from odoo import models, api: Imports essential Odoo libraries.class ResPartner(models.Model): Defines our new Python class, inheriting frommodels.Model._inherit = "res.partner": Crucially tells Odoo that this class is extending the existingres.partnermodel, not creating a new one.@api.model: Declares this as a class method, operating on the model itself.@api.returns('res.partner'): This is the star of the show! It explicitly states that this method will return ares.partnerrecord. This ensures that internal calls get a recordset, while externalOdoo 18 API returnsan ID.self.search([('email', '=', email)], limit=1): Searches theres.partnermodel for a record matching the provided email.limit=1ensures we only fetch the first match.self.create(...): If no partner is found by the search, a new partner record is created with the given email and a name derived from the email ifnameisn’t provided.return partner: The method returns either the found partner recordset or the newly created one.
- Update the
__manifest__.pyfile: Make sure your custom module’s manifest file includes a dependency on thebasemodule. This ensuresres.partneris loaded.
'depends': ['base'],
- Install/Upgrade the Module: In your Odoo instance, navigate to Apps, update the Apps list, and then install or upgrade your custom module (
my_custom_partners). - Test the Method: You can easily test this method using the Odoo shell:
# Access the Odoo environment (env)
# Open Odoo shell: odoo-bin shell -c your-config-file
env = self.env
# Test creating a new partner
new_partner = env['res.partner'].get_or_create_by_email('tutorial_user@example.com', 'Tutorial User')
print(f"New Partner ID: {new_partner.id}, Name: {new_partner.name}")
# Test getting an existing partner
existing_partner = env['res.partner'].get_or_create_by_email('tutorial_user@example.com', 'Another Name (ignored)')
print(f"Existing Partner ID: {existing_partner.id}, Name: {existing_partner.name}")
# Verify that both refer to the same record
assert new_partner.id == existing_partner.id
You’ll notice that the Odoo 18 API returns a recordset in the shell, allowing direct attribute access. If called via RPC, it would return the ID.
Building Robustness: Real-World Odoo 18 API Returns for Integrations
For external integrations and portal modules, ensuring data integrity and safety is paramount. Let’s look at another common pattern where Odoo 18 API returns significantly enhances method reliability.
Scenario: We need a method, ensure_customer, that guarantees a customer record exists for a given email. This method should be robust enough for both internal server actions and external JSON-RPC integrations, potentially including additional details like a phone number.
Steps:
- Add the Following Method to
res_partner.py: Extend yourResPartnerclass with this new method:
@api.model
@api.returns('res.partner')
def ensure_customer(self, email, name=None, phone=None):
"""
Ensures a customer exists for the given email, creating one if necessary.
Designed for robust portal/integration use.
Returns a res.partner recordset (internal) or ID (external API).
"""
partner = self.search([('email', '=', email)], limit=1)
if partner:
return partner # Return existing partner immediately
vals = {
'name': name or email.split('@')[0],
'email': email
}
if phone:
vals['phone'] = phone # Conditionally add phone if provided
return self.create(vals)
- Explanation of the Code:
ensure_customerMethod: This method takesemail, optionalname, and optionalphone.- Prioritize Search: The logic is subtly reversed compared to
get_or_create_by_email. It first searches for an existing partner. If found, it immediately returns that partner, avoiding any unnecessary creation. This is a common pattern for “upsert” (update or insert) operations, making it highly efficient. - Conditional Creation: Only if no partner is found, a dictionary of values (
vals) is prepared. Thephonefield is added tovalsonly if it’s actually provided, ensuring flexibility in API calls. @api.returns('res.partner'): Again, this crucial decorator ensures the consistentOdoo 18 API returnsbehavior, providing a recordset internally and an ID externally.
- Install/Upgrade the Module: Upgrade your custom module.
- Test the Method:
# Ensure a customer with email and phone
customer_with_phone = env['res.partner'].ensure_customer(
'integration@example.com', 'Integration Customer', '123-456-7890'
)
print(f"Customer 1 ID: {customer_with_phone.id}, Name: {customer_with_phone.name}, Phone: {customer_with_phone.phone}")
# Call again with only email, should return the existing one
customer_no_phone = env['res.partner'].ensure_customer('integration@example.com')
print(f"Customer 2 ID: {customer_no_phone.id}, Name: {customer_no_phone.name}, Phone: {customer_no_phone.phone}")
# Create another customer without a phone initially
new_customer = env['res.partner'].ensure_customer('another@example.com', 'Another User')
print(f"New Customer ID: {new_customer.id}, Name: {new_customer.name}, Phone: {new_customer.phone}")
This example showcases how robust Odoo 18 API returns combined with careful logic can simplify complex integration scenarios, ensuring data is handled predictably.
Elevating Your Code: Advanced Odoo 18 API Returns with Custom Conversions
While @api.returns('model') is usually sufficient, there are scenarios where you need finer control over how values are converted between recordsets and primitive IDs, especially when dealing with external systems that expect very specific data formats. This is where the downgrade and upgrade parameters of Odoo 18 API returns come into play.
Scenario: Implement a method find_primary_contact that identifies the primary contact for a given company. For external API calls, we might want to ensure only the contact’s ID is returned, and if an ID is passed back, it’s correctly re-converted to a recordset.
Steps:
- Add the Following Method to
res_partner.py:
@api.model
@api.returns('res.partner',
downgrade=lambda self, value, *a, **kw: value.id if value else False,
upgrade=lambda self, value, *a, **kw: self.browse(value) if value else self.env['res.partner'])
def find_primary_contact(self, company):
"""
Finds the first child partner of type 'contact' for a given company.
Uses custom downgrade/upgrade for advanced API returns.
"""
if not company:
return self.env['res.partner'] # Return empty recordset if no company
# Filter children to find the first contact-type partner
primary_contact = company.child_ids.filtered(lambda p: p.type == 'contact')[:1]
return primary_contact
- Explanation of the Code:
@api.returns(...)withdowngradeandupgrade: This is the advanced configuration.downgrade=lambda self, value, *a, **kw: value.id if value else False: This lambda function dictates how a recordset (value) is converted (downgraded) for external consumption. Here, it simply extracts theidof the record. Ifvalueis an empty recordset, it returnsFalse.upgrade=lambda self, value, *a, **kw: self.browse(value) if value else self.env['res.partner']: This lambda function specifies how a primitive value (like an ID) received from an external call is converted back (upgraded) into an Odoo recordset. It usesself.browse(value)to create a recordset from the ID. IfvalueisFalseorNone, it returns an empty recordset.
find_primary_contact(self, company): This method takes acompanyrecordset as an argument.company.child_ids.filtered(...)[:1]: This powerful Odoo method chain retrieves all child contacts of thecompany, filters them to only include those withtype == 'contact', and then takes the first one ([:1]).
- Install/Upgrade the Module: Upgrade your module.
- Test the Method:
# Create a dummy company and a child contact for testing
company_test = env['res.partner'].create({'name': 'Test Company', 'is_company': True})
contact_test = env['res.partner'].create({
'name': 'Primary Contact',
'parent_id': company_test.id,
'type': 'contact'
})
env['res.partner'].create({ # Another child, but not 'contact' type
'name': 'Other Address',
'parent_id': company_test.id,
'type': 'invoice'
})
# Find the primary contact using our method
primary_contact_record = env['res.partner'].find_primary_contact(company_test)
print(f"Primary Contact (Recordset) ID: {primary_contact_record.id}, Name: {primary_contact_record.name}")
# Simulate an external call's return (would return ID due to downgrade)
# This part is illustrative, showing what an external API *would* receive/expect.
simulated_id_return = primary_contact_record.id
print(f"Simulated API Return (ID): {simulated_id_return}")
# If an external API were to pass this ID back, 'upgrade' would convert it.
upgraded_record = env['res.partner'].browse(simulated_id_return)
print(f"Upgraded Record (from ID) ID: {upgraded_record.id}, Name: {upgraded_record.name}")
The Odoo 18 API returns with custom conversions provides a flexible and robust mechanism for handling complex data interactions between Odoo’s internal recordset logic and external API requirements.
Beyond Returns: Method Overriding in Odoo 18 for Dynamic Logic
While focusing on Odoo 18 API returns, it’s worth noting how custom logic often involves overriding existing Odoo methods. This is another area where understanding Odoo’s API decorators and inheritance mechanisms is key. For a fantastic example, let’s refer to the video “Odoo 18 Development: Auto-Generate Product Internal Reference Based on Product Category.”
Scenario: Automatically generate a unique product internal reference (SKU) based on the product category when a new product is created. This ensures consistency and simplifies product management.
Steps:
- Create a Custom Module (if you don’t have one).
- Create
product.py: Inside your custom module’smodelsdirectory, create aproduct.pyfile. - Override the
create()method: Add the following code toproduct.py:
from odoo import models, fields, api
class ProductTemplate(models.Model):
_inherit = 'product.template'
@api.model
def create(self, vals):
# Get the product category from the values provided during creation
category_id = vals.get('categ_id')
if category_id:
# Browse the category to get its recordset
category = self.env['product.category'].browse(category_id)
# Generate a unique sequence number for the internal reference
# Ensure 'product.internal.reference' sequence is defined (see step 5)
sequence = self.env['ir.sequence'].next_by_code('product.internal.reference')
# Construct the default_code (internal reference)
vals['default_code'] = f"{category.name.upper()}-{sequence}"
# Call the original 'create' method to process the product creation
# This is crucial to ensure all default Odoo logic is still executed.
result = super().create(vals)
return result
- Explanation of the Code:
_inherit = 'product.template': We are extending the coreproduct.templatemodel.@api.model: This indicates thatcreateis a model method.category_id = vals.get('categ_id'): We safely retrieve the category ID from thevalsdictionary (the values passed tocreate).category = self.env['product.category'].browse(category_id): We convert the ID into a recordset to access category details likecategory.name.sequence = self.env['ir.sequence'].next_by_code('product.internal.reference'): This line generates the next number from a custom sequence.vals['default_code'] = ...: We construct the new internal reference and inject it into thevalsdictionary.result = super().create(vals): This is essential. It calls the originalcreatemethod of theproduct.templatemodel, ensuring that Odoo’s default product creation logic is still executed after our custom modifications.return result: The overridden method returns the created product recordset.
- Create the Sequence in XML:** You’ll need to define the
product.internal.referencesequence in an XML data file within your module (e.g.,data/ir_sequence.xml).
<odoo>
<data noupdate="1">
<record id="seq_product_internal_reference" model="ir.sequence">
<field name="name">Product Internal Reference</field>
<field name="code">product.internal.reference</field>
<field name="prefix">PROD/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>
Remember to add this XML file to your __manifest__.py‘s data section.
- Update
__manifest__.py: Addproductto your module’s dependencies.
'depends': ['base', 'product'],
'data': [
'data/ir_sequence.xml', # Make sure to include your sequence file
],
- Install/Upgrade the Module: Upgrade your custom module.
- Test:** Create a new product in Odoo, ensuring you select a product category. Verify that the “Internal Reference” (technical name
default_code) field is automatically populated with a value like “CATEGORY_NAME-PROD/0001”. This perfectly illustrates dynamic behavior using method overriding.
Conclusion
Mastering Odoo 18 API returns is a critical skill for any Odoo developer aiming to write clean, maintainable, and highly functional code. By consistently using @api.returns() for methods that yield recordsets, you ensure predictable behavior across internal and external calls, significantly streamlining your integration efforts and enhancing overall system reliability.
From basic get_or_create patterns to complex custom conversions and robust integration-safe methods, understanding Odoo 18 API returns empowers you to build more sophisticated and dependable Odoo applications. Couple this with strong method overriding practices, and you’re well on your way to becoming an Odoo 18 development pro. Start implementing these practices today and watch your Odoo solutions shine!
Discover more from teguhteja.id
Subscribe to get the latest posts sent to your email.

