I. Introduction

WARNING
This is the english translation of the french tutorial I made.
My native language is french. I have some basic knowledge of english language, thus I translated the tutorial by myself and sometimes by the use of a famous translation service, so I hope the whole text is easily understandable.
Whatever, the french version prevails.

In case of misunderstanding, do not hesitate to contact me at OpenERP Official ForumContact me

Thank you for reading

OpenERP programming is a rather complicated task. It is necessary to know several programming languages ​​and you must also have a good idea of ​​how OpenERP does work.

The documentation is quite limited and very brief. There are very few forums or specialized blogs about OpenERP, and almost all are in English.

The few tutorials found on the Web are limited to specific problems.

They do not fully achieve a complex module, they just allow you to add a button or function somewhere. It's not enough.

The tutorial that I propose is the result of hundreds of hours of learning everything I could find in the official documentation and on the Web, as well as number of lines of code, of countless tests, bugs and other gaieties. You know what I mean...

This is due to a request from a customer that I had the mission to make a module that will allow the creation of cashiers for the Point Of Sale.

The context

OpenERP is installed on a Debian server. The client sells products through franchises. Each society can access to OpenERP, which is configured with Multi-Company feature.

Each company has a PC in its location that is connected to the Point of Sale. And each company has several cashiers as employees.

Customer demand
How could we make purchases with the Point Of Sale on the same computer (PC) without having to create a session each time a cashier wants to make a sale?

Point Of Sale actually allows to make sales with several seller. To do this, simply create users in the company, put them in the Point Of Sale/User Group and assign them a Point Of Sale and voila.

But each seller must then log in to make the sale.

These functions, however, native, did not suit the customer needs.

So I made a module that allows to create a different kind of users, the cashiers. You'll see later that this is not very complicated.

Where it gets complicated is the part of the module, (The Web module), which operates in the Point Of Sale, and which allows to open only one session in the morning, make sales by several cashiers without leaving the Point Of Sale, and thus prevent to close close the previous session to open a new one.

Finally, the last statement of the customer: the cashier is required. The sale must not be possible if the Point Of Sale has no cashiers.

Here is what it is.

I will show you the steps that will help you for the creation of your own modules.

The source code certainly needs some improvements and optimizations, do not hesitate to share your comments, it will also help me to move further into the intricacies of the OpenERP programming.

II. Requirements

To go with this tutorial, you need a good knowledge of programming languages ​​used in OpenERP :

  • Python ;
  • XML ;
  • JavaScript ;
  • Qweb/JQuery ;
  • Linux command line

III. Basics

To make it short, we can say that the making of a conventional module is done in three steps.:

  • The creation of the initialization file of the module.
  • The creation of the Python Object.
  • The creation of the XML view.

Regarding the Point Of Sale, it is a « Web » module. When you login to the Point Of Sale, OpenERP disapears and the Point Of Sale interface appears. You must then close the Point Of Sale to return to OpenERP.

This is a special case which will require additional programming steps. The POS is not integrated with OpenERP, so its entire Web interface will keep our attention.

IV. Necessary tools

Here are the softwares I use, feel free to use them or find similar softwares.

  • WinSCPWinSCP : SFTP/FTP client, very pleasant to use, and allows to easily navigate through the file tree.
  • PuTTYPuTTY : Telnet / SSH client that will run Linux commands on the server (WinSCP also allows, but with some limitations)
  • SublimeSublime : a very well made text editor. It is aptly named.
  • FirefoxFirefox with FirebugFirebug extension. Essential for JavaScript debugging.

OpenERP is installed on a Debian server (Dev one). I'm working on a PC running Windows 7 pro.

Configure WinSCP
We will configure WinSCP for it opens directly with your favorite code editor when you double-click a file.

  • Launch WinSCP.
  • Go to See menu, then Settings.
  • In the dialog form, click on Editor in the left column.
  • In the frame Editors preferences, click Add.
  • In the dialog box, clik External editor then select the exe file of your text editor.
  • In Use this editor for following files, select filter *.* (all files).
  • Validate.

In this tutorial, we assume that OpenERP is installed on a Linux/Debian server and that we are working on a Windows PC.
I have installed OpenERP Version 7.0-20130703-231023 (version therefore dated 03 July 2013).
If you are working directly on the server, you will adapt the guidelines relating to the server administration (files, rights, etc..). The construction of the module remains the same.

V. Stages of completion

Creation of OpenERP internal module

  • Creation of the module initialization file .
  • Creation of the file that contains Python Object
  • creation of the views (Form view, Tree view).
  • Creation of a menu for the POS/Manager users group
  • Creation of the access rights for the module
  • Creation of the recording rules
  • Creation of the icon

Creation of the Point Of Sale module

  • Creation of the JavaScript file that contains module's actions and functions
  • Creation of the GUI elements (drop-down list, labels, etc.) in a XML file
  • Creation of the style file *.css for the design of the elements

Module internationalization

  • Creation of the language template (*.pot)
  • Creation of the french tanslated file (*.po)

Warning :
In this tutorial, Point Of Sale may be called POS
Source code will be written in english, but texts and labels will be translated later
The module is called pos_cashier.

Once completed, the POS Cashiers Module will appear in the list of installed modules, like in the image below

The pos_cashier module installed
The pos_cashier module installed

Warning
The tutorial may seem very long, but I tried to dissect all stages of the implementation of a module and I also tried to explain as best as I could all the source code and the various files that compose the module

VI. Module structure

Here is the file-tree of the pos_cashier module

pos_cashier file tree
pos_cashier file tree

i18n directory :
Contains the internationalization files of the module.

security directory :
Contains the access control file and recording rules file.

static directory :
Contains the « Web » part of the module.

It contains the css directory that will host the stylesheet, the img directory that will host the icon of the module and the requiered pictures, the js directory that will host the JavaScript file and the xml directory that will host the views of the module.

Also , at the root of the module, there is the Python module and the XML views.

VII. Creation of a basic OpenERP module

First, we will achieve the module that will allow the creation of the cashiers within OpenERP. The Web part of the module (which is in POS) will be discussed later.

VII-A. Creation of the directories

The OpenERP modules are usually placed in the « addons » directory, but I recommend that you create a special directory (outside OpenERP) where you will place your own modules.

For the tutorial, we will create a modules-openerp directory in the /opt directory on your server.

Log in with WinSCP (as root) and connect to your dev server.
Navigate to the /opt directory and create a new folder. Name it modules-openerp.

Log in using PuTTY (from WinSCP) and enter the following commands:

Modify owner and rights
Sélectionnez
cd /opt [+Enter]
chown openerp:openerp ./modules-openerp [+Enter]
chmod 0755 ./modules-openerp [+Enter]

We assigned the /opt/modules-openerp directory to the user openerp and the group openerp, then we changed the rights.

For our future modules to be taken into account by OpenERP, we have to modify the server configuration file and add the path to our directory.

Modify openerp-server.conf file

  • In WinSCP, go to /etc.
  • Depending on the version, the openerp-server.conf file can be found in /etc or /etc /openerp.
  • Double-click the file to edit.
  • Change the line below by adding the full path to the modules directory that we have just created.
openerp-server.conf
Sélectionnez
addons_path = /opt/openerp/addons,/opt/openerp/server/openerp/addons,/opt/openerp/web/addons,/opt/modules-openerp

If this line does not exist, add it by putting only the path to your directory.
There may be many paths to directories. They must be separated by a comma.

Save and close the file.
Restart the server using the following command

Restart OpenERP
Sélectionnez
/etc/init.d/openerp-server restart [+Enter]

Make sure the server is started by opening a Web page with the corresponding URL

http://IP_OF_YOUR_SERVER:8069

Then create different directories within the modules-openerp directory

  • pos_cashier
    • i18n
    • security
    • static
      • src
        • css
        • img
        • js
        • xml

Then change the user, group and rights with the following command

Modify owner and rights
Sélectionnez
cd /opt [+Enter]
chown openerp:openerp ./modules-openerp -R [+Enter]
chmod 0755 ./modules-openerp -R [+Enter]

VII-B. Required Python files

There are three mandatory files when you create a module.

  • __init__.py
  • __openerp__.py
  • our_famous_module.py

VII-B-1. The __init__.py file

This is the file that will invite OpenERP to load our module.
The contents of this file is very simple:

__init__.py
Sélectionnez
import pos_cashier

Put the name of the module. It is also the name of the directory.

VII-B-2. The __openerp__.py file

 

This is the file that contains all the information about the module : name, version, category, description, files to load, etc.

__openerp__.py
Sélectionnez
# -*- coding: utf-8 -*-
{
    'name': 'POS Cashiers',
    'version': '1.0.0',
    'category': 'Point Of Sale',
    'sequence': 3,
    'author': 'Thierry Godin',
    'summary': 'Manage cashiers for Point Of Sale',
    'description': """
Manage several cashiers for each Point Of Sale
======================================

This could be handy in case of using the same POS at the same cash register while it is used by several cashiers.
Cashier's name is displayed on the payement receipt and on the order.

Cashiers are allowed to change the current cashier (by choosing their name in the drop-down list) and can make a sell without creating a new session.

Cashier's name is mandatory. You cannot perform a sell if no cashier has been created or is active in your POS.

The shop manager will know who made the sell.
    """,
    'depends': ["point_of_sale"],
    'data': [
        'security/pos_cashier_security.xml',
        'security/ir.model.access.csv',
        'cashier_view.xml',
        'order_cashier_view.xml',
    ],
    'js': [
        'static/src/js/pos_cashier.js',
    ],
    'css': [
        'static/src/css/pos_cashier.css',
    ],
    'qweb': [
        'static/src/xml/pos_cashier.xml',
    ],
    'installable': True,
    'application': False,
    'auto_install': False,
}

If you copy the code from this page
Beware of Python code indentation. This can be estimated in this article. This is due to the editor I used to write this article.
The source code of the module can be downloaded at the bottom of the page.

The file is to fill like this

 
Sélectionnez
'parameter': 'value',
'parameter': ['value1','value2','value3'],

The data are noted as key:value separated by a comma (end of line).
The value can also contain an array like in the case of depends, data, js parameters , etc..

Parameters :

  • name : the module name;
  • version : the module version;
  • category : the category in which you put the module;
  • sequence : This is a number that will show your module in the list of modules. 1, it will be up, 100 it will be down ;
  • author : author of the module ;
  • summary : a summary that explains what is your module. A very short text, it appears under the name of the module in the module list ;
  • description : the full description of the module ;
  • depends : modules that your module depends;
  • data : files to load ;
  • js : in case of a Web module like this one, the JavaScript files;
  • css : the stylesheet;
  • qweb : View of the Web Part (Template);
  • installable : if your module is installable or not;
  • application : set it to False. Your module will not be recognized as an application. It's OpenERP that issues certificates that qualify your module as an application;
  • auto_install : set it to False, we will install the module by hand. (With a button, anyway ...)

There are other parameters, but for this module we have enough like that,

The « description » parameter
To insert a text on multiple lines, you must enclose with three double quotes
(""" text here """)

Put the fullest possible description.
To return to the line, you actually have to jump one more, otherwise the new line will not be visible in OpenERP.

Underline text with the = sign will appear as the <h1></h1> web tag.

We created two files (which still bear the same names) for module initialization. But this is not enough!

As we stated in the __ init__.py file, OpenERP will attempt to load the pos_cashier module. We must now create the pos_cashier.py file (the module itself).

VII-B-3. The pos_cashier.py file

I put the contents of the entire file.

Do not worry, we'll dissect it all quietly.

pos_cashier.py
Sélectionnez
 # -*- coding: utf-8 -*-
##############################################################################
#    
# Module : pos_cashier
# Créé le : 2013-06-06 par Thierry Godin
#
# Module permettant la création de vendeurs pour les points de vente
#
##############################################################################
import openerp
from openerp import netsvc, tools, pooler
from openerp.osv import fields, osv
from openerp.tools.translate import _
import time

class pos_cashier(osv.osv):
    _name = 'pos.cashier'
    _order = 'cashier_name asc'

    _columns = {
        'pos_config_id' : fields.many2one('pos.config', 'Point Of Sale', required=True),
        'cashier_name': fields.char('Cashier', size=128, required=True),
        'active': fields.boolean('Active', help="If a cashier is not active, it will not be displayed in POS"),
    }

    _defaults = {
        'cashier_name' : '',
        'active' : True,
        'pos_config_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).pos_config.id,
    }

    _sql_constraints = [
        ('uniq_name', 'unique(cashier_name, pos_config_id)', "A cashier already exists with this name in this Point Of sale. Cashier's name must be unique!"),
    ]


