Skip to content

Powerful Guide: Mastering Odoo 18 API Returns for Stellar Integrations

odoo 18 api returns total score 7 search intent 3

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 returns truly 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 like partner.name or call record-specific methods like partner.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 returns automatically converts the recordset into its corresponding ID or a list of IDs (e.g., 42 or [42, 43]). If no record is found, it returns False. You don’t need to write any manual conversion code!
  • 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.one and @api.multi decorators. Write your methods in the modern record-style, operating on recordsets.
  • @api.model Usage: Only use @api.model when the method truly operates on the model itself, and self (the recordset on which the method is called) does not represent specific records, but rather the model as a whole (e.g., for search() or create() 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:

  1. 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.
  2. Create res_partner.py: Inside your custom module’s models directory, create a Python file named res_partner.py. This file will contain the code that extends the res.partner model.
  3. 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
  1. Explanation of the Code:
    • from odoo import models, api: Imports essential Odoo libraries.
    • class ResPartner(models.Model): Defines our new Python class, inheriting from models.Model.
    • _inherit = "res.partner": Crucially tells Odoo that this class is extending the existing res.partner model, 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 a res.partner record. This ensures that internal calls get a recordset, while external Odoo 18 API returns an ID.
    • self.search([('email', '=', email)], limit=1): Searches the res.partner model for a record matching the provided email. limit=1 ensures 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 if name isn’t provided.
    • return partner: The method returns either the found partner recordset or the newly created one.
  2. Update the __manifest__.py file: Make sure your custom module’s manifest file includes a dependency on the base module. This ensures res.partner is loaded.
'depends': ['base'],
  1. 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).
  2. 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:

  1. Add the Following Method to res_partner.py: Extend your ResPartner class 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)
  1. Explanation of the Code:
    • ensure_customer Method: This method takes email, optional name, and optional phone.
    • 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. The phone field is added to vals only if it’s actually provided, ensuring flexibility in API calls.
    • @api.returns('res.partner'): Again, this crucial decorator ensures the consistent Odoo 18 API returns behavior, providing a recordset internally and an ID externally.
  2. Install/Upgrade the Module: Upgrade your custom module.
  3. 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:

  1. 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
  1. Explanation of the Code:
    • @api.returns(...) with downgrade and upgrade: 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 the id of the record. If value is an empty recordset, it returns False.
      • 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 uses self.browse(value) to create a recordset from the ID. If value is False or None, it returns an empty recordset.
    • find_primary_contact(self, company): This method takes a company recordset as an argument.
    • company.child_ids.filtered(...)[:1]: This powerful Odoo method chain retrieves all child contacts of the company, filters them to only include those with type == 'contact', and then takes the first one ([:1]).
  2. Install/Upgrade the Module: Upgrade your module.
  3. 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:

  1. Create a Custom Module (if you don’t have one).
  2. Create product.py: Inside your custom module’s models directory, create a product.py file.
  3. Override the create() method: Add the following code to product.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
  1. Explanation of the Code:
    • _inherit = 'product.template': We are extending the core product.template model.
    • @api.model: This indicates that create is a model method.
    • category_id = vals.get('categ_id'): We safely retrieve the category ID from the vals dictionary (the values passed to create).
    • category = self.env['product.category'].browse(category_id): We convert the ID into a recordset to access category details like category.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 the vals dictionary.
    • result = super().create(vals): This is essential. It calls the original create method of the product.template model, 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.
  2. Create the Sequence in XML:** You’ll need to define the product.internal.reference sequence 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.

  1. Update __manifest__.py: Add product to your module’s dependencies.
'depends': ['base', 'product'],
'data': [
    'data/ir_sequence.xml', # Make sure to include your sequence file
],
  1. Install/Upgrade the Module: Upgrade your custom module.
  2. 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.

Leave a Reply

WP Twitter Auto Publish Powered By : XYZScripts.com