I. Introduction

WARNING

This is the english translation of the french article 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 Forum

Thank you for reading

Point Of Sale module (POS OpenERP) is a basic one . That is to say, it can sell products and that's all.

However, when used in a company as the main software, it turns out to be too short in this form, and request certain changes . This is also where we can truly realize the flexibility of OpenERP and quality of ORM . In fact , I think ,for me, it's a great program because it allows precisely to do everything you want on a very well organized and very well thought out basis . It is my opinion, but it is shared by many people in the OpenERP community.

As all software/firmware, it is not flawless , but they can be corrected fairly quickly, and you can expand its capabilities or functions at infinity.

In this case, in the event that kept me busy for quite some time , we wanted the sellers to connect with only one account, they can access customer files to modify or create new ones and that they may also have a view of the history of their purchases. We also wanted to sell "on demand" packages and apply various discounts . Also we have imagined being able to communicate with all shops by sending a simple message that would be displayed directly in the point of sale.

Here then, after many hours of research and development , a module that adds these features to the point of sale, tg_pos_enhanced Module

My knowledge of OpenERP is still limited, it is likely that I have not used the ORM or JQuery maximum. However, in this article you may understand certain concepts or functions of the system and it should guide you in developing your own modules.

However, if you find any defect or error, or if you want to clarify something, do not hesitate to discuss on the forum.

Module tg_pos_enhanced : le point de vente revisité
Module tg_pos_enhanced : POS revisited

To recap, the point of sale is the main application used by vendors, we wanted all of these functions are accessible directly from the POS itself, that is to say without having to disconnect to open another application.

II. Warning

tg_pos_enhanced module matches certain needs and it is possible that your company does not have the same, which is why you might find this or that useless function in your case, but then you are free to modify the module to adapt it for your needs.

You will have in all cases, an additional basis on which to rely, especially as I'll explain the operation of the module and some important functions.

Due to the design of tg_pos_enhanced module, the POS will no longer work in « Off-line » mode, because as the module makes calls to the OpenERP database , it requires a quasi-permanent connection therewith.

This is the choice that I made because in the current operating point of sale, all the data is loaded at the opening of the session. This can be playable if you have few products, but tg_pos_enhanced module allowing, among other things, select, edit or create customers, it is best not to load all at the opening of the POS, especially if you have tens of thousands. On the other hand, the module can also display sales history, it was not thinkable to load all the data in the browser (LocalStorage).

III. Module Overview

For practical reasons, the module has been divided into four modules:

  • le module tg_partner_firstname : a very simple module, it adds a « firstname » field to customers;
  • le module tg_pos_packs : this is the Custom Pack module ;
  • le module tg_pos_message : this is the internal POS messaging module;
  • le module tg_pos_enhanced : this is main module.

III-A. tg_partner_firstname module

Few lines of Python and a few lines of XML suffice for this module simply adds a firstname field in res_partner model.

Names are formatted in uppercase and firstname are capitalized.

III-B. tg_pos_packs module

This module allows to create packages from existing products.

Pack created becomes a « service », it will not generate stock movement, it can contain multiple items (as many as you want), each item can contain several products (as many as you want).

When selling, we choose a product item, and we can also choose the variant of the product (color, size, etc.).

This is the package price that will be displayed in the order, the products in turn have their prices reduced to 0.00 euro and they generate stock movements.

III-C. tg_pos_message module

It allows to create and schedule HTML messages that will be displayed in the selected POS during a specified period.

This is useful in the case of a company with several stores in order to quickly communicate with them.

III-D. tg_pos_enhanced module

It incorporates all the functions and uses of previous modules.

tg_pos_enhanced module provides the following features and changes:

  • « cashiers » function : it can make sales using the « manager » account from the point of sale without having to disconnect/reconnect each time a seller makes a sale;
  • customer management functions: they allow you to select, edit, or add customers without leaving the point of sale;
  • purchases history : After selecting a customer, you can view purchases he made. This will, for example, to verify that the customer has indeed purchased the product that brings in case of malfunction (after sales service);
  • POS receipt : It provides details of products, taxes and discounts as well as the cumulative;
  • « Custom Packs » : These are packs of products. You can choose the products that make up the pack as well as product variants if any;
  • special discount : as account « manager » is used by everyone, we need global discounts applied is controlled by the store manager. This function allows him to apply a special discount to a customer. To do this, he must enter a password for this purpose, and must specify the reason for the discount;
  • Internal messaging : messages are displayed in the point of sale. They can be programmed to appear only once or every X hours;
  • return of product : this function allows to return a product generating a stock movement. In this present case, a negative amount can not succeed, because we wanted a returned product is subject to a minimum of an exchange;

