Skip to content

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

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

"total_amount": fields.Decimal("Total", function="get_total", store=True)

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

_defaults = {
    "state": "draft",
    "active": True,
    "date": "2023-01-01"
}

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=True for related computed fields
  • Add store=True for frequently accessed function fields
  • Use index=True for 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