Models¶
Models are the foundation of any Netforce application. They define your data structure, business logic, and behavior. The framework uses an ORM-like approach where models inherit from the base Model class.
See Also: ORM (Database) for detailed information about CRUD operations, browse records, and database interactions.
Basic Model Structure¶
from netforce.model import Model, fields
class MyModel(Model):
_name = "my.model"
_string = "My Model"
_fields = {
"name": fields.Char("Name", required=True, search=True),
"description": fields.Text("Description"),
"active": fields.Boolean("Active"),
}
_defaults = {
"active": True,
}
Model Configuration Attributes¶
Core Attributes¶
| Attribute | Description | Example |
|---|---|---|
_name |
Unique model identifier | "account.invoice" |
_string |
Human-readable name | "Invoice" |
_fields |
Field definitions dictionary | {"name": fields.Char(...)} |
_defaults |
Default field values | {"state": "draft"} |
_order |
Default sorting | "date desc, name" |
Advanced Attributes¶
| Attribute | Description | Example |
|---|---|---|
_audit_log |
Enable change tracking | True |
_multi_company |
Company-specific records | True |
_key |
Unique constraint fields | ["code", "company_id"] |
_constraints |
Validation methods | ["check_fields"] |
_content_search |
Full-text search enabled | True |
_name_field |
Field used for display name | "number" |
_export_name_field |
Field for export operations | "code" |
Model Lifecycle Methods¶
Models support lifecycle methods that are called during record operations:
| Method | Description | When Called |
|---|---|---|
create() |
Create new record | Before record creation |
write() |
Update record | Before record update |
delete() |
Delete record | Before record deletion |
validate() |
Validate data | During validation |
get_data() |
Get form data | When loading forms |
on_change() |
Handle field changes | When field values change |
Database Constraints¶
Enforce data integrity at the database level:
class Contact(Model):
_name = "contact"
# SQL constraints
_sql_constraints = [
("unique_email", "unique (email)", "Email must be unique"),
("positive_credit_limit", "check (credit_limit >= 0)", "Credit limit must be positive")
]
# Python constraints
_constraints = ["check_email_format", "check_phone_format"]
def check_email_format(self, ids):
"""Validate email format"""
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
for contact in self.browse(ids):
if contact.email and not re.match(email_pattern, contact.email):
raise Exception(f"Invalid email format: {contact.email}")
Advanced Model Features¶
Computed Fields with Store¶
Fields that calculate values and optionally store them:
class SaleOrder(Model):
_name = "sale.order"
_fields = {
"lines": fields.One2Many("sale.order.line", "order_id", "Order Lines"),
"amount_total": fields.Decimal("Total Amount",
function="get_amount_total",
store=True),
"line_count": fields.Integer("Line Count",
function="get_line_count"),
}
def get_amount_total(self, ids, context={}):
"""Calculate total amount"""
res = {}
for obj in self.browse(ids):
total = sum(line.amount for line in obj.lines)
res[obj.id] = total
return res
def get_line_count(self, ids, context={}):
"""Count order lines"""
res = {}
for obj in self.browse(ids):
res[obj.id] = len(obj.lines)
return res
Related Fields¶
Access fields from related models:
class SaleOrderLine(Model):
_name = "sale.order.line"
_fields = {
"order_id": fields.Many2One("sale.order", "Order"),
"product_id": fields.Many2One("product", "Product"),
"customer_name": fields.Char("Customer",
related="order_id.customer_id.name"),
"product_category": fields.Char("Category",
related="product_id.category_id.name"),
}
Selection Fields with Dynamic Options¶
class Task(Model):
_name = "project.task"
_fields = {
"state": fields.Selection("get_state_selection", "Status"),
"priority": fields.Selection([
["1", "Low"],
["2", "Medium"],
["3", "High"],
["4", "Urgent"]
], "Priority"),
}
def get_state_selection(self):
"""Dynamic selection options"""
return [
["draft", "Draft"],
["in_progress", "In Progress"],
["review", "Under Review"],
["done", "Completed"],
["cancelled", "Cancelled"]
]
Example: Complete Model Configuration¶
class Account(Model):
_name = "account.account"
_string = "Account"
_audit_log = True
_key = ["code", "company_id"]
_multi_company = True
_content_search = True
_order = "code"
_fields = {
"code": fields.Char("Account Code", required=True, search=True, index=True),
"name": fields.Char("Account Name", required=True, search=True),
"type": fields.Selection([
["asset", "Asset"],
["liability", "Liability"],
["equity", "Equity"],
["income", "Income"],
["expense", "Expense"]
], "Type", required=True),
"active": fields.Boolean("Active"),
"company_id": fields.Many2One("company", "Company"),
}
_defaults = {
"active": True,
"company_id": lambda *a: get_active_company(),
}
_constraints = ["check_code_unique"]
Field Types and Attributes¶
Basic Field Types¶
# Text fields
"name": fields.Char("Name", size=256, required=True, search=True)
"description": fields.Text("Description")
# Numeric fields
"amount": fields.Decimal("Amount", scale=2, required=True)
"quantity": fields.Integer("Quantity")
"rate": fields.Float("Rate")
# Boolean and dates
"active": fields.Boolean("Active")
"date": fields.Date("Date")
"datetime": fields.DateTime("Created At")
# Selection (dropdown)
"state": fields.Selection([
["draft", "Draft"],
["confirmed", "Confirmed"],
["done", "Done"]
], "Status")
# Files and JSON
"attachment": fields.File("Attachment")
"metadata": fields.Json("Metadata")
Relational Fields¶
# Many-to-One (foreign key)
"customer_id": fields.Many2One("contact", "Customer", required=True)
# One-to-Many (reverse foreign key)
"lines": fields.One2Many("invoice.line", "invoice_id", "Lines")
# Many-to-Many
"tags": fields.Many2Many("tag", "Tags")
# Reference (polymorphic)
"related_id": fields.Reference([
["sale.order", "Sales Order"],
["purchase.order", "Purchase Order"]
], "Related To")
Advanced Field Attributes¶
Common Attributes¶
"field_name": fields.Char("Label",
required=True, # Mandatory field
search=True, # Searchable in UI
index=True, # Database index
readonly=True, # Read-only in UI
size=256, # Max length for Char
scale=2, # Decimal places
translate=True, # Multi-language support
default="value" # Static default
)
Relational Attributes¶
# Filtered relationships
"parent_id": fields.Many2One("account.account", "Parent",
condition=[["type", "=", "view"]])
# Ordered relationships
"transactions": fields.One2Many("account.move.line", "account_id",
"Transactions", order="date desc")
Function Fields (Computed Fields)¶
Function fields are computed dynamically and can optionally be stored in the database.
Basic Function Field¶
class Invoice(Model):
_fields = {
"amount_total": fields.Decimal("Total", function="get_amount_total"),
}
def get_amount_total(self, ids, context={}):
vals = {}
for obj in self.browse(ids):
total = sum(line.amount for line in obj.lines)
vals[obj.id] = total
return vals
Multi-Function Fields¶
Compute multiple fields in one method for efficiency:
_fields = {
"debit": fields.Decimal("Debit", function="get_balance", function_multi=True),
"credit": fields.Decimal("Credit", function="get_balance", function_multi=True),
"balance": fields.Decimal("Balance", function="get_balance", function_multi=True),
}
def get_balance(self, ids, context={}):
vals = {}
for obj in self.browse(ids):
debit = sum(line.debit for line in obj.move_lines)
credit = sum(line.credit for line in obj.move_lines)
vals[obj.id] = {
"debit": debit,
"credit": credit,
"balance": debit - credit
}
return vals
Stored Function Fields¶
Searchable Function Fields¶
"product_id": fields.Many2One("product", "Product",
function="get_product",
function_search="search_product",
store=False, search=True)
def search_product(self, clause, context={}):
# Return domain filter for search
product_id = clause[2]
line_ids = self.env['invoice.line'].search([('product_id', '=', product_id)])
invoice_ids = [line.invoice_id.id for line in line_ids]
return [['id', 'in', invoice_ids]]
SQL Function Fields¶
Database-computed fields for reporting:
"year": fields.Char("Year", sql_function=["year", "date"])
"month": fields.Char("Month", sql_function=["month", "date"])
"total_sales": fields.Decimal("Total Sales", agg_function=["sum", "amount"])
Model Methods¶
CRUD Operations¶
# Create
invoice_id = get_model("account.invoice").create({
"number": "INV001",
"date": "2023-01-01",
"lines": [
[0, 0, {"product_id": 1, "qty": 5, "price": 100}]
]
})
# Read
invoices = get_model("account.invoice").browse([invoice_id])
for inv in invoices:
print(inv.number, inv.amount_total)
# Update
get_model("account.invoice").write([invoice_id], {"state": "posted"})
# Delete
get_model("account.invoice").delete([invoice_id])
# Search
invoice_ids = get_model("account.invoice").search([
["state", "=", "draft"],
["amount_total", ">", 1000]
])
Custom Methods¶
class Invoice(Model):
def confirm(self, ids, context={}):
for obj in self.browse(ids):
if obj.state != "draft":
raise Exception("Can only confirm draft invoices")
obj.write({"state": "confirmed"})
self.create_journal_entry([obj.id])
def create_journal_entry(self, ids, context={}):
for obj in self.browse(ids):
# Create accounting entry
move_vals = {
"journal_id": obj.journal_id.id,
"date": obj.date,
"lines": self._get_move_lines(obj)
}
get_model("account.move").create(move_vals)
Validation Methods¶
_constraints = ["check_amounts"]
def check_amounts(self, ids, context={}):
for obj in self.browse(ids):
if obj.amount_total < 0:
raise Exception("Total amount cannot be negative")
if not obj.lines:
raise Exception("Invoice must have at least one line")
Override Methods¶
def create(self, vals, context={}):
# Auto-generate number if not provided
if not vals.get("number"):
vals["number"] = self.get_next_number(context)
return super().create(vals, context)
def write(self, ids, vals, context={}):
# Prevent changes to posted invoices
for obj in self.browse(ids):
if obj.state == "posted" and vals.keys() != ["state"]:
raise Exception("Cannot modify posted invoice")
return super().write(ids, vals, context)
Default Values¶
Static Defaults¶
Dynamic Defaults¶
def _get_default_date(self, context={}):
return time.strftime("%Y-%m-%d")
def _get_default_company(self, context={}):
return get_active_company()
_defaults = {
"date": _get_default_date,
"company_id": _get_default_company,
"user_id": lambda *a: get_active_user()
}
Context-Based Defaults¶
def _get_number(self, context={}):
inv_type = context.get("inv_type", "invoice")
if inv_type == "invoice":
seq_type = "customer_invoice"
else:
seq_type = "credit_note"
return get_model("sequence").get_next_number(seq_type)
Best Practices¶
1. Naming Conventions¶
- Model names:
module.model(e.g.,account.invoice) - Field names:
snake_case - Method names:
snake_case - Constants:
UPPER_CASE
2. Field Organization¶
_fields = {
# Basic fields first
"name": fields.Char("Name", required=True),
"code": fields.Char("Code"),
# Relational fields
"customer_id": fields.Many2One("contact", "Customer"),
"lines": fields.One2Many("invoice.line", "invoice_id", "Lines"),
# Computed fields
"amount_total": fields.Decimal("Total", function="get_amount_total"),
# Status and metadata
"state": fields.Selection([...], "Status"),
"active": fields.Boolean("Active"),
}
3. Performance Considerations¶
- Use
function_multi=Truefor related computed fields - Add
store=Truefor frequently accessed function fields - Use
index=Truefor searchable fields - Implement efficient search methods for function fields
4. Security¶
- Always validate input in constraint methods
- Use proper access control in custom methods
- Sanitize user input in search methods
5. Multi-Company Support¶
# Always include company_id for multi-company models
"company_id": fields.Many2One("company", "Company")
_defaults = {
"company_id": lambda *a: get_active_company()
}
# Filter by company in search methods
def search_method(self, clause, context={}):
company_id = get_active_company()
# Include company filter in domain
return [["company_id", "=", company_id]]
Next Steps¶
- Learn about Layouts to create UI for your models
- Explore the API Reference for detailed method documentation
- Try the Quick Start Tutorial to build a complete module