Browse Source

Merge pull request #39 from ah-taktik/8.0-improve-pos-pricelist

[8.0] Improve pos_pricelist
pull/43/head
Pedro M. Baeza 9 years ago
parent
commit
5811f4f412
  1. 89
      pos_pricelist/README.rst
  2. 12
      pos_pricelist/__init__.py
  3. 11
      pos_pricelist/__openerp__.py
  4. 29
      pos_pricelist/data/pos_order.yml
  5. 141
      pos_pricelist/i18n/es.po
  6. 141
      pos_pricelist/i18n/pos_pricelist.pot
  7. 30
      pos_pricelist/migrations/8.0.1.1.0/post-migration.py
  8. 20
      pos_pricelist/models/__init__.py
  9. 160
      pos_pricelist/models/point_of_sale.py
  10. 28
      pos_pricelist/models/pos_pricelist.py
  11. 36
      pos_pricelist/report/report_receipt.xml
  12. 3
      pos_pricelist/security/ir.model.access.csv
  13. 100
      pos_pricelist/static/src/css/style.css
  14. 100
      pos_pricelist/static/src/js/db.js
  15. 527
      pos_pricelist/static/src/js/models.js
  16. 60
      pos_pricelist/static/src/js/tests.js
  17. 32
      pos_pricelist/static/src/js/widgets.js
  18. 30
      pos_pricelist/static/src/xml/pos.xml
  19. 28
      pos_pricelist/views/point_of_sale_view.xml
  20. 15
      pos_pricelist/views/pos_pricelist_views.xml

89
pos_pricelist/README.rst

@ -1,3 +1,6 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Dynamic Price for Odoo Point of Sale
====================================
@ -20,8 +23,26 @@ Goal of the module
The goal of this module is to bring the pricelist computation engine to the POS.
This module loads all the necessary data into the POS in order to have a coherent behaviour (offline/online/backend).
Implemented features
--------------------
Installation
============
Nothing special is needed to install this module.
Configuration
=============
You'll have new configuration parameters at Point of Sale > Configuration > Point of Sales
* Price with Taxes: Show prices with taxes in POS session or not
Usage
=====
Implemented features at POS Session
-----------------------------------
1. Attached pricelist on partner will take effect on the POS, which means that if we attach a pricelist to a partner.
The POS will recognize it and will compute the price according to the rule defined.
@ -35,9 +56,69 @@ The POS will recognize it and will compute the price according to the rule defin
2. (-2) : Rule based on supplierinfo
3. (default) : Any price type which is set on the product form
3. An new option is introduced in the POS config to let the user show price with taxes in product widget.
the UI is updated when we change the customer in order to adapt the prices.
The computation take in account the pricelist and the fiscal position of the customer
4. When we mouseover the price tag, a tooltip is shown to indicate the computation depending on the quantity like this output :
1x -> 100 €
3x -> 70 €
5x -> 50 €
Implemented features at backend
-------------------------------
1. Tax details
- Tax details per order line
- Tax details aggregated by tax at order level
2. Ticket
- Tax details table added at end of printed ticket
Known issues / Roadmap
======================
Missing features
----------------
* As you may know, product template is not fully implemented in the POS, so I decided to drop it from this module.
* When there are more than one price depending on the quantity, only the price
for first interval is shown.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/pos/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/pos/issues/new?body=module:%20pos_pricelist%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Adil Houmadi <ah@taktik.be>
* Pablo Cayuela <pablo.cayuela@aserti.es>
* Antonio Espinosa <antonioea@antiun.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.

12
pos_pricelist/__init__.py

@ -16,3 +16,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import models
def set_pos_line_taxes(cr, registry):
"""Copy the product taxes to the pos.line"""
cr.execute("""insert into pline_tax_rel
select l.id, t.id
from pos_order_line l
join pos_order o on l.order_id = o.id
join product_taxes_rel rel on rel.prod_id = l.product_id
join account_tax t on rel.tax_id = t.id
where t.company_id = o.company_id""")

11
pos_pricelist/__openerp__.py

@ -18,7 +18,7 @@
##############################################################################
{
'name': 'POS Pricelist',
'version': '1.0.0',
'version': '1.1.0',
'category': 'Point Of Sale',
'sequence': 1,
'author': "Adil Houmadi @Taktik,Odoo Community Association (OCA)",
@ -33,10 +33,19 @@ New feature for the Point Of Sale:
],
'data': [
"views/pos_pricelist_template.xml",
"views/pos_pricelist_views.xml",
"views/point_of_sale_view.xml",
"report/report_receipt.xml",
"security/ir.model.access.csv",
"data/pos_order.yml",
],
'demo': [
'demo/pos_pricelist_demo.yml',
],
'qweb': [
'static/src/xml/pos.xml'
],
'post_init_hook': "set_pos_line_taxes",
'installable': True,
'application': False,
'auto_install': False,

29
pos_pricelist/data/pos_order.yml

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Python source code encoding : https://www.python.org/dev/peps/pep-0263/
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright :
# (c) 2015 Antiun Ingenieria, SL (Madrid, Spain, http://www.antiun.com)
# Antonio Espinosa <antonioea@antiun.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
-
!context
noupdate: 1
- !function {model: pos.order, name: _install_tax_detail}

141
pos_pricelist/i18n/es.po

@ -0,0 +1,141 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * pos_pricelist
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-07-24 14:44+0000\n"
"PO-Revision-Date: 2015-07-24 14:44+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_pricelist
#: field:pos.order.tax,amount:0
#: view:website:point_of_sale.report_receipt
msgid "Amount"
msgstr "Cuota"
#. module: pos_pricelist
#. openerp-web
#: code:addons/pos_pricelist/static/src/js/models.js:386
#, python-format
msgid "At least one pricelist has no active version ! Please create or activate one."
msgstr "¡Al menos una lista de precios no tiene ua versión activa! Por favor, cree o active una."
#. module: pos_pricelist
#: field:pos.order.tax,base:0
#: view:website:point_of_sale.report_receipt
msgid "Base"
msgstr "Base imponible"
#. module: pos_pricelist
#: field:pos.order.tax,create_uid:0
msgid "Created by"
msgstr "Creado por"
#. module: pos_pricelist
#: field:pos.order.tax,create_date:0
msgid "Created on"
msgstr "Creado en"
#. module: pos_pricelist
#: help:pos.config,display_price_with_taxes:0
msgid "Display Prices with taxes on POS"
msgstr "Mostrar los precios con impuestos incluidos en el TPV"
#. module: pos_pricelist
#: field:pos.order.tax,id:0
msgid "ID"
msgstr "ID"
#. module: pos_pricelist
#: field:pos.order.tax,write_uid:0
msgid "Last Updated by"
msgstr "Última actualización por"
#. module: pos_pricelist
#: field:pos.order.tax,write_date:0
msgid "Last Updated on"
msgstr "Última actualización en"
#. module: pos_pricelist
#: model:ir.model,name:pos_pricelist.model_pos_order_line
msgid "Lines of Point of Sale"
msgstr "Líneas del Terminal Punto de Venta"
#. module: pos_pricelist
#: field:pos.order.tax,pos_order:0
msgid "POS Order"
msgstr "Pedido"
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_1_product_template
msgid "POS Product 1"
msgstr "TPV Producto 1"
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_2_product_template
msgid "POS Product 2"
msgstr "TPV Producto 2"
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_3_product_template
msgid "POS Product 3"
msgstr "TPV Producto 3"
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_4_product_template
msgid "POS Product 4"
msgstr "TPV Producto 4"
#. module: pos_pricelist
#: view:pos.order:pos_pricelist.view_pos_pos_form
msgid "Payments"
msgstr "Pagos"
#. module: pos_pricelist
#: model:ir.model,name:pos_pricelist.model_pos_order
msgid "Point of Sale"
msgstr "Terminal Punto de Venta"
#. module: pos_pricelist
#: field:pos.config,display_price_with_taxes:0
msgid "Price With Taxes"
msgstr "Precios con impuestos incluidos"
#. module: pos_pricelist
#. openerp-web
#: code:addons/pos_pricelist/static/src/js/models.js:385
#, python-format
msgid "Pricelist Error"
msgstr "Error en lista de precios"
#. module: pos_pricelist
#: field:pos.order.tax,tax:0
#: view:website:point_of_sale.report_receipt
msgid "Tax"
msgstr "Impuesto"
#. module: pos_pricelist
#: field:pos.order.tax,name:0
msgid "Tax Description"
msgstr "Descripción del impuesto"
#. module: pos_pricelist
#: view:pos.order:pos_pricelist.view_pos_pos_form
#: field:pos.order,taxes:0
#: field:pos.order.line,tax_ids:0
msgid "Taxes"
msgstr "Impuestos"
#. module: pos_pricelist
#: view:pos.order:pos_pricelist.view_pos_pos_form
msgid "Taxes detail"
msgstr "Detalle de impuestos"