IV. tg_partner_firstname detail

This module allows you to simply add the « firstname » field (firstname) to customers (res.partner)

Indeed, OpenERP has not provided this field and the problem is that when using the search box to find a customer by entering three or four letters of a name, the returned results will not always correspond to your expectations.

For example, if you want the "Richet" customer by entering « ric » in the field to refine your search, OpenERP may return this:

  • richard alain ;
  • mathurot eric ;
  • stephan brice ;
  • richet alain.

Although this function can sometimes be useful, it is not when it comes to finding people by name. This is when the « firstname » field occurs. You'll have to set correctly « name » and « firstname » fields, but once in the point of sale, when you use the search field, the search will then only on the "name" field.

To see the effects of this module, go to the « Sale » module (sale) and click on the « Partners » menu (customers) and click the « Edit » button. The following form appears:

tg_partner_firstname : firstname field in customer form
tg_partner_firstname : firstname field in customer form

As previously stated, the full names are formatted. We detected that sellers entering names / firstnames anyhow (uppercase, lowercase), so we apply the following rule: all names in uppercase firstnames capitalized.

Development level, it does not require much time, as shown in the following class:

It simply adds a field then create() and write() functions are overriden to take into account the formatting of names/firstnames.

tg_partner_firstname.py
Sélectionnez
class inherit_res_partner(osv.osv):
    _name='res.partner'
    _inherit='res.partner'

    def write(self, cr, uid, ids, vals, context=None):
        v_name = None
        v_firstname = None

        if vals.get('name'):
            # name to Uppercase
            v_name = vals['name'].strip()
            vals['name'] = v_name.upper()

        if vals.get('firstname'):
            # firstname capitalized
            v_firstname = vals['firstname'].strip()
            vals['firstname'] = v_firstname.title()
            
        result = super(inherit_res_partner,self).write(cr, uid, ids, vals, context=context)
        return result

    def create(self, cr, uid, vals, context=None):
        v_name = None
        v_firstname = None

        if vals.get('name'):
            # name to Uppercase
            v_name = vals['name'].strip()
            vals['name'] = v_name.upper()

        if vals.get('firstname'):
            # firstname capitalized
            v_firstname = vals['firstname'].strip()
            vals['firstname'] = v_firstname.title()

        result = super(inherit_res_partner,self).create(cr, uid, vals, context=context)
        return result

    _columns = {
        'firstname' : fields.char('Firstname', size=128),
        }

Above is the illustration of overriding the native functions of the ORM. Here, our write() and create() functions will be executed before calling the functions of the ORM with the super() function

See ORM functions in Memento Technique

About the view, a simple overload of the original view to add the field:

view_tg_partner_form
Sélectionnez
<!-- Vue formulaire -->
<record model="ir.ui.view" id="view_tg_partner_form">
    <field name="model">res.partner</field>
    <field name="name">view.tg.partner.form</field>
    <field name="view_type">form</field>
    <field name="inherit_id" ref="base.view_partner_form"/>
    <field name="arch" type="xml">
        <field name="name" position="after">
            <h5 class="oe_edit_only">
                <label for="firstname"/>
            </h5>
            <h2>
                <field name="firstname"/> 
            </h2>     
        </field>
    </field>
</record>

The field is placed just after the « name » field .

As you can see in tg_partner_view.xml I also overloaded « tree » and « kanban » views.

V. tg_pos_packs detail

tg_pos_packs module depends on product_variant_multi module developed by Akretion. You will also find in the zip file under "Download"

Exist in OpenERP module for creating BOM. But these bills are frozen, they do not allow you to select « on demand » products that compose them.

That is why I imagined a kind of Custom Pack.

Custom Pack can conatin as many products as you want. During the sale, simply select among products.

Obviously, you'll have to build the pack with products having substantially the same price.

V-A. Build a Custom Pack

