Developers

This page documents several things OpenERP developers will find that are different in Tryton. Many architecture and designs behind both projects is very similar: heavily web services based, record management and loading with XML files, XML views rendered by the client or inheritance, just to name a few. At the same time there is a good number of things that have evolved a lot in Tryton, some are more obvious and others is good to have them listed here because are more subtle changes.

They are listed in no specific order.

1   Pythonic Syntax

One of the first things you realize when starting to code in Tryton is how model fields are defined. Tryton simply declares fields like class properties instead of adding keys in a dictionary.

The name of the model is defined by __name__ instead of _name and what in OpenERP you define with _description you just write it as the first line of class' __doc__. Further lines will not be part of the model's description.

For a simple example of that syntax see the following code from party/party.py:

class Party(ModelSQL, ModelView):
    "Party"
    __name__ = 'party.party'

    name = fields.Char('Name', required=True, select=True, states=STATES,
        depends=DEPENDS)

2   Active Record

Since version 2.6, Tryton supports an Active Record pattern for module development. This has reduced the number of lines of code of the project although it makes it a little more difficult to write new modules. The reason is that Tryton still supports operations on sets of records (you can write a value to several records at once, for example), but by definition Active Record works in a single record.

So basically you will need to decide weather the function you need to write involves a single record (instance method) or several of them (class method). Or even if no record is involved (static method). This is a simplification and the documentation is more clear, but hopefully you get the idea.

Here are some example taken from party/party.py:

class Party(ModelSQL, ModelView):
    "Party"
    __name__ = 'party.party'

    full_name = fields.Function(fields.Char('Full Name'), 'get_full_name')
    active = fields.Boolean('Active', select=True)

    @staticmethod
    def default_active():
        return True

    @classmethod
    def write(cls, parties, vals):
        if vals.get('code'):
            vals = vals.copy()
            vals['code_length'] = len(vals['code'])
        super(Party, cls).write(parties, vals)

    def get_full_name(self, name):
        return self.name

3   Workflows

Another important change and in this case a huge simplification is the definition of workflows. OpenERP has a complex and cumbersome framework for workflows and unfortunately it does not yet even support passing the context which is something fundamental. It is also slow and makes debugging a real pain.

Tryton removed that workflow and simplified it to a whole deal. Many will probably think it is too simple but it really does what it is needed:

class Move(Workflow, ModelSQL, ModelView):
    @classmethod
    def __setup__(cls):
        super(Move, cls).__setup__()
        cls._transitions |= set((
                ('draft', 'assigned'),
                ('draft', 'done'),
                ('draft', 'cancel'),
                ('assigned', 'draft'),
                ('assigned', 'done'),
                ('assigned', 'cancel'),
                ))
        cls._buttons.update({
                'cancel': {
                    'invisible': ~Eval('state').in_(['draft', 'assigned']),
                    },
                'draft': {
                    'invisible': ~Eval('state').in_(['assigned']),
                    },
                'assign': {
                    'invisible': ~Eval('state').in_(['assigned']),
                    },
                'do': {
                    'invisible': ~Eval('state').in_(['draft', 'assigned']),
                    },
                })

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, moves):
        pass

Note

As you can see, transitions between states are managed with cls._transitions indicating the valid transitions between values of the state field. Then there is the @Workflow.transition() decorator which simply updates the value of the state field. Note that the decorator @ModelView.button provides the management of permissions.

This is extremely simple, more performant and doesn't have the inconsistency problems one might get with OpenERP workflows.

Why would I say that previous (and existing OpenERP) framework was (is) not the right tool? In my opinion that framework tries to implement a Workflow engine as conceived by a BPM and the proof that it doesn't work is:

  • OpenERP later added a process view which shows that the workflow didn't solve the process representation.
  • It does not manage tasks. One of the main features of a BPM, apart from the proper process definition is its ability to organize tasks and let users assign those to themselves or others, do the work and continue. Those, then trigger new actions, etc. But there is no central point for managing those tasks in OpenERP.
  • The workflow is document based but a real Workflow engine is a tool that works inter-document. It models real world processes. The OpenERP engine tries to do that with subflows but it is really very complex and still a workflow is tight to a single model.
  • It is very tight to the way the ERP needs to work. It has no sense to use a BPM approach for dependencies of information between documents that are very tight. This creates the false illusion that the workflow can be changed but in reality in most cases the ERP will misbehave if the workflow is broken. Real workflow systems make the information flow through the workflow edges and there is usually no dependency between the nodes or processes except by the information which is received and sent by the workflow engine.
  • It is not used for stock moves. Stock moves do have a workflow (you cannot cancel a move in Done state, for example) but the framework is not used because it is too slow. Tryton's workflow will be used in stock moves from version 2.8.

