Models
Contents
Models#
For: developers
This page covers the rules and patterns for defining Odoo models in OpenSPP modules. These conventions are enforced by pre-commit hooks — violations will block your commits.
Model namespace#
All OpenSPP models must use the spp.* namespace:
class MyModel(models.Model):
_name = "spp.my.feature" # Single domain
_description = "My Feature"
class MySubModel(models.Model):
_name = "spp.my.feature.item" # Domain + entity
_description = "My Feature Item"
The deprecated g2p.* namespace is rejected by linting.
Standard Odoo models (res.partner, res.users, ir.model, etc.) are exempt from this rule when using _inherit to extend them.
Field naming rules#
OpenSPP enforces specific naming conventions for fields. These are checked by the openspp-check-naming pre-commit hook.
Boolean fields#
Must start with a recognized prefix:
# Correct
is_active = fields.Boolean()
has_children = fields.Boolean()
can_approve = fields.Boolean()
enable_notifications = fields.Boolean()
show_details = fields.Boolean()
# Will fail linting
active_flag = fields.Boolean() # No recognized prefix
approved = fields.Boolean() # No recognized prefix
Recognized prefixes include (non-exhaustive): is_, has_, can_, allow_, enable_, disable_, show_, hide_, include_, exclude_, skip_, auto_, force_, use_, apply_, check_, notify_on_, send_, create_, copy_, close_, assign_, link_to_, manage_, validate_, store_, track_. The canonical list is in scripts/lint/check_naming.py — consult it if the lint hook rejects a name you think should be valid.
Many2one fields#
Must end with _id:
# Correct
program_id = fields.Many2one("spp.program")
alert_type_id = fields.Many2one("spp.vocabulary.code")
# Will fail linting
program = fields.Many2one("spp.program")
Common exceptions (these don't need _id): parent, company, partner, user, currency, country, state.
One2many and Many2many fields#
Must end with _ids:
# Correct
membership_ids = fields.One2many("spp.group.membership", "group")
tag_ids = fields.Many2many("spp.vocabulary.code")
# Will fail linting
memberships = fields.One2many("spp.group.membership", "group")
Model structure pattern#
Here is a typical model following OpenSPP conventions:
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class MyFeature(models.Model):
"""Brief description of what this model represents.
Longer description explaining the purpose and key behaviors.
"""
_name = "spp.my.feature"
_description = "My Feature"
_inherit = ["mail.thread"]
_order = "create_date desc"
_check_company_auto = True
# === IDENTIFICATION ===
name = fields.Char(required=True, tracking=True)
reference = fields.Char(
required=True,
copy=False,
readonly=True,
default=lambda self: _("New"),
)
# === CLASSIFICATION ===
feature_type_id = fields.Many2one(
"spp.vocabulary.code",
string="Type",
# Filter by namespace_uri (stored on the code itself) rather than
# vocabulary name — names are translated and break in non-English
# locales. namespace_uri is the stable filter key.
domain="[('namespace_uri', '=', 'urn:openspp:vocab:feature-type')]",
)
# === STATE ===
state = fields.Selection(
[
("draft", "Draft"),
("active", "Active"),
("closed", "Closed"),
],
default="draft",
tracking=True,
)
# === RELATIONSHIPS ===
partner_id = fields.Many2one("res.partner", string="Registrant")
item_ids = fields.One2many("spp.my.feature.item", "feature_id")
# === FLAGS ===
is_archived = fields.Boolean(default=False)
# === COMPANY ===
company_id = fields.Many2one(
"res.company",
required=True,
default=lambda self: self.env.company,
)
Key patterns#
Use
mail.threadfor models that need change tracking (adds the chatter widget)Use
_check_company_auto = Truefor multi-company safetyAdd
company_idto every model that needs multi-company isolationUse
spp.vocabulary.codefor type/category fields instead of hardcoded selections (this allows configuration through the vocabulary system)Use
tracking=Trueon important fields to log changes in the chatter
Extending existing models#
To add fields to an existing OpenSPP or Odoo model, use _inherit without _name:
class ResPartnerExtension(models.Model):
_inherit = "res.partner"
my_custom_field = fields.Char(string="My Custom Field")
is_my_feature_enabled = fields.Boolean(default=False)
This adds your fields to the existing model's database table. No new table is created.
Auto-generated sequences#
For models that need auto-incrementing reference numbers, define a sequence in data/ir_sequence.xml:
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="seq_spp_my_feature" model="ir.sequence">
<field name="name">My Feature Sequence</field>
<field name="code">spp.my.feature</field>
<field name="prefix">MF/%(year)s/</field>
<field name="padding">5</field>
</record>
</odoo>
Then use it in your model's create method:
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("reference", _("New")) == _("New"):
vals["reference"] = self.env["ir.sequence"].next_by_code(
"spp.my.feature"
) or _("New")
return super().create(vals_list)
openspp.org