141
pos_pricelist/i18n/pos_pricelist.pot

@ -0,0 +1,141 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * pos_pricelist
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-07-24 14:44+0000\n"
"PO-Revision-Date: 2015-07-24 14:44+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_pricelist
#: field:pos.order.tax,amount:0
#: view:website:point_of_sale.report_receipt
msgid "Amount"
msgstr ""
#. module: pos_pricelist
#. openerp-web
#: code:addons/pos_pricelist/static/src/js/models.js:386
#, python-format
msgid "At least one pricelist has no active version ! Please create or activate one."
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,base:0
#: view:website:point_of_sale.report_receipt
msgid "Base"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,create_uid:0
msgid "Created by"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,create_date:0
msgid "Created on"
msgstr ""
#. module: pos_pricelist
#: help:pos.config,display_price_with_taxes:0
msgid "Display Prices with taxes on POS"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,id:0
msgid "ID"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,write_uid:0
msgid "Last Updated by"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,write_date:0
msgid "Last Updated on"
msgstr ""
#. module: pos_pricelist
#: model:ir.model,name:pos_pricelist.model_pos_order_line
msgid "Lines of Point of Sale"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,pos_order:0
msgid "POS Order"
msgstr ""
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_1_product_template
msgid "POS Product 1"
msgstr ""
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_2_product_template
msgid "POS Product 2"
msgstr ""
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_3_product_template
msgid "POS Product 3"
msgstr ""
#. module: pos_pricelist
#: model:product.template,name:pos_pricelist.pos_product_product_4_product_template
msgid "POS Product 4"
msgstr ""
#. module: pos_pricelist
#: view:pos.order:pos_pricelist.view_pos_pos_form
msgid "Payments"
msgstr ""
#. module: pos_pricelist
#: model:ir.model,name:pos_pricelist.model_pos_order
msgid "Point of Sale"
msgstr ""
#. module: pos_pricelist
#: field:pos.config,display_price_with_taxes:0
msgid "Price With Taxes"
msgstr ""
#. module: pos_pricelist
#. openerp-web
#: code:addons/pos_pricelist/static/src/js/models.js:385
#, python-format
msgid "Pricelist Error"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,tax:0
#: view:website:point_of_sale.report_receipt
msgid "Tax"
msgstr ""
#. module: pos_pricelist
#: field:pos.order.tax,name:0
msgid "Tax Description"
msgstr ""
#. module: pos_pricelist
#: view:pos.order:pos_pricelist.view_pos_pos_form
#: field:pos.order,taxes:0
#: field:pos.order.line,tax_ids:0
msgid "Taxes"
msgstr ""
#. module: pos_pricelist
#: view:pos.order:pos_pricelist.view_pos_pos_form
msgid "Taxes detail"
msgstr ""

