Bu makale şu anda yalnızca İngilizce olarak mevcuttur. Çeviri yakında eklenecektir.
By the end of this recipe, you will have added a fully functional custom field to any Odoo 19 model without using Studio — meaning your change is in version control, reviewable in pull requests, idempotent across environments, and survives database migrations cleanly. Skill required: Odoo developer comfortable with Python and XML. Time required: 30 minutes for a simple field, 90 minutes for a computed field with view tweaks. ECOSIRE has built thousands of custom fields this way, and the recipe below is the playbook.
The reason engineering teams shun Studio: Studio creates fields in the database without an __manifest__.py to track them, making it impossible to replicate from dev to staging to production except by manual repetition. The custom-module approach below puts every field under git, with diff-able XML, and lets you ship the change via your normal CI/CD pipeline.
What you will need
- Odoo version: 17, 18, or 19. The pattern is identical.
- Module skeleton: a custom module with
__manifest__.py,__init__.py,models/,views/,security/. - Skill: basic Python class definition, basic Odoo XML view structure.
- Time: 30 minutes simple, 90 minutes computed.
Step-by-step
1. Scaffold the module
cd /opt/custom-addons
odoo-bin scaffold custom_partner_extension .
This creates the standard layout. Edit __manifest__.py:
{
'name': 'Partner Extension',
'version': '19.0.1.0.0',
'depends': ['base'],
'data': [
'views/partner_views.xml',
],
'installable': True,
}
Verification: odoo-bin -u custom_partner_extension --stop-after-init runs without errors.
2. Add the field to the model
In models/__init__.py: from . import res_partner. Then models/res_partner.py:
from odoo import models, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
customer_segment = fields.Selection(
selection=[
('strategic', 'Strategic'),
('enterprise', 'Enterprise'),
('mid_market', 'Mid-Market'),
('smb', 'SMB'),
('consumer', 'Consumer'),
],
string='Customer Segment',
default='smb',
tracking=True,
help='Strategic segmentation used for CLV and account management.',
)
tracking=True means changes are logged in the chatter. Verification: upgrade the module; the new field exists in the database (SELECT customer_segment FROM res_partner LIMIT 1 runs).
3. Expose the field in the form view
views/partner_views.xml:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_segment" model="ir.ui.view">
<field name="name">res.partner.form.segment</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales_purchases']//field[@name='ref']" position="after">
<field name="customer_segment"/>
</xpath>
</field>
</record>
<record id="view_partner_tree_segment" model="ir.ui.view">
<field name="name">res.partner.tree.segment</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='email']" position="after">
<field name="customer_segment" optional="show"/>
</xpath>
</field>
</record>
</odoo>
Verification: open any partner form; the Customer Segment dropdown appears.
4. Add a search filter
<record id="view_partner_search_segment" model="ir.ui.view">
<field name="name">res.partner.search.segment</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='supplier']" position="after">
<separator/>
<filter name="strategic" string="Strategic" domain="[('customer_segment','=','strategic')]"/>
<filter name="enterprise" string="Enterprise" domain="[('customer_segment','=','enterprise')]"/>
<separator/>
<filter string="Customer Segment" name="group_by_segment" context="{'group_by':'customer_segment'}"/>
</xpath>
</field>
</record>
Verification: in the Contacts list, the search panel shows "Strategic" and "Enterprise" filters and a "Group By > Customer Segment" option.
5. Add a computed field (advanced)
For derived data like total revenue from a customer:
from odoo import api
class ResPartner(models.Model):
_inherit = 'res.partner'
total_revenue = fields.Float(
string='Total Revenue',
compute='_compute_total_revenue',
store=True,
)
@api.depends('sale_order_ids.amount_untaxed', 'sale_order_ids.state')
def _compute_total_revenue(self):
for partner in self:
partner.total_revenue = sum(
so.amount_untaxed for so in partner.sale_order_ids
if so.state in ('sale', 'done')
)
store=True writes to the database, allowing efficient queries and search. Without it, the field is computed on every read (slower, but always fresh).
Verification: SELECT total_revenue FROM res_partner WHERE id = 1 returns the computed value.
6. Add validation constraints
from odoo.exceptions import ValidationError
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.constrains('customer_segment', 'total_revenue')
def _check_segment_revenue_consistency(self):
for partner in self:
if partner.customer_segment == 'strategic' and partner.total_revenue < 100000:
raise ValidationError(
f"{partner.name} cannot be Strategic with revenue under $100k."
)
Verification: try to mark a low-revenue customer as Strategic; the validation fires.
7. Add field-level security
In security/ir.model.access.csv, you can already restrict access at model level. For field-level, use groups on the field definition:
customer_segment = fields.Selection(
selection=[...],
groups='sales_team.group_sale_manager',
)
Only Sales Manager group sees the field. Verification: a regular sales user does not see the field on the form.
8. Migrate existing data
If you're adding a field to a populated model, you may need to backfill defaults. Add a pre_init_hook or migration script:
# migrations/19.0.1.0.1/post-migrate.py
def migrate(cr, version):
cr.execute("""
UPDATE res_partner
SET customer_segment = CASE
WHEN total_revenue > 1000000 THEN 'strategic'
WHEN total_revenue > 100000 THEN 'enterprise'
WHEN total_revenue > 10000 THEN 'mid_market'
ELSE 'smb'
END
WHERE customer_segment = 'smb' AND total_revenue > 100000
""")
Verification: after upgrade, customers are correctly segmented based on history.
Common mistakes
- Using Studio for production fields. Hard to track, hard to deploy.
- Forgetting
_inherit. Without it, you create a new model, not extend the existing one. store=Trueon every computed field. Stored fields write on every dependency change, hurting write performance. Use only when you need to search/filter.- Hardcoded selection labels. Use translation strings:
string=_('Strategic')for i18n support. - Skipping
tracking=True. Auditors love changes logged automatically.
Going further
Default values from context: default=lambda self: self.env.context.get('default_customer_segment', 'smb') for context-aware defaults. Useful when creating a record from a related record's button — propagate the parent's segment as a default.
Onchange handlers: react to user input before save with @api.onchange. Useful for field interdependencies — when the user picks a country, auto-set currency and language defaults.
Custom widgets: render the field with a custom OWL widget for richer UX (color picker, status pill, badge with icon, color-graded score bar). Reference: <field name="customer_segment" widget="badge" decoration-success="customer_segment == 'strategic'"/> uses the built-in badge widget.
Dynamic selection options: selection=lambda self: self._get_dynamic_segments() to pull options from a config table. Lets non-developers add new segments via the UI without code changes.
Related fields: partner_country = fields.Char(related='partner_id.country_id.code') shorthand for displaying joined data without a stored field.
Ranged numeric fields: fields.Integer(string='Score', help='1-10') with constraint validation to enforce range. Use widget="progressbar" for visual feedback.
Json fields for flexible data: Odoo 17+ supports fields.Json natively for storing structured data without a relational table. Useful for storing API response payloads or feature-flag configs.
Translatable text fields: fields.Char(translate=True) lets multilingual stores have per-language values for the same field. Critical for international e-commerce.
Audit-tracked fields: tracking=True plus a mail.thread mixin gives you full audit chatter. Every change shows who changed what when.
Computed fields with @api.depends_context: when a field's value depends on self.env.user or self.env.company, declare the dependency explicitly. Otherwise the cache returns stale values across users.
Field-level translation: pair Selection's translate parameter with PO file translation so the dropdown options appear in user's language.
For complex Odoo extensions including custom widgets and integration-driven fields, ECOSIRE Odoo customization ships fixed-price engagements. Pair this with how to override an existing method using inheritance.
Frequently Asked Questions
Should I use Studio or custom module?
Studio for non-developers and quick experiments. Custom module for anything going to production. Most ECOSIRE engagements ship pure custom modules — they're version-controlled, reviewable in PRs, and survive database migrations cleanly. Studio outputs are technically also custom modules under the hood, but the generated XML is harder to read than hand-written equivalents.
What if I need to remove a field later?
Add the field name to a __pre_uninstall__ script that drops the column. Or simply leave the column in place and stop referencing it — Postgres doesn't suffer. For multi-environment cleanup, write a migration script that drops the column under a feature flag.
Can I add fields without restarting Odoo?
No — Odoo registers models at startup. After adding a field, you must -u module_name (which restarts the worker handling that DB). For active development with dev_mode = reload, the change appears within seconds.
What about field types like JSON?
Odoo 17+ supports fields.Json natively. Useful for structured data without a relational table. Pair with a JSON schema validator in @api.constrains to enforce structure. JSON fields don't support search filters efficiently — if you need to filter by a JSON sub-field often, consider extracting it to a regular field.
How do I add a field to a built-in Odoo model?
Same pattern: _inherit = 'res.partner' and declare your field. Odoo merges your field into the existing model definition. The new column gets added to the existing res_partner table on next module update.
What's the maximum number of fields I should add to a model?
Soft limit: ~20 custom fields per model before the form gets unwieldy. Beyond that, consider splitting into a related One2many model (e.g., partner.profile.extra keyed off partner_id) so the data model stays clean.
Can I conditionally show fields in the form?
Yes — XML attribute invisible="customer_segment != 'strategic'" hides the field unless the partner is Strategic. Saves form clutter for irrelevant fields.
How do I handle field-level permissions in a multi-team setup?
The groups parameter on the field definition + custom record rules are the two layers. Field-level: only Sales Manager sees customer_segment. Record-level: Sales Reps see only their assigned partners.
For complex schema extensions, ECOSIRE Odoo customization builds the full module. Or read how to write an Odoo unit test to test your new field.
Yazan
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Odoo ERP ile İşinizi Dönüştürün
Operasyonlarınızı kolaylaştırmak için uzman Odoo uygulaması, özelleştirme ve destek.
İlgili Makaleler
Odoo Form Görünümüne Özel Düğme Nasıl Eklenir (2026)
Odoo 19 form görünümlerine özel eylem düğmeleri ekleyin: Python eylem yöntemi, görünüm devralma, koşullu görünürlük, onay diyalogları. Üretimde test edilmiştir.
Odoo'da Harici Düzeni Kullanarak Özel Rapor Nasıl Eklenir?
Web.external_layout'u kullanarak Odoo 19'da markalı bir PDF raporu oluşturun: QWeb şablonu, paperformat, action bağlama. Baskı logosu + altbilgi geçersiz kılmalarıyla.
Odoo Veritabanı Nasıl Yedeklenir ve Geri Yüklenir (Sıfır Kesinti Süresi)
Üretim taktik kitabı: pg_dump + filestore tarball, S3 yaşam döngüsü, belirli bir noktaya kurtarma, geri yükleme testi cron'u, 30 dakikadan kısa RTO. ECOSIRE tarafından test edilmiştir.