4   False vs None

Tryton no longer uses False to refer to NULL values. None is NULL in Tryton.

OpenERP uses False instead of None due to historical reasons: XML-RPC by default does not support None and so False was used instead. Also, all over the code, OpenERP uses tests like "if value:" in order to find out if the field is empty, which makes it impossible to distinguish between float and integer fields where its value is zero and where it is NULL. In fact, setting a numeric field as NOT NULL has no sense in OpenERP and you cannot predict whether zero will be 0 or False in some circumstances.

On the other hand Tryton does distinguish between zero and NULL and you should use "if value is None:" to check for emptiness.

5   PYSON

From the docs, PYSON is a lightweight domain-specific language for general representation of statements and it is a deterministic algorithm which will always succeed to evaluate statements.

PYSON is a small language used in field domain definitions, or when a button should be visible only in certain circumstances, for example. What is particularly interesting about PYSON is:

  • It has a clear syntax and available expressions. In OpenERP, it is not clearly stated anywhere what are the available expressions to be used in a domain, for example.
  • It is easy to implement in other languages. For example, it has been implemented in JavaScript in the new web client. This means that those expressions are properly evaluated on the client side.
  • Tryton can evaluate them on client but also on the server side.

Take a look at this example from account_statement_import module:

class Statement(Workflow, ModelSQL, ModelView):
    _name = 'account.statement'
    imported_lines = fields.One2Many('account.statement.imported.line',
        'statement', 'Imported Lines')
    imported_attachment = fields.Many2One('ir.attachment',
        'Imported Attachment')

    def __init__(self):
        super(Statement, self).__init__()
        self._buttons.update({
                'import_attachment': {
                    'invisible': ~Bool(Eval('imported_attachment')),
                    },
                'update_statement_lines': {
                    'invisible': ~Bool(Eval('imported_lines')),
                    },
                })

As you can see, the field imported_attachment is a Many2One and imported_lines a One2Many and both can be used to indicate that if those are empty a button should not be visible.

6   Domains

OpenERP and Tryton Domain Expressions are very similar but there are some differences:

  • Tryton allows as many dots as you may need. For example, the following expression in OpenERP would not be valid: [('sale.address.country.code', '=', 'US')]
  • Tryton uses OR and AND instead of & and |, although as in OpenERP, AND is implicit.

7   Coding style

Tryton follows a strict PEP8 coding style, this includes:

  • Imports of standard libraries should go first
  • Between classes there must be two empty lines
  • Between functions there must be one empty line

Not defined in PEP8:

  • Brackets are closed at the same level of the last line
  • Each extra brackets adds one tab (if also adds a tab). Example:
d = {
    'key': 'value,
    }

dd = [{
        'key': value,
        }]

if (a == b
        and c == d):
    pass
  • We usually avoid backslashes. Example:
# wrong
if 'A large line does not fit' == \
    'Another text':
    pass

# right
if ('A large line does not fit' ==
    'Another text'):
    pass
  • In if clauses we put and and or in the line below. Example:
if ('This text is very large' == 'This text is very large'
        and 'another check' == 'another check'):
    pass
  • Pool and Pool classes are defined at the very beginning of the function by convention:
def function(self, value):
    pool = Pool()
    Party = pool.get('party.party')
    Product = pool.get('product.product')

    if not value:
        return

    # other stuff here

8   How does Transaction() work?

In order to avoid the cr, uid, ..., context stuff, Tryton uses a smart solution. The with statement available since Python 2.5 allows developers to create a variable that will be available to all the code within the statement and any function that is called from within.

http://preshing.com/20110920/the-python-with-statement-by-example http://effbot.org/zone/python-with-statement.htm

9   Creating libraries

Encapsulation is a key programming pattern but reusability is also very important. That's why in Tryton we try to create python libraries when possible instead of Tryton modules. You should think if part the functionality you're going to develop can be split and put the most generic stuff in a python library so other developers can take advantage of it in other pages.

Some of the libraries that have already emerged from Tryton needs include:

  • vatnumber
  • relatorio
  • webdav
  • bankaccount
  • retrofix

10   Using in IDS