class inherit_pos_order_for_cashiers(osv.osv):
    _name='pos.order'
    _inherit='pos.order'
                   
    def create_from_ui(self, cr, uid, orders, context=None):
        #_logger.info("orders: %r", orders)
        order_ids = []
        for tmp_order in orders:
            order = tmp_order['data']
            order_id = self.create(cr, uid, {
                'name': order['name'],
                'user_id': order['user_id'] or False,
                'session_id': order['pos_session_id'],
                'lines': order['lines'],
                'pos_reference':order['name'],
                'cashier_name': order['cashier_name']
            }, context)

            for payments in order['statement_ids']:
                payment = payments[2]
                self.add_payment(cr, uid, order_id, {
                    'amount': payment['amount'] or 0.0,
                    'payment_date': payment['name'],
                    'statement_id': payment['statement_id'],
                    'payment_name': payment.get('note', False),
                    'journal': payment['journal_id']
                }, context=context)

            if order['amount_return']:
                session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
                cash_journal = session.cash_journal_id
                cash_statement = False
                if not cash_journal:
                    cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids)
                    if not len(cash_journal_ids):
                        raise osv.except_osv( _('error!'),
                            _("No cash statement found for this session. Unable to record returned cash."))
                    cash_journal = cash_journal_ids[0].journal_id
                self.add_payment(cr, uid, order_id, {
                    'amount': -order['amount_return'],
                    'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
                    'payment_name': _('return'),
                    'journal': cash_journal.id,
                }, context=context)
            order_ids.append(order_id)
            wf_service = netsvc.LocalService("workflow")
            wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
        return order_ids

    _columns = {
        'cashier_name': fields.char('Cashier', size=128),
    }


inherit_pos_order_for_cashiers()

At the very beginning of the file
We will import the libraries we need for the module.

 
Sélectionnez
import openerp
from openerp import netsvc, tools, pooler
from openerp.osv import fields, osv
from openerp.tools.translate import _
import time 

This is necessary because we will use native OpenERP and Python functions.

Then we create the Python object

Object is stated like this
Sélectionnez
 class pos_cashier(osv.osv): 

From now on, we must be very careful with the code indentation. You will notice that there is no signal of the end of the object.
This is why we must pay attention to the code editor we use, it must be able to handle Python for apropriate indentation.
If in doubt, do not hesitate to read this page : Image non disponibleFAQ PythonFAQ Python and this one : Image non disponibleCours PythonCours Python.

VII-B-3-a. Statements

_name : is the name of the table in OpenERP database. In fact, the table is really named « pos_cashier » in the database.

_name
Sélectionnez
 _name = 'pos.cashier' 

_order : as you may understand , this is like SQL statement « ORDER BY ». Here, cashiers will be sorted by their names

_order
Sélectionnez
 _order = 'cashier_name asc' 

_columns : these are the fields that will be created in the pos_cashier table.

_columns
Sélectionnez
 _columns = {
'pos_config_id' : fields.many2one('pos.config', 'Point Of Sale', required=True),
'cashier_name': fields.char('Cashier', size=128, required=True),
'active': fields.boolean('Active', help="If a cashier is not active, it will not be displayed in POS"),
} 

pos_config_id field
Here we record the ID of the Point of Sale of the user. This field relationship with the pos_config table wich contains settings of each Point Of Sale.

cashier_name field
It is in this field that will record the name of the cashier.

active field
This field will allow us to enable or disable a cashier (when on leave due to illness or travel, for example).

In OpenERP, the active field is a special field. When active = False, the record is automatically not visible.

_defaults : these are the default values ​​for records .

_columns
Sélectionnez
 _defaults = {
'cashier_name' : '',
'active' : True,
'pos_config_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).pos_config.id,
} 

By default, the active check box in the form will be checked and we automatically retrieve the ID of the Point of Sale of the user. That means, that when we create a new cashier, the Point of Sale of the user will be selected in (pos_config_id) drop-down list in the form. The name field (cashier_name) will be empty.

_sql_constraints : that are record rules , which is in SQL is called CONSTRAINT.

_sql_constraints
Sélectionnez
 _sql_constraints = [
('uniq_name', 'unique(cashier_name, pos_config_id)', "A cashier already exists with this name in this Point Of sale. Cashier's name must be unique!"),
] 

The rules are to be recorded as follows:

 
Sélectionnez
 ('NAME OF THE RULE', 'RULE', "MESSAGE IN CASE OF CONSTRAINT VIOLATION") 

The rule unique(cashier_name, pos_config_id) means that there can be only one cashier with the same name in the same Point Of Sale.

_sql_constraints is an array. You can set multiple rules separated by a comma.

The error message is displayed in a message box
The error message is displayed in a message box

VII-B-3-b. Overload pos.order object of Point Of Sale

In addition to creating the pos_cashier object that allows us to manage the cashiers, we need to override the original pos_order object of the Point Of sale

This module is in the original directory of Point Of Sale. You find it in the file point_of_sale.py around line 479.

openerp_path/addons/point_of_sale/point_of_sale.py

We need to change the create_from_ui () function and add a cashier_name field in the orders table called pos_order.

For this, we will create a new object that inherits from the original parent class.

This is why we stated that our module depended of point_of_sale module in the __ openerp__.py file.

inherit_pos_order_for_cashiers
Sélectionnez
 class inherit_pos_order_for_cashiers(osv.osv):
    _name='pos.order'
    _inherit='pos.order' 

For our module inherits pos_order module, we will give it the same name

_name
Sélectionnez
 _name='pos.order' 

And we will add the _inherit statement specifying the name of the parent module

_inherit
Sélectionnez
 _inherit='pos.order' 

About OpenERP objects inheritance I suggest you to read this page on the publisher's website :
OpenERP Object InheritanceOpenERP Object Inheritance.
Attention, this is the documentation for the version 6.x, but the instructions are still valid for version 7.x of OpenERP.

We will then just copy the entire original create_from_ui() function in our file.

Once done, we will add a field to the orders.

A little explanation
Point Of Sale works with JavaScript scripts.
Orders are stored in the browser (LocalStorage).
See : Principe de fonctionnement du Point De VentePrincipe de fonctionnement du Point De Vente.
As long as you make an order, it is stored in the browser. When you confirm the order, the create_from_ui() function is called. It will send the order to the database of OpenERP.
In fact, it will send all valid orders that are stored in the browser.
This allows the Point Of Sale to work in offline mode

In the loop for tmp_order in orders:, we add the cashier_name field din self.create() function like below

for tmp_order in orders loop:
Sélectionnez
 for tmp_order in orders:
    order = tmp_order['data']
    order_id = self.create(cr, uid, {
        'name': order['name'],
        'user_id': order['user_id'] or False,
        'session_id': order['pos_session_id'],
        'lines': order['lines'],
        'pos_reference':order['name'],          # <---------- COMMA     
        'cashier_name': order['cashier_name']   # <---------- ADD THE FIELD HERE
    }, context) 

Do not forget to add a comma at the end of the previous line.

The cashier's name will be recorded in order[] array.
We can then send the cashier's name in the cashier_name field of the pos_order table

This is all we add to this function.

The last thing to do is to add the cashier_name field to the pos_order table.
As in the previous module, simply add the field in the _columns statement

_columns
Sélectionnez
 _columns = {
        'cashier_name': fields.char('Cashier', size=128),
    } 

We're almost there. It remains the call to the object for OpenERP to take it into account..

 
Sélectionnez
 inherit_pos_order_for_cashiers() 

We achieved pos_cashier Python module.
Save the file to the root of the module.

VII-B-4. cashier_view.xml file

This is the file of the views of pos_cashier module. More exact, there are the views of the cashiers. We will put the views of the orders in another file.

In this file we will create the tree_view, the form_view, menus, search_view, and the action of the « Cashiers » menu.

As before, I put the complete code, and we will see that quietly.

cashier_view.xml
Sélectionnez
 <?xml version="1.0" encoding="utf-8"?>
<openerp>
  <data>

    <record id="pos_cashier_form" model="ir.ui.view">
        <field name="name">pos.cashier.form</field>
        <field name="model">pos.cashier</field>
        <field name="arch" type="xml">
          <form string="Cashiers" version="7.0">  
          <group col="4">     
            <field name="cashier_name" />
            <field name="pos_config_id" widget="selection" eval="ref('pos.config.name')" />  
            <field name="active"/>
            </group>
          </form>
        </field>
    </record>
  
    <record id="pos_cashier_tree" model="ir.ui.view">
      <field name="name">pos.cashier.tree</field>
      <field name="model">pos.cashier</field>
      <field name="arch" type="xml">
        <tree string="Cashiers">
          <field name="cashier_name"/> 
          <field name="pos_config_id" ref="pos.config.name"/>       
          <field name="active"/> 
        </tree>
      </field>
    </record>

    <record model="ir.ui.view" id="pos_cashier_search">
      <field name="name">pos.cashier.search</field>
      <field name="model">pos.cashier</field>
      <field name="arch" type="xml">
        <search string="Point of Sale Cashier">
          <field name="cashier_name" />
          <filter name="filter_see_all" string="All" domain="['|', ('active', '=',True), ('active', '=',False)]" />
          <filter name="filter_see_active" string="Active" domain="[('active', '=',True)]" />
          <filter name="filter_see_inactive" string="Inactive" domain="[('active', '=',False)]" />
        </search>
      </field>
    </record>

    <!-- L'action du menu -->
    <record model="ir.actions.act_window" id="action_pos_cashier">
      <field name="name">Cashiers</field>
      <field name="type">ir.actions.act_window</field>
      <field name="res_model">pos.cashier</field>
      <field name="view_type">form</field>
      <field name="view_mode">tree,form</field>
      <field name="view_id" ref="pos_cashier_tree"/>
      <field name="context">{"search_default_filter_see_all":1}</field>
      <field name="help" type="html">
        <p class="oe_view_nocontent_create">
          Click here to create a cashier for the Point Of Sale.
        </p>
      </field>
    </record>

    <!-- Menu gauche  Vendeurs -->   
    <menuitem 
              name="Cashiers" 
              id="menu_point_of_sale_cashiers" 
              parent="point_of_sale.menu_point_root" 
              sequence="16" 
              groups="point_of_sale.group_pos_manager"
              />
    <menuitem
            id="menu_action_pos_cashier"
            parent="menu_point_of_sale_cashiers"
            action="action_pos_cashier"    
            />
    <!-- # -->
    
  </data>
</openerp> 

OpenERP view file is always built this way

Structure of an OpenERP view
Sélectionnez
 <?xml version="1.0" encoding="utf-8"?>
<openerp>
  <data>
  
      <record>
          <!-- Here the various fields of the view -->
      </record>
      
      <menuitem/>
      
      <!-- Etc. -->

  </data>
</openerp> 

VII-B-4-a. The form view

pos_cashier_form View
Sélectionnez
 <record id="pos_cashier_form" model="ir.ui.view">
        <field name="name">pos.cashier.form</field>
        <field name="model">pos.cashier</field>
        <field name="arch" type="xml">
          <form string="Cashiers" version="7.0">  
          <group col="4">     
            <field name="cashier_name" />
            <field name="pos_config_id" widget="selection" eval="ref('pos.config.name')" />  
            <field name="active"/>
            </group>
          </form>
        </field>
    </record> 

The first line contains the identifier of the view and the model used.

id="pos_cashier_form"
To make it easier to debug later, I recommend you put the module name, followed by the type of view.

model="ir.ui.view"
Since this is a « View », the model will always be ir.ui.view (the view is stored in the ir_ui_view table of OpenERP).

The following three fields (mandatory)

 
Sélectionnez
 <field name="name">pos.cashier.form</field>
<field name="model">pos.cashier</field>
<field name="arch" type="xml">
    <!-- other objects inside "arch" field -->
</field> 

name="name" field
This is the name of the view. Return the identifier of the view, but replace underscores with a dot so that there is no confusion.

name="model" field
This is the name of the table. Here we use the pos.cashier table.

name="arch" field
It is inside this tag that will put the view itself.

So we will insert a form

 
Sélectionnez
 <form string="Cashiers" version="7.0">
    <group col="4">
        <field name="cashier_name" />
        <field name="pos_config_id" widget="selection" eval="ref('pos.config.name')" />  
        <field name="active"/>
    </group>
</form> 

When you add the string attribute in a field, this is the text that will be displayed (instead of the name of the field in the database, if that is the case).

So we add the nedded form fields.

name="cashier_name"
This is the text field that allows you to enter the name of the cashier.

