Files
tryton/doc/models.rst
T
Cédric Krier 65d0a9494b Update model documentation.
Use autodoc in sphinx
Add missing and fix docstring
2009-03-01 13:37:31 +01:00

483 lines
12 KiB
ReStructuredText

:tocdepth: 2
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:
.. highlight:: python
::
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:
Model
*****
.. _trytond.model.Model:
.. autoclass:: trytond.model.Model
:members:
:undoc-members:
.. _trytond.model.ModelView:
.. autoclass:: trytond.model.ModelView
:members:
:undoc-members:
.. _trytond.model.ModelStorage:
.. autoclass:: trytond.model.ModelStorage
:members:
:undoc-members:
.. _trytond.model.ModelSQL:
.. autoclass:: trytond.model.ModelSQL
:members:
:undoc-members:
.. _search_clause:
Search clauses
^^^^^^^^^^^^^^
Simple clause are a list of condition, with an implicit ``AND``
operator:
.. highlight:: python
::
[('name', '=', 'Bob'),('age','>=', 20)]
More complex clause can be made this way:
.. highlight:: python
::
[ '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.):
.. highlight:: sql
::
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')
.. _define_inheritance:
Models Inheritance
^^^^^^^^^^^^^^^^^^
Model Inheritance allow add or override fields, methods and
constraints on existing models. To inherit an existing model (like
``Party`` on the first example), one just need to instantiate a class
with the same ``_name``:
.. highlight:: python
::
class Car(OSV):
_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(OSV):
_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 party cannot use the same car!'),
]
Party()
This show how to define a new model and link an existing one to it.
This is also a way to define 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.
Fields
******
Fields are class attributes which a name that can not start with an underscore.
.. _trytond.model.fields.Boolean:
.. autoclass:: trytond.model.fields.Boolean
:members:
:undoc-members:
.. _trytond.model.fields.Integer:
.. autoclass:: trytond.model.fields.Integer
:members:
:undoc-members:
.. _trytond.model.fields.BigInteger:
.. autoclass:: trytond.model.fields.BigInteger
:members:
:undoc-members:
.. _trytond.model.fields.Char:
.. autoclass:: trytond.model.fields.Char
:members:
:undoc-members:
.. _trytond.model.fields.Sha:
.. autoclass:: trytond.model.fields.Sha
:members:
:undoc-members:
.. _trytond.model.fields.Text:
.. autoclass:: trytond.model.fields.Text
:members:
:undoc-members:
.. _trytond.model.fields.Float:
.. autoclass:: trytond.model.fields.Float
:members:
:undoc-members:
.. _trytond.model.fields.Numeric:
.. autoclass:: trytond.model.fields.Numeric
:members:
:undoc-members:
.. _trytond.model.fields.Date:
.. autoclass:: trytond.model.fields.Date
:members:
:undoc-members:
.. _trytond.model.fields.DateTime:
.. autoclass:: trytond.model.fields.DateTime
:members:
:undoc-members:
.. _trytond.model.fields.Time:
.. autoclass:: trytond.model.fields.Time
:members:
:undoc-members:
.. _trytond.model.fields.Binary:
.. autoclass:: trytond.model.fields.Binary
:members:
:undoc-members:
.. _trytond.model.fields.Selection:
.. autoclass:: trytond.model.fields.Selection
:members:
:undoc-members:
.. _trytond.model.fields.Reference:
.. autoclass:: trytond.model.fields.Reference
:members:
:undoc-members:
.. _trytond.model.fields.Many2One:
.. autoclass:: trytond.model.fields.Many2One
:members:
:undoc-members:
.. _trytond.model.fields.One2Many:
.. autoclass:: trytond.model.fields.One2Many
:members:
:undoc-members:
.. _trytond.model.fields.Many2Many:
.. autoclass:: trytond.model.fields.Many2Many
:members:
:undoc-members:
.. _trytond.model.fields.Function:
.. autoclass:: trytond.model.fields.Function
:members:
:undoc-members:
.. _trytond.model.fields.Property:
.. autoclass:: trytond.model.fields.Property
:members:
:undoc-members:
.. _use_function:
How to use Function fields
^^^^^^^^^^^^^^^^^^^^^^^^^^
Let's say that the following field is defined on the invoice model:
.. highlight:: python
::
total = fields.Function('get_total', type='float', string='Total')
The ``get_total`` method should look like this:
.. highlight:: python
::
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*:
.. highlight:: python
::
total_service = fields.Function('get_total', type='float', string='Total Service')
For this field the method ``get_total`` can be defined this way:
.. highlight:: python
::
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:
.. highlight:: python
::
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:
.. highlight:: python
::
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:
.. highlight:: python
::
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:
.. highlight:: python
::
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.