Because of the way both Tryton and OpenERP work, it is relatively usual for the ORM to end up building queries using SQL IN statement (such as in (12, 13, 14, 15, 42)). In large tables one may end up with a query with a huge number of ids and that is very inefficient at PostgreSQL level.

Fortunately, newer versions PostgreSQL versions have improved a big deal. For example, only from upgrading from 8.4 to 9.1 in an installation we saw an ERP an ERP operation to go down by a couple of orders of magnitude.

Tryton, however, tries to be somewhat smarter in some circumstances and has a nice reduce_ids() function in tools/misc.py which converts a large list of ids into a PostgreSQL friendly expression (IE: ((field >= 4) AND (field <= 20)) OR ((field >= 65) AND (field <= 105)) OR (field IN (203, 215, 501))).

11   Views

In Tryton, view inheritance is processed in module dependency order and you must indicate the original view instead of the one of the module you inherit. For example, if module A creates a view, module B extends (inherits) that view and module C extends the same view, the view definition of C should not point to the view in B but the one in A.

Usually views in modules B and C will have the same id of the view in A (only that the system will prepend the module name and a dot in front of it). That is not a requirement but by convention and avoids you to having to think of a view id.

Since 2.6, it is possible to store the View itself in a separate XML file and not upload it to the database. This makes debugging and development easier because the view is read from the file when needed so you don't need to reload the module.

12   Module auto-reloading

Tryton supports module auto reloading by setting auto_reload = True in trytond.conf. This means that just after you modify a .py file the server reloads it and those changes take effect immediately.

This is very useful during development because you avoid restarting the server on each and every code change. Note that the module will not be updated so if you add a new field you will have to update the module manually.

13   Security

13.1   Buttons

In Tryton, buttons are simpler to use and have access permissions. So you don't only have access rules for reading, writing, creating and removing records, and reading and writing fields but also for the actions of the model.

This action (or button) may be part of a workflow or and individual action and permissions will be checked either if the action is called directly from the web service or called from within another function because it is implemented using a decorator.

Also, the client will disable the button in the interface if the user has no rights for execute the action.

14   Data Consistency

Restrictions such as the ones added in field domains are validated on the client but also on the server. This makes data much more consistent because:

  • You are guaranteed that information is correct even if it was added using web services. For example, because it is connected to an online shop.
  • Information is correct even if there are client errors or corner cases, which is more probable when there are several clients: desktop, web, android...
  • You are protected from your own programming mistakes. Those restrictions end up as an extension of SQL constraints. There is no doubt that SQL constraints are important for data consistency so the same applies to those restrictions defined in python or PYSON.

15   Historize

Tryton has a unique feature called historization. A model with history enabled will automatically copy a record each time is modified and kept for later usage. Modules such as account_invoice_history use this functionality in order to keep invoices with the information the party had at the moment the invoice was created and thus fulfilling legal requirements that invoices must be kept intact even if a customer has changed his address the next day after creating the invoice.

Activating this feature in Parties is as simple as this:

class Party:
    __name__ = 'party.party'
    _history = True

Although, later some changes need to be done in related documents if those should point to the old version of partner instead of the current one.

16   Fields

16.2   One2One, One2Many & Many2Many

  • Unlike OpenERP, in Tryton One2One is not deprecated but fully supported.
  • Both One2One & Many2Many use a relation table which must be declared explicitly. See the following example:
class ActionGroup(ModelSQL):
    "Action - Group"
    __name__ = 'ir.action-res.group'
    action = fields.Many2One('ir.action', 'Action', ondelete='CASCADE',
        select=True, required=True)
    group = fields.Many2One('res.group', 'Group', ondelete='CASCADE',
        select=True, required=True)
  • Both One2Many and Many2Many fields in Tryton accept the order parameter which override the order set in the target model. Take a look here for more information.
  • Also both 2Many fields accept a size parameter with a PYSON expression denoting the maximum number of records allowed in the relation.
  • The values for managing 2Many fields in OpenERP (0, 1, 2, 3, 4, 5 & 6) are replaced by more clear expressions: create, write, delete, delete_all, unlink, add, unlink_all & set:
move, = Move.create([{
            'journal': journal.id,
            'period': period_id,
            'date': date,
            'lines': [
                ('create', [{
                        'account': reconcile_account.id,
                        'debit': (amount < Decimal('0.0')
                        and - amount or Decimal('0.0')),
                        'credit': (amount > Decimal('0.0')
                        and amount or Decimal('0.0')),
                        }, {
                        'account': account.id,
                        'debit': (amount > Decimal('0.0')
                        and amount or Decimal('0.0')),
                        'credit': (amount < Decimal('0.0')
                        and - amount or Decimal('0.0')),
                        }]),
                ],
            }])