name="pos_config_id"
This field will display the dropdown list of available Points Of Sale through the widget="selection" attribute

With the « eval » attribute, we ask OpenERP to display the name of the Point Of Sale

name="active"
This is the checkbox that enables / disable a cashier.

You will notice that the fields are inside a <group> tag.
This tells OpenERP how to display the fields in the form page

Here we specify, with the col attribute, the number of columns to use.

Form fields appear like this

Distribution of the fields in a form with <group> tag and  col=4 attribute
Distribution of the fields in a form with <group> tag and col=4 attribute

Here's how the cashiers creation form will be displayed

Creation form of the cashiers
Creation form of the cashiers

VII-B-4-b. The Tree view

pos_cashier_tree View
Sélectionnez
 <record id="pos_cashier_tree" model="ir.ui.view">
    <field name="name">pos.cashier.tree</field>
    <field name="model">pos.cashier</field>
    <field name="arch" type="xml">
        <tree string="Cashiers">
            <field name="cashier_name"/> 
            <field name="pos_config_id" ref="pos.config.name"/>       
            <field name="active"/> 
        </tree>
    </field>
</record> 

Instead of <form> tag, we will insert a <tree> tag.
These columns appear in the cashiers table.

VII-B-4-c. The search view

This is a special view. It will create search filters that appear by clicking the arrow in the search form in the upper right of the page.

pos_cashier_search View
Sélectionnez
 <record model="ir.ui.view" id="pos_cashier_search">
    <field name="name">pos.cashier.search</field>
    <field name="model">pos.cashier</field>
    <field name="arch" type="xml">
        <search string="Point of Sale Cashier">
            <field name="cashier_name" />
            <filter name="filter_see_all" string="All" domain="['|', ('active', '=',True), ('active', '=',False)]" />
            <filter name="filter_see_active" string="Active" domain="[('active', '=',True)]" />
            <filter name="filter_see_inactive" string="Inactive" domain="[('active', '=',False)]" />
        </search>
    </field>
</record> 

This time, instead of <tree> tag, we will insert a <search> tag in which we specify a search field and several filters.

So we add the following field

 
Sélectionnez
 <field name="cashier_name" /> 

We will now add a search filter with the <filter> tag

 
Sélectionnez
 <filter name="filter_see_all" string="All" domain="['|', ('active', '=',True), ('active', '=',False)]" /> 

name="filter_see_all"
Here we call the filter : filter_see_all.

string="All"
This is the word that will be displayed in the search form

domain="['|', ('active', '=',True), ('active', '=',False)]"
This is the area of research.
Here we search for active or non-active cashiers. We want to see all cashiers.

The domain attribute is an array in which you put the search parameters

here, the « | » (or) operator indicates that at least one condition must be met.

Inactive objects are not visible in tree view nor form view by default. We must create a filter to display inactive cashiers in order to activate them when needed.

The other two filters will display the active or inactive cashiers

Search filters appear when we click on the arrow near the search field
Search filters appear when we click on the arrow near the search field

VII-B-4-d. The Cashiers menu

We will now create a Cashiers menu that will appear in Cashiers heading in the Point Of Sale left menu

We could have simply to add only Cashiers menu without adding a Cashiers heading, but it will show you how to create a menu inside a heading.
Moreover, you will see that this heading will be visible only by one user group. If in the future you decide to add a menu in this heading, only authorized users in the group can see it..

I present to you the creation of the menu before the creation of the action for a better understanding, but in fact, the code will require that the menu is written after the action, because as referring to the action, if the menu is written before, Python will return the error action_pos_cashier does not exist.

The « Cashiers » heading

Cashiers heading
Sélectionnez
 <menuitem 
        name="Cashiers" 
        id="menu_point_of_sale_cashiers" 
        parent="point_of_sale.menu_point_root" 
        sequence="16" 
        groups="point_of_sale.group_pos_manager"
        /> 

A menu is written in <menuitem/> tag.

name="Cashiers"
This is the name of the heading.

id
As usual, it specifies an identifier for the heading.

parent
This is what allows us to insert the item in an existing menu.
Here we insert our menu item in the Point of Sale menu, we must retrieve the identifier of the menu in the original file of the Point Of Sale. As this menu does not belong to our module, we refer to it by using the usual dot syntax.

sequence
This is the number that is used to sort the heading. The lower the number, the higher the heading is.

groups
We want to restrict access to this menu for POS/Manager user group.

We use here also dot syntax to refer to the group. We could allow multiple groups, by adding them separated by a comma.

A menu which has no action attribute becomes a heading

The « Cashiers » menu

Cashiers menu
Sélectionnez
 <menuitem
        id="menu_action_pos_cashier"
        parent="menu_point_of_sale_cashiers"
        action="action_pos_cashier"    
        /> 

This time, we add the action attribute that refers to the action that we will define later.
You will also notice that the parent menu is the heading we created earlier. Clearly, the menu will be within this heading.

The menu appears in the Menu tab of the POS/Manager group (from Configuration/Groups menu) as shown in the image below.

The menu appears in POS/Manager group
The menu appears in POS/Manager group

Here we can see the sorting of the menus according to their sequence.

VII-B-4-e. The action of the menu

When we click on the Cashiers menu, the following action will be executed.

action_pos_cashier
Sélectionnez
 <record model="ir.actions.act_window" id="action_pos_cashier">
    <field name="name">Cashiers</field>
    <field name="type">ir.actions.act_window</field>
    <field name="res_model">pos.cashier</field>
    <field name="view_type">form</field>
    <field name="view_mode">tree,form</field>
    <field name="view_id" ref="pos_cashier_tree"/>
    <field name="context">{"search_default_filter_see_all":1}</field>
    <field name="help" type="html">
        <p class="oe_view_nocontent_create">
            Click here to create a cashier for the Point Of Sale.
        </p>
    </field>
</record> 

When it comes to action, then we use ir.actions.act_window model (the action will be recorded in the ir_act_window table of OpenERP).

name="type"
This is the type of the action

name="res_model"
This is the name of the table used

name="view_type"
This is the type of the view

name="view_mode"
This is the type of available views. Here we will allow form view and tree view. There is another type of view, the Kaban view.

name="view_id"
This is the ID of the view that this action will apply. Here, the tree view.

name="context"
The context of the view that apply to the tree view. Here, a default filter is applied, the filter_see_all filter we created earlier. So we will see all cashiers.

name="help" type="html"
This field will be used to display the content in HTML format if the table is empty.
Using oe_view_nocontent_create special class, a text will be displayed with an arrow to the Create button.

If no cashier has been created in the POS, the above text is displayed
If no cashier has been created in the POS, the above text is displayed

In the Point Of Sale, when we will click on the « Cashiers » menu, the tee view will appear with the « All » filter that will display all cashiers. A « Create » button will be displayed on top of the table.

The Cashiers menu in the POS heading
The Cashiers menu in the POS heading

VII-B-5. The order_cashier_view.xml file

We will now create the view for orders to the cashier's name appears.

order_cashier_view.xml
Sélectionnez
 <?xml version="1.0" encoding="utf-8"?>
<openerp>
    <data>   
        <!-- Vue formulaire -->
        <record model="ir.ui.view" id="view_pos_cashier_form">
            <field name="model">pos.order</field>
            <field name="name">view.inherit.pos.order.form</field>
            <field name="view_type">form</field>
            <field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
            <field name="arch" type="xml">
                <field name="partner_id" position="after">
                    <field name="cashier_name"/>
                </field>
            </field>
        </record>

        <!-- Vue Tree -->
        <record model="ir.ui.view" id="view_pos_cashier_tree">
            <field name="model">pos.order</field>
            <field name="name">view.inherit.pos.order.tree</field>
            <field name="view_type">tree</field>
            <field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
            <field name="arch" type="xml">
                <field name="user_id" position="replace">
                    <field name="cashier_name"/>
                </field>
            </field>
        </record>
    </data>
</openerp> 

As in the previous file, we will create a « form » view and a « tree » view.

Warning !
If you remember, the inherit_pos_order_for_cashiers object inherits from the pos_order object, the original object of the Point Of Sale.

We must therefore take the header of the original view, change the name field and add the inherit_id field.

inherit_id
Sélectionnez
 <field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/> 

You will notice that we put the identifier of the original view, and do not forget to precede with the name of the original module (dot syntax), since this view does not belong to our module, but to its parent.

VII-B-5-a. The form view

Vue formulaire
Sélectionnez
 <record model="ir.ui.view" id="view_pos_cashier_form">
    <field name="model">pos.order</field>
    <field name="name">view.inherit.pos.order.form</field>
    <field name="view_type">form</field>
    <field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
    <field name="arch" type="xml">
        <field name="partner_id" position="after">
            <field name="cashier_name"/>
        </field>
    </field>
</record> 

We will insert the cashier_name field in the form.
For this, we use a field that already exists and add the position attribute.

Availble positions

  • after : the field will be inserted after the one that contains the position attribute ;
  • before : the field will be inserted before the one that contains the position attribute;
  • replace : replace the field that contains the position attribute ;

In this case, the cashier_name field will be placed after partner_id field (the client) in the form

The cashier_name field in the order form
The cashier_name field in the order form

VII-B-5-b. The Tree View

Tree view
Sélectionnez
 <record model="ir.ui.view" id="view_pos_cashier_tree">
    <field name="model">pos.order</field>
    <field name="name">view.inherit.pos.order.tree</field>
    <field name="view_type">tree</field>
    <field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
    <field name="arch" type="xml">
        <field name="user_id" position="replace">
            <field name="cashier_name"/>
        </field>
    </field>
</record> 

In the tree view, we will replace the user_id field (User of the Point Of Sale) by cashier_name field.

Remember that we create this module for several cashiers can place orders without having to log in each time the cashier changes. This is why we do not want the user name of the Point Of Sale, which will be the same for all cashiers, appears in the orders view. However, reveal the name of the cashier will allow immediately the manager to see wich cashier has made such sale.

The cashier_name field in tree view of the orders
The cashier_name field in tree view of the orders

VII-C. Security Settings of the module

As we have defined the menu access to a particular group, we will now apply some security policies on the module to restrict access and set a rule on the records.

VII-C-1. Access Rights

We will create a special file in the security directory of the module.

/opt/modules-openerp/pos_cashier/security

The file that defines the access rights to the database records is a CSV file.
It has always the same name : ir.model.access.csv.

The first line contains the names of the fields separated by a comma.

CSV fields
Sélectionnez
 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 

id
A unique identifier for the access rule

name
The name of the rule. It will appear in the configuration pages of OpenERP.

model_id:id
The table to which this rule applies. The table name should always be prefixed with model_.

group_id:id
The user group to which this rule applies.

perm_read
Permission to read the data (1 or 0).

perm_write
Permission to modify the data (1 or 0).

perm_create
Permission to create data (1 or 0).

perm_unlink
Permission to delete the data (1 or 0).

We will now add two additional lines.

A line for the rights for the users of the POS (POS/User group) with read rights only.
And a line for the rights of the managers of the POS (POS/Manager group) with all rights.

CSV fields
Sélectionnez
 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_pos_cashier_u,pos.cashier user,model_pos_cashier,point_of_sale.group_pos_user,1,0,0,0
access_pos_cashier_m,pos.cashier manager,model_pos_cashier,point_of_sale.group_pos_manager,1,1,1,1 

About POS/User group

  • id = access_pos_cashier_u
  • name = pos.cashier user
  • model_id:id = model_pos_cashier
  • group_id:id = point_of_sale.group_pos_user
  • perm_read = 1
  • perm_write = 0
  • perm_create = 0
  • perm_unlink = 0

About POS/Manager group

  • id = access_pos_cashier_m
  • name = pos.cashier manager
  • model_id:id = model_pos_cashier
  • group_id:id = point_of_sale.group_pos_manager
  • perm_read = 1
  • perm_write = 1
  • perm_create = 1
  • perm_unlink = 1

When you go to the Configuration/Security/Access controls List menu of OpenERP you will see these two lines in the tree view.

pos_cashier access rights
pos_cashier access rights

VII-C-2. Record rules

Now let's define a record rule to ensure that the manager of a Point Of Sale can create a cashier for its Point Of Sale only.

When creating a cashier, if the manager selects a Point Of Sale in the list which does not belong to him, an error message will be displayed.

The administrator of OpenERP (Admin user) can create cashiers in any Point Of Sale.
The administrator has full rights to the database. It can therefore configure any application. Security rules do not apply to him.

To do this, still in the security directory of the module, we will create an XML file that we'll call pos_cashier_security.xml.

