Model

Model is python classes that usually represents business logical or concepts. Such classes consist essentially of keywords, fields, constraints and helper functions. There is different kind of Model for different purpose:

  • ModelView: Define Model with views.
  • ModelStorage: Define Model with storage capability.
  • ModelSQL: Define ModelStorage with SQL database for storage backend.
  • ModelWorkflow: Define Model with workflow.

The following snippet gives a first idea of what can be done:

from trytond.model import ModelView, ModelSQL, fields

class Category(ModelSQL, ModelView):
    "Party category"
    _name = 'relationship.category'
    _description = __doc__
    name = fields.Char('Name')
Category()

class Country(ModelSQL, ModelView):
    "Country"
    _name = 'relationship.country'
    _description = __doc__
    name = fields.Char('Name')
Category()

class Address(ModelSQL, ModelView):
    "Address"
    _name = 'relationship.address'
    _description = __doc__
    name = fields.Char('Contact Name')
    party = fields.Many2One('relationship.party', 'Party')
    country = fields.Many2One('relationship.country', 'Country')
    city = fields.Char('City')
Address()

class Party(ModelSQL, ModelView):
    "Party"
    _description = __doc__
    _name = "relationship.party"
    name = fields.Char('Name')
    addresses = fields.One2Many('relationship.address', 'party', 'Addresses')
    categories = fields.Many2Many('relationship.category', 'relationship_category_rel',
                                  'party', 'category', 'Categories')
Party()

Instantiating the class register the Model class in the framework. Later they will be instanciate once per database and stored in the Pool. Model instances are essentially accessors to records.

Model properties defines meta-informations about the model, they are class attributes starting with and underscore. Some Model Properties are instance attributes which allow to update them at other places in the framework.

Model

Search clauses

Simple clause are a list of condition, with an implicit AND operator:

[('name', '=', 'Bob'),('age','>=', 20)]

More complex clause can be made this way:

[ 'OR', [('name', '=', 'Bob'),('city','in', ['Brussels', 'Paris'])],
        [('name', '=', 'Charlie'),('country.name','=', 'Belgium')],
]

Where country is a Many2One field on the current field. The number dots in the left hand side of a condition is not limited, but the underlying relation must be a Many2One.

Which if used in a search call on the Address model will result in something similar to the following sql code (the actual sql query will be more complex since it has to take care of the access rights of the user.):

SELECT relationship_address.id FROM relationship_address
JOIN relationship_country ON
     (relationship_address.country = relationship_country.id)
WHERE (relationship_address.name = 'Bob' AND
       relationship_address.city in ('Brussels', 'Paris'))
      OR
      (relationship_address.name = 'Charlie' AND
       relationship_country.name  = 'Belgium')

Models Inheritance

Model Inheritance provides the ability to add or override fields, methods and constraints on existing models.

Adding fields to an existing model

To inherit an existing model (like Party on the first example), one just needs to instantiate a class with the same _name attribute:

class Car(ModelSQL, ModelView):
    _name = "vehicle.car"
    _rec_name = model
    model = fields.Char("Model", required=True)
    manufacturer = fields.Char("Manufacturer")
    first_owner = fields.Many2One('relationship.party', 'First Owner')
Car()

class Party(ModelSQL, ModelView):
    _name = "relationship.party"
    current_car = fields.Many2One('vehicle.car', 'Current car')

    def __init__(self):
        super(Party, self).__init__()
        self._sql_constraints += [
            ('party_car_uniq', 'UNIQUE(model)',
                'Two parties cannot use the same car!'),
        ]

Party()

This example shows how to define and relate a new model to an existing model. The example also demonstrates how to define a reflecting Many2One: It’s not possible to create the two models without using inheritance because each of the foreign key (first_owner and current_car) need the other model table.

Extending existing fields from an existing model

An existing field can be extended by calling copy.copy on it, modifying its attributes and then calling self._reset_columns.

import copy

class Line(ModelSQL, ModelView):
    _name = 'timesheet.line'

    #...

    def __init__(self):
        super(Line, self).__init__()
        self.employee = copy.copy(self.employee)
        if self.employee.on_change is None:
            self.employee.on_change = []
        if 'employee' not in self.employee.on_change:
            self.employee.on_change += ['employee']
            self._reset_columns()

In this example the extended model wants on_change_employee(...) to be called so it adds ‘employee’ to the on_change attribute of the employee field. Notice that only if the the field is modified then self._reset_columns is called. Also notice that a developer should try to make no assumptions about the field so that additional modules could also extend the same field.