30
pos_pricelist/migrations/8.0.1.1.0/post-migration.py

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2015 Aserti Global Solutions (http://www.aserti.es/).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
__name__ = "Copy the product taxes to the pos.line"
def migrate(cr, version):
cr.execute("""insert into pline_tax_rel
select l.id, t.id
from pos_order_line l
join pos_order o on l.order_id = o.id
join product_taxes_rel rel on rel.prod_id = l.product_id
join account_tax t on rel.tax_id = t.id
where t.company_id = o.company_id""")

20
pos_pricelist/models/__init__.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
##############################################################################
# Point Of Sale - Pricelist for POS Odoo
# Copyright (C) 2015 Taktik (http://www.taktik.be)
# @author Adil Houmadi <ah@taktik.be>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import pos_pricelist
from . import point_of_sale

160
pos_pricelist/models/point_of_sale.py

@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2015 Aserti Global Solutions (http://www.aserti.es/).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields, api
from openerp.addons import decimal_precision as dp
import logging
_logger = logging.getLogger(__name__)
class PosOrderTax(models.Model):
_name = 'pos.order.tax'
pos_order = fields.Many2one('pos.order', string='POS Order',
ondelete='cascade', index=True)
tax = fields.Many2one('account.tax', string='Tax')
name = fields.Char(string='Tax Description', required=True)
base = fields.Float(string='Base', digits=dp.get_precision('Account'))
amount = fields.Float(string='Amount', digits=dp.get_precision('Account'))
class PosOrderLine(models.Model):
_inherit = "pos.order.line"
@api.multi
def _compute_taxes(self):
res = {
'total': 0,
'total_included': 0,
'taxes': [],
}
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_ids.compute_all(
price, line.qty, product=line.product_id,
partner=line.order_id.partner_id)
res['total'] += taxes['total']
res['total_included'] += taxes['total_included']
res['taxes'] += taxes['taxes']
return res
@api.one
@api.depends('tax_ids', 'qty', 'price_unit',
'product_id', 'discount', 'order_id.partner_id')
def _amount_line_all(self):
taxes = self._compute_taxes()
self.price_subtotal = taxes['total']
self.price_subtotal_incl = taxes['total_included']
tax_ids = fields.Many2many(
'account.tax', 'pline_tax_rel', 'pos_line_id', 'tax_id',
"Taxes", domain=[('type_tax_use', '=', 'sale')])
price_subtotal = fields.Float(compute="_amount_line_all", store=True)
price_subtotal_incl = fields.Float(compute="_amount_line_all", store=True)
class PosOrder(models.Model):
_inherit = "pos.order"
taxes = fields.One2many(comodel_name='pos.order.tax',
inverse_name='pos_order', readonly=True)
@api.model
def _amount_line_tax(self, line):
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_ids.compute_all(
price, line.qty, product=line.product_id,
partner=line.order_id.partner_id)['taxes']
val = 0.0
for c in taxes:
val += c.get('amount', 0.0)
return val
@api.multi
def _tax_list_get(self):
agg_taxes = {}
tax_lines = []
for order in self:
for line in order.lines:
tax_lines.append({
'base': line.price_subtotal,
'taxes': line._compute_taxes()['taxes']
})
for tax_line in tax_lines:
base = tax_line['base']
for tax in tax_line['taxes']:
tax_id = str(tax['id'])
if tax_id in agg_taxes:
agg_taxes[tax_id]['base'] += base
agg_taxes[tax_id]['amount'] += tax['amount']
else:
agg_taxes[tax_id] = {
'tax_id': tax_id,
'name': tax['name'],
'base': base,
'amount': tax['amount'],
}
return agg_taxes
@api.multi
def compute_tax_detail(self):
taxes_to_delete = False
for order in self:
taxes_to_delete = self.env['pos.order.tax'].search(
[('pos_order', '=', order.id)])
# Update order taxes list
for key, tax in order._tax_list_get().iteritems():
current = taxes_to_delete.filtered(
lambda r: r.tax.id == tax['tax_id'])
if current:
current.write({
'base': tax['base'],
'amount': tax['amount'],
})
taxes_to_delete -= current
else:
self.env['pos.order.tax'].create({
'pos_order': order.id,
'tax': tax['tax_id'],
'name': tax['name'],
'base': tax['base'],
'amount': tax['amount'],
})
if taxes_to_delete:
taxes_to_delete.unlink()
@api.multi
def action_paid(self):
result = super(PosOrder, self).action_paid()
self.compute_tax_detail()
return result
@api.model
def _install_tax_detail(self):
"""Create tax details to pos.order's already paid, done or invoiced.
"""
# Find orders with state : paid, done or invoiced
orders = self.search([('state', 'in', ('paid', 'done', 'invoiced')),
('taxes', '=', False)])
# Compute tax detail
orders.compute_tax_detail()
_logger.info("%d orders computed installing module.", len(orders))

28
pos_pricelist/models/pos_pricelist.py

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
##############################################################################
# Point Of Sale - Pricelist for POS Odoo
# Copyright (C) 2015 Taktik (http://www.taktik.be)
# @author Adil Houmadi <ah@taktik.be>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields
class PosPriceListConfig(models.Model):
_inherit = 'pos.config'
display_price_with_taxes = fields.Boolean(
string='Price With Taxes',
help="Display Prices with taxes on POS"
)

36
pos_pricelist/report/report_receipt.xml

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_receipt"
inherit_id="point_of_sale.report_receipt"
name="Add taxes detail">
<xpath expr="//div[@class='page']" position="inside">
<table class="table table-condensed">
<thead>
<tr>
<th>Tax</th>
<th class="text-right">Base</th>
<th class="text-right">Amount</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.taxes" t-as="tax">
<td>
<span t-esc="tax.name"/>
</td>
<td class="text-right">
<span t-esc="formatLang(tax.base, currency_obj=res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="formatLang(tax.amount, currency_obj=res_company.currency_id)"/>
</td>
</tr>
</tbody>
</table>
</xpath>
</template>
</data>
</openerp>

3
pos_pricelist/security/ir.model.access.csv

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_pos_order_tax,pos.order.tax,model_pos_order_tax,point_of_sale.group_pos_user,1,0,0,0
access_pos_order_tax_manager,pos.order.tax manager,model_pos_order_tax,point_of_sale.group_pos_manager,1,1,1,1

100
pos_pricelist/static/src/css/style.css

@ -44,3 +44,103 @@
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0;
}
/* ToolTip */
.tooltip {
position: absolute;
z-index: 1070;
display: block;
font-size: 12px;
line-height: 1.4;
visibility: visible;
filter: alpha(opacity=0);
opacity: 0;
}
.tooltip.in {
filter: alpha(opacity=90);
opacity: .9;
}
.tooltip.top {
padding: 5px 0;
margin-top: -3px;
}
.tooltip.right {
padding: 0 5px;
margin-left: 3px;
}
.tooltip.bottom {
padding: 5px 0;
margin-top: 3px;
}
.tooltip.left {
padding: 0 5px;
margin-left: -3px;
}
.tooltip-inner {
max-width: 200px;
padding: 3px 8px;
color: #fff;
text-align: center;
text-decoration: none;
background-color: #000;
border-radius: 4px;
}
.tooltip-arrow {
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.tooltip.top .tooltip-arrow {
bottom: 0;
left: 50%;
margin-left: -5px;
border-width: 5px 5px 0;
border-top-color: #000;
}
.tooltip.top-left .tooltip-arrow {
bottom: 0;
left: 5px;
border-width: 5px 5px 0;
border-top-color: #000;
}
.tooltip.top-right .tooltip-arrow {
right: 5px;
bottom: 0;
border-width: 5px 5px 0;
border-top-color: #000;
}
.tooltip.right .tooltip-arrow {
top: 50%;
left: 0;
margin-top: -5px;
border-width: 5px 5px 5px 0;
border-right-color: #000;
}
.tooltip.left .tooltip-arrow {
top: 50%;
right: 0;
margin-top: -5px;
border-width: 5px 0 5px 5px;
border-left-color: #000;
}
.tooltip.bottom .tooltip-arrow {
top: 0;
left: 50%;
margin-left: -5px;
border-width: 0 5px 5px;
border-bottom-color: #000;
}
.tooltip.bottom-left .tooltip-arrow {
top: 0;
left: 5px;
border-width: 0 5px 5px;
border-bottom-color: #000;
}
.tooltip.bottom-right .tooltip-arrow {
top: 0;
right: 5px;
border-width: 0 5px 5px;
border-bottom-color: #000;
}

100
pos_pricelist/static/src/js/db.js

@ -1,27 +1,26 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
function pos_pricelist_db(instance, module) {
module.PosDB = module.PosDB.extend({
init: function (options) {
options = options || {};
this._super(options);
this.default_pricelist_id = 0;
this.pricelist_by_id = {};
this.pricelist_version_by_id = {};
this.pricelist_item_by_id = {};
@ -40,7 +39,8 @@ function pos_pricelist_db(instance, module) {
}
var fiscal_position_tax;
while (fiscal_position_tax = fiscal_position_taxes.pop()) {
this.fiscal_position_tax_by_id[fiscal_position_tax.id] = fiscal_position_tax;
this.fiscal_position_tax_by_id[fiscal_position_tax.id]
= fiscal_position_tax;
}
},
add_pricelist_partnerinfo: function (pricelist_partnerinfos) {
@ -49,7 +49,8 @@ function pos_pricelist_db(instance, module) {
}
var partner_info;
while (partner_info = pricelist_partnerinfos.pop()) {
this.pricelist_partnerinfo_by_id[partner_info.id] = partner_info;
this.pricelist_partnerinfo_by_id[partner_info.id]
= partner_info;
}
},
add_supplierinfo: function (supplierinfos) {
@ -61,11 +62,6 @@ function pos_pricelist_db(instance, module) {
this.supplierinfo_by_id[supplier_info.id] = supplier_info;
}
},
add_default_pricelist: function (res_id) {
if (res_id && res_id.length) {
this.default_pricelist_id = res_id[0].res_id;
}
},
add_pricelists: function (pricelists) {
if (!(pricelists instanceof Array)) {
pricelists = [pricelists];
@ -110,7 +106,8 @@ function pos_pricelist_db(instance, module) {
var category;
while (category = categories.pop()) {
this.product_category_by_id[category.id] = category;
this.product_category_children[category.id] = category.child_id
this.product_category_children[category.id] =
category.child_id;
}
this._make_ancestors();
},
@ -121,7 +118,9 @@ function pos_pricelist_db(instance, module) {
ancestors = [];
while (category.parent_id) {
ancestors.push(category.parent_id[0]);
category = category.parent_id ? this.product_category_by_id[category.parent_id[0]] : false;
category = category.parent_id ?
this.product_category_by_id[category.parent_id[0]] :
false;
}
this.product_category_ancestors[parseInt(id)] = ancestors;
}
@ -141,15 +140,56 @@ function pos_pricelist_db(instance, module) {
});
return list;
},
find_taxes_by_fiscal_position_id: function (fiscal_position_id) {
map_tax: function (fiscal_position_id, taxes_ids) {
var taxes = [];
var found_taxes = {};
for (var id in this.fiscal_position_tax_by_id) {
var tax = this.fiscal_position_tax_by_id[id];
if (tax && tax.position_id && tax.position_id[0] == fiscal_position_id) {
taxes.push(tax);
var fp_line = this.fiscal_position_tax_by_id[id];
if (fp_line && fp_line.position_id &&
fp_line.position_id[0] == fiscal_position_id &&
taxes_ids.indexOf(fp_line.tax_src_id[0]) > -1) {
taxes.push(fp_line.tax_dest_id[0]);
found_taxes[fp_line.tax_src_id[0]] = true;
}
}
for (var i = 0, len = taxes_ids.length; i < len; i++) {
var tax_id = taxes_ids[i];
if (!(tax_id in found_taxes)) {
taxes.push(tax_id);
}
}
return taxes;
},
add_products: function (products) {
this._super(products);
var pos = posmodel.pos_widget.pos;
for (var id in this.product_by_id) {
if (this.product_by_id.hasOwnProperty(id)) {
var product = this.product_by_id[id];
var orderline = new openerp.point_of_sale.Orderline({}, {
pos: pos,
order: null,
product: product,
price: product.price
});
var prices = orderline.get_all_prices();
this.product_by_id[id].price_with_taxes
= prices['priceWithTax']
}
}
},
find_product_rules: function (product) {
var len = this.pricelist_item_sorted.length;
var rules = [];
for (var i = 0; i < len; i++) {
var rule = this.pricelist_item_sorted[i];
if ((rule.product_id && rule.product_id[0] == product.id) ||
(rule.categ_id && product.categ_id
&& rule.categ_id[0] == product.categ_id[0])) {
rules.push(rule);
}
}
return rules;
}
})
}

527
pos_pricelist/static/src/js/models.js

@ -1,42 +1,42 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
function pos_pricelist_models(instance, module) {
var _t = instance.web._t;
var round_pr = instance.web.round_precision;
var round_di = instance.web.round_decimals;
/**
* @param funcName
* @returns {*}
* @private
*/
Backbone.Model.prototype._super = function (funcName) {
return this.constructor.__super__[funcName].apply(this, _.rest(arguments));
};
/**
* Extend the POS model
*/
var PosModelParent = module.PosModel;
module.PosModel = module.PosModel.extend({
/**
* @param session
* @param attributes
*/
initialize: function (session, attributes) {
this._super('initialize', session, attributes);
this.pricelist_engine = new module.PricelistEngine({'pos': this, 'db': this.db, 'pos_widget': this.pos_widget});
PosModelParent.prototype.initialize.apply(this, arguments);
this.pricelist_engine = new module.PricelistEngine({
'pos': this,
'db': this.db,
'pos_widget': this.pos_widget
});
arrange_elements(this);
},
/**
@ -54,11 +54,20 @@ function pos_pricelist_models(instance, module) {
}
return lookup
},
/**
* @param removed_order
* @param index
* @param reason
*/
on_removed_order: function (removed_order, index, reason) {
this._super('on_removed_order', removed_order, index, reason);
if ((reason === 'abandon' || removed_order.temporary) && this.get('orders').size() > 0) {
var current_order = (this.get('orders').at(index) || this.get('orders').last());
var partner = current_order.get_client() ? current_order.get_client() : false;
PosModelParent.prototype.on_removed_order.apply(this, arguments);
if ((reason === 'abandon' || removed_order.temporary)
&& this.get('orders').size() > 0) {
var current_order = (this.get('orders').at(index)
|| this.get('orders').last());
var partner = current_order.get_client() ?
current_order.get_client() :
false;
this.pricelist_engine.update_products_ui(partner);
}
}
@ -80,7 +89,11 @@ function pos_pricelist_models(instance, module) {
var attr = JSON.parse(JSON.stringify(product));
attr.pos = this.pos;
attr.order = this;
var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
var line = new module.Orderline({}, {
pos: this.pos,
order: this,
product: product
});
var self = this;
var found = false;
@ -100,7 +113,8 @@ function pos_pricelist_models(instance, module) {
}
for (var i = 0; i < orderlines.length; i++) {
var _line = orderlines[i];
if (_line && _line.can_be_merged_with(line) && options.merge !== false) {
if (_line && _line.can_be_merged_with(line) &&
options.merge !== false) {
_line.merge(line);
found = true;
break;
@ -116,16 +130,23 @@ function pos_pricelist_models(instance, module) {
/**
* Extend the Order line
*/
var OrderlineParent = module.Orderline;
module.Orderline = module.Orderline.extend({
/**
* @param attr
* @param options
*/
initialize: function (attr, options) {
this._super('initialize', attr, options);
this.manuel_price = false;
if (options.product !== undefined) {
var qty = this.compute_qty(options.order, options.product);
var partner = options.order.get_client();
var product = options.product;
OrderlineParent.prototype.initialize.apply(this, arguments);
this.manual_price = false;
if (this.product !== undefined) {
var qty = this.compute_qty(this.order, this.product);
var partner = this.order ? this.order.get_client() : null;
var product = this.product;
var db = this.pos.db;
var price = this.pos.pricelist_engine.compute_price_all(db, product, partner, qty);
var price = this.pos.pricelist_engine.compute_price_all(
db, product, partner, qty
);
if (price !== false && price !== 0.0) {
this.price = price;
}
@ -134,18 +155,20 @@ function pos_pricelist_models(instance, module) {
/**
* @param state
*/
set_manuel_price: function (state) {
this.manuel_price = state;
set_manual_price: function (state) {
this.manual_price = state;
},
/**
* @param quantity
*/
set_quantity: function (quantity) {
this._super('set_quantity', quantity);
OrderlineParent.prototype.set_quantity.apply(this, arguments);
var partner = this.order.get_client();
var product = this.product;
var db = this.pos.db;
var price = this.pos.pricelist_engine.compute_price_all(db, product, partner, quantity);
var price = this.pos.pricelist_engine.compute_price_all(
db, product, partner, quantity
);
if (price !== false && price !== 0.0) {
this.price = price;
}
@ -154,81 +177,30 @@ function pos_pricelist_models(instance, module) {
/**
* override this method to take fiscal positions in consideration
* get all price
* TODO : find a better way to do it : need some refactoring in the pos standard
* @returns {{priceWithTax: *, priceWithoutTax: *, tax: number, taxDetails: {}}}
* TODO : find a better way to do it : need some refactoring
* in the pos standard
* @returns {{
* priceWithTax: *, priceWithoutTax: *, tax: number, taxDetails: {}
* }}
*/
get_all_prices: function () {
var self = this;
var currency_rounding = this.pos.currency.rounding;
var base = this.get_base_price();
var totalTax = base;
var totalNoTax = base;
var product = this.get_product();
var taxes = this.get_applicable_taxes();
var taxtotal = 0;
var taxdetail = {};
// Add by pos_pricelist
var partner = this.order.get_client();
var fiscal_position_taxes = [];
if (partner && partner.property_account_position) {
fiscal_position_taxes = self.pos.db.find_taxes_by_fiscal_position_id(partner.property_account_position[0]);
}
var product_taxes = [];
for (var i = 0, ilen = fiscal_position_taxes.length; i < ilen; i++) {
var fp_tax = fiscal_position_taxes[i];
for (var j = 0, jlen = taxes.length; j < jlen; j++) {
var p_tax = taxes[j];
if (fp_tax && p_tax && fp_tax.tax_src_id[0] === p_tax.id) {
var dest_tax = _.detect(this.pos.taxes, function (t) {
return t.id === fp_tax.tax_dest_id[0];
});
product_taxes.push(dest_tax);
}
}
}
if (product_taxes.length === 0) {
for (var i = 0, ilen = product.taxes_id.length; i < ilen; i++) {
var _id = product.taxes_id[i];
var p_tax = _.detect(this.pos.taxes, function (t) {
return t.id === _id;
});
product_taxes.push(p_tax);
}
}
_.each(product_taxes, function (tax) {
var product_taxes = this.get_applicable_taxes_for_orderline();
var all_taxes = _(this.compute_all(product_taxes, base)).flatten();
_(all_taxes).each(function (tax) {
if (tax.price_include) {
var tmp;
if (tax.type === "percent") {
tmp = base - round_pr(base / (1 + tax.amount), currency_rounding);
} else if (tax.type === "fixed") {
tmp = round_pr(tax.amount * self.get_quantity(), currency_rounding);
} else {
throw "This type of tax is not supported by the point of sale: " + tax.type;
}
tmp = round_pr(tmp, currency_rounding);
taxtotal += tmp;
totalNoTax -= tmp;
taxdetail[tax.id] = tmp;
totalNoTax -= tax.amount;
} else {
var tmp;
if (tax.type === "percent") {
tmp = tax.amount * base;
} else if (tax.type === "fixed") {
tmp = tax.amount * self.get_quantity();
} else {
throw "This type of tax is not supported by the point of sale: " + tax.type;
}
tmp = round_pr(tmp, currency_rounding);
if (tax.include_base_amount) {
base += tmp;
}
taxtotal += tmp;
totalTax += tmp;
taxdetail[tax.id] = tmp;
totalTax += tax.amount;
}
taxtotal += tax.amount;
taxdetail[tax.id] = tax.amount;
});
totalNoTax = round_pr(totalNoTax, this.pos.currency.rounding);
return {
"priceWithTax": totalTax,
"priceWithoutTax": totalNoTax,
@ -237,16 +209,22 @@ function pos_pricelist_models(instance, module) {
};
},
/**
* Override this method to avoid a return false if the price is different
* Check super method : (this.price !== orderline.price) is not necessary in our case
* Override this method to avoid a return false
* if the price is different
* Check super method : (this.price !== orderline.price)
* is not necessary in our case
* @param orderline
* @returns {boolean}
*/
can_be_merged_with: function (orderline) {
var result = this._super('can_be_merged_with', orderline);
var result = OrderlineParent.prototype.can_be_merged_with.apply(
this, arguments
);
if (!result) {
if (!this.manuel_price) {
return (this.get_product().id === orderline.get_product().id);
if (!this.manual_price) {
return (
this.get_product().id === orderline.get_product().id
);
} else {
return false;
}
@ -258,7 +236,7 @@ function pos_pricelist_models(instance, module) {
* @param orderline
*/
merge: function (orderline) {
this._super('merge', orderline);
OrderlineParent.prototype.merge.apply(this, arguments);
this.set_unit_price(orderline.price);
},
/**
@ -269,23 +247,78 @@ function pos_pricelist_models(instance, module) {
compute_qty: function (order, product) {
var qty = 1;
var orderlines = [];
if (order.get('orderLines').models !== undefined) {
if (order && order.get('orderLines').models !== undefined) {
orderlines = order.get('orderLines').models;
}
for (var i = 0; i < orderlines.length; i++) {
if (orderlines[i].product.id === product.id && !orderlines[i].manuel_price) {
if (orderlines[i].product.id === product.id
&& !orderlines[i].manual_price) {
qty += orderlines[i].quantity;
}
}
return qty;
},
/**
* @returns {Array}
*/
get_applicable_taxes_for_orderline: function () {
// find applicable taxes for this product and this customer
var product = this.get_product();
var product_tax_ids = product.taxes_id;
var product_taxes = [];
var taxes = this.pos.taxes;
var partner = this.order ? this.order.get_client() : null;
if (partner && partner.property_account_position) {
product_tax_ids =
this.pos.db.map_tax(
partner.property_account_position[0], product_tax_ids
);
}
for (var i = 0, ilen = product_tax_ids.length;
i < ilen; i++) {
var tax_id = product_tax_ids[i];
var tax = _.detect(taxes, function (t) {
return t.id === tax_id;
});
product_taxes.push(tax);
}
return product_taxes;
},
/**
* @returns {*}
*/
get_display_price: function () {
if (this.pos.config.display_price_with_taxes) {
return this.get_price_with_tax();
}
return OrderlineParent.prototype.get_display_price.apply(
this, arguments
);
},
export_as_JSON: function() {
var res = OrderlineParent.prototype.export_as_JSON.apply(this, arguments);
var product_tax_ids = this.get_product().taxes_id || [];
var partner = this.order ? this.order.get_client() : null;
if (partner && partner.property_account_position) {
product_tax_ids =
this.pos.db.map_tax(
partner.property_account_position[0], product_tax_ids
);
}
res["tax_ids"] = [[6, false, product_tax_ids]];
return res;
}
});
/**
* Pricelist Engine to compute price
*/
module.PricelistEngine = instance.web.Class.extend({
init: function(options){
/**
* @param options
*/
init: function (options) {
options = options || {};
this.pos = options.pos;
this.db = options.db;
@ -304,9 +337,11 @@ function pos_pricelist_models(instance, module) {
if (partner && partner.property_product_pricelist) {
price_list_id = partner.property_product_pricelist[0];
} else {
price_list_id = db.default_pricelist_id;
price_list_id = this.pos.config.pricelist_id[0];
}
return this.compute_price(db, product, partner, qty, parseInt(price_list_id));
return this.compute_price(
db, product, partner, qty, parseInt(price_list_id)
);
},
/**
* loop find a valid version for the price list id given in param
@ -320,8 +355,10 @@ function pos_pricelist_models(instance, module) {
var pricelist = db.pricelist_by_id[pricelist_id];
for (var i = 0, len = pricelist.version_id.length; i < len; i++) {
var v = db.pricelist_version_by_id[pricelist.version_id[i]];
if (((v.date_start == false) || (new Date(v.date_start) <= date)) &&
((v.date_end == false) || (new Date(v.date_end) >= date))) {
if (((v.date_start == false)
|| (new Date(v.date_start) <= date)) &&
((v.date_end == false)
|| (new Date(v.date_end) >= date))) {
version = v;
break;
}
@ -346,7 +383,8 @@ function pos_pricelist_models(instance, module) {
var version = this.find_valid_pricelist_version(db, pricelist_id);
if (version == false) {
var message = _t('Pricelist Error');
var comment = _t('At least one pricelist has no active version ! Please create or activate one.');
var comment = _t('At least one pricelist has no active ' +
'version ! Please create or activate one.');
show_error(this, message, comment);
return false;
}
@ -355,15 +393,19 @@ function pos_pricelist_models(instance, module) {
var categ_ids = [];
if (product.categ_id) {
categ_ids.push(product.categ_id[0]);
categ_ids = categ_ids.concat(db.product_category_ancestors[product.categ_id[0]]);
categ_ids = categ_ids.concat(
db.product_category_ancestors[product.categ_id[0]]
);
}
// find items
var items = [], i, len;
for (i = 0, len = db.pricelist_item_sorted.length; i < len; i++) {
var item = db.pricelist_item_sorted[i];
if ((item.product_id === false || item.product_id[0] === product.id) &&
(item.categ_id === false || categ_ids.indexOf(item.categ_id[0]) !== -1) &&
if ((item.product_id === false
|| item.product_id[0] === product.id) &&
(item.categ_id === false
|| categ_ids.indexOf(item.categ_id[0]) !== -1) &&
(item.price_version_id[0] === version.id)) {
items.push(item);
}
@ -381,7 +423,8 @@ function pos_pricelist_models(instance, module) {
if (rule.min_quantity && qty < rule.min_quantity) {
continue;
}
if (rule.product_id && rule.product_id[0] && product.id != rule.product_id[0]) {
if (rule.product_id && rule.product_id[0]
&& product.id != rule.product_id[0]) {
continue;
}
if (rule.categ_id) {
@ -390,7 +433,7 @@ function pos_pricelist_models(instance, module) {
if (cat_id == rule.categ_id[0]) {
break;
}
cat_id = db.product_category_by_id[cat_id].parent_id[0];
cat_id = db.product_category_by_id[cat_id].parent_id[0]
}
if (!(cat_id)) {
continue;
@ -400,7 +443,10 @@ function pos_pricelist_models(instance, module) {
switch (rule.base) {
case -1:
if (rule.base_pricelist_id) {
price = self.compute_price(db, product, false, qty, rule.base_pricelist_id[0]);
price = self.compute_price(
db, product, false, qty,
rule.base_pricelist_id[0]
);
}
break;
case -2:
@ -408,17 +454,20 @@ function pos_pricelist_models(instance, module) {
for (var index in product.seller_ids) {
var seller_id = product.seller_ids[index];
var _tmp_seller = db.supplierinfo_by_id[seller_id];
if ((!partner) || (_tmp_seller.name.length && _tmp_seller.name[0] != partner.name))
if ((!partner) || (_tmp_seller.name.length
&& _tmp_seller.name[0] != partner.name))
continue;
seller = _tmp_seller
}
if (!seller && product.seller_ids) {
seller = db.supplierinfo_by_id[product.seller_ids[0]];
seller =
db.supplierinfo_by_id[product.seller_ids[0]];
}
if (seller) {
for (var _id in seller.pricelist_ids) {
var info_id = seller.pricelist_ids[_id];
var line = db.pricelist_partnerinfo_by_id[info_id];
var line =
db.pricelist_partnerinfo_by_id[info_id];
if (line.min_quantity <= qty) {
price = line.price
}
@ -427,25 +476,39 @@ function pos_pricelist_models(instance, module) {
break;
default:
if (!price_types.hasOwnProperty(rule.base)) {
price_types[rule.base] = db.product_price_type_by_id[rule.base];
price_types[rule.base] =
db.product_price_type_by_id[rule.base];
}
var price_type = price_types[rule.base];
if (db.product_by_id[product.id].hasOwnProperty(price_type.field)) {
price = db.product_by_id[product.id][price_type.field];
if (db.product_by_id[product.id]
.hasOwnProperty(price_type.field)) {
price =
db.product_by_id[product.id][price_type.field];
}
}
if (price !== false) {
var price_limit = price;
price = price * (1.0 + (rule['price_discount'] ? rule['price_discount'] : 0.0))
price = price * (1.0 + (rule['price_discount']
? rule['price_discount']
: 0.0));
if (rule['price_round']) {
price = parseFloat(price.toFixed(Math.ceil(Math.log(1.0 / rule['price_round']) / Math.log(10))));
price = parseFloat(price.toFixed(
Math.ceil(Math.log(1.0 / rule['price_round'])
/ Math.log(10)))
);
}
price += (rule['price_surcharge'] ? rule['price_surcharge'] : 0.0);
price += (rule['price_surcharge']
? rule['price_surcharge']
: 0.0);
if (rule['price_min_margin']) {
price = Math.max(price, price_limit + rule['price_min_margin'])
price = Math.max(
price, price_limit + rule['price_min_margin']
)
}
if (rule['price_max_margin']) {
price = Math.min(price, price_limit + rule['price_min_margin'])
price = Math.min(
price, price_limit + rule['price_min_margin']
)
}
}
break;
@ -457,20 +520,74 @@ function pos_pricelist_models(instance, module) {
*/
update_products_ui: function (partner) {
var db = this.db;
if(!this.pos_widget.product_screen) return;
var product_list_ui = this.pos_widget.product_screen.$('.product-list span.product');
if (!this.pos_widget.product_screen) return;
var product_list_ui
= this.pos_widget.product_screen.$(
'.product-list span.product'
);
for (var i = 0, len = product_list_ui.length; i < len; i++) {
var product_ui = product_list_ui[i];
var product_id = $(product_ui).data('product-id');
var product = db.get_product_by_id(product_id);
var price = this.compute_price_all(db, product, partner, 1);
if (price !== false && price !== 0.0) {
price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']);
price = this.pos_widget.format_currency(price);
$(product_ui).find('.price-tag').html(price);
var product = $.extend({}, db.get_product_by_id(product_id));
var rules = db.find_product_rules(product);
var quantities = [];
quantities.push(1);
for (var j = 0; j < rules.length; j++) {
quantities.push(rules[j].min_quantity);
}
quantities = quantities.sort();
var prices_displayed = '';
for (var k = 0; k < quantities.length; k++) {
var qty = quantities[k];
var price = this.compute_price_all(
db, product, partner, qty
);
if (price !== false && price !== 0.0) {
if (this.pos.config.display_price_with_taxes) {
var prices = this.simulate_price(
product, partner, price, qty
);
price = prices['priceWithTax']
}
price = round_di(parseFloat(price)
|| 0, this.pos.dp['Product Price']);
price = this.pos_widget.format_currency(price);
if (k == 0) {
$(product_ui).find('.price-tag').html(price);
}
prices_displayed += qty
+ 'x &#8594; ' + price + '<br/>';
}
}
if (prices_displayed != '') {
$(product_ui).find('.price-tag').attr(
'data-original-title', prices_displayed
);
$(product_ui).find('.price-tag').attr(
'data-toggle', 'tooltip'
);
$(product_ui).find('.price-tag').tooltip(
{delay: {show: 50, hide: 100}}
);
}
}
},
simulate_price: function (product, partner, price, qty) {
// create a fake order in order to get price
// for this customer
var order = new module.Order({pos: this.pos});
order.set_client(partner);
var orderline = new openerp.point_of_sale.Orderline
({}, {
pos: this.pos, order: order,
product: product, price: price
});
orderline.set_quantity(qty);
// reset the sequence
this.pos.pos_session.sequence_number--;
var prices = orderline.get_all_prices();
return prices;
},
/**
*
* @param partner
@ -482,7 +599,9 @@ function pos_pricelist_models(instance, module) {
var line = orderLines[i];
var product = line.product;
var quantity = line.quantity;
var price = this.compute_price_all(db, product, partner, quantity);
var price = this.compute_price_all(
db, product, partner, quantity
);
if (price !== false && price !== 0.0) {
line.price = price;
}
@ -490,7 +609,6 @@ function pos_pricelist_models(instance, module) {
}
}
});
/**
* show error
* @param context
@ -513,33 +631,55 @@ function pos_pricelist_models(instance, module) {
var product_model = pos_model.find_model('product.product');
if (_.size(product_model) == 1) {
var product_index = parseInt(Object.keys(product_model)[0]);
pos_model.models[product_index].fields.push('categ_id', 'seller_ids');
pos_model.models[product_index].fields.push(
'categ_id', 'seller_ids'
);
}
var res_product_pricelist = pos_model.find_model('product.pricelist');
if (_.size(res_product_pricelist) == 1) {
var pricelist_index = parseInt(Object.keys(res_product_pricelist)[0]);
var pricelist_index = parseInt(Object.keys(
res_product_pricelist)[0]
);
pos_model.models.splice(++pricelist_index, 0,
{
model: 'account.fiscal.position.tax',
fields: ['display_name', 'position_id', 'tax_src_id', 'tax_dest_id'],
fields: ['display_name',
'position_id',
'tax_src_id',
'tax_dest_id'],
domain: null,
loaded: function (self, fiscal_position_taxes) {
self.db.add_fiscal_position_taxes(fiscal_position_taxes);
self.db.add_fiscal_position_taxes(
fiscal_position_taxes
);
}
},
{
model: 'pricelist.partnerinfo',
fields: ['display_name', 'min_quantity', 'name', 'price', 'suppinfo_id'],
fields: ['display_name',
'min_quantity',
'name',
'price',
'suppinfo_id'],
domain: null,
loaded: function (self, pricelist_partnerinfos) {
self.db.add_pricelist_partnerinfo(pricelist_partnerinfos);
self.db.add_pricelist_partnerinfo(
pricelist_partnerinfos
);
}
},
{
model: 'product.supplierinfo',
fields: ['delay', 'name', 'min_qty', 'pricelist_ids', 'product_code', 'product_name', 'sequence',
'qty', 'product_tmpl_id'],
fields: ['delay',
'name',
'min_qty',
'pricelist_ids',
'product_code',
'product_name',
'sequence',
'qty',
'product_tmpl_id'],
domain: null,
loaded: function (self, supplierinfos) {
self.db.add_supplierinfo(supplierinfos);
@ -547,29 +687,22 @@ function pos_pricelist_models(instance, module) {
},
{
model: 'product.category',
fields: ['name', 'display_name', 'parent_id', 'child_id'],
fields: ['name',
'display_name',
'parent_id',
'child_id'],
domain: null,
loaded: function (self, categories) {
self.db.add_product_categories(categories);
}
},
{
model: 'ir.model.data',
fields: ['res_id'],
domain: function () {
return [
['module', '=', 'product'],
['name', '=', 'property_product_pricelist']
]
},
loaded: function (self, res) {
self.db.add_default_pricelist(res);
}
},
{
model: 'product.pricelist',
fields: ['display_name', 'name', 'version_id', 'currency_id'],
fields: ['display_name',
'name',
'version_id',
'currency_id'],
domain: function () {
return [
['type', '=', 'sale']
@ -581,7 +714,11 @@ function pos_pricelist_models(instance, module) {
},
{
model: 'product.pricelist.version',
fields: ['name', 'pricelist_id', 'date_start', 'date_end', 'items'],
fields: ['name',
'pricelist_id',
'date_start',
'date_end',
'items'],
domain: null,
loaded: function (self, versions) {
self.db.add_pricelist_versions(versions);
@ -589,9 +726,20 @@ function pos_pricelist_models(instance, module) {
},
{
model: 'product.pricelist.item',
fields: ['name', 'base', 'base_pricelist_id', 'categ_id', 'min_quantity',
'price_discount', 'price_max_margin', 'price_min_margin', 'price_round', 'price_surcharge',
'price_version_id', 'product_id', 'product_tmpl_id', 'sequence'
fields: ['name',
'base',
'base_pricelist_id',
'categ_id',
'min_quantity',
'price_discount',
'price_max_margin',
'price_min_margin',
'price_round',
'price_surcharge',
'price_version_id',
'product_id',
'product_tmpl_id',
'sequence'
],
domain: null,
loaded: function (self, items) {
@ -603,14 +751,21 @@ function pos_pricelist_models(instance, module) {
fields: ['name', 'field', 'currency_id'],
domain: null,
loaded: function (self, price_types) {
// we need to add price type field to product.product model if not the case
var product_model = posmodel.find_model('product.product');
for(var i = 0, len = price_types.length; i < len; i++) {
// we need to add price type
// field to product.product model if not the case
var product_model =
posmodel.find_model('product.product');
for (var i = 0, len = price_types.length;
i < len; i++) {
var p_type = price_types[i].field;
if (_.size(product_model) == 1) {
var product_index = parseInt(Object.keys(product_model)[0]);
if(posmodel.models[product_index].fields.indexOf(p_type) === -1) {
posmodel.models[product_index].fields.push(p_type);
var product_index =
parseInt(Object.keys(product_model)[0]);
if (posmodel.models[product_index]
.fields.indexOf(p_type) === -1) {
posmodel.models[product_index].fields.push(
p_type
);
}
}
}
@ -622,8 +777,12 @@ function pos_pricelist_models(instance, module) {
var res_partner_model = pos_model.find_model('res.partner');
if (_.size(res_partner_model) == 1) {
var res_partner_index = parseInt(Object.keys(res_partner_model)[0]);
pos_model.models[res_partner_index].fields.push('property_account_position', 'property_product_pricelist');
var res_partner_index =
parseInt(Object.keys(res_partner_model)[0]);
pos_model.models[res_partner_index].fields.push(
'property_account_position',
'property_product_pricelist'
);
}
}

60
pos_pricelist/static/src/js/tests.js

@ -32,7 +32,8 @@
wait: 2000,
title: 'Load the Session',
waitNot: '.oe_loading:visible',
element: 'span:contains("Resume Session"),span:contains("Start Session")'
element: 'span:contains("Resume Session"),' +
'span:contains("Start Session")'
},
{
title: 'Loading Screen',
@ -45,54 +46,67 @@
},
{
title: "We will buy some Products!, let's add (POS Product 1)",
element: '.product-list .product-name:contains("POS Product 1")'
element: '.product-list ' +
'.product-name:contains("POS Product 1")'
},
{
wait: 5000,
title: 'The order total has been updated to the correct value : 100€',
title: 'The order total has been ' +
'updated to the correct value : 100€',
waitFor: '.order .total .value:contains("100.00 €")'
},
{
wait: 5000,
title: 'We will add one more unit!',
element: '.product-list .product-name:contains("POS Product 1")'
element: '.product-list ' +
'.product-name:contains("POS Product 1")'
},
{
wait: 4000,
title: 'We will add another unit',
element: '.product-list .product-name:contains("POS Product 1")'
element: '.product-list ' +
'.product-name:contains("POS Product 1")'
},
{
wait: 4000,
title: 'The order total should be updated : 270€ which means 90€/Unit (Rule 10% Discount from 3 Units)',
title: 'The order total should be updated ' +
': 270€ which means 90€/Unit (Rule 10% Discount from 3 Units)',
waitFor: '.order .total .value:contains("270.00 €")'
},
{
wait: 8000,
title: 'We will add another product',
element: '.product-list .product-name:contains("POS Product 2")'
element: '.product-list ' +
'.product-name:contains("POS Product 2")'
},
{
wait: 4000,
title: 'We will add another unit for this product (POS Product 2)',
element: '.product-list .product-name:contains("POS Product 2")'
title: 'We will add another unit for this product ' +
'(POS Product 2)',
element: '.product-list ' +
'.product-name:contains("POS Product 2")'
},
{
wait: 4000,
title: "Let's verify the total that we should pay, it's should be equal to : 450€, which means that <br>" +
"10% Discount if offered if we buy 2 units of (POS Product 2), Rule based on standard price",
title: "Let's verify the total that we should pay," +
" it's should be equal to : 450€, which means that <br>" +
"10% Discount if offered if we buy 2 units of " +
"(POS Product 2), Rule based on standard price",
waitFor: '.order .total .value:contains("450.00 €")'
},
{
wait: 10000,
title: "Now, we will add (POS Product 3), for this product if we buy more then 2 units <br>" +
title: "Now, we will add (POS Product 3), for this " +
"product if we buy more then 2 units <br>" +
"20% Discount is given by supplier to our customers",
element: '.product-list .product-name:contains("POS Product 3")'
},
{
wait: 10000,
title: 'We will add another unit for this product (POS Product 3)',
element: '.product-list .product-name:contains("POS Product 3")'
title: 'We will add another unit for ' +
'this product (POS Product 3)',
element: '.product-list ' +
'.product-name:contains("POS Product 3")'
},
{
wait: 5000,
@ -101,14 +115,18 @@
},
{
wait: 5000,
title: "Now, we will add (POS Product 4), this product belong to (Comptuer) category in which " +
title: "Now, we will add (POS Product 4), this product " +
"belong to (Comptuer) category in which " +
"we apply 5% if customer buy more then 2 products",
element: '.product-list .product-name:contains("POS Product 4")'
element: '.product-list ' +
'.product-name:contains("POS Product 4")'
},
{
wait: 10000,
title: 'We will add another unit for this product (POS Product 4)',
element: '.product-list .product-name:contains("POS Product 4")'
title: 'We will add another unit for ' +
'this product (POS Product 4)',
element: '.product-list ' +
'.product-name:contains("POS Product 4")'
},
{
wait: 5000,
@ -134,7 +152,8 @@
{
wait: 1000,
title: "Let's finish the order",
element: ".button:not(.disabled) .iconlabel:contains('Next'):visible"
element: ".button:not(.disabled) " +
".iconlabel:contains('Next'):visible"
},
{
wait: 1000,
@ -156,7 +175,8 @@
},
{
title: "Wait for the backend to ready itself",
element: 'span:contains("Resume Session"),span:contains("Start Session")'
element: 'span:contains("Resume Session"),' +
'span:contains("Start Session")'
}
]
});

32
pos_pricelist/static/src/js/widgets.js

@ -24,7 +24,7 @@ function pos_pricelist_widgets(instance, module) {
if (this.editable && order.getSelectedLine()) {
var mode = this.numpad_state.get('mode');
if (mode === 'price') {
order.getSelectedLine().set_manuel_price(true);
order.getSelectedLine().set_manual_price(true);
}
}
}
@ -33,9 +33,35 @@ function pos_pricelist_widgets(instance, module) {
module.OrderButtonWidget = module.OrderButtonWidget.extend({
selectOrder: function (event) {
this._super(event);
var partner = this.order.get_client() ? this.order.get_client() : false;
var partner = this.order.get_client()
? this.order.get_client()
: false;
this.pos.pricelist_engine.update_products_ui(partner);
}
})
});
instance.point_of_sale.ProductListWidget.include({
init: function (parent, options) {
this._super(parent, options);
this.display_price_with_taxes = false;
if (
posmodel
&& posmodel.config
&& posmodel.config.display_price_with_taxes
) {
this.display_price_with_taxes
= posmodel.config.display_price_with_taxes
}
},
renderElement: function () {
this._super();
var order = posmodel.get_order();
var customer = null;
if(order) {
customer = order.get_client();
}
this.pos.pricelist_engine.update_products_ui(customer);
}
});
}

30
pos_pricelist/static/src/xml/pos.xml

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-extend="Product">
<t t-jquery=".product-img" t-operation="replace">
<div class="product-img">
<img t-att-src='image_url' />
<t t-if="!product.to_weight">
<span class="price-tag">
<t t-if="widget.display_price_with_taxes">
<t t-esc="widget.format_currency(product.price_with_taxes,'Product Price')"/>
</t>
<t t-if="!widget.display_price_with_taxes">
<t t-esc="widget.format_currency(product.price,'Product Price')"/>
</t>
</span>
</t>
<t t-if="product.to_weight">
<span class="price-tag">
<t t-if="widget.display_price_with_taxes">
<t t-esc="widget.format_currency(product.price_with_taxes,'Product Price')+'/Kg'"/>
</t>
<t t-if="!widget.display_price_with_taxes">
<t t-esc="widget.format_currency(product.price,'Product Price')+'/Kg'"/>
</t>
</span>
</t>
</div>
</t>
</t>
</templates>

28
pos_pricelist/views/point_of_sale_view.xml

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="view_pos_pos_form" model="ir.ui.view">
<field name="name">pos.order</field>
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='lines']/tree/field[@name='discount']" position="after">
<field name="tax_ids" widget="many2many_tags"/>
</xpath>
<page string="Payments" position="after">
<page string="Taxes">
<field name="taxes">
<tree editable="bottom" string="Taxes detail">
<field name="name"/>
<field name="base"/>
<field name="amount"/>
</tree>
</field>
</page>
</page>
</field>
</record>
</data>
</openerp>

15
pos_pricelist/views/pos_pricelist_views.xml

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="view_pos_config_form_pos_pricelist" model="ir.ui.view">
<field name="name">view.pos.config.form.pos.pricelist</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form"/>
<field name="arch" type="xml">
<field name="iface_big_scrollbars" position="after">
<field name="display_price_with_taxes"/>
</field>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save