pos_cashier_security.xml
Sélectionnez
 <?xml version="1.0" encoding="utf-8"?>
<openerp>
    <data noupdate="0">
        <record id="rule_pos_cashier" model="ir.rule">
            <field name="name">Point Of Sale Cashiers</field>
            <field name="model_id" ref="model_pos_cashier" />
            <field name="global" eval="True" />
            <field name="domain_force">[('pos_config_id', '=', user.pos_config.id)]</field>
        </record>
    </data>
</openerp> 

Here, as it comes to security rules, the model will be always ir.rule (the rule will be recorded in the ir_rule table of OpenERP).

As in previous XML file we will put an identifier, then we will add some fields.

name="model_id"
This is the name of the affected table (prefixed with model_).

name="global" eval="True"
If this rule is global, it applies to everyone. If it is not global, it must then specify the user groups to which it applies.

name="domain_force"
This is the rule itself,

domain_force
Sélectionnez
 [('pos_config_id', '=', user.pos_config.id)] 

Here, a recording can be done only if the selected Point Of Sale belongs to the user.

Here is how this rule will appear in the Configuration/Security/Records rules menu of OpenERP

Record rule for pos_cashier module (tree view)
Record rule for pos_cashier module (tree view)

If you click on this rule, it appears in the form below.

Record rule for pos_cashier module (form view)
Record rule for pos_cashier module (form view)

VII-D. Add an icon to the module

For your module displays an icon in the Configuration/Modules menu of OpenERP, we'll just create a PNG image of 64 pixels by 64 pixels we call icon.png.

This icon is put in the img subdirectory of the static directory of the module.

/opt/modules-openerp/pos_cashier/static/src/img

At the loading of OpenERP, and at the loading of the modules, the application looks for icon.png file into this directory to display next to the name of the module.

Icon is displayed near the module's name
Icon is displayed near the module's name

VII-E. End of the basic OpenERP module

We finished the «basic» module for OpenERP

All files that we have created are added to the data[] array in the __ openerp__.py file
See here.

We still can not install our module, because as we stated other files in the __ openerp__.py file, if they are not created, OpenERP will return an error.

We'll have to wait a little… Image non disponible

VIII. Creation of the Web module for the Point Of Sale

Now that we have created the module to create cashiers in the Point Of Sale, we will create the necessary files that will allow to use the cashiers in the Point Of Sale.

I remind you that the Point Of Sale is not, strictly speaking, integrated into OpenERP. This is a completely different web interface.

The original POS GUI
The original POS GUI

When we are finished, you will see the cashiers list at the bottom left of the Point Of Sale, under the keypad, as shown in the image below.

The new POS GUI with cashiers dropdown list
The new POS GUI with cashiers dropdown list

VIII-A. The pos_cashier.js file

Well, here is a little hot. I put you though all the script then I will explain to you the different functions.

This file is created in the js directory of the module.

/opt/modules-openerp/pos_cashier/static/src/js
pos_cashier.js
Sélectionnez
 
