Wasting $11K Annually? Use Odoo 18 fields_get() for Dynamic Fields
By Braincuber Team
Published on December 22, 2025
Developer needs to make "Salary" field readonly for everyone except HR managers. Current solution: Create 2 separate form views. View #1 (salary readonly) for regular users. View #2 (salary editable) for HR managers. Maintain access rights. Update both views whenever form changes. Takes 3 hours to implement.
Your codebase: 47 different XML views with hardcoded field properties. Product manager says: "Make 'Price' field readonly in 8 different views when invoice is confirmed." You edit 8 XML files. Next week: "Also make it readonly in quote view." Edit another view. Week after: "Actually, Sales Managers should still edit confirmed prices." Now need attrs with groups in 9 views.
Cost: Developer spends 4.7 hours monthly updating field properties across multiple XML views = $11,280/year (at $40/hour). Bug risk: Forgot to update one view, users see inconsistent behavior. Maintenance nightmare: Same field behavior duplicated in 47 places. No central control. Plus inflexibility: Can't make fields readonly based on record state without complex attrs in every view.
Odoo 18 fields_get() method fixes this: Override field properties dynamically in Python. Change label, readonly status, help text, selection options—all in one place. Apply logic based on user role, record state, context. No XML changes needed. Here's how to use fields_get() so you stop wasting $11,280/year on XML maintenance.
You're Wasting Time If:
What fields_get() Does
Returns dictionary of field metadata for any Odoo model. Each field's properties: type, label (string), help text, required status, readonly status, selection options. Override this method to dynamically modify field properties based on:
- User role (Sales Manager vs Sales User)
- Record state (draft vs confirmed)
- Context variables
- Business logic
Common Use Cases:
- 1. Role-Based Field Access: Make salary field readonly for non-HR users
- 2. Dynamic Labels: Change "Email" to "Contact Email" without XML changes
- 3. State-Based Readonly: Lock fields when invoice confirmed
- 4. Dynamic Selection Options: Update city choices based on selected country
- 5. Conditional Help Text: Show different tooltips for different user roles
How fields_get() Works
When Odoo renders form view:
1. Load XML view definition
2. For each field in view, call model.fields_get()
3. Get field metadata (label, type, readonly, etc.)
4. Apply metadata to render field in browser
5. If you override fields_get(), your changes applied automatically
Result: Change field properties in Python, affects ALL views using that field.
Example 1: Change Field Label Dynamically
Scenario: Want to change "Email" field label to "Contact Email" across all partner views.
Without fields_get() (Old Way)
Problem: Edit XML in every view showing email field. 8 different views = 8 XML edits.
<field name="email" string="Contact Email"/>
<!-- Repeat in 8 different XML files -->
With fields_get() (Smart Way)
Solution: Override fields_get() once. Change applies to ALL views automatically.
# models/res_partner.py
from odoo import models
class ResPartner(models.Model):
_inherit = "res.partner"
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
# Change email field label
if "email" in res:
res["email"]["string"] = "Contact Email"
return res
Result: All 8 views now show "Contact Email" automatically. No XML edits.
Example 2: Role-Based Readonly Fields
Scenario: Phone field should be readonly for Sales Users, but editable for Sales Managers.
Code Implementation
# models/res_partner.py
from odoo import models, api
class ResPartner(models.Model):
_inherit = "res.partner"
@api.model
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
# Check if phone field exists
if "phone" in res:
# Only Sales Managers can edit phone
if not self.env.user.has_group("sales_team.group_sale_manager"):
res["phone"]["readonly"] = True
res["phone"]["help"] = "Only Sales Managers can edit this field."
return res
How It Works
_inherit = "res.partner"extends existing Contact modelfields_get()overridden to intercept field metadatasuper().fields_get()gets original metadata from parent class- Check if
phonefield exists in metadata self.env.user.has_group()checks if current user is Sales Manager- If NOT Sales Manager: Set
readonly = Trueand add help text - Return modified metadata
- Odoo UI automatically applies readonly attribute
User Experience
| User Role | Phone Field | Tooltip |
|---|---|---|
| Sales User | Readonly (grayed out) | "Only Sales Managers can edit" |
| Sales Manager | Editable | Normal help text |
Example 3: State-Based Readonly (Invoice)
Scenario: Once invoice is posted, lock all financial fields. Don't want accountant accidentally changing amounts.
# models/account_move.py
from odoo import models, api
class AccountMove(models.Model):
_inherit = "account.move"
@api.model
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
# If invoice is posted, lock financial fields
if self and self.state == "posted":
financial_fields = ["amount_total", "invoice_line_ids", "partner_id"]
for field_name in financial_fields:
if field_name in res:
res[field_name]["readonly"] = True
return res
Testing with Server Actions
Quick way to test fields_get() changes without creating custom views.
Step-by-Step
- Enable Developer Mode: Settings icon → Activate Developer Mode
- Go to Settings → Technical → Actions → Server Actions
- Click Create
- Fill form:
- Action Name: "Test fields_get"
- Model: Contact (res.partner)
- Action To Do: Execute Python Code
- Paste test code (see below)
- Save
- Open any Contact record
- Action menu → Run "Test fields_get"
Server Action Code
# Fetch metadata for email and phone fields
field_metadata = model.fields_get(allfields=['email', 'phone'])
# Log to server (check in terminal/logs)
import logging
_logger = logging.getLogger(__name__)
_logger.info("Email field metadata: %s", field_metadata.get('email'))
_logger.info("Phone field metadata: %s", field_metadata.get('phone'))
# Open new contact form to see changes
action = {
"type": "ir.actions.act_window",
"res_model": "res.partner",
"view_mode": "form",
"target": "new",
}
Real-World Use Cases
Use Case 1: HR Salary Protection
Problem:
Employee records have salary field. Only HR Managers should edit. Everyone else: readonly.
Old Solution (3 hours work):
- Create 2 form views (one readonly, one editable)
- Set access rights: HR Managers see editable view
- Regular users see readonly view
- Maintain both views when adding fields
New Solution (15 minutes work):
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
if "wage" in res: # salary field in hr.employee
if not self.env.user.has_group("hr.group_hr_manager"):
res["wage"]["readonly"] = True
return res
Result:
Saved 2.75 hours. One view. One source of truth. No maintenance overhead.
Use Case 2: Multi-Company Field Labels
Problem:
Company A calls it "Reference Number." Company B calls it "Order ID." Same field, different labels per company.
Solution:
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
if "name" in res:
company = self.env.company
if company.id == 1: # Company A
res["name"]["string"] = "Reference Number"
elif company.id == 2: # Company B
res["name"]["string"] = "Order ID"
return res
Result:
Each company sees their preferred terminology. No duplicate views.
Use Case 3: Dynamic Selection Options
Problem:
City selection field should show different cities based on selected country. Hardcoding all countries × cities = thousands of options.
Solution:
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
if "city_id" in res:
# Get cities for selected country
country_id = self.country_id.id if self else False
if country_id:
cities = self.env['res.city'].search([
('country_id', '=', country_id)
])
res["city_id"]["selection"] = [
(city.id, city.name) for city in cities
]
return res
Result:
City dropdown only shows relevant cities. Better UX, cleaner data.
Advanced: Combining Multiple Conditions
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields=allfields, attributes=attributes)
# Make discount field readonly if:
# 1. User is NOT Sales Manager AND
# 2. Order is confirmed AND
# 3. Discount > 20%
if "discount" in res:
is_manager = self.env.user.has_group("sales_team.group_sale_manager")
is_confirmed = self.state in ["sale", "done"]
high_discount = self.discount > 20
if not is_manager and is_confirmed and high_discount:
res["discount"]["readonly"] = True
res["discount"]["help"] = "Contact Sales Manager to modify discounts > 20%"
return res
Common Mistakes
1. Not Calling super()
Forgot super().fields_get(). Lost all parent field definitions. Form breaks.
Fix: ALWAYS call res = super().fields_get() first. Modify res, then return it.
2. Checking self When No Record
Code: if self.state == "posted". Crashes when opening form for new record (self is empty).
Fix: Check if self exists: if self and self.state == "posted"
3. Modifying Original res Dictionary
Multiple modules override fields_get(). Modifying same dictionary causes conflicts.
Fix: Check field exists before modifying: if "phone" in res: res["phone"]["readonly"] = True
4. Performance Issues in Loop
Calling fields_get() inside loop for 1,000 records. Takes 30 seconds.
Fix: @api.model decorator makes it model-level (not record-level). Cache results if possible.
Real-World Impact Example
Scenario: SaaS Company (12 Custom Modules, 47 Form Views)
Before Using fields_get():
- Field behavior hardcoded in 47 XML views
- Want to make "Price" readonly when order confirmed
- Need to add
attrs="{'readonly': [('state', '=', 'sale')]}"to 8 views - Developer time: 4.7 hours monthly updating field properties across views
- Annual cost: 4.7 × 12 × $40/hour = $2,256
- Bugs: Forgot to update 2 views, users confused (price editable in some views, readonly in others)
- Maintenance: Every new requirement = edit multiple XMLs
- Inflexibility: Can't make field readonly based on user role without complex attrs
- Total waste: $11,280/year (dev time) + bug fixes + technical debt
After Implementing fields_get():
- Created fields_get() override for 5 critical models
- Price field: Readonly logic in ONE place (Python)
- Salary field: Protected for non-HR users automatically
- Discount field: Readonly if > 20% and not manager
- Field label changes: Modify in Python, affects ALL views instantly
- Developer time saved: 4.7 hours → 0.8 hours monthly = 3.9 hours saved
- Annual savings: 3.9 × 12 × $40 = $1,872
- Zero field behavior bugs (single source of truth)
- New requirements: 15 minutes to implement (was 3 hours)
- Can add complex logic (role + state + context) easily
- Total saved: $9,360/year + zero inconsistency bugs + flexible architecture
Impact: $9,360 saved annually + 83% faster field changes + zero XML maintenance
Quick Implementation Checklist
- Identify repetitive field attrs: Find fields with same attrs in multiple views
- Choose model: Decide which model to extend (res.partner, sale.order, etc.)
- Create Python file: models/model_name.py in your custom module
- Import required:
from odoo import models, api - Inherit model:
_inherit = "model.name" - Override fields_get(): Use @api.model decorator
- Call super():
res = super().fields_get(allfields, attributes) - Add your logic: Check field exists, modify properties
- Test with Server Action: Quick verification before deployment
- Remove XML attrs: Clean up redundant attrs in views (optional but recommended)
Pro Tip: Don't migrate all field logic to fields_get() on day one. Start with most painful duplications (fields in 5+ views). Prove concept. Then gradually move more logic from XML to Python.
Wasting $11K Annually on XML Field Maintenance?
We implement fields_get() overrides to centralize field behavior logic, eliminate XML duplication, enable role-based field access. Stop maintaining 47 different XML files.
