Point Of Sale development in Odoo v12

This document will describe how to develop new features in the POS module. It will use Odoo v12 as an example, but everything should be applicable to all versions before v14.

The POS is very different from the rest of Odoo because it had to work offline.

1 Backbone

The POS uses a subset of backbone.js to handle user interface updates and persistence. Let's go over the concepts of backbone.js that are relevant in Odoo.

1.1 Models

The POS defines the following backbone models:

Sorry, your browser does not support SVG.

The arrows represent ownership, e.g. Order owns many Orderlines. Most of these have references back to Posmodel, but they have not been drawn to keep the diagram simple.

Wrapped in collection

1.2 Backbone Events

Backbone is used to monitor changes in datamodels. This is accomplished using the on function (sometimes the POS uses its old name: bind). This function allows us to specify a callback that's executed whenever a backbone model changes. Backbone tracks attributes on models that are created/modified using set. Regular JS properties are not tracked. on also accepts Backbone.Collection objects as described above. Here's an abbreviated example from the Order model:

exports.Order = Backbone.Model.extend({
  initialize: function(attributes,options){
    Backbone.Model.prototype.initialize.apply(this, arguments)
    ...
    this.set({ client: null });
    ...
    this.on('change', function(){ this.save_to_db("order:change"); }, this);
    this.orderlines.on('change',   function(){ this.save_to_db("orderline:change"); }, this);
    this.orderlines.on('add',      function(){ this.save_to_db("orderline:add"); }, this);
    this.orderlines.on('remove',   function(){ this.save_to_db("orderline:remove"); }, this);
    this.paymentlines.on('change', function(){ this.save_to_db("paymentline:change"); }, this);
    this.paymentlines.on('add',    function(){ this.save_to_db("paymentline:add"); }, this);
    this.paymentlines.on('remove', function(){ this.save_to_db("paymentline:rem"); }, this);
    ...
  }
}

This will call the the specified function when the client attribute and the orderlines or paymentlines collection changes. Backbone will emit various events but in Odoo we mainly use change for attributes on the object itself and add and remove for collections.

These Backbone events drive two POS features: UI updates and persistence.

1.2.1 Updating the UI

The UI has to reflect changes that happened to the datamodels. Let's look at a specific example and following flow of events. The diagram shows what happens after a user clicks on a product, adding it to the current order. Because this happens through events visualizing stack traces doesn't really work. The diagram shows the code triggering the event and the code responding to the event. Each event is ordered with a number.

Sorry, your browser does not support SVG.

Note that triggering an event doesn't automatically cause the UI to update. It only happens after the current message in the JS runtime message queue is fully processed. Practically this means that triggering an event multiple times in response to the same click will result in only a single rerender, for more info read up on the JS event loop.

manual triggers

1.2.2 Persistence

As shown in 1.2 changes to orders, orderlines and paymentlines all trigger save_to_db. This calls save_unpaid_order which is part of the point_of_sale.DB module.

This module is responsible for persistent storage in the POS. It uses load() and save() to store data like orders in localStorage. It uses JSON.stringify() to serialize and JSON.parse() to deserialize data it saves to localStorage.

Sorry, your browser does not support SVG.

A common mistake is to forget adding new properties to init_from_JSON and export_from_JSON. When possible the properties should be added as Backbone properties (using set). If they are added as regular JS properties then manual changes need to be triggered every time the property changes. A good way to test if properties are saved properly is to see if they survive a reload (F5). An example showcasing this is the note property on Orderline added in pos_restaurant. Since it's a regular JS property changes are manually triggered.

2 Offline

The POS is meant to work offline. It loads all it's data when the POS opens, after this it's capable of working without a connection to the Odoo server. When orders cannot be synced to Odoo the POS will retry later.

If a new feature requires new fields to be available in the POS use the load_fields function in point_of_sale.models. If you require the POS to load a new model use load_models instead.

3 Adding new features

I highly recommend using an existing working module as a template. I recommend using the pos_discount module. I won't describe every single detail but when you start from a working module you should be fine.

3.1 JS modules

You should structure your code in modules. Here's how the point_of_sale.models module is defined:

odoo.define('point_of_sale.models', function (require) {
  var exports = {};
  exports.PosModel = Backbone.Model.extend({...
  exports.load_fields = function(model_name, fields) {...
  exports.load_models = function(models,options) {...
  exports.Product = Backbone.Model.extend({...
  exports.Orderline = Backbone.Model.extend({...
  exports.Packlotline = Backbone.Model.extend({...
  exports.Paymentline = Backbone.Model.extend({...
  exports.Order = Backbone.Model.extend({...
  exports.NumpadState = Backbone.Model.extend({...
  
  return exports;
});

The passed in require function can be used to require other modules. It will take care of dependency resolution automatically. What's exported from a module can be accessed via the return value of require. Here's an example of a new module using the exported load_fields function to load a new field on res.partner:

odoo.define('pos_example.models', function (require) {
  'use strict';
  var models = require('point_of_sale.models');

  models.load_fields('res.partner, 'new_field_name');
}

3.2 Adding buttons

3.3 Supers

There's two main ways you can call the method you're inheriting (super(MyClass, self).my_method() in Python).

3.3.1 Backbone model

If the model you're inheriting is a Backbone model (e.g. Order in point_of_sale.models) then you need to explicitly reference the parent function in some way. Usually this is done by referencing the original prototype, here's an example extending Order in pos_restaurant:

var _super_order = models.Order.prototype;
models.Order = models.Order.extend({
    initialize: function() {
        _super_order.initialize.apply(this,arguments);

3.3.2 Odoo JS model (core.Class or web.Widget)

These behave as regular Odoo JS classes and can be extended as described in the official documentation. In the POS you usually modify them using include and the original definition can be called with _super. Here's another example from pos_restaurant:

screens.OrderWidget.include({
    update_summary: function(){
        this._super();
        if (this.getParent().action_buttons &&
            this.getParent().action_buttons.guests) {
            this.getParent().action_buttons.guests.renderElement();
        }
    },
});

3.4 XML add JS

3.5 JS QWeb

export_as_json <-> init_from_json backbone (+ what backbone models there are in js) loading models/fields js modules files -> use PosBaseWidget db/persistence two supers popups? define_action_button export to json customization example

Emacs 26.1 (Org mode 9.1.9)

Published on May 14 2020