All posts
Developer7 min read

Odoo field-level security, domain filters, and invisible conditions in views

Odoo views support three visibility mechanisms: security groups (render the field or not), domain filters (filter records displayed), and dynamic attrs (conditionally show/hide elements based on field values). Each controls a different layer of what users see.

Odoo gives you three independent mechanisms to control what users see: the groups attribute on field definitions (who can see a field at all), the domain attribute on list and search views (which records appear), and the attrs and invisible conditions on view elements (whether specific elements display for a given record state). Mixing them up - using invisible where groups is needed, or using groups where domain is needed - creates subtle bugs that pass testing but fail in production.

Layer 1: groups - who can see the field#

The groups attribute on a field definition controls whether the field renders at all for users not in the specified group. It is evaluated server-side at view rendering time.

On the Python field:

python
class SaleOrder(models.Model):
    _inherit = 'sale.order'

    margin = fields.Float(groups='product.group_show_purchase_price')

Non-members see the field as empty (False/0). The field is not redacted in the API response for security purposes - it is simply excluded from the default fields_get and not rendered in the view.

On a view element:

xml
<field name="margin" groups="product.group_show_purchase_price"/>

The view element is removed from the rendered arch before sending to the client. Non-members see no trace of the element in the DOM.

When to use groups:

  • Hiding salary, cost, or margin data from non-managers.
  • Hiding advanced configuration fields from basic users.
  • Hiding admin-only buttons from regular users.

Important: groups on a view element does not protect the underlying data through the ORM. A user without the group can still call env['sale.order'].read([1], ['margin']) via RPC if they know the field name. True data protection requires the groups= attribute on the Python field definition.

Layer 2: domain - which records appear#

A domain expression filters which records a view displays or a Many2one field offers as options.

On a list/search view:

xml
<record id="view_sale_order_list_mine" model="ir.ui.view">
  <field name="model">sale.order</field>
  <field name="arch" type="xml">
    <list>
      <field name="name"/>
      <field name="partner_id"/>
      <field name="amount_total"/>
    </list>
  </field>
  <field name="domain">[('user_id', '=', uid)]</field>
</record>

This view only shows orders assigned to the current user. The domain is evaluated server-side; it does not prevent users from querying other records via the API.

On a Many2one field in a view:

xml
<field name="product_id" domain="[('type', '=', 'consu'), ('sale_ok', '=', True)]"/>

The dropdown only shows consumable, saleable products.

Dynamic domain:

xml
<field name="product_id" domain="[('categ_id', '=', categ_id)]"/>

The domain references another field's value (categ_id). Odoo evaluates this on the client side - it changes as the user edits the form.

Layer 3: invisible - conditional display#

The invisible attribute (or legacy attrs={'invisible': [...]}) controls whether a view element renders based on the current record's field values. It is evaluated client-side.

Modern syntax (Odoo 16+):

xml
<field name="delivery_date" invisible="state not in ['sale', 'done']"/>
<button name="action_cancel" invisible="state == 'cancel'"/>
<group invisible="partner_id == False">
  <field name="partner_credit_limit"/>
</group>

Legacy attrs syntax (still valid):

xml
<field name="delivery_date" attrs="{'invisible': [('state', 'not in', ['sale', 'done'])]}"/>

When to use invisible:

  • Hiding fields that are irrelevant in certain states (e.g., delivery date on a draft order).
  • Hiding buttons after an action completes (e.g., "Confirm" button after state = confirmed).
  • Conditional display of related records that only exist in certain workflows.

When NOT to use invisible:

  • Do not use invisible as a security mechanism. The element is still in the DOM and the field value is still in the JSON response. A determined user can read invisible fields from the browser.

readonly - preventing edits#

readonly prevents editing without hiding the field.

xml
<field name="amount_total" readonly="1"/>
<!-- Or conditionally: -->
<field name="partner_id" readonly="state != 'draft'"/>

Server-side readonly protection:

The view-level readonly is cosmetic. It prevents UI edits but not API writes. To prevent API writes, override write() or use _check_constraint():

python
def write(self, vals):
    for rec in self:
        if rec.state != 'draft' and 'partner_id' in vals:
            raise UserError("Cannot change partner after confirmation.")
    return super().write(vals)

required - mandatory fields#

required on a field means the form cannot be saved without a value.

xml
<field name="street" required="partner_type == 'delivery'"/>

Conditional required: the field is mandatory only when partner_type equals delivery.

Server-side required enforcement for fields not always required:

python
@api.constrains('partner_type', 'street')
def _check_delivery_address(self):
    for rec in self:
        if rec.partner_type == 'delivery' and not rec.street:
            raise ValidationError("Delivery address requires a street.")

Combining the layers#

A typical pattern for a manager-only, confirmed-order-only field:

python
# Model: visible only to managers
internal_margin = fields.Float(groups='sales_team.group_sale_manager')
xml
<!-- View: hidden on draft orders, editable only by managers -->
<field name="internal_margin"
       groups="sales_team.group_sale_manager"
       readonly="state != 'sale'"
       invisible="state == 'draft'"/>

This combination:

  • Hides the field from non-managers at the Python level (no API exposure).
  • Hides the UI element from non-managers.
  • Hides it from everyone on draft orders.
  • Allows only managers to edit it when the order is confirmed.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free