First and foremost, you must have inserted items in the « Sale » module (sale).

To create a package, proceed as to create a normal product. Warning, do not use the « Product template » menu of product_variant_multi module, it allows to create product models with variations, then generate products.

Custom Pack : onglet Informations
Custom Pack : Informations tab

As you see in the picture above, when you select the « Custom Pack » box, the type of the product becomes a « service », it is not editable, and the tab « Pack » appears.

Click « Pack » tab.

Custom Pack : onglet Pack
Custom Pack : Pack tab

Click on the « Add an item » link to add products. Here, in fact you add models (Product template). Do not worry, the templates are created automatically.

Products are grouped by numbered groups.

That you understand the principle of operation, here's how the package will appear in the point of sale:

Custom Pack : Point de vente - sélection de produits
Custom Pack : POS - product selection

You have noticed that in the pack, two elements were in the « 2 » group (yellow).

Each group of items will be displayed in the « Item » drop down list. If several products are in the same group, then they appear in the same list (item). So you can choose among the products from the list and propose several combinations to your customers.

Here, the customer will have the choice between « Ride Cymbal Meinl Byzance 20 » and « Zildjian K Custom Ride 20 » pack.

To group products together, simply change the number in the « Group number » field of the form.

To make it more readable, I made ​​sure that the groups appear in different colors.

Similarly, a product can have several variants as in the image below:

Custom Pack : Point de vente - choix de la variante
Custom Pack : POS- variant choice

Product variants appear in the list on the right. If a product has no variant, there will be no choice, the list will be the name of the product.

If a product has several variants, then simply select the desired variant by the customer. Here , the variant is a color.

The pack will contain a « Sonor Essential Floor Tom 16x16 » and the customer can choose the color:

  • Piano Black ;
  • Dark Forest ;
  • Maple 46 ;
  • Blue fade ;
  • Brown fade ;
  • Green fade.

Obviously, these products already exist in the point of sale, they can also be sold individually.

All combinations are available to you.

If you sell car parts and you want in your package the customer can buy two batteries + a wheel, you will then need 3 groups.

You can quite offer the same group twice.

For example, you offer several batteries of different brands and different colors. You can make a first group:

  • Batterie Fulmen 12V (rouge, vert, bleu) ;
  • Batterie Varta 12V (grise, rouge) ;
  • Batterie Feu Vert 12V (noire) : pas de variante.

Then create the second group identical and your customer will go with a red Fulmen and Black Green Fire if desired.

Add in your pack, the train wheels in the « 3 » group and the deal is done.

You can put as many items as you want in a Custom Pack.

At installation, the module creates a category of « Custom Packs » in the point of sale. The packs are automatically placed in this category when you create them.

VI. tg_pos_message detail

This module allows you to create messages that appear in the point of sale.

Here is a concrete use case:

A customer uses a product you sold, it hurts, poisons, or whatnot ... in short, the product should be withdrawn from sale urgently.

Then you just create a new message and schedule it appears every hour for the next 3 days.

Another case in point: sales of product X are down, encourage sellers to highlight by displaying a message every morning for a week.

Or just send encouragement to your sales teams or wish them the new year!

The module menu appears only for the administrator (base.group_no_one) in the Point Of Sale /Tools menu.

Le module de messagerie du point de vente
POS messaging

You can set a start date and an end date, choose to display the message only once or to display every X hours.

You can select points of sale recipients of the message.

An icon will be displayed on the message according to the message type, like dialog boxes in software.

The message can be in HTML format. You can put images (hosted) taking into account the average screen size of your stores. Do not post an image of size greater than the smaller screen resolutions.

Messages are retrieved to the logon point of sale, and whenever you create a new order.

Le module de messagerie du point de vente : un message
POS messaging : a message

Requires that the user clicks on the button with the X in the upper right corner to close the message and continue sales.

At this time, the playback date is stored in the database (datetime), it will then be used when retrieving new messages according to the time interval that you set if this is the case.

VII. tg_pos_enhanced detail

This is the biggest piece.

Le point de vente
The Point Of sale

Here discussed overloading the original module (point_of_sale) so that in case of updating OpenERP changes are not overwritten

Here is the point of sale revisited. All modules previously reported find their use here.