Fields

Fields are class attributes which a name that can not start with an underscore.

How to use Function fields

Let’s say that the following field is defined on the invoice model:

total = fields.Function('get_total', type='float', string='Total')

The get_total method should look like this:

def get_total(self, cursor, user, ids, name, arg, context=None):
    res = {}.fromkeys(ids, 0.0)
    for invoice in self.browse(cursor, user, ids, context=context):
        for line in invoice:
            if invoice.id in res:
                res[invoice.id] += line.amount
            else:
                res[invoice.id] = line.amount
    return res

One should note that the dictionary res should map a value for each id in ids.

One method to rule them all

The first variant of this we’ll look at is defining a unique function for several fields. Let’s consider this new field which returns the total for the invoice lines of kind service:

total_service = fields.Function('get_total', type='float', string='Total Service')

For this field the method get_total can be defined this way:

def get_total(self, cursor, user, ids, name, arg, context=None):
    res = {}.fromkeys(ids, 0.0)
    for invoice in self.browse(cursor, user, ids, context=context):
        for line in invoice:
            if name == 'total_service' and line.kind != "service":
                continue
            if invoice.id in res:
                res[invoice.id] += line.amount
            else:
                res[invoice.id] = line.amount
    return res

Or even better:

def get_total(self, cursor, user, ids, names, arg, context=None):
    res = {'total': {}.fromkeys(ids, 0.0),
           'total_service': {}.fromkeys(ids, 0.0)}
    for invoice in self.browse(cursor, user, ids, context=context):
        for line in invoice:
            if invoice.id in res['total']:
                res['total'][invoice.id] += line.amount
            else:
                res['total'][invoice.id] = line.amount

            if line.kind != "service":
                continue
            if invoice.id in res['total_service']:
                res['total_service'][invoice.id] += line.amount
            else:
                res['total_service'][invoice.id] = line.amount
    return res

The framework is able to check if names (instead of name) is used in the method definition, hence adapting the way the method is called.

Another way to tackle Function implementation is to pass a dictionary to the args argument on the field definition. It will be forwarded to the function call:

state = fields.Function(
    'get_state', type='selection', string='Total Service',
    args={'key':'value'},
    selection=[('draft','Draft'),('done','Done')],
    )

def get_state(self, cursor, user, ids, names, arg, context=None):
    # [...]
    if arg.get('key'):
        pass # do something with 'value'

Search on Function fields

Another improvement would be to provide a search function. Indeed without it the user will not be able to search across invoices for a certain amount. If we forget about the total_service field, solution could be something like the following:

total = fields.Function('get_total', type='float', string='Total',
                        fnct_search='search_total')


def get_total(self, cursor, user, ids, name, arg, context=None):
    pass #<See first example>

def search_total(self, cursor, user, name, domain=[], context=None):
    # First fetch all the invoice ids
    invoice_ids = self.search(cursor, user, [], context=context)
    # Then collect total for each one, implicitly calling get_total:
    lines = []
    for invoice in self.browse(cursor, user, invoice_ids, context=context):
        lines.append({'invoice': invoice.id, 'total': invoice.total})

    res= [l['invoice'] for l in lines if self._eval_domain(l, domain)]

    return [('id', 'in', res)]

def _eval_domain(self, line, domain):
    # domain is something like: [('total', '<', 20), ('total', '>', 10)]
    res = True
    for field, operator, operand in domain:
        value = line.get(field)
        if value == None:
            return False
        if operator not in ("=", ">=", "<=", ">", "<", "!="):
            return False
        if operator == "=":
            operator= "=="
        res = res and (eval(str(value) + operator + str(operand)))
    return res

One should note that such an implementation would be very slow for a large number of invoices.

Write on Function fields

It’s also possible to allow the user to write on a function field:

name = fields.Function('get_name', type='char', string='Total',
                        fnct_inv='set_name')
hidden_name= fields.Char('Hidden')

def set_name(self, cursor, user, id, name, value, arg, context=None):
  self.write(cursor, user, id, {'hidden_name': value}, context=context)

def get_name(self, cursor, user, ids, name, arg, context=None):
  res = {}
  for party in self.browse(cursor, user, ids, context=context):
     res[party.id] = party.hidden_name or "unknown"
  return res

This simplistic (and inefficient) example is another way to handle a default value on the name field.