function openerp_pos_cashier(instance, module){ //module is instance.point_of_sale
    var module = instance.point_of_sale;
    var QWeb = instance.web.qweb;
    _t = instance.web._t;

    globalCashier = null;

    module.CashierWidget = module.PosWidget.include({
        template: 'PosWidget',  

        init: function(parent, options) {
            this._super(parent);
            var  self = this;    
        },

        // recuperation de l'ID du POS
        get_cur_pos_config_id: function(){
            var self = this;
            var config = self.pos.get('pos_config');
            var config_id = null;
                     
            if(config){
                config_id = config.id;
                
                return config_id;
            }        
            return '';    
        },

        fetch: function(model, fields, domain, ctx){
            return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
        },

        cashier_change: function(name){
            globalCashier = name;

            $('#pay-screen-cashier-name').html(name);
            console.log('cashier_change : ' + name);
            
            if(name != ''){
                $('.gotopay-button').removeAttr('disabled');                 
            } else{
                $('.gotopay-button').attr('disabled', 'disabled');
            }
        },

        get_cashiers: function(config_id){
            var self = this;
            var cashier_list = [];

            var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])
                .then(function(cashiers){
                     for(var i = 0, len = cashiers.length; i < len; i++){
                        cashier_list.push(cashiers[i].cashier_name);
                     }

                    if(cashier_list.length > 0){
                        
                        for(var i = 0, len = cashier_list.length; i < len; i++){
                            var content = self.$('#cashier-select').html();
                            var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
                            self.$('#cashier-select').html(content + new_option);
                            }

                        self.$('#AlertNoCashier').css('display', 'none');
                        self.$('#cashier-select').selectedIndex = 0;
                        globalCashier = cashier_list[0];
                        self.cashier_change(globalCashier);

                    } else{

                        // if there are no cashier
                        self.$('#AlertNoCashier').css('display', 'block');
                        self.$('.gotopay-button').attr('disabled', 'disabled');
                    }
                });
        }, 

        renderElement: function() {
            var self = this;
            this._super();

            self.$('#cashier-select').change(function(){
                var name = this.value;
                self.cashier_change(name);
            });
        },

        
        build_widgets: function() {
            var self = this;

            // --------  Screens ---------

            this.product_screen = new module.ProductScreenWidget(this,{});
            this.product_screen.appendTo($('#rightpane'));

            this.receipt_screen = new module.ReceiptScreenWidget(this, {});
            this.receipt_screen.appendTo($('#rightpane'));

            this.payment_screen = new module.PaymentScreenWidget(this, {});
            this.payment_screen.appendTo($('#rightpane'));

            this.welcome_screen = new module.WelcomeScreenWidget(this,{});
            this.welcome_screen.appendTo($('#rightpane'));

            this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
            this.client_payment_screen.appendTo($('#rightpane'));

            this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
            this.scale_invite_screen.appendTo($('#rightpane'));

            this.scale_screen = new module.ScaleScreenWidget(this,{});
            this.scale_screen.appendTo($('#rightpane'));

            // --------  Popups ---------

            this.help_popup = new module.HelpPopupWidget(this, {});
            this.help_popup.appendTo($('.point-of-sale'));

            this.error_popup = new module.ErrorPopupWidget(this, {});
            this.error_popup.appendTo($('.point-of-sale'));

            this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
            this.error_product_popup.appendTo($('.point-of-sale'));

            this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
            this.error_session_popup.appendTo($('.point-of-sale'));

            this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
            this.choose_receipt_popup.appendTo($('.point-of-sale'));

            this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
            this.error_negative_price_popup.appendTo($('.point-of-sale'));

            // --------  Misc ---------

            this.notification = new module.SynchNotificationWidget(this,{});
            this.notification.appendTo(this.$('#rightheader'));

            this.username   = new module.UsernameWidget(this,{});
            this.username.replace(this.$('.placeholder-UsernameWidget'));

            this.action_bar = new module.ActionBarWidget(this);
            this.action_bar.appendTo($(".point-of-sale #rightpane"));

            this.left_action_bar = new module.ActionBarWidget(this);
            this.left_action_bar.appendTo($(".point-of-sale #leftpane"));

            this.gotopay = new module.GoToPayWidget(this, {});
            this.gotopay.replace($('#placeholder-GoToPayWidget'));

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));

            this.numpad = new module.NumpadWidget(this);
            this.numpad.replace($('#placeholder-NumpadWidget'));

            this.order_widget = new module.OrderWidget(this, {});
            this.order_widget.replace($('#placeholder-OrderWidget'));

            this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
                'keyboard_model': 'simple'
            });
            this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 

            this.close_button = new module.HeaderButtonWidget(this,{
                label: _t('Close'),
                action: function(){ self.try_close(); },
            });
            this.close_button.appendTo(this.$('#rightheader'));

            this.client_button = new module.HeaderButtonWidget(this,{
                label: _t('Self-Checkout'),
                action: function(){ self.screen_selector.set_user_mode('client'); },
            });
            this.client_button.appendTo(this.$('#rightheader'));

            
            // --------  Screen Selector ---------

            this.screen_selector = new module.ScreenSelector({
                pos: this.pos,
                screen_set:{
                    'products': this.product_screen,
                    'payment' : this.payment_screen,
                    'client_payment' : this.client_payment_screen,
                    'scale_invite' : this.scale_invite_screen,
                    'scale':    this.scale_screen,
                    'receipt' : this.receipt_screen,
                    'welcome' : this.welcome_screen,
                },
                popup_set:{
                    'help': this.help_popup,
                    'error': this.error_popup,
                    'error-product': this.error_product_popup,
                    'error-session': this.error_session_popup,
                    'error-negative-price': this.error_negative_price_popup,
                    'choose-receipt': this.choose_receipt_popup,
                },
                default_client_screen: 'welcome',
                default_cashier_screen: 'products',
                default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
            });

            if(this.pos.debug){
                this.debug_widget = new module.DebugWidget(this);
                this.debug_widget.appendTo(this.$('#content'));
            }
        },

    });  

    module.CashierPayScreenWidget = module.PaymentScreenWidget.include({
        template: 'PaymentScreenWidget', 

        show: function(){
            this._super();
            var self = this;
            this.$('#pay-screen-cashier-name').html(globalCashier);
            this.$('#ticket-screen-cashier-name').html(globalCashier);
            this.pos.get('selectedOrder').set_cashier_name(globalCashier);

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));
        },

    }); 

    module.CashierReceiptScreenWidget = module.ReceiptScreenWidget.include({

        refresh: function() {
            this._super();
            $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this}));

            if(globalCashier != ''){
                this.$('#ticket-screen-cashier-name').html(globalCashier);           
            }         
        },
        
    });

    module.GoToPayWidget = module.PosBaseWidget.extend({
        template: 'GoToPayWidget',
        init: function(parent, options) {
            this._super(parent);
        },

        renderElement: function() {
            var self = this;
            this._super();

            var button = new module.GoToPayButtonWidget(self);
            button.appendTo(self.$el);
        },
    });

    module.GoToPayButtonWidget = module.PosBaseWidget.extend({
        template: 'GoToPayButtonWidget',
        init: function(parent, options) {
            this._super(parent);
        },

        renderElement: function() {
            var self = this;
            this._super();

            this.$el.click(function(){
                self.pos_widget.screen_selector.set_current_screen('payment');
            });
        },
    });

    
    module.Order = Backbone.Model.extend({
        initialize: function(attributes){
            Backbone.Model.prototype.initialize.apply(this, arguments);
            this.set({
                creationDate:   new Date(),
                orderLines:     new module.OrderlineCollection(),
                paymentLines:   new module.PaymentlineCollection(),
                name:           "Order " + this.generateUniqueId(),
                client:         null,
                cashier_name:   null,
            });
            this.pos =     attributes.pos; 
            this.selected_orderline = undefined;
            this.screen_data = {};  // see ScreenSelector
            this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
            return this;
        },
        generateUniqueId: function() {
            return new Date().getTime();
        },
        addProduct: function(product, options){
            options = options || {};
            var attr = product.toJSON();
            attr.pos = this.pos;
            attr.order = this;
            var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});

            if(options.quantity !== undefined){
                line.set_quantity(options.quantity);
            }
            if(options.price !== undefined){
                line.set_unit_price(options.price);
            }

            var last_orderline = this.getLastOrderline();
            if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){
                last_orderline.merge(line);
            }else{
                this.get('orderLines').add(line);
            }
            this.selectLine(this.getLastOrderline());
        },
        removeOrderline: function( line ){
            this.get('orderLines').remove(line);
            this.selectLine(this.getLastOrderline());
        },
        getLastOrderline: function(){
            return this.get('orderLines').at(this.get('orderLines').length -1);
        },
        addPaymentLine: function(cashRegister) {
            var paymentLines = this.get('paymentLines');
            var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
            if(cashRegister.get('journal').type !== 'cash'){
                newPaymentline.set_amount( this.getDueLeft() );
            }
            paymentLines.add(newPaymentline);
        },
        getName: function() {
            return this.get('name');
        },
        getSubtotal : function(){
            return (this.get('orderLines')).reduce((function(sum, orderLine){
                return sum + orderLine.get_display_price();
            }), 0);
        },
        getTotalTaxIncluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_with_tax();
            }), 0);
        },
        getDiscountTotal: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
            }), 0);
        },
        getTotalTaxExcluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_without_tax();
            }), 0);
        },
        getTax: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_tax();
            }), 0);
        },
        getPaidTotal: function() {
            return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
                return sum + paymentLine.get_amount();
            }), 0);
        },
        getChange: function() {
            return this.getPaidTotal() - this.getTotalTaxIncluded();
        },
        getDueLeft: function() {
            return this.getTotalTaxIncluded() - this.getPaidTotal();
        },
        set_cashier_name: function(name){
            this.set('cashier_name', name);
        },
        // sets the type of receipt 'receipt'(default) or 'invoice'
        set_receipt_type: function(type){
            this.receipt_type = type;
        },
        get_receipt_type: function(){
            return this.receipt_type;
        },
        // the client related to the current order.
        set_client: function(client){
            this.set('client',client);
        },
        get_client: function(){
            return this.get('client');
        },
        get_client_name: function(){
            var client = this.get('client');
            return client ? client.name : "";
        },
        // the order also stores the screen status, as the PoS supports
        // different active screens per order. This method is used to
        // store the screen status.
        set_screen_data: function(key,value){
            if(arguments.length === 2){
                this.screen_data[key] = value;
            }else if(arguments.length === 1){
                for(key in arguments[0]){
                    this.screen_data[key] = arguments[0][key];
                }
            }
        },
        //see set_screen_data
        get_screen_data: function(key){
            return this.screen_data[key];
        },
        // exports a JSON for receipt printing
        export_for_printing: function(){
            var orderlines = [];
            this.get('orderLines').each(function(orderline){
                orderlines.push(orderline.export_for_printing());
            });

            var paymentlines = [];
            this.get('paymentLines').each(function(paymentline){
                paymentlines.push(paymentline.export_for_printing());
            });
            var client  = this.get('client');
            var cashier = this.pos.get('cashier') || this.pos.get('user');
            var company = this.pos.get('company');
            var shop    = this.pos.get('shop');
            var date = new Date();

            return {
                orderlines: orderlines,
                paymentlines: paymentlines,
                subtotal: this.getSubtotal(),
                total_with_tax: this.getTotalTaxIncluded(),
                total_without_tax: this.getTotalTaxExcluded(),
                total_tax: this.getTax(),
                total_paid: this.getPaidTotal(),
                total_discount: this.getDiscountTotal(),
                change: this.getChange(),
                name : this.getName(),
                client: client ? client.name : null ,
                invoice_id: null,   //TODO
                cashier: cashier ? cashier.name : null,
                date: { 
                    year: date.getFullYear(), 
                    month: date.getMonth(), 
                    date: date.getDate(),       // day of the month 
                    day: date.getDay(),         // day of the week 
                    hour: date.getHours(), 
                    minute: date.getMinutes() 
                }, 
                company:{
                    email: company.email,
                    website: company.website,
                    company_registry: company.company_registry,
                    contact_address: company.contact_address, 
                    vat: company.vat,
                    name: company.name,
                    phone: company.phone,
                },
                shop:{
                    name: shop.name,
                },
                currency: this.pos.get('currency'),
            };
        },
        exportAsJSON: function() {
            var orderLines, paymentLines;
            orderLines = [];
            (this.get('orderLines')).each(_.bind( function(item) {
                return orderLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            paymentLines = [];
            (this.get('paymentLines')).each(_.bind( function(item) {
                return paymentLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            return {
                name: this.getName(),
                amount_paid: this.getPaidTotal(),
                amount_total: this.getTotalTaxIncluded(),
                amount_tax: this.getTax(),
                amount_return: this.getChange(),
                lines: orderLines,
                statement_ids: paymentLines,
                pos_session_id: this.pos.get('pos_session').id,
                partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
                user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
                cashier_name: this.pos.get('selectedOrder').get('cashier_name'),
            };
        },
        getSelectedLine: function(){
            return this.selected_orderline;
        },
        selectLine: function(line){
            if(line){
                if(line !== this.selected_orderline){
                    if(this.selected_orderline){
                        this.selected_orderline.set_selected(false);
                    }
                    this.selected_orderline = line;
                    this.selected_orderline.set_selected(true);
                }
            }else{
                this.selected_orderline = undefined;
            }
        },
    });

    

};

openerp.point_of_sale = function(instance) {
    instance.point_of_sale = {};

    var module = instance.point_of_sale;

    openerp_pos_db(instance,module);            // import db.js
    openerp_pos_models(instance,module);        // import pos_models.js
    openerp_pos_basewidget(instance,module);    // import pos_basewidget.js
    openerp_pos_keyboard(instance,module);      // import  pos_keyboard_widget.js
    openerp_pos_scrollbar(instance,module);     // import pos_scrollbar_widget.js
    openerp_pos_screens(instance,module);       // import pos_screens.js
    openerp_pos_widgets(instance,module);       // import pos_widgets.js
    openerp_pos_devices(instance,module);       // import pos_devices.js

    // cashiers
    openerp_pos_cashier(instance,module);       // import openerp_pos_cashier

    instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
}; 

There are two main functions in this file.

openerp_pos_cashier() function
It will allow us to add the necessary functions to the module and it will also allow us to modify some original features.

openerp.point_of_sale() function
We will change that for our module is taken into account in the Point Of Sale.

VIII-A-1. openerp_pos_cashier() function

This function is declared as follows:

openerp_pos_cashier()function declaration
Sélectionnez
 function openerp_pos_cashier(instance, module){ 
} 

This function will be called later in the function that creates the Point Of Sale.

I'm not going to be able to explain all, because I'm not an OpenERP Guru. I proceeded by studying the scripts of OpenERP modules magnifying glass to try to deduce how to do it.
In fact, if at any time you find an error or an approximation in my explanations, or even in the code, do not hesitate to contact me to let me know.

Then we will instantiate some objects:

 
Sélectionnez
 var module = instance.point_of_sale;
var QWeb = instance.web.qweb;
_t = instance.web._t; 

The module will be an instance of the Point Of Sale.
QWeb is the template rendering engine. I invite you to read the documentation on the OpenERP website: Image non disponibleDocumentation QWebDocummentation QWeb
_t is an instance of _t (?!??). To be honest, I do not know what it is. When I created the module for the first time, this instance does not exist in OpenERP version I had. It was been added later. It is used for the action of the close button is at the top of the Point Of Sale. As you will see later, we import the close button of Point Of Sale, this instance is required.

We then declare a global variable. We will use it further to store the name of the selected cashier.

Global variable
Sélectionnez
 globalCashier = null; 

For a variable to be global, it is stated without the var keyword.
A local variable is declared like this: var myvar = 'something';

VIII-A-2. The CashierWidget module

Now we will override the original PosWidget module.
This is in fact the module that contains the entire Point Of Sale.

We are not really going to « overload » the module, we will include additional functions when possible, with the include() function.

module.CashierWidget
Sélectionnez
 
    module.CashierWidget = module.PosWidget.include({
        template: 'PosWidget',  

        // recuperation de l'ID du POS
        get_cur_pos_config_id: function(){
            var self = this;
            var config = self.pos.get('pos_config');
            var config_id = null;
                     
            if(config){
                config_id = config.id;
                
                return config_id;
            }        
            return '';    
        },

        fetch: function(model, fields, domain, ctx){
            return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
        },

        cashier_change: function(name){
            globalCashier = name;

            $('#pay-screen-cashier-name').html(name);
            //console.log('cashier_change : ' + name);
            
            if(name != ''){
                $('.gotopay-button').removeAttr('disabled');                 
            } else{
                $('.gotopay-button').attr('disabled', 'disabled');
            }
        },

        get_cashiers: function(config_id){
            var self = this;
            var cashier_list = [];

            var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])
                .then(function(cashiers){
                     for(var i = 0, len = cashiers.length; i < len; i++){
                        cashier_list.push(cashiers[i].cashier_name);
                     }

                    if(cashier_list.length > 0){
                        
                        for(var i = 0, len = cashier_list.length; i < len; i++){
                            var content = self.$('#cashier-select').html();
                            var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
                            self.$('#cashier-select').html(content + new_option);
                            }

                        self.$('#AlertNoCashier').css('display', 'none');
                        self.$('#cashier-select').selectedIndex = 0;
                        globalCashier = cashier_list[0];
                        self.cashier_change(globalCashier);

                    } else{

                        // if there are no cashier
                        self.$('#AlertNoCashier').css('display', 'block');
                        self.$('.gotopay-button').attr('disabled', 'disabled');
                    }
                });
        }, 

        renderElement: function() {
            var self = this;
            this._super();

            self.$('#cashier-select').change(function(){
                var name = this.value;
                self.cashier_change(name);
            });
        },

        
        build_widgets: function() {
            var self = this;

            // --------  Screens ---------

            this.product_screen = new module.ProductScreenWidget(this,{});
            this.product_screen.appendTo($('#rightpane'));

            this.receipt_screen = new module.ReceiptScreenWidget(this, {});
            this.receipt_screen.appendTo($('#rightpane'));

            this.payment_screen = new module.PaymentScreenWidget(this, {});
            this.payment_screen.appendTo($('#rightpane'));

            this.welcome_screen = new module.WelcomeScreenWidget(this,{});
            this.welcome_screen.appendTo($('#rightpane'));

            this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
            this.client_payment_screen.appendTo($('#rightpane'));

            this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
            this.scale_invite_screen.appendTo($('#rightpane'));

            this.scale_screen = new module.ScaleScreenWidget(this,{});
            this.scale_screen.appendTo($('#rightpane'));

            // --------  Popups ---------

            this.help_popup = new module.HelpPopupWidget(this, {});
            this.help_popup.appendTo($('.point-of-sale'));

            this.error_popup = new module.ErrorPopupWidget(this, {});
            this.error_popup.appendTo($('.point-of-sale'));

            this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
            this.error_product_popup.appendTo($('.point-of-sale'));

            this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
            this.error_session_popup.appendTo($('.point-of-sale'));

            this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
            this.choose_receipt_popup.appendTo($('.point-of-sale'));

            this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
            this.error_negative_price_popup.appendTo($('.point-of-sale'));

            // --------  Misc ---------

            this.notification = new module.SynchNotificationWidget(this,{});
            this.notification.appendTo(this.$('#rightheader'));

            this.username   = new module.UsernameWidget(this,{});
            this.username.replace(this.$('.placeholder-UsernameWidget'));

            this.action_bar = new module.ActionBarWidget(this);
            this.action_bar.appendTo($(".point-of-sale #rightpane"));

            this.left_action_bar = new module.ActionBarWidget(this);
            this.left_action_bar.appendTo($(".point-of-sale #leftpane"));

            this.gotopay = new module.GoToPayWidget(this, {});
            this.gotopay.replace($('#placeholder-GoToPayWidget'));

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));

            this.numpad = new module.NumpadWidget(this);
            this.numpad.replace($('#placeholder-NumpadWidget'));

            this.order_widget = new module.OrderWidget(this, {});
            this.order_widget.replace($('#placeholder-OrderWidget'));

            this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
                'keyboard_model': 'simple'
            });
            this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 

            this.close_button = new module.HeaderButtonWidget(this,{
                label: _t('Close'),
                action: function(){ self.try_close(); },
            });
            this.close_button.appendTo(this.$('#rightheader'));

            this.client_button = new module.HeaderButtonWidget(this,{
                label: _t('Self-Checkout'),
                action: function(){ self.screen_selector.set_user_mode('client'); },
            });
            this.client_button.appendTo(this.$('#rightheader'));

            
            // --------  Screen Selector ---------

            this.screen_selector = new module.ScreenSelector({
                pos: this.pos,
                screen_set:{
                    'products': this.product_screen,
                    'payment' : this.payment_screen,
                    'client_payment' : this.client_payment_screen,
                    'scale_invite' : this.scale_invite_screen,
                    'scale':    this.scale_screen,
                    'receipt' : this.receipt_screen,
                    'welcome' : this.welcome_screen,
                },
                popup_set:{
                    'help': this.help_popup,
                    'error': this.error_popup,
                    'error-product': this.error_product_popup,
                    'error-session': this.error_session_popup,
                    'error-negative-price': this.error_negative_price_popup,
                    'choose-receipt': this.choose_receipt_popup,
                },
                default_client_screen: 'welcome',
                default_cashier_screen: 'products',
                default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
            });

            if(this.pos.debug){
                this.debug_widget = new module.DebugWidget(this);
                this.debug_widget.appendTo(this.$('#content'));
            }
        },

    }); 

You will notice that the statements or functions within the module are separated by a comma.

First, we declare the template that will be used for the module.

template
Sélectionnez
 template: 'PosWidget',  

This means that when the module is loaded, it is also made to the PosWidget view, which we will study further.

We will then create a function that will allow to retrieve the ID of the Point Of Sale and then get cashiers who belong to this Point Of Sale.

 
get_cur_pos_config_id()
Sélectionnez
 
        // recuperation de l'ID du POS
        get_cur_pos_config_id: function(){
            var self = this;
            var config = self.pos.get('pos_config');
            var config_id = null;
                     
            if(config){
                config_id = config.id;
                
                return config_id;
            }        
            return '';    
        }, 

