Introduction
In Odoo 17, res.config.settings is commonly used to store global or company-related configurations. The many2many fields in odoo Many2many fields in Odoo represent bidirectional many-to-many relationships between two models. They're commonly used when records from both models can be linked to multiple records from the other model. However, storing Many2many fields in settings is not straightforward because ir.config_parameter only supports string values. The model behind system settings, res.config.settings, is a Transient Model. This means:
- Records are temporary
- Values are not stored automatically
- ir.config_parameter only supports string values
Because of this:
- Boolean fields can use config_parameter directly
- Many2many fields cannot and must be handled manually
To store Many2many settings in Odoo 17, we need to:
- Define the Many2many fields in res.config.settings
- Save their record IDs into ir.config_parameter
- Restore those IDs when the Settings view is opened
This is done using:
- set_values() → save data
- get_values() → load data
Defining the Settings Fields
Below is a generalized settings model. In this example, the configuration controls CIF/CIP breakdown behavior, but the pattern applies to any Many2many setting.
from odoo import fields, models, api
class SgeedeResConfig(models.TransientModel):
_inherit = 'res.config.settings'
cif_breakdown = fields.Boolean(string="CIF/CIP Breakdown", config_parameter="base.cif_breakdown")
company_choose = fields.Many2many('res.company',string="Company")
customer_choose = fields.Many2many('res.partner',string="Customer")
incoterm = fields.Many2many('account.incoterms', string="Incoterms")
shipment_term = fields.Many2many('purchase.shipment.term', string="Shipment Term")
@api.model
def get_values(self):
res = super().get_values()
ICP = self.env['ir.config_parameter'].sudo()
res.update({
'company_choose': [(6, 0, eval(ICP.get_param('base.cif_company_ids', '[]')))],
'customer_choose': [(6, 0, eval(ICP.get_param('base.cif_customer_ids', '[]')))],
'incoterm': [(6, 0, eval(ICP.get_param('base.cif_incoterm_ids', '[]')))],
'shipment_term': [(6, 0, eval(ICP.get_param('base.cif_shipment_term_ids', '[]')))],
})
return res
After defining the fields and create views, the configuration will looks like this:

Saving Many2many Values (set_values)
When the user clicks Save in Settings, set_values() is executed.
def set_values(self):
super().set_values()
ICP = self.env['ir.config_parameter'].sudo()
ICP.set_param('base.cif_company_ids', self.company_choose.ids)
ICP.set_param('base.cif_customer_ids', self.customer_choose.ids)
ICP.set_param('base.cif_incoterm_ids', self.incoterm.ids)
ICP.set_param('base.cif_shipment_term_ids', self.shipment_term.ids)
What happens:
- .ids returns a Python list like [1, 5, 7]
- str() converts it to string "[1, 5, 7]"
- This string is stored in ir.config_parameter
Loading Many2many Values (get_values)
To display previously saved values, we override get_values().
import ast
@api.model def get_values(self): res = super().get_values() ICP = self.env['ir.config_parameter'].sudo() def _get_ids(key): return [(6, 0, ast.literal_eval(ICP.get_param(key, '[]')))] res.update({ 'company_choose': _get_ids('base.cif_company_ids'), 'customer_choose': _get_ids('base.cif_customer_ids'), 'incoterm': _get_ids('base.cif_incoterm_ids'), 'shipment_term': _get_ids('base.cif_shipment_term_ids'), }) return res
Why ast.literal_eval ?
In Odoo, ir.config_parameter stores all values as strings, even when the original data represents complex structures such as lists or dictionaries. To work with these values in Python, they must be converted back into their original data types. ast.literal_eval() provides a safe way to perform this conversion by parsing only valid Python literals—such as lists, dictionaries, tuples, numbers, and strings—without executing arbitrary code. This makes it a secure and recommended alternative to eval(), especially in production environments where configuration values may be modified through the user interface or database.
Use Configuration in Business Logic
Retrieve saved settings elsewhere in your module:
import ast
def _get_cif_config(self): ICP = self.env['ir.config_parameter'].sudo() def _get_ids(key): return set(ast.literal_eval(ICP.get_param(key, '[]'))) return { 'enabled': ICP.get_param('base.cif_breakdown') == 'True', 'company_ids': _get_ids('base.cif_company_ids'), 'customer_ids': _get_ids('base.cif_customer_ids'), 'incoterm_ids': _get_ids('base.cif_incoterm_ids'), 'shipment_term_ids': _get_ids('base.cif_shipment_term_ids'), } def _compute_is_qualify(self): QUALIFYING_BUSINESS_UNITS = {'EM', 'IS', 'LS', 'ST', 'RCC', 'Website', 'Chemicals'} for rec in self: cfg = rec._get_cif_config() rec.is_qualify_to_appear = ( cfg['enabled'] and rec.company_id.id in cfg['company_ids'] and rec.partner_id.id in cfg['customer_ids'] and rec.incoterm.id in cfg['incoterm_ids'] and rec.shipment_term_id.id in cfg['shipment_term_ids'] and self._has_qualifying_business_unit(rec.order_line, QUALIFYING_BUSINESS_UNITS) )
Conclusion
Storing Many2many fields in Odoo 17 settings requires manual handling but follows a consistent pattern. By overriding get_values() and set_values() methods and using safe data conversion, you can create flexible configuration screens that persist Many2many selections across sessions. This approach ensures your settings are both user-friendly and secure, protecting your Odoo instance from potential injection attacks while providing powerful configuration capabilities.