16.3   On Change

One Tryton improvement that OpenERP developers certainly miss is the proper inheritance of on_change calls. Let's see the improvements of this feature in Tryton:

  • They are not defined on the view but on the field
  • They are extensible so if a module needs to take more fields from the view it just needs to extend the list of required fields.
  • Supports on_change and on_change_with
  • In some cases the same on_change_with can also be used as function to implement a calculated field.
  • It can update all fields including Many2Many and One2Many fields. It can insert, update or remove any number of records on those widgets.

16.4   Depends

Depends is one of those features that make you realize that the system is well thought. Take this example taken from stock move:

uom = fields.Many2One("product.uom", "Uom", required=True, states=STATES,
    domain=[
        ('category', '=', Eval('product_uom_category')),
        ],
    on_change=['product', 'currency', 'uom', 'company', 'from_location',
        'to_location'],
    depends=['state', 'product_uom_category'])

As you can see, in order for the client to know which units of measure the user can choose, the field product_uom_category is needed. But what happens if the uom field is added in a view which does not have the product_uom_category field?

The answer is that if depends is properly defined, the client will have automatically picked product_uom_category as if it was in the view definition. The nicest thing, though, is that there is a standard test named test_depends() which will check if all the necessary fields are in the depends attribute.

Note

B2CK guys initially made the depends attribute to be calculated automatically but that made the server to take a long time to start. So it was decided to leave it with the current design.

16.5   rec_name

In OpenERP, there is the name_get() and name_search() functions. Those are replaced by the rec_name field in Tryton, which is of type fields.Function. The default definition uses get_rec_name() and search_rec_name() functions which are the ones you will want to override.

The fact that it is treated as a field makes it very clean and it is quite handy in several cases such as when an exception needs to be risen and you want to provide useful information to the user.

16.6   Dict Fields

Since version 2.8, Tryton (will support) supports Dict fields. This fields are treated as Dict objects in Tryton and stored as a JSON string in the database. They are useful for storing a large number of attributes without degrading performance.

The typical scenario for this kind of fields is the product form for companies hosting an online shop. Online shops usually host a large amount of information about the product where typically the attributes depend on the product category. For example, TVs will have resolution & inches, whereas Fridges will have Energy Class & height.

Take a look at this code review for an example adding product attributes depending on product template.

17   Triggers

Tryton supports triggers. That is, any function of any model can be called in the following circumstances:

  • On record creation (create)
  • On record removal (delete)
  • On record update (write)
  • On time intervals (5 by default)

A trigger can be configured from the user interface and users can provide a python expression to determine if the function must be executed or not, and can also configure the maximum number of records for which the trigger will be executed and the minimum delay between calls.

OpenERP has a similar feature but it requires a workflow to be configured on the model and can only be triggered when the workflow changes. This feature was used by PowerEmail, for example, but for models with no workflow (such as CRM Cases) it didn't work.

18   Regression Tests

Tests is yet another great thing about Tryton because they are so easy to use. Here's a quick list of what you can expect from them:

  • Use standard unittest library
  • Supports scenario testing using doctest and proteus
  • You can easily check that:
    • The module and its dependencies install properly
    • All views work: fields exists and inheritance is OK
    • All fields have depends attribute properly set
  • The test suit can be executed from the command line and using the SQLite backend. No need to setup or create a PostgreSQL server and database.
  • You can reuse data from other modules in order to avoid creating records (such as company definitions, parties, etc) only to check the new functionality of your module.

19   Database Independence

Although the primary development database for Tryton is PostgreSQL and it is also the recommended RDBMS, Tryton supports both SQLite and MySQL too. As previously exposed, SQLite is great for running tests because it is very fast and can also be useful in some circumstances where you do not need a larger RDBMS. MySQL on the other hand, works but has issues with Decimal fields because MySQL has no such field in the database and the most you can use is floats which is not very appropriate for accounting stuff. In fact, Tryton tests check for that consistency and MySQL fails on them.

© Copyright 2013 NaN·tic & Zikzakmedia. The content of this site is subject to the Creative Commons Attribution-ShareAlike 3.0 Unported License.
OpenERP is a registered trademark of OpenERP S.A.
Tryton is a registered trademark of the Tryton Foundation.