This function uses the pos.get() function that was defined in the original module.
When a function belongs to the original Point Of Sale Module, it is writen like this : pos.the_function(). If a record is found, it returns the ID of the POS currently used.

We will then add a function that I grabbed from another module. This function allows to make a query to a table of the database.

fetch()
Sélectionnez
 fetch: function(model, fields, domain, ctx){
    return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
}, 

With the correct settings, you can get the data from the table. We will see this later.

We will then create a function that will be called from the POS interface (when onchange() event of the cashiers list, for example).

cashier_change()
Sélectionnez
 
cashier_change: function(name){
    globalCashier = name;

    $('#pay-screen-cashier-name').html(name);
    //console.log('cashier_change : ' + name);
            
    if(name != ''){
        $('.gotopay-button').removeAttr('disabled');                 
    } else{
        $('.gotopay-button').attr('disabled', 'disabled');
    }
}, 

I left you for a special comment:
//console.log('cashier_change : ' + name);
If you use Firebug extension, which is very effective for JavaScript debugging, and if you uncomment this line, you will see the message in the Firebug console each time the cashier_change() function will be called .
When you build your personal unit, do not hesitate to use this trick in your JavaScripts, in particular to verify that the functions are executed and your variables do contain the values ​​you expect.
Obviously, you do not forget to remove your console.log() commands or comment them before going into production.
Finally, the last little tip, if you use console.log() by passing an object, you will not forget to use this: console.log(JSON.stringify (myObject));

When the function is called, the name of the cashier will be stored in the globalCashier global variable.
Then the cashier's name will also be sent to <div id="pay-screen-cashier-name"></div> tag which appears on the payment page.

Also, if the cashier's name is empty (so there is no cashier in the Point Of Sale), it will disable the Pay button of the Point Of Sale. No sale must be performed.

A little explanation !
The left side of the Point Of Sale normally displays the keypad and some payment buttons.
There can be as many buttons as you set up payment methods for the Point Of Sale.
To prohibit the sale if no cashier has been created in the Point Of Sale, I moved the payment buttons on the payment page, and instead I put a Pay button.
It is easier to disable one button that is « hard coded » in the view, rather than make a function that disables many payment buttons that are dynamically generated.

The standart payment interface with several payment buttons
The standart payment interface with
several payment buttons
The payment interface with only one Pay button + the dropdown list of the cashiers
The payment interface with only one Pay button
+ the dropdown list of the cashiers

We will now add the get_cashiers() function that will retrieve the cashiers from the database and build options of the cashiers drop down list.

get_cashiers()
Sélectionnez
 
get_cashiers: function(config_id){
    var self = this;
    var cashier_list = [];

    var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])
               .then(function(cashiers){
                   for(var i = 0, len = cashiers.length; i < len; i++){
                       cashier_list.push(cashiers[i].cashier_name);
                   }

                   if(cashier_list.length > 0){
                        
                       for(var i = 0, len = cashier_list.length; i < len; i++){
                           var content = self.$('#cashier-select').html();
                           var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
                           self.$('#cashier-select').html(content + new_option);
                       }

                       self.$('#AlertNoCashier').css('display', 'none');
                       self.$('#cashier-select').selectedIndex = 0;
                       globalCashier = cashier_list[0];
                       self.cashier_change(globalCashier);

                  } else{

                      // if there are no cashier
                      self.$('#AlertNoCashier').css('display', 'block');
                      self.$('.gotopay-button').attr('disabled', 'disabled');
                  }
              });
}, 

We first create an empty array

 
Sélectionnez
 var cashier_list = []; 

Then we make a query with the fetch() function that we seen earlier.

 
Sélectionnez
 var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']]) 

Here, we will perform a query (SELECT) on pos_cashier table, we will retrieve the field cashier_name of the cashiers who belong to the Point Of Sale which pos_config_id is equal to config_id we have passed as a parameter AND that are active!

Then we will create options of the drop down list with the function that is performed after the query.

 
Sélectionnez
 
.then(function(cashiers){
     for(var i = 0, len = cashiers.length; i < len; i++){
        cashier_list.push(cashiers[i].cashier_name);
    }

    if(cashier_list.length > 0){
                        
        for(var i = 0, len = cashier_list.length; i < len; i++){
            var content = self.$('#cashier-select').html();
            var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
            self.$('#cashier-select').html(content + new_option);
        }

        self.$('#AlertNoCashier').css('display', 'none');
        self.$('#cashier-select').selectedIndex = 0;
        globalCashier = cashier_list[0];
        self.cashier_change(globalCashier);

    } else{

        // if there are no cashier
        self.$('#AlertNoCashier').css('display', 'block');
        self.$('.gotopay-button').attr('disabled', 'disabled');
    }
}); 

I let you dissect the function above.
Note that if there is no cashier, we display an error message in the <div id="AlertNoCashier"></div> tag and we disable the Pay button.

An error message is displayed and the Pay button is inactive
An error message is displayed and the Pay button is inactive

You also see that once the list of the cashiers is built, it stores the name of the first cashier in the globalCashier global variable then we call cashier_change() function, passing it the name of the first cashier of the list.

Then we will add a function to be called when loading the module and call the cashier_change() function to initialize the Point Of Sale.

renderElement()
Sélectionnez
 
renderElement: function() {
    var self = this;
    this._super();

    self.$('#cashier-select').change(function(){
        var name = this.value;
        self.cashier_change(name);
    });
}, 

And finally, we'll copy/paste and modify the original build_widgets() function, which is in widgets.js file of point_of_sale module.

build_widgets()
Sélectionnez
 build_widgets: function() {
            var self = this;

            // --------  Screens ---------

            this.product_screen = new module.ProductScreenWidget(this,{});
            this.product_screen.appendTo($('#rightpane'));

            this.receipt_screen = new module.ReceiptScreenWidget(this, {});
            this.receipt_screen.appendTo($('#rightpane'));

            this.payment_screen = new module.PaymentScreenWidget(this, {});
            this.payment_screen.appendTo($('#rightpane'));

            this.welcome_screen = new module.WelcomeScreenWidget(this,{});
            this.welcome_screen.appendTo($('#rightpane'));

            this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
            this.client_payment_screen.appendTo($('#rightpane'));

            this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
            this.scale_invite_screen.appendTo($('#rightpane'));

            this.scale_screen = new module.ScaleScreenWidget(this,{});
            this.scale_screen.appendTo($('#rightpane'));

            // --------  Popups ---------

            this.help_popup = new module.HelpPopupWidget(this, {});
            this.help_popup.appendTo($('.point-of-sale'));

            this.error_popup = new module.ErrorPopupWidget(this, {});
            this.error_popup.appendTo($('.point-of-sale'));

            this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
            this.error_product_popup.appendTo($('.point-of-sale'));

            this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
            this.error_session_popup.appendTo($('.point-of-sale'));

            this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
            this.choose_receipt_popup.appendTo($('.point-of-sale'));

            this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
            this.error_negative_price_popup.appendTo($('.point-of-sale'));

            // --------  Misc ---------

            this.notification = new module.SynchNotificationWidget(this,{});
            this.notification.appendTo(this.$('#rightheader'));

            this.username   = new module.UsernameWidget(this,{});
            this.username.replace(this.$('.placeholder-UsernameWidget'));

            this.action_bar = new module.ActionBarWidget(this);
            this.action_bar.appendTo($(".point-of-sale #rightpane"));

            this.left_action_bar = new module.ActionBarWidget(this);
            this.left_action_bar.appendTo($(".point-of-sale #leftpane"));

            this.gotopay = new module.GoToPayWidget(this, {});         // We add here the creation
            this.gotopay.replace($('#placeholder-GoToPayWidget'));     // of the widget that displays the Pay button

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));

            this.numpad = new module.NumpadWidget(this);
            this.numpad.replace($('#placeholder-NumpadWidget'));

            this.order_widget = new module.OrderWidget(this, {});
            this.order_widget.replace($('#placeholder-OrderWidget'));

            this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
                'keyboard_model': 'simple'
            });
            this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 

            this.close_button = new module.HeaderButtonWidget(this,{
                label: _t('Close'),
                action: function(){ self.try_close(); },
            });
            this.close_button.appendTo(this.$('#rightheader'));

            this.client_button = new module.HeaderButtonWidget(this,{
                label: _t('Self-Checkout'),
                action: function(){ self.screen_selector.set_user_mode('client'); },
            });
            this.client_button.appendTo(this.$('#rightheader'));

            
            // --------  Screen Selector ---------

            this.screen_selector = new module.ScreenSelector({
                pos: this.pos,
                screen_set:{
                    'products': this.product_screen,
                    'payment' : this.payment_screen,
                    'client_payment' : this.client_payment_screen,
                    'scale_invite' : this.scale_invite_screen,
                    'scale':    this.scale_screen,
                    'receipt' : this.receipt_screen,
                    'welcome' : this.welcome_screen,
                },
                popup_set:{
                    'help': this.help_popup,
                    'error': this.error_popup,
                    'error-product': this.error_product_popup,
                    'error-session': this.error_session_popup,
                    'error-negative-price': this.error_negative_price_popup,
                    'choose-receipt': this.choose_receipt_popup,
                },
                default_client_screen: 'welcome',
                default_cashier_screen: 'products',
                default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
            });

            if(this.pos.debug){
                this.debug_widget = new module.DebugWidget(this);
                this.debug_widget.appendTo(this.$('#content'));
            }
        }, 

We add the widget that will display the Pay button we will see later to the list of widgets to build .

 
Sélectionnez
 
this.gotopay = new module.GoToPayWidget(this, {});                  
this.gotopay.replace($('#placeholder-GoToPayWidget'));  

Here it says that the widget will be placed in the <div id="placeholder-GoToPayWidget"></ div> tag that we put in the view later.

VIII-A-3. The CashierPayScreenWidget module

This module will allow us to add a function to the original module.PaymentScreenWidget module (which is in screen.js file of point_of_sale module), again with the include() function

PaymentScreenWidget
Sélectionnez
 module.CashierPayScreenWidget = module.PaymentScreenWidget.include({
    template: 'PaymentScreenWidget', 

    show: function(){
        this._super();
        var self = this;
        this.$('#pay-screen-cashier-name').html(globalCashier);
        this.$('#ticket-screen-cashier-name').html(globalCashier);
        this.pos.get('selectedOrder').set_cashier_name(globalCashier);

        this.paypad = new module.PaypadWidget(this, {});
        this.paypad.replace($('#placeholder-PaypadWidget'));
    },

});  

The module will be attached to the PaymentScreenWidget template view .

We will add some instructions in the show() function of the module.

We will first of all get the name of the cashier who is in the globalCashier global variable that will be displayed on the payment page and on the receipt.

It will then record the name of the cashier in the current order with the following function

set_cashier_name
Sélectionnez
 this.pos.get('selectedOrder').set_cashier_name(globalCashier); 

This function does not exist yet, we'll add it later.

Finally, we will recreate the payment buttons that I removed,. In addition, they will be destroyed at the end of each order.

PaypadWidget
Sélectionnez
 this.paypad = new module.PaypadWidget(this, {});
this.paypad.replace($('#placeholder-PaypadWidget')); 

The PaypadWidget module is the original one created for the Point Of Sale. This is the one that was removed before it was next to the keypad, which was replaced with the Pay button.

VIII-A-4. The CashierReceiptScreenWidget module

Here we will add instructions in the original function refresh() of ReceiptScreenWidget module that is in screen.js file of point_of_sale module. This is the module that will display the name of the cashier on the receipt.

In fact, it will copy the original function and add the last three lines (the if() statement).

CashierReceiptScreenWidget
Sélectionnez
 module.CashierReceiptScreenWidget = module.ReceiptScreenWidget.include({

    refresh: function() {
        this._super();
        $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this}));

        if(globalCashier != ''){
            this.$('#ticket-screen-cashier-name').html(globalCashier);           
        }         
    }, 
 }); 

You see that we get the name of the cashier who is in globalCashier variable to send it to <div id="ticket-screen-cashier-name"></ div> tag that is diplayed on the receipt. We'll see that later when we take care of the views (XML).

VIII-A-5. GoToPayWidget module