VII-A. The left panel (#leftpane)

To start, here is the modified left side panel(#leftpane) 

#leftpane
#leftpane

Includes (1) the cashiers moduleOpenERP Tutorial: Module creation and modification of the Point Of Sale , who this time is integrated into the tg_pos_enhanced module itself.

Above the cart (2), the selected customer panel with search and creation buttons for customers.

VII-A-1. About cashiers module

A small change was made.

In case you have created more than one cashier, each new command resets cashiers list and you must select your cashier name again in the list .

La liste des caissiers se réinitialise à chaque nouvelle commande
The list of cashiers resets each new command

This is to ensure that all vendors do not use the same cashier.

Sélection d'un caissier
Cashier selection

The portion of code that allows this :

tg_pos.js (ligne 2837)
Sélectionnez
if(nbCashiers > 1){
    var new_option = '<option value="nobody"></option>\n';
    self.$('#cashier-select').html(content + new_option);
}

If more than one cashier is detected, we add an empty element to the list of the cashiers. It will be selected automatically for each new order as shown in the code below.

tg_pos.js (ligne 905)
Sélectionnez
if(nbCashiers > 1){
    $('#cashier-select').val('nobody');
    globalCashier = 'nobody';
    cashier_change(globalCashier);
}

VII-B. Find a customer

Image non disponible

To find a customer, just click on the icon in the left panel.

You can search for a customer by the first letter of its name or by using the search form.

VII-B-1. Search by letter

Click on a letter to view the customers whose name starts with that letter.

Search a customer by letter
Search a customer by letter

The left panel (1) is grayed out. Click on a letter (2), customers will be displayed in the table (3). When you select a customer, the panel automatically closes, the left panel is no longer grayed out and Products reappear.

You can refresh the table at any times (4), Close the panel or see the number of customers in the table.

VII-B-2. Search with the form

Recherche à l'aide du formulaire
Search with the form

Enter the first letters of the name and then press « Enter » on your keyboard or click on the magnifying glass button.

The search function has been changed.

In OpenERP the search field using the SQL keyword « ilike »but it is actually translated like this « ilike '% word%' ».

Putting the % sign before and after the search word, lets find the object names where the search word appears anywhere. This could be handy at times, but in this case it did not help us too much.

By adding the = sign before ilike, we get the expression « ilike 'word%' ». This is the effect we wanted.

To search the database, I use the following function:

tg_pos.js (ligne 430)
Sélectionnez
// get data from DB
var fetch = function(model, fields, domain, ctx){
    return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all();
};

Here we see the « filter(domain) » attribute.

Suffice it to write a proper filter to get customers by the first letter of their name or by the words entered in the search form.

tg_pos.js (ligne 1966)
Sélectionnez
 var l_filter = [['customer', '=', true], 
                 ['name','=ilike', letter + '%']];

if(letter == '0-9'){
    l_filter = [['customer', '=', true],
                 '|', '|', '|', '|', '|', '|', '|', '|', '|',
                ['name','=ilike', '0%'],
                ['name','=ilike', '1%'],
                ['name','=ilike', '2%'],
                ['name','=ilike', '3%'],
                ['name','=ilike', '4%'],
                ['name','=ilike', '5%'],
                ['name','=ilike', '6%'],
                ['name','=ilike', '7%'],
                ['name','=ilike', '8%'],
                ['name','=ilike', '9%']
               ];
}

In case you would click on the button « 0-9 » to search for a customer whose name begins with a digit , the l_filter filter is then redefined to retrieve all customers whose names begin with any digit.

The « letter » variable here is the letter that was clicked, or the word entered in the search field using this function:

tg_pos.js (ligne 2421)
Sélectionnez
this.$('#input_search').keypress(function(e) {
    if(e.which == 13) {
        var l = $(this).val().trim();

        if(l != ''){
            $('#input_search').val(l);
            self.get_clients(l);
        }
    }
});

VII-C. A selected customer

When a customer is selected, it appears above the cart as shown in the image below.

Selected customer
Selected customer

Here the customer has accumulated 1040.00 euros purchase in our shops. This is a « VIP »

VII-C-1. Edit or create a customer

You can change the customer information by clicking on the icon with a pencil or create a new customer by clicking on the icon with the + sign.

The following form appears:

Crearte or edit customer
Create or edit customer

When you submit the form, the information is sent directly to the database and the customer is automatically selected for the current order.

To write to the database from JavaScript, you can use the following function:

tg_pos.js (ligne 2066)
Sélectionnez
var Partners = new instance.web.Model('res.partner');

Partners.call('write_partner_from_pos', [cid, cname, cfirstname, czip, cphone, cmobile, cemail, ccomment],undefined,{ shadow:true })
.fail(function(clientId){
    alert('Error : customer has not been created nor updated');
})
.done(function(clientId){

    var sel_client = cname.toUpperCase() + ' ' + cfirstname;
    $('#selected-customer-name').html(ellipseName(sel_client));
    $('#input_partner_id').val(clientId);

    global_letter = sel_client[0];

    self.get_clients(global_letter);
    self.pos.get('selectedOrder').set_partner_id(clientId);
    self.select_client(clientId, cname.toUpperCase(), cfirstname, c_montant_cumule);

    self.hide_form_client();
});

I will not go into too much detail, but the call() function above is applied to the write_partner_from_pos() function found in the Python file:

tg_pos_enhanced.py (ligne 68)
Sélectionnez
# créer / modifier un client depuis le POS
def write_partner_from_pos(self, cr, uid, cid, cname, cfirstname, czip, cphone, cmobile, cemail, ccomment, context=None):
    if context is None:
        context = {}
    name = cname.upper()
    firstname = cfirstname.capitalize()
    # mise a jour client

    client_id = int(cid)

    if client_id != 0:
        
        self.write(cr, uid, client_id, {
             'name': name,
             'firstname': firstname,
             'zip': czip,
             'phone': cphone,
             'mobile': cmobile,
             'email': cemail,
             'pos_comment': ccomment,
             }, context=context)
        idClient = client_id
        
    # creation client
    else:

        idClient = self.create(cr, uid, {
             'name':name,
             'firstname':firstname,
             'zip':czip,
             'phone':cphone,
             'mobile':cmobile,
             'email':cemail,
             'pos_comment': ccomment,
             'company_id':False,
        }, context=context)
        
    return idClient

This function is used to create or modify a customer. Basically, when you send a customer ID is an update, if there is no ID or the ID is 0, it is a creation.

VII-C-2. About « Comment » field

The « comment » field (comment) is a special field. The text you enter here will be displayed on the receipt.

Here is one possible application:

For example, in Portugal, a receipt must have a tax number of the customer. It is this field that will serve to inform this issue.

VII-D. The purchase history for the selected customer

Image non disponible

When a customer is selected and it has already made ​​purchases in your store, you can see the history of their shopping by clicking on the icon above.

Historique des ventes
Purchase history

The left panel is grayed again and the customer sales appear in the main panel.

Click on an order to see the detail.

VII-D-1. To retrieve orders from a customer

Here we call a Python function from the JavaScript file of Point Of Sale with the call() function:

show_histo() - tg_pos.js ligne 2469
Sélectionnez
show_histo: function(){
    var self = this;
            
    // remove sales lines
    $('#sales-list tr').remove();

    var cid = $('#input_partner_id').val();
    if(cid && cid != 0){

        var Mread = new instance.web.Model('pos.order');

        Mread.call('get_partner_orders', [cid], undefined, { shadow:true })
       .fail(function(orders){
            alert('Partner orders : get_partner_orders = failed');
        })
        .done(function(orders){
                    
            if(orders.length > 0){ 
                // la suite du code
            }
        };
     }
},

In the code above, we call get_partner_orders() function is in the file tg_pos_enhanced.py by sending the customer ID.

tg_pos_enhanced (tg_pos_enhanced.py ligne 295)
Sélectionnez
    def get_partner_orders(self, cr, uid, partner_id, context=None):
        if context is None:
            context = {}

        result = []
        o_ids = self.search(cr, SUPERUSER_ID, [('partner_id', '=', int(partner_id))])

        if o_ids:

            for o_id in o_ids:
                order = self.browse(cr, SUPERUSER_ID, o_id)
                
                res_o = {
                    'id': o_id,
                    'pos_reference': order.pos_reference,
                    'date_order': order.date_order,
                    'name': order.name,
                    'state': order.state,
                    'cashier_name': order.cashier_name,
                    'discount': order.discount,
                    'amount_total': order.amount_total
                }
                
                result.append(res_o)
        return result

The function browses the pos.order table and returns the customer's orders if any.

Attention in the case of use OpenERP multi-company, a user can not retrieve orders made ​​by another user, which is why we use here the constant SUPERUSER_ID that allows you to perform actions on ORM as administrator (use with caution).

To this must be added at the beginning of Python file:

SUPERUSER_ID (tg_pos_enhanced.py ligne 29)
Sélectionnez
from openerp import SUPERUSER_ID

VII-E. SellCustom Packs

As we have seen before, it is now possible to sell Custom Packs.

When selecting a Custom Pack in available products, the form below appears:

Formulaire du Custom Pack
Custom Pack form

This is where you can select the products and variants according to the desires of the customer.

When you click on the button « Add Pack to order », the pack appears in the cart as below:

Le Custom Pack dans le panier
Custom Pack in the cart

As you see in the picture above, the name of the package and its components have been modified. They display ■ character for the pack and ├ for the elements (1).

In addition, as you see, only the pack displays its prices, items are pack them to 0.00 euros (2). This will help to display all the products on the receipt and will also remove the products from stock, except the package itself I remind you is a service which generates no stock movement.

On the payment page, nothing changed:

Page de paiement
Paiement page

In the other hand, on the receipt, the Custom Pack appears as in the cart:

Le ticket de caisse avec un Custom Pack
Receipt with a Custom Pack

VII-E-1. How does it work?

Yes, this is probably the question you're asking.

It is not easy but I'll try to explain how it works.

First, the tg_pos_packs module adds a field to the table product.product. The field 'is_pack':

tg_pos_product_pack.py (ligne 52)
Sélectionnez
_columns = {
    'is_pack': fields.boolean('Custom Pack'),
    'pack_ids': fields.one2many('product.pack', 'product_id', 'Items in the pack'),
    }

When a product is a Custom Pack, the field 'is_pack is then set to' True '.

Then, in the tg_pos.js file, it will detect if a product is a Custom Pack or not. For this, I had to modify the original function of the point of sale addProduct() :

add_product() (tg_pos.js ligne 1110)
Sélectionnez
        addProduct: function(product, options){

            options = options || {};
            var attr = product.toJSON();

            // if is_pack = false, this is a real product
            // we send it to the order
            if(attr.is_pack == false){
                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());
            }else{

                // this is a Pack !!
                this.show_screen_pack(attr.name);

                // get templates 
                this.get_templates(attr.id);

            }
        },

It retrieves the attribute 'attr.is_pack' of the product. If it is 'false', it is a common product and it is normally added to the order.

If it is a Custom Pack, then displays the form of Custom Packs and elements/variants of products are retrieved.

When then validates the form of Custom Pack to send the order, we will rename the products for they appear in the cart as a Custom Pack and we will put the price of products to 0.00 euros.

add_products_from_pack() (tg_pos.js ligne 1221)
Sélectionnez
        add_products_from_pack: function(){
            var self = this;
            var nb_items = $('#input_nb_items').val();
            var selectedOrder = this.pos.get('selectedOrder');

            var pack_id = $('#pack_product_id').val()
            var pack_product = null;

            pack_product = self.pos.db.get_product_by_id(parseInt(pack_id));
            
            // add pack product to the order
            if(pack_product){
                var is_pack_previous = pack_product.is_pack;
                var pack_name = pack_product.name;

                pack_product.is_pack = false;
                pack_product.name = ' ' + pack_product.name;

                var m_pack_product = new module.Product(pack_product);
                selectedOrder.addProduct(m_pack_product);

                var cur_oline = selectedOrder.getSelectedLine();
                cur_oline.product.set('is_pack', is_pack_previous);

                pack_product.is_pack = is_pack_previous;
                pack_product.name = pack_name;
            }    

            for(var i = 1; i <= nb_items; i++){
                var field = $('#v_' + i);              
                var product_id = parseInt(field.val());
                var product = self.pos.db.get_product_by_id(product_id);  

                //add products to the order
                if(product){
                    // change name (suffix)  + price = 0.00
                    var previous_name = product.name;
                    var previous_price = product.price;

                    product.name = '├ ' + product.name;
                    product.price = '0.00';

                    var m_product = new module.Product(product);
                    selectedOrder.addProduct(m_product);

                    // change name + price back
                    product.name = previous_name;
                    product.price = previous_price;
                }   
            };

            self.hide_screen_pack();
        },

For the Pack itself and its components, the product are retrieved from the temporary database loaded on boot of POS (LocalStorage) , change the name and the price for the items, and then sends them to the order as standard products by recalling the original function:

tg_pos_js ligne 1239
Sélectionnez
var m_pack_product = new module.Product(pack_product);
selectedOrder.addProduct(m_pack_product);

Then we rename again the products as they were originally.

It's a little twisted, but it is currently the best way I've found.

VII-E-2. Special Features of Custom Pack

You can not select the items of a Custom Pack in the cart. And you can not change the quantity of a Custom Pack. The reason is simple: it is to display Custom Packs separately in the cart and on the receipt.

So you can not delete an item of a Custom pack, however you can delete an entire Custom Pack. In this case, the elements of the Custom Pack are also deleted.

That is why I also changed the function of selecting items in the cart.

select_line() (tg_pos.js ligne 1511)
Sélectionnez
        selectLine: function(line){
            if(line){
                var product_name = line.product.attributes.name;
                var is_pack = line.product.attributes.is_pack;

                if(product_name[1] != ''){
                   // this.selected_orderline.set_selected(undefined);

                    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);
                    }

                    if(is_pack == false){
                        $('#numpad-return').removeAttr('disabled');
                        $('#return_img').attr('src', 'tg_pos_enhanced/static/src/img/return_product.png');
                    } else{
                        $('#numpad-return').attr('disabled', 'disabled');
                        $('#return_img').attr('src', 'tg_pos_enhanced/static/src/img/disabled_return_product.png');
                    }

                }else{
                    $('#numpad-return').attr('disabled', 'disabled');
                    $('#return_img').attr('src', 'tg_pos_enhanced/static/src/img/disabled_return_product.png');
                }
            }else{
                this.selected_orderline = undefined;
                $('#numpad-return').attr('disabled', 'disabled');
                $('#return_img').attr('src', 'tg_pos_enhanced/static/src/img/disabled_return_product.png');
            }

        },

In the above function, all products whose name starts with '├' (items of the pack) are no longer selectable.

Also, and we'll see later, if a product is a Custom Pack, it can not be returned. We can only return products unit.

VII-E-3. Delete a Custom Pack

When you delete a product, it actually changes the quantity to reduce it to zero.

In fact, when a product should be removed from the cart, the quantity becomes 'remove'.

When you want to delete a Custom Pack, you must also delete the items it contains.

To do this, I also had to modify the original point of sale set_quantity() function:

set_quantity() (tg_pos.js ligne 1588)
Sélectionnez
if(quantity === 'remove'){

                // when we remove a pack
                // we have too remove items too !
                if(product_name[0] == ''){

                    var o_lines = [];
                    var cur_order = this.pos.get('selectedOrder');
                    var sel_line = cur_order.selected_orderline;

                    (cur_order.get('orderLines')).each(_.bind( function(item) {
                        return o_lines.push(item);
                    }, this));

                    var flag_cur = false;

                    for(var i = 0,  len = o_lines.length; i < len; i++){                 

                        // when we found current line
                        if(o_lines[i] == sel_line){           
                            flag_cur = true;
                        }

                        // we delete items of the pack
                        if(flag_cur == true){
                            var cur_product_name = o_lines[i].product.attributes.name;

                            if(cur_product_name[1] == ''){
                                this.order.removeOrderline(o_lines[i]);
                            }else{
                                // until we found that this is not an item of the selected pack
                                if(cur_product_name[0] != ''){
                                    flag_cur = false; 
                                }  
                            }
                        }
                    }

                    // then we delete the pack
                    this.order.removeOrderline(this);

                }

So here it is the same principle, it will detect if a product is a Custom Pack by retrieving the first character of its name.

If the first character is ■ , so we will first remove all product lines below that begin with the character ├ . These are the elements of the pack..

Then finally, remove the pack itself.

VII-F. Return a product

The return of a product is an interesting point of the module.

Imagine you sell a customer a Custom Pack contains 2 pairs of drumsticks Vic Firth 5A (5A is the size).

The customer comes back a week later and tells you that the 5A are really too heavy for him and he wants you to exchange him a pair of 5A against a pair of 7A thinner and lighter.

No problem, the customer is king.

You will return the pair of 5A drumsticks and exchange it against a pair of 7A.

Le bouton de retour de produit
The return product button

I just changed the keypad. I removed the buttons that we were not helpful and I added a button « Product Return ».

You can completely recover the original buttons and place the button « Product Return » where you want. You just need to edit the template file NumpadWidget in tg_pos.xml

To return a product, simply add it to your cart, then click the button « Product Return ».

Below I returned a pair of 5A drumsticks and I added a pair of 7A drumsticks to the cart. So it is an exchange of product.

Echange de produit
Product exchange

As seen in the image above, the pair of returned drumsticks has a negative price.

Page de paiement
Paiement page

On the payment page, returned product + sold product = 0.

Ticket de caisse
receipt

On the receipt, the two products appear as in the cart.

Reminder:

You can not return a Custom Pack.

VII-F-1. About stock

We have returned a product and then sold another product.

If we go to the « Warehouse » application (Warehouse) and that we display the movements of stock, we can see the two lines concerning our products we traded.

Mouvements de stock
Stock movement

In the image above, we see that the pair of 5A drumsticks came from the outside to the stock and 7A went from the stock to the customer.

To achieve this sleight of hand, it will be enough to change the function of the point of sale create_picking() and ensure that when a price is negative to reverse the stock locations.

tg_pos_enhanced.py ligne 268
Sélectionnez
# line price < 0 = c'est un retour produit
# on retourne le produit de sortie -> Stock
if line.price_subtotal < 0:
    location_id, output_id = output_id, location_id

Here, the input and the output are inverted if the price is negative.

VII-G. Special disount

In our stores, we have restricted the functions of discounts so that sellers do not apply discounts, or several discounts to a customer.

For this, I added a function that allows the store manager to apply a special discount. Some of the stores are owned by the company, the rest are franchises, we wanted the manager specifies why he applied a discount to such customer.

Special discount is applied after entering the password provided for this purpose.

For this, an additional field was added to the table res.users

Image non disponible
The password field for special discount

The password is not encrypted, but it is only visible to the administrator (Technical settings)

VII-G-1. Apply a special discount

When you want to apply a special discount to a customer, you should call the store manager.

Appliquer une remise spéciale
Apply a special discount

The manager must then click on the icon for this purpose. The screen turns black and a form appears to allow the manager to set a discount.

Image non disponible
Special discount screen

Enter the amount of the rebate (value, not a percentage), then the password.

Then click « Validate »

Remise spéciale
Special discount

As seen in the image above, a discount of 30.00 euros was applied to the order that has been recalculated.

Ticket de caisse avec remise spéciale
Receipt with special discount

On receipt the discount is very clear.

VIII. Modules installation

First, make a backup of your database.

Unzip the ZIP file into a directory and copy the files of each module in your modules directory.

Create a modules directory out of OpenERP tree, this will help you find your way more easily.

Voir : Comment ajouter un chemin vers les modulesComment ajouter un chemin vers les modules

Be sure to install the following packages in the order below. You can restart the server after installing a module to check the operation.

  1. Installez bien sûr le module « Point Of Sale » (Point de Vente) original ;

I myself made ​​a OpenERP installation dating from 12.06.2013

(version 7.0-20131206-002433) and I did not use the demo data. I have no problems running.

  1. Install module product_vatiant_multi ;
  2. Install module tg_partner_firstname ;
  3. Install module tg_pos_packs ;
  4. Install module tg_pos_message ;
  5. Installez finally module tg_pos_enhanced ;
  • Then create some products, some customers;
  • Create a Custom Pack using the above products;
  • Create at least one cashier to the point of sale (POS Menu / Cashiers);
  • Create a password for special discounts (Settings Menu /Users → Point of sale Tab );
  • Create a welcome message to the POS (Point Of Sale Menu / Tools / Messages);

Do not forget to restart the server, then open a new session in the Point Of Sale and use the new features.

Once you have toured the new, then you can install these packages on your production environment.

IX. Download

X. Remerciements

Thanks : OpenERP SA for OpenERPOpenERP software.