Now we will create the module that will host the Pay button.
Warning, this is not the button itself, it is simply the « container » that will host the button.

 
Sélectionnez
 module.GoToPayWidget = module.PosBaseWidget.extend({
    template: 'GoToPayWidget',
    init: function(parent, options) {
        this._super(parent);
    },

    renderElement: function() {
        var self = this;
        this._super();

        var button = new module.GoToPayButtonWidget(self);
        button.appendTo(self.$el);
    },
}); 

As for the other original Point Of Sale modules, these are extensions of PosBaseWidget module.

We assign the template view with the following statement:

 
Sélectionnez
 template: 'GoToPayWidget', 

Then adding the init() function like the original modules.

We will then add a function that will command QWeb (the rendering engine templates) to add the Pay button, we will see just after this module (in its container, somehow).

renderElement
Sélectionnez
 renderElement: function() {
    var self = this;
    this._super();

    var button = new module.GoToPayButtonWidget(self);
    button.appendTo(self.$el);
}, 

That's all for this module

VIII-A-6. GoToPayButtonWidget module (Pay button)

This time, we will create the module of the Pay button.
As with the previous module, it will be an extension of the basic PosBaseWidget module, we assign the GoToPayButtonWidget template view , we add the init() function that goes well, then we also add the renderElement() function.

GoToPayButtonWidget
Sélectionnez
 module.GoToPayButtonWidget = module.PosBaseWidget.extend({
    template: 'GoToPayButtonWidget',
    init: function(parent, options) {
        this._super(parent);
    },

    renderElement: function() {
        var self = this;
        this._super();

        this.$el.click(function(){
            self.pos_widget.screen_selector.set_current_screen('payment');
         });
    },
 }); 

We add click() function in renderElement().
This function is performed, you guessed it, when clicking on the button (onclick() event).
This function itself will call the function to display different views of Point Of Sale.

In this case, it will display the payment page.

set_current_screen()
Sélectionnez
 this.$el.click(function(){
    self.pos_widget.screen_selector.set_current_screen('payment');
}); 

VIII-A-7. Order module

This is the module that creates the orders, which stores them in the browser (LocalStorage), and which then sends the orders to the database.

It is not possible to overload the original module, because when initializing, the module returns an object (the order) to the Point Of Sale, and we need to modify it to take into account the name of the cashier.

We'll copy/paste the entire module you will find in model.js file of point_of_sale module and we will add two or three things.

I put the code of the module and I'll explain only the functions or objects I added.

module.Order
Sélectionnez
 
module.Order = Backbone.Model.extend({
        initialize: function(attributes){
            Backbone.Model.prototype.initialize.apply(this, arguments);
            this.set({
                creationDate:   new Date(),
                orderLines:     new module.OrderlineCollection(),
                paymentLines:   new module.PaymentlineCollection(),
                name:           "Order " + this.generateUniqueId(),
                client:         null,
                cashier_name:   null,
            });
            this.pos =     attributes.pos; 
            this.selected_orderline = undefined;
            this.screen_data = {};  // see ScreenSelector
            this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
            return this;
        },
        generateUniqueId: function() {
            return new Date().getTime();
        },
        addProduct: function(product, options){
            options = options || {};
            var attr = product.toJSON();
            attr.pos = this.pos;
            attr.order = this;
            var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});

            if(options.quantity !== undefined){
                line.set_quantity(options.quantity);
            }
            if(options.price !== undefined){
                line.set_unit_price(options.price);
            }

            var last_orderline = this.getLastOrderline();
            if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){
                last_orderline.merge(line);
            }else{
                this.get('orderLines').add(line);
            }
            this.selectLine(this.getLastOrderline());
        },
        removeOrderline: function( line ){
            this.get('orderLines').remove(line);
            this.selectLine(this.getLastOrderline());
        },
        getLastOrderline: function(){
            return this.get('orderLines').at(this.get('orderLines').length -1);
        },
        addPaymentLine: function(cashRegister) {
            var paymentLines = this.get('paymentLines');
            var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
            if(cashRegister.get('journal').type !== 'cash'){
                newPaymentline.set_amount( this.getDueLeft() );
            }
            paymentLines.add(newPaymentline);
        },
        getName: function() {
            return this.get('name');
        },
        getSubtotal : function(){
            return (this.get('orderLines')).reduce((function(sum, orderLine){
                return sum + orderLine.get_display_price();
            }), 0);
        },
        getTotalTaxIncluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_with_tax();
            }), 0);
        },
        getDiscountTotal: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
            }), 0);
        },
        getTotalTaxExcluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_without_tax();
            }), 0);
        },
        getTax: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_tax();
            }), 0);
        },
        getPaidTotal: function() {
            return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
                return sum + paymentLine.get_amount();
            }), 0);
        },
        getChange: function() {
            return this.getPaidTotal() - this.getTotalTaxIncluded();
        },
        getDueLeft: function() {
            return this.getTotalTaxIncluded() - this.getPaidTotal();
        },
        set_cashier_name: function(name){
            this.set('cashier_name', name);
        },
        // sets the type of receipt 'receipt'(default) or 'invoice'
        set_receipt_type: function(type){
            this.receipt_type = type;
        },
        get_receipt_type: function(){
            return this.receipt_type;
        },
        // the client related to the current order.
        set_client: function(client){
            this.set('client',client);
        },
        get_client: function(){
            return this.get('client');
        },
        get_client_name: function(){
            var client = this.get('client');
            return client ? client.name : "";
        },
        // the order also stores the screen status, as the PoS supports
        // different active screens per order. This method is used to
        // store the screen status.
        set_screen_data: function(key,value){
            if(arguments.length === 2){
                this.screen_data[key] = value;
            }else if(arguments.length === 1){
                for(key in arguments[0]){
                    this.screen_data[key] = arguments[0][key];
                }
            }
        },
        //see set_screen_data
        get_screen_data: function(key){
            return this.screen_data[key];
        },
        // exports a JSON for receipt printing
        export_for_printing: function(){
            var orderlines = [];
            this.get('orderLines').each(function(orderline){
                orderlines.push(orderline.export_for_printing());
            });

            var paymentlines = [];
            this.get('paymentLines').each(function(paymentline){
                paymentlines.push(paymentline.export_for_printing());
            });
            var client  = this.get('client');
            var cashier = this.pos.get('cashier') || this.pos.get('user');
            var company = this.pos.get('company');
            var shop    = this.pos.get('shop');
            var date = new Date();

            return {
                orderlines: orderlines,
                paymentlines: paymentlines,
                subtotal: this.getSubtotal(),
                total_with_tax: this.getTotalTaxIncluded(),
                total_without_tax: this.getTotalTaxExcluded(),
                total_tax: this.getTax(),
                total_paid: this.getPaidTotal(),
                total_discount: this.getDiscountTotal(),
                change: this.getChange(),
                name : this.getName(),
                client: client ? client.name : null ,
                invoice_id: null,   //TODO
                cashier: cashier ? cashier.name : null,
                date: { 
                    year: date.getFullYear(), 
                    month: date.getMonth(), 
                    date: date.getDate(),       // day of the month 
                    day: date.getDay(),         // day of the week 
                    hour: date.getHours(), 
                    minute: date.getMinutes() 
                }, 
                company:{
                    email: company.email,
                    website: company.website,
                    company_registry: company.company_registry,
                    contact_address: company.contact_address, 
                    vat: company.vat,
                    name: company.name,
                    phone: company.phone,
                },
                shop:{
                    name: shop.name,
                },
                currency: this.pos.get('currency'),
            };
        },
        exportAsJSON: function() {
            var orderLines, paymentLines;
            orderLines = [];
            (this.get('orderLines')).each(_.bind( function(item) {
                return orderLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            paymentLines = [];
            (this.get('paymentLines')).each(_.bind( function(item) {
                return paymentLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            return {
                name: this.getName(),
                amount_paid: this.getPaidTotal(),
                amount_total: this.getTotalTaxIncluded(),
                amount_tax: this.getTax(),
                amount_return: this.getChange(),
                lines: orderLines,
                statement_ids: paymentLines,
                pos_session_id: this.pos.get('pos_session').id,
                partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
                user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
                cashier_name: this.pos.get('selectedOrder').get('cashier_name'),
            };
        },
        getSelectedLine: function(){
            return this.selected_orderline;
        },
        selectLine: function(line){
            if(line){
                if(line !== this.selected_orderline){
                    if(this.selected_orderline){
                        this.selected_orderline.set_selected(false);
                    }
                    this.selected_orderline = line;
                    this.selected_orderline.set_selected(true);
                }
            }else{
                this.selected_orderline = undefined;
            }
        },
    });   

At the beginning of the module, we will add a cashier_name field to the order.

initialize()
Sélectionnez
 initialize: function(attributes){
    Backbone.Model.prototype.initialize.apply(this, arguments);
    this.set({
        creationDate:   new Date(),
        orderLines:     new module.OrderlineCollection(),
        paymentLines:   new module.PaymentlineCollection(),
        name:           "Order " + this.generateUniqueId(),
        client:         null,           <!-- DO NOT FORGET COMMA -->
        cashier_name:   null,           <!--  ADD cashier_name FIELD HERE -->
    });
    this.pos =     attributes.pos; 
    this.selected_orderline = undefined;
    this.screen_data = {};  // see ScreenSelector
    this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
    return this;
}, 

As you can see, the order contains multiple fields. We simply add the cashier_name field.
Now that the field is created, we can send the name of the cashier to the order when needed.

Then we will add a function in the list of functions that already exist.

set_cashier_name()
Sélectionnez
 set_cashier_name: function(name){
    this.set('cashier_name', name);
}, 

When set_cashier_name() function is called, it will send the name of the cashier in the field we added earlier.

If you well remember, this function is called in the show() function of CashierPayScreenWidget module.
So when the payment page will be displayed, the name of the cashier will be sent to the order.

Now we will modify the function that sends the order to the database.

exportAsJSON()
Sélectionnez
 exportAsJSON: function() {
    var orderLines, paymentLines;
    orderLines = [];
    (this.get('orderLines')).each(_.bind( function(item) {
        return orderLines.push([0, 0, item.export_as_JSON()]);
    }, this));
    paymentLines = [];
    (this.get('paymentLines')).each(_.bind( function(item) {
        return paymentLines.push([0, 0, item.export_as_JSON()]);
    }, this));
    return {
        name: this.getName(),
        amount_paid: this.getPaidTotal(),
        amount_total: this.getTotalTaxIncluded(),
        amount_tax: this.getTax(),
        amount_return: this.getChange(),
        lines: orderLines,
        statement_ids: paymentLines,
        pos_session_id: this.pos.get('pos_session').id,
        partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
        user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
        cashier_name: this.pos.get('selectedOrder').get('cashier_name'),
    };
}, 

We added the cashier_name field in the return of the function.

cashier_name
Sélectionnez
 cashier_name: this.pos.get('selectedOrder').get('cashier_name'), 

This time, we get the name of the cashier who was sent to the order earlier.

The openerp_pos_cashier module() is finished!

VIII-A-8. The best for last

The JavaScript file is almost complete.
We now include our module inside the Point Of Sale.

For this, there is no other way than to take the function that creates the Point Of Sale and add our module.

So we add, following the module, the openerp.point_of_sale() function that is in the main.js file of point_of_sale module.

openerp.point_of_sale()
Sélectionnez
 openerp.point_of_sale = function(instance) {
    instance.point_of_sale = {};

    var module = instance.point_of_sale;

    openerp_pos_db(instance,module);            // import db.js
    openerp_pos_models(instance,module);        // import pos_models.js
    openerp_pos_basewidget(instance,module);    // import pos_basewidget.js
    openerp_pos_keyboard(instance,module);      // import  pos_keyboard_widget.js
    openerp_pos_scrollbar(instance,module);     // import pos_scrollbar_widget.js
    openerp_pos_screens(instance,module);       // import pos_screens.js
    openerp_pos_widgets(instance,module);       // import pos_widgets.js
    openerp_pos_devices(instance,module);       // import pos_devices.js

    // cashiers
    openerp_pos_cashier(instance,module);       // import openerp_pos_cashier

    instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
}; 

We add the openerp_pos_cashier line.

And that's it for the pos_cashier.js file !!!

VIII-B. the pos_cashier.xml file

This is the file of views needed to display the data in the Point Of Sale.

This file is placed in the xml directory of the module.

/opt/modules-openerp/pos_cashiers/static/src/xml
pos_cashier.xml
Sélectionnez
 <?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates id="template" xml:space="preserve">

    <!-- Cashiers drop-down list under NumPad -->
    <t t-extend="PosWidget" >
        <t t-jquery="footer" t-operation="append">
            <div id="AlertNoCashier">You must create at least one cashier!</div>
            <div id="cashier-footer">
                <div id="cashier-title">
                    Select a cashier :            
                </div>
                <div id="cashier-frame">
                    <t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
                    <select id="cashier-select"></select>   
                </div>           
            </div>
        </t>
    </t>

    <!-- Name of the cashier on Payement Page -->
    <t t-extend="PaymentScreenWidget" >
        <t t-jquery=".pos-step-container" t-operation="prepend">
            <div id="pay-screen-cashier">Cashier : 
                <span id="pay-screen-cashier-name">
                </span>
            </div>
        </t>
    </t>

    <!-- Name of the cashier on Ticket -->
    <t t-extend="PosTicket" >
        <t t-jquery="#header-ticket" t-operation="append">
            Cashier : <span id="ticket-screen-cashier-name"></span>
        </t>
    </t>

</templates> 

Here we will use special tags that will allow Qweb rendering engine to insert objects in the page.

VIII-B-1. The dropdown cashiers

We will place the dropdown cashiers under the keypad.

PosWidget
Sélectionnez
 
    <!-- Cashiers drop-down list under NumPad -->
    <t t-extend="PosWidget" >
        <t t-jquery="footer" t-operation="append">
            <div id="AlertNoCashier">You must create at least one cashier!</div>
            <div id="cashier-footer">
                <div id="cashier-title">
                    Select a cashier :            
                </div>
                <div id="cashier-frame">
                    <t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
                    <select id="cashier-select"></select>   
                </div>           
            </div>
        </t>
    </t> 

To modify the original view, we use the t-extend attribute.

t-extend
Sélectionnez
 
    <!-- Cashiers drop-down list under NumPad -->
    <t t-extend="PosWidget" >
        -
        -
        -
    </t> 

As you see above, a template is written inside <t></t> tags.

To see the different attributes and functions, I invite you to read this page on the publisher's website:
Documentation QWebDocumentation QWeb

Then, just as in the XML views of Python module where we used the position attribute to place objects before, after or in place of the objects of the original template, we will use here the t-operation attribute preceded with the t-jquery attribute to specify the object of original template.

t-jquery
Sélectionnez
 
        <t t-jquery="footer" t-operation="append">
            <div id="AlertNoCashier">You must create at least one cashier!</div>
            <div id="cashier-footer">
                <div id="cashier-title">
                    Select a cashier :            
                </div>
                <div id="cashier-frame">
                    <t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
                    <select id="cashier-select"></select>   
                </div>           
            </div>
        </t> 

Here, we wish to add objects to <footer></ footer> tag , following those of the original PosWidget template.

We can see the tag which contains the error message in case of no cashier, followed by the drop-down list of cashiers.

The dropdown list
La liste déroulante
The error message if no cashier
Le message d'erreur pour défaut de caissier
Cashiers dorpdown list
Sélectionnez
 
<t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
<select id="cashier-select"></select>   

The first tag uses the t-esc attribute that can insert standard JavaScript commands.
At the loading of the Point Of Sale, we get the ID of the Point Of Sale with get_cur_pos_config_id() function.

Then, just below, we insert the dropdown list of cashiers who belong to this Point Of Sale.

To display the name of the cashier on the payment page, we will extend the original PaymentScreenWidget module.

PaymentScreenWidget
Sélectionnez
 
    <!-- Name of the cashier on Payement Page -->
    <t t-extend="PaymentScreenWidget" >
        <t t-jquery=".pos-step-container" t-operation="prepend">
            <div id="pay-screen-cashier">Cashier : 
                <span id="pay-screen-cashier-name">
                </span>
            </div>
        </t>
    </t> 
Name of the cashier on Payement Page
Name of the cashier on Payement Page

Finally, the cashier's name must also appear on the receipt, so we will also extend the original PosTicket module.

PosTicket
Sélectionnez
 
    <!-- Name of the cashier on Ticket -->
    <t t-extend="PosTicket" >
        <t t-jquery="#header-ticket" t-operation="append">
            Cashier : <span id="ticket-screen-cashier-name"></span>
        </t>
    </t> 
Name of the cashier on receipt
Name of the cashier on receipt

That's it for the XML file.

VIII-C. pos_cashier.css file

A simple *.CSS file to put in css directory of the module :

/opt/modules-openerp/pos_cashiers/static/src/css
pos_cashier.css
Sélectionnez
 
#cashier-title{
    vertical-align: middle;
    display:inline-block;
    text-align: left;
    font-size: 16px;
    font-weight: normal;
    font-style: italic;
    width: 45%;
}

#cashier-frame {
    text-align: center;
    vertical-align: middle;
    display:inline-block;
    border: 1px solid #000000;
    width: 55%;
    padding: 5px 0px 5px 0px;
}

#cashier-select{
    width:95%;
}

#cashier-footer{
    background: linear-gradient(#7B7979, #393939) repeat scroll 0 0 transparent;
    display:block;  
    color: #ffcc00;
    padding: 10px 5px 10px 5px;
}

#pay-screen-cashier{
    color: black;
    border-bottom: 1px dashed #666666;
    padding: 2px 2px 2px 2px;
    text-align: left;
    font-size: 14px;
    font-weight: normal;
    font-style: italic;
}

#ticket-screen-cashier{
    font-style: italic;
    border-bottom : 1px solid gray;
    padding-bottom: 2px;
}

#AlertNoCashier{
    background: red url("../img/error.png") no-repeat 4px;
    color: white;
    font-size: 14px;
    font-weight: bold;
    padding: 12px 4px 4px 30px;
    height: 24px;
    text-transform: uppercase;
} 

Here you can put your additional styles for your module, and you can also edit the original ones, if needed,

IX. Installation of the module

This time, we can install our module.
Connect to OpenERP as an administrator, and then click the Configuration menu.

Click on « Update list of modules » link
Then click « Installed Modules » and delete the « Installed » filter in the search bar.

Our module will appear.

Click the Install button and wait until the end of the installation.

The POS Cashiers module in installed modules
The POS Cashiers module in installed modules

X. Internationalisation

You have noticed that the text, labels and titles were all in English?
There are two reasons for this.

  • The first one , is that the module is fully functional, it may therefore be useful to other people. It will just make them translated into their language. I could do it, but I speak not very well Hungarian nor Korean ...
  • The second reason is that I wanted to explain how to do the translation of a module. This is the best reason, finally.

The international system is a bit complex.
Know that we need to create a pos_cashier.pot file, we put in the i18n directory.

/opt/modules-openerp/pos_cashier/i18n

From this pos_cashier.pot file, we can create files for different languages​​.

A *.pot file is a translation template file. It contains only the original terms, it does not contain the translated words.

But creating a *.pot from A to Z is a bit complicated.
Fortunately, our beloved OpenERP Dev Team have thought of everything.

Make a *.pot file

  • Connect to OpenERP as administrator
  • click Configuration in top menu.
  • In Translation heading, click Export translation.
  • The form below appears
Export translation file
Export translation file
  • In Langage field, select New Language.
  • In File format, select PO File.
  • In Modules to export, select POS Cashiers.
  • Click Export.
  • A second window is displayed.
Download translation file
Download translation file
  • Download the file by clicking the download link.
  • Rename the file to pos_cashier.pot.
  • And open it with your text editor.
pos_cashier.pot
Sélectionnez
 
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
#    * pos_cashier
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0-20130703-231023\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-13 00:04+0000\n"
"PO-Revision-Date: 2013-07-13 00:04+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: pos_cashier
#: view:pos.cashier:0
msgid "All"
msgstr ""

#. module: pos_cashier
#: model:ir.model,name:pos_cashier.model_pos_order
msgid "Point of Sale"
msgstr ""

#. module: pos_cashier
#: model:ir.actions.act_window,help:pos_cashier.action_pos_cashier
msgid "<p class=\"oe_view_nocontent_create\">\n"
"          Click here to create a cashier for the Point Of Sale.\n"
"        </p>\n"
"      "
msgstr ""

#. module: pos_cashier
#: view:pos.cashier:0
msgid "Point of Sale Cashier"
msgstr ""

#. module: pos_cashier
#: field:pos.cashier,cashier_name:0
#: field:pos.order,cashier_name:0
msgid "Cashier"
msgstr ""

#. module: pos_cashier
#: view:pos.cashier:0
msgid "Inactive"
msgstr ""

#. module: pos_cashier
#: help:pos.cashier,active:0
msgid "If a cashier is not active, it will not be displayed in POS"
msgstr ""

#. module: pos_cashier
#: sql_constraint:pos.cashier:0
msgid "A cashier already exists with this name in this Point Of sale. Cashier's name must be unique!"
msgstr ""

#. module: pos_cashier
#: view:pos.cashier:0
#: field:pos.cashier,active:0
msgid "Active"
msgstr ""

#. module: pos_cashier
#: model:ir.model,name:pos_cashier.model_pos_cashier
msgid "pos.cashier"
msgstr ""

#. module: pos_cashier
#: model:ir.actions.act_window,name:pos_cashier.action_pos_cashier
#: model:ir.ui.menu,name:pos_cashier.menu_action_pos_cashier
#: model:ir.ui.menu,name:pos_cashier.menu_point_of_sale_cashiers
#: view:pos.cashier:0
msgid "Cashiers"
msgstr ""

#. module: pos_cashier
#: field:pos.cashier,pos_config_id:0
msgid "Point Of Sale"
msgstr ""

Here's what looks like a *.pot.

Before the copy, we will add some instructions.
You may have noticed that the words that are in this file are the names of the fields, constraints or comments that we have created in the tables.

We also want to translate words that we « hard coded » in the XML files of some views.

In particular, we want to translate the error message that appears in the Point Of Sale when there is no cashier, we also want to translate the word « cashier », etc..

We will add portions of code as below.

pos_cashier.pot
Sélectionnez

#. module: pos_cashier
#. openerp-web
#: code:static/src/xml/pos_cashier.xml:9
#, python-format
msgid "You must create at least one cashier!"
msgstr ""

Again, since there is no documentation on this, I poked around in the translation files of other modules.
It can translate a word or phrase in an XML file specifying the source of the file (from the root module) followed by the line number.

In the example above, the sentence to be translated is line 9 of static/src/xml/pos_cashier.xml file.

Then we add the following two translations.

pos_cashier.pot
Sélectionnez

#. module: pos_cashier
#. openerp-web
#: code:static/src/xml/pos_cashier.xml:12
#, python-format
msgid "Select a cashier :"
msgstr ""

#. module: pos_cashier
#. openerp-web
#: code:static/src/xml/pos_cashier.xml:26
#: code:static/src/xml/pos_cashier.xml:36
#, python-format
msgid "Cashier :"
msgstr ""

Please note that the string to be translated is in front of the msgid keyword. This is the identifier of the chain. On the next line, we have the msgstr keyword followed by an empty string.
Save the file, then copy the file renaming, this time to fr.po. This will be our translation file for the French language. Obviously, you'll understand it then just add the translations of the strings in the corresponding msgstr.

Here is an excerpt of fr.po  file:

fr.po
Sélectionnez

#. module: pos_cashier
#: help:pos.cashier,active:0
msgid "If a cashier is not active, it will not be displayed in POS"
msgstr "Un caissier désactivé ne sera pas visible dans le Point De Vente"

#. module: pos_cashier
#: sql_constraint:pos.cashier:0
msgid ""
"A cashier already exists with this name in this Point Of sale. Cashier's "
"name must be unique!"
msgstr ""
"Un caissier existe déjà avec le même nom dans ce Point De Vente. Le nom du "
"caissier doit être unique!" 

If you want to translate into several languages​​, simply copy and rename the pos_cashier.pot file to a xx.po file . For different languages ​​taken into account, just look in the i18n directory of the other modules.

While you're there, then duplicate the fr.po file for Belgium and Switzerland.

When you have completed the translation files, you will need to restart OpenERP sure everything loads properly.

Then you will be able to go into the POS, tickle the drop-down list after creating two or three cashiers!

XI. Conclusion

In this tutorial we have seen a lot of things, finally:

  • the creation of a Python object;
  • the creation of XML views (form view and tree view) ;
  • the creation of search filters;
  • the creation of a menu ;
  • the module access rights;
  • the records rules ;
  • add an icon to the module ;
  • modify the Point Of sale (add a dropdown list and a button) ;
  • QWeb  and templates;
  • the installation of a module ;
  • the translation of a module.

Do not hesitate to look at documentation on the publisher's website, including :

If you have any improvements or even corrections, do not hesitate to send me your comments.

Thank you.

XII. Download module

XIII. Thanks

Many thanks to the members of the Developpez.com team for their advice and corrections.