From 280f0934a11f95ba525868f3635307c019f41ef7 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Thu, 22 Feb 2018 12:03:39 +0000 Subject: [PATCH] [MIG] pos_pricelist: Migrate to v10 by backporting from v11 (#247) This module included previously support for taxes and pricelists. Taxes are now included in upstream Odoo v10, so that feature is removed. The pricelist feature, however, is supported in upstream Odoo v11. Thus, to ease the migration path, that functionality has been scalpel-backported from there. This also means a great diff between previous OCA-only code and current Odoo-backport-only code, but with the advantage that the end user gets exactly the behavior that he would have when updating to v11. Since most of the code has been copied from Odoo, the addon now has to become LGPL, and Odoo has been added as an author. --- pos_pricelist/README.rst | 109 +- pos_pricelist/__init__.py | 37 +- pos_pricelist/__manifest__.py | 47 +- pos_pricelist/demo/pos_pricelist_demo.yml | 95 -- pos_pricelist/hooks.py | 17 + pos_pricelist/i18n/es.po | 200 ++-- pos_pricelist/i18n/pos_pricelist.pot | 141 --- .../migrations/8.0.1.1.0/post-migration.py | 30 - pos_pricelist/models/__init__.py | 24 +- pos_pricelist/models/point_of_sale.py | 160 --- pos_pricelist/models/pos_config.py | 123 +++ pos_pricelist/models/pos_order.py | 15 + pos_pricelist/models/pos_pricelist.py | 28 - pos_pricelist/models/res_partner.py | 20 + pos_pricelist/report/report_receipt.xml | 36 - pos_pricelist/security/ir.model.access.csv | 4 +- pos_pricelist/static/src/css/style.css | 150 --- pos_pricelist/static/src/js/db.js | 195 ---- pos_pricelist/static/src/js/main.js | 24 - pos_pricelist/static/src/js/models.js | 998 +++++------------- pos_pricelist/static/src/js/screens.js | 191 +++- pos_pricelist/static/src/js/tests.js | 184 ---- pos_pricelist/static/src/js/widgets.js | 87 -- pos_pricelist/static/src/xml/pos.xml | 74 +- pos_pricelist/templates/assets.xml | 16 + pos_pricelist/test/test.py | 28 - pos_pricelist/views/point_of_sale_view.xml | 28 - pos_pricelist/views/pos_config_view.xml | 41 + .../views/pos_pricelist_template.xml | 29 - pos_pricelist/views/pos_pricelist_views.xml | 15 - requirements.txt | 1 + 31 files changed, 836 insertions(+), 2311 deletions(-) delete mode 100644 pos_pricelist/demo/pos_pricelist_demo.yml create mode 100644 pos_pricelist/hooks.py delete mode 100644 pos_pricelist/i18n/pos_pricelist.pot delete mode 100644 pos_pricelist/migrations/8.0.1.1.0/post-migration.py delete mode 100644 pos_pricelist/models/point_of_sale.py create mode 100644 pos_pricelist/models/pos_config.py create mode 100644 pos_pricelist/models/pos_order.py delete mode 100644 pos_pricelist/models/pos_pricelist.py create mode 100644 pos_pricelist/models/res_partner.py delete mode 100644 pos_pricelist/report/report_receipt.xml delete mode 100644 pos_pricelist/static/src/css/style.css delete mode 100644 pos_pricelist/static/src/js/db.js delete mode 100644 pos_pricelist/static/src/js/main.js delete mode 100644 pos_pricelist/static/src/js/tests.js delete mode 100644 pos_pricelist/static/src/js/widgets.js create mode 100644 pos_pricelist/templates/assets.xml delete mode 100644 pos_pricelist/test/test.py delete mode 100644 pos_pricelist/views/point_of_sale_view.xml create mode 100644 pos_pricelist/views/pos_config_view.xml delete mode 100644 pos_pricelist/views/pos_pricelist_template.xml delete mode 100644 pos_pricelist/views/pos_pricelist_views.xml diff --git a/pos_pricelist/README.rst b/pos_pricelist/README.rst index 16ef83b8..aa6f6fa0 100644 --- a/pos_pricelist/README.rst +++ b/pos_pricelist/README.rst @@ -1,101 +1,58 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :alt: License: AGPL-3 +.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg + :target: https://www.gnu.org/licenses/lgpl + :alt: License: LGPL-3 -Dynamic Price for Odoo Point of Sale -==================================== - -Motivation ----------- - -Many issues report this feature. This why I took decision to start this module - -Reported issues : - -`odoo 8 POS price list discount has no effect. `_ - -`ODOO POS Pricelist - Public Price & Discounted Price in Receipt. `_ - -`V8.0 pos gives wrong price when using min qty in pos pricelist `_ - -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). +POS Pricelist +============= +Bring the pricelist computation engine to the Point of Sale. Installation ============ -Nothing special is needed to install this module. +You need this dependency:: + pip install oca.decorators Configuration ============= -You'll have new configuration parameters at Point of Sale > Configuration > Point of Sales +To configure a pricelist-enabled POS: -* Price with Taxes: Show prices with taxes in POS session or not +#. Go to *Point of Sale > Configuration > Point of Sale* and pick/create one. +#. Enable *Pricelists > Use pricelists*. +#. Set the *Available Pricelists* and choose a *Default Pricelist* from + among them. +#. You can use the *Pricelists* button to manage them easily, in case you need + more. +#. Save. +To see the effect, you should for instance apply the pricelist to a customer. 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. - -2. Fiscal Position of each partner will also be present so taxes will be correctly computed -(conforming to the fiscal position). - -- Implemented Rules are : - -1. (-1) : Rule based on other pricelist -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 - +#. Go to *Point of Sale > Dashboard* and open the POS session you configured. +#. Use the new pricelist button to change it on the fly. +#. When a new order is created, it always has the default pricelist. +#. When you change to a customer that has a different pricelist, the current + order and the listed product prices are updated accordingly. 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. - +* This module is a backport from Odoo 11.0 core pricelist functionalities. As + such, do not migrate it to that version or higher. +* Conflicts with ``pos_backend_partner`` make that when both are installed, + changing the partner does not change the pricelist. Bug Tracker =========== -Bugs are tracked on `GitHub 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 `_. - +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. Credits ======= @@ -106,7 +63,9 @@ Contributors * Adil Houmadi * Pablo Cayuela * Antonio Espinosa - +* Odoo S.A. +* `Tecnativa `_: + * Jairo Llopis Maintainer ---------- @@ -121,4 +80,4 @@ 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. +To contribute to this module, please visit https://odoo-community.org. diff --git a/pos_pricelist/__init__.py b/pos_pricelist/__init__.py index 576e94c5..10e21b51 100644 --- a/pos_pricelist/__init__.py +++ b/pos_pricelist/__init__.py @@ -1,34 +1,5 @@ -# -#- coding: utf-8 -#- -############################################################################## -# Point Of Sale - Pricelist for POS Odoo -# Copyright (C) 2014 Taktik (http://www.taktik.be) -# @author Adil Houmadi -# -# 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 . -# -############################################################################## -from . import models -from openerp import SUPERUSER_ID - +# -*- coding: utf-8 -*- +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -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_product p on l.product_id = p.id - join product_template pt on pt.id = p.product_tmpl_id - join product_taxes_rel rel on rel.prod_id = pt.id - join account_tax t on rel.tax_id = t.id - where t.company_id = o.company_id""") - registry['pos.order']._install_tax_detail(cr, SUPERUSER_ID) +from .hooks import post_init_hook +from . import models diff --git a/pos_pricelist/__manifest__.py b/pos_pricelist/__manifest__.py index ba867b47..b21f2839 100644 --- a/pos_pricelist/__manifest__.py +++ b/pos_pricelist/__manifest__.py @@ -1,46 +1,31 @@ # -*- coding: utf-8 -*- -############################################################################## -# Point Of Sale - Pricelist for POS Odoo -# Copyright (C) 2014 Taktik (http://www.taktik.be) -# @author Adil Houmadi -# -# 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 . -# -############################################################################## +# Copyright 2018 Tecnativa - Jairo Llopis +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + { 'name': 'POS Pricelist', - 'version': '8.0.1.2.0', + 'version': '10.0.1.0.0', 'category': 'Point Of Sale', - 'sequence': 1, - 'author': "Adil Houmadi @Taktik,Odoo Community Association (OCA)", + 'author': "Tecnativa, " + "Odoo SA, " + "Odoo Community Association (OCA)", 'summary': 'Pricelist for Point of sale', + 'license': 'LGPL-3', + 'post_init_hook': 'post_init_hook', 'depends': [ "point_of_sale", ], + "external_dependencies": { + "python": [ + "oca.decorators", + ], + }, '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", - ], - 'demo': [ - 'demo/pos_pricelist_demo.yml', + "templates/assets.xml", + "views/pos_config_view.xml", ], 'qweb': [ 'static/src/xml/pos.xml' ], - 'post_init_hook': "set_pos_line_taxes", - 'installable': False, - 'application': False, - 'auto_install': False, } diff --git a/pos_pricelist/demo/pos_pricelist_demo.yml b/pos_pricelist/demo/pos_pricelist_demo.yml deleted file mode 100644 index 4c2e9888..00000000 --- a/pos_pricelist/demo/pos_pricelist_demo.yml +++ /dev/null @@ -1,95 +0,0 @@ -- - This product will have two rule (min_qty:3 => 10%, min_qty:5 => 30%) -- - !record {model: product.product, id: pos_product_product_1}: - default_code: ABC123 - name: POS Product 1 - type: product - categ_id: product.product_category_1 - list_price: 100.0 - standard_price: 50.0 - uom_id: product.product_uom_unit - uom_po_id: product.product_uom_unit - available_in_pos: True - -- - This product will have one rule (min_qty:2 => 10%) -- - !record {model: product.product, id: pos_product_product_2}: - default_code: ABC124 - name: POS Product 2 - type: product - categ_id: product.product_category_1 - list_price: 100.0 - standard_price: 100.0 - uom_id: product.product_uom_unit - uom_po_id: product.product_uom_unit - available_in_pos: True - -- - This product will have a rule that (based on supplier discount) -- - !record {model: product.product, id: pos_product_product_3}: - default_code: ABC125 - name: POS Product 3 - type: product - categ_id: product.product_category_1 - list_price: 100.0 - standard_price: 50.0 - uom_id: product.product_uom_unit - uom_po_id: product.product_uom_unit - available_in_pos: True - seller_ids: - - delay: 1 - name: base.res_partner_3 - pricelist_ids: - - min_quantity : 2.0 - price : 80 - -- - This product belgon to computer category (5% dicount) -- - !record {model: product.product, id: pos_product_product_4}: - default_code: ABC125 - name: POS Product 4 - type: product - categ_id: product.product_category_4 - list_price: 100.0 - standard_price: 50.0 - uom_id: product.product_uom_unit - uom_po_id: product.product_uom_unit - available_in_pos: True -- - Prepare pricelist items -- - !record {model: product.pricelist.version, id: product.ver0}: - items_id: - - name: 10% Discount on POS Product 1 (Qty 3) - sequence: 2 - product_id: pos_product_product_1 - base: 1 - price_discount: -0.10 - min_quantity: 3 - - name: 30% Discount on POS Product 1 (Qty 5) - sequence: 1 - product_id: pos_product_product_1 - min_quantity: 5 - base: 1 - price_discount: -0.30 - - name: 10% Discount (POS Product 2) - sequence: 1 - product_id: pos_product_product_2 - base: 2 - price_discount: -0.10 - min_quantity: 2 - - name: 20% Discount given by supplier - sequence: 1 - min_quantity: 2 - product_id: pos_product_product_3 - base: -2 - - name: 5% Discount on all Computer related products (Qty 2) - sequence: 1 - min_quantity: 2 - base: 1 - categ_id: product.product_category_4 - price_discount: -0.05 \ No newline at end of file diff --git a/pos_pricelist/hooks.py b/pos_pricelist/hooks.py new file mode 100644 index 00000000..4cdbc530 --- /dev/null +++ b/pos_pricelist/hooks.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Tecnativa - Jairo Llopis +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, SUPERUSER_ID + + +def post_init_hook(cr, registry): + """Set default pricelists for existing POS configurations""" + with api.Environment.manage(): + env = api.Environment(cr, SUPERUSER_ID, {}) + nopricelist = env["pos.config"].search([ + ("available_pricelist_ids", "=", False), + ]) + for one in nopricelist: + one.available_pricelist_ids = (one.pricelist_id or + one._default_pricelist()) diff --git a/pos_pricelist/i18n/es.po b/pos_pricelist/i18n/es.po index 26cf25be..63629182 100644 --- a/pos_pricelist/i18n/es.po +++ b/pos_pricelist/i18n/es.po @@ -1,141 +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" +# * point_of_sale +"Project-Id-Version: Odoo Server 11.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" +"POT-Creation-Date: 2018-02-15 09:10+0000\n" +"PO-Revision-Date: 2018-02-15 09:18+0000\n" +"Last-Translator: Jairo Llopis \n" +"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.3\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 +#: code:addons/pos_pricelist/models/pos_config.py:57 #, 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" +msgid "" +"All available pricelists must be in the same currency as the company or as " +"the Sales Journal set on this point of sale if you use the Accounting " +"application." +msgstr "" +"Todas las tarifas deben estar en la misma moneda que la compañía o el diario " +"de ventas escogido para este punto de venta si usted está usando la " +"aplicación de contabilidad." #. module: pos_pricelist -#: field:pos.order.tax,write_uid:0 -msgid "Last Updated by" -msgstr "Última actualización por" +#: code:addons/pos_pricelist/models/pos_config.py:69 +#, python-format +msgid "" +"All payment methods must be in the same currency as the Sales Journal or the " +"company currency if that is not set." +msgstr "" +"Todos los métodos de pago deben estar en la misma moneda que el diario de " +"ventas o, si no lo hay, de la compañía." #. module: pos_pricelist -#: field:pos.order.tax,write_date:0 -msgid "Last Updated on" -msgstr "Última actualización en" +#: model:ir.model.fields,help:pos_pricelist.field_pos_config_group_sale_pricelist +msgid "" +"Allows to manage different prices based on rules per category of customers. " +"Example: 10% for retailers, promotion of 5 EUR on this product, etc." +msgstr "" +"Permite gestionar diferentes precios basándose en reglas por categoría de " +"clientes. Por ejemplo: 10% para minoristas, promoción de 5€ en este " +"producto, etc." #. 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" +#: model:ir.model.fields,field_description:pos_pricelist.field_pos_config_available_pricelist_ids +msgid "Available Pricelists" +msgstr "Listas de Precio Disponibles" #. module: pos_pricelist -#: field:pos.order.tax,pos_order:0 -msgid "POS Order" -msgstr "Pedido" +#: model:ir.model.fields,help:pos_pricelist.field_pos_config_available_pricelist_ids +msgid "" +"Make several pricelists available in the Point of Sale. You can also apply a " +"pricelist to specific customers from their contact form (in Sales tab). To " +"be valid, this pricelist must be listed here as an available pricelist. " +"Otherwise the default pricelist will apply." +msgstr "" +"Hacer disponibles varias tarifas en este punto de venta. También puede " +"aplicar una tarifa a clientes específicos desde su formulario de contacto " +"(en la pestaña de ventas). Para que sea válida, esa tarifa debe estar " +"disponible aquí. En caso contrario, se le aplicará la tarifa por defecto." #. module: pos_pricelist -#: model:product.template,name:pos_pricelist.pos_product_product_1_product_template -msgid "POS Product 1" -msgstr "TPV Producto 1" +#. openerp-web +#: code:addons/pos_pricelist/static/src/xml/pos.xml:36 +#, python-format +msgid "N/A" +msgstr "N/A" #. module: pos_pricelist -#: model:product.template,name:pos_pricelist.pos_product_product_2_product_template -msgid "POS Product 2" -msgstr "TPV Producto 2" +#: model:ir.model,name:pos_pricelist.model_pos_order +msgid "Point of Sale Orders" +msgstr "Pedidos del TPV" #. module: pos_pricelist -#: model:product.template,name:pos_pricelist.pos_product_product_3_product_template -msgid "POS Product 3" -msgstr "TPV Producto 3" +#. openerp-web +#: code:addons/pos_pricelist/static/src/js/screens.js:186 +#: code:addons/pos_pricelist/static/src/xml/pos.xml:15 +#: code:addons/pos_pricelist/static/src/xml/pos.xml:31 +#, python-format +msgid "Pricelist" +msgstr "Tarifa" #. module: pos_pricelist -#: model:product.template,name:pos_pricelist.pos_product_product_4_product_template -msgid "POS Product 4" -msgstr "TPV Producto 4" +#: model:ir.ui.view,arch_db:pos_pricelist.view_pos_config_form +msgid "Pricelists" +msgstr "Tarifas" #. module: pos_pricelist -#: view:pos.order:pos_pricelist.view_pos_pos_form -msgid "Payments" -msgstr "Pagos" +#. openerp-web +#: code:addons/pos_pricelist/static/src/js/screens.js:173 +#, python-format +msgid "Select pricelist" +msgstr "Seleccionar tarifa" #. module: pos_pricelist -#: model:ir.model,name:pos_pricelist.model_pos_order -msgid "Point of Sale" -msgstr "Terminal Punto de Venta" +#: model:ir.model.fields,help:pos_pricelist.field_pos_config_use_pricelist +msgid "Set shop-specific prices, seasonal discounts, etc." +msgstr "" +"Establecer precios específicos por tienda, descuentos por campaña especial, " +"etc." #. module: pos_pricelist -#: field:pos.config,display_price_with_taxes:0 -msgid "Price With Taxes" -msgstr "Precios con impuestos incluidos" +#: model:ir.model.fields,field_description:pos_pricelist.field_pos_config_group_pricelist_item +msgid "Show pricelists to customers" +msgstr "Mostrar tarifas a los clientes" #. module: pos_pricelist -#. openerp-web -#: code:addons/pos_pricelist/static/src/js/models.js:385 +#: code:addons/pos_pricelist/models/pos_config.py:52 #, python-format -msgid "Pricelist Error" -msgstr "Error en lista de precios" +msgid "The default pricelist must be included in the available pricelists." +msgstr "La tarifa por defecto debe estar incluida entre las disponibles." #. module: pos_pricelist -#: field:pos.order.tax,tax:0 -#: view:website:point_of_sale.report_receipt -msgid "Tax" -msgstr "Impuesto" +#: code:addons/pos_pricelist/models/pos_config.py:63 +#, python-format +msgid "" +"The invoice journal must be in the same currency as the Sales Journal or the " +"company currency if that is not set." +msgstr "" +"El diario de facturación debe estar en la misma moneda que el de ventas o, " +"si no lo hay, que la compañía." #. module: pos_pricelist -#: field:pos.order.tax,name:0 -msgid "Tax Description" -msgstr "Descripción del impuesto" +#: model:ir.model.fields,field_description:pos_pricelist.field_pos_config_use_pricelist +msgid "Use pricelists" +msgstr "Usar tarifas" #. 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" +#: model:ir.model.fields,field_description:pos_pricelist.field_pos_config_group_sale_pricelist +msgid "Use pricelists to adapt your price per customers" +msgstr "Usar tarifas para adaptar los precios a cada cliente" #. module: pos_pricelist -#: view:pos.order:pos_pricelist.view_pos_pos_form -msgid "Taxes detail" -msgstr "Detalle de impuestos" - +#: model:ir.model,name:pos_pricelist.model_pos_config +msgid "pos.config" +msgstr "pos.config" diff --git a/pos_pricelist/i18n/pos_pricelist.pot b/pos_pricelist/i18n/pos_pricelist.pot deleted file mode 100644 index 64bf283b..00000000 --- a/pos_pricelist/i18n/pos_pricelist.pot +++ /dev/null @@ -1,141 +0,0 @@ -# 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 "" - diff --git a/pos_pricelist/migrations/8.0.1.1.0/post-migration.py b/pos_pricelist/migrations/8.0.1.1.0/post-migration.py deleted file mode 100644 index 48cb6450..00000000 --- a/pos_pricelist/migrations/8.0.1.1.0/post-migration.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- 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 . -# -############################################################################## -__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""") diff --git a/pos_pricelist/models/__init__.py b/pos_pricelist/models/__init__.py index e139cc36..96f1e7a3 100644 --- a/pos_pricelist/models/__init__.py +++ b/pos_pricelist/models/__init__.py @@ -1,20 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# Point Of Sale - Pricelist for POS Odoo -# Copyright (C) 2015 Taktik (http://www.taktik.be) -# @author Adil Houmadi -# -# 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 . -# -############################################################################## -from . import pos_pricelist -from . import point_of_sale +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from . import pos_config +from . import pos_order +from . import res_partner diff --git a/pos_pricelist/models/point_of_sale.py b/pos_pricelist/models/point_of_sale.py deleted file mode 100644 index 1b0e1d83..00000000 --- a/pos_pricelist/models/point_of_sale.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- 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 . -# -############################################################################## - - -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)) diff --git a/pos_pricelist/models/pos_config.py b/pos_pricelist/models/pos_config.py new file mode 100644 index 00000000..e30b03af --- /dev/null +++ b/pos_pricelist/models/pos_config.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Tecnativa - Jairo Llopis +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +import logging + +_logger = logging.getLogger(__name__) + +try: + from oca.decorators import foreach +except ImportError: # pragma: no-cover + _logger.warn("Missing dependency", exc_info=True) + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + # Redefine preexisting field + pricelist_id = fields.Many2one( + string="Default Pricelist", + help="The pricelist used if no customer is selected or if the " + "customer has no Sale Pricelist configured.", + ) + # New fields + available_pricelist_ids = fields.Many2many( + 'product.pricelist', + string='Available Pricelists', + default=lambda self: self._default_pricelist(), + help="Make several pricelists available in the Point of Sale. " + "You can also apply a pricelist to specific customers from " + "their contact form (in Sales tab). To be valid, this pricelist " + "must be listed here as an available pricelist. Otherwise the " + "default pricelist will apply.", + ) + use_pricelist = fields.Boolean( + "Use pricelists", + help="Set shop-specific prices, seasonal discounts, etc.", + ) + group_sale_pricelist = fields.Boolean( + "Use pricelists to adapt your price per customers", + implied_group='product.group_sale_pricelist', + help="Allows to manage different prices based on rules per " + "category of customers. Example: 10% for retailers, promotion " + "of 5 EUR on this product, etc.", + ) + group_pricelist_item = fields.Boolean( + "Show pricelists to customers", + implied_group='product.group_pricelist_item', + ) + + @api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', + 'invoice_journal_id', 'journal_ids') + @foreach() + def _check_currencies(self): + if self.pricelist_id not in self.available_pricelist_ids: + raise ValidationError(_( + "The default pricelist must be included in " + "the available pricelists.")) + if self.available_pricelist_ids.filtered( + lambda pricelist: pricelist.currency_id != self.currency_id): + raise ValidationError(_( + "All available pricelists must be in the same currency " + "as the company or as the Sales Journal set on this " + "point of sale if you use the Accounting application.")) + if (self.invoice_journal_id.currency_id and + self.invoice_journal_id.currency_id != self.currency_id): + raise ValidationError(_( + "The invoice journal must be in the same currency as the " + "Sales Journal or the company currency if that is not set.")) + if self.journal_ids.filtered( + lambda journal: (journal.currency_id and + journal.currency_id != self.currency_id)): + raise ValidationError(_( + "All payment methods must be in the same currency as the " + "Sales Journal or the company currency if that is not set.")) + + @api.onchange('use_pricelist') + def _onchange_use_pricelist(self): + """If the 'pricelist' box is unchecked, reset the pricelist_id + + This makes the posbox to stop using a pricelist. + """ + if not self.use_pricelist: + self.pricelist_id = self.available_pricelist_ids = \ + self._default_pricelist() + else: + self.update({ + 'group_sale_pricelist': True, + 'group_pricelist_item': True, + }) + + @api.onchange('available_pricelist_ids') + def _onchange_available_pricelist_ids(self): + if self.pricelist_id not in self.available_pricelist_ids: + self.pricelist_id = False + + @foreach() + def _check_groups_implied(self): + for field_name in (f for f in self.fields_get_keys() + if f.startswith('group_')): + field = self._fields[field_name] + if (field.type in {'boolean', 'selection'} and + hasattr(field, 'implied_group')): + field_group_xmlids = getattr( + field, 'group', 'base.group_user').split(',') + field_groups = self.env['res.groups'].concat( + *(self.env.ref(it) for it in field_group_xmlids)) + field_groups.write({ + 'implied_ids': [(4, self.env.ref(field.implied_group).id)], + }) + + @api.model + def create(self, vals): + result = super(PosConfig, self).create(vals) + result.sudo()._check_groups_implied() + return result + + def write(self, vals): + result = super(PosConfig, self).write(vals) + self.sudo()._check_groups_implied() + return result diff --git a/pos_pricelist/models/pos_order.py b/pos_pricelist/models/pos_order.py new file mode 100644 index 00000000..4f18d763 --- /dev/null +++ b/pos_pricelist/models/pos_order.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Tecnativa - Jairo Llopis +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, models + + +class PosOrder(models.Model): + _inherit = "pos.order" + + @api.model + def _order_fields(self, ui_order): + result = super(PosOrder, self)._order_fields(ui_order) + result.setdefault('pricelist_id', ui_order['pricelist_id']) + return result diff --git a/pos_pricelist/models/pos_pricelist.py b/pos_pricelist/models/pos_pricelist.py deleted file mode 100644 index 36e385d6..00000000 --- a/pos_pricelist/models/pos_pricelist.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# Point Of Sale - Pricelist for POS Odoo -# Copyright (C) 2015 Taktik (http://www.taktik.be) -# @author Adil Houmadi -# -# 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 . -# -############################################################################## -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" - ) diff --git a/pos_pricelist/models/res_partner.py b/pos_pricelist/models/res_partner.py new file mode 100644 index 00000000..b12ea717 --- /dev/null +++ b/pos_pricelist/models/res_partner.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Tecnativa - Jairo Llopis +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + @api.model + def create_from_ui(self, partner): + try: + # If there's an incoming pricelist, it must be an int + partner["property_product_pricelist"] = \ + int(partner["property_product_pricelist"]) + except KeyError: + # There's no incoming pricelist, no problem + pass + return super(ResPartner, self).create_from_ui(partner) diff --git a/pos_pricelist/report/report_receipt.xml b/pos_pricelist/report/report_receipt.xml deleted file mode 100644 index 80572662..00000000 --- a/pos_pricelist/report/report_receipt.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/pos_pricelist/security/ir.model.access.csv b/pos_pricelist/security/ir.model.access.csv index aec8cb15..a9d488df 100644 --- a/pos_pricelist/security/ir.model.access.csv +++ b/pos_pricelist/security/ir.model.access.csv @@ -1,3 +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 +access_product_pricelist_manager,product.pricelist manager,product.model_product_pricelist,point_of_sale.group_pos_manager,1,1,1,1 +access_product_pricelist_user,product.pricelist user,product.model_product_pricelist,point_of_sale.group_pos_user,1,0,0,0 diff --git a/pos_pricelist/static/src/css/style.css b/pos_pricelist/static/src/css/style.css deleted file mode 100644 index 1ad36f1c..00000000 --- a/pos_pricelist/static/src/css/style.css +++ /dev/null @@ -1,150 +0,0 @@ -/****************************************************************************** -* Point Of Sale - Pricelist for POS Odoo -* Copyright (C) 2014 Taktik (http://www.taktik.be) -* @author Adil Houmadi -* -* 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 . -* -******************************************************************************/ -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - text-align: left; - white-space: normal; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - box-shadow: 0 5px 10px rgba(0, 0, 0, .2); -} -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - font-weight: normal; - line-height: 18px; - background-color: #f7f7f7; - 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; -} -.pos .pos-right-align { - text-align: right; - vertical-align: top; -} diff --git a/pos_pricelist/static/src/js/db.js b/pos_pricelist/static/src/js/db.js deleted file mode 100644 index d2f2be5d..00000000 --- a/pos_pricelist/static/src/js/db.js +++ /dev/null @@ -1,195 +0,0 @@ -/****************************************************************************** - * Point Of Sale - Pricelist for POS Odoo - * Copyright (C) 2014 Taktik (http://www.taktik.be) - * @author Adil Houmadi - * - * 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 . - * - ******************************************************************************/ -function pos_pricelist_db(instance, module) { - - module.PosDB = module.PosDB.extend({ - init: function (options) { - options = options || {}; - this._super(options); - this.pricelist_by_id = {}; - this.pricelist_version_by_id = {}; - this.pricelist_item_by_id = {}; - this.pricelist_item_sorted = []; - this.product_category_by_id = {}; - this.product_category_children = {}; - this.product_category_ancestors = {}; - this.product_price_type_by_id = {}; - this.supplierinfo_by_id = {}; - this.pricelist_partnerinfo_by_id = {}; - this.fiscal_position_tax_by_id = {}; - }, - add_fiscal_position_taxes: function (fiscal_position_taxes) { - if (!(fiscal_position_taxes instanceof Array)) { - fiscal_position_taxes = [fiscal_position_taxes]; - } - 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; - } - }, - add_pricelist_partnerinfo: function (pricelist_partnerinfos) { - if (!(pricelist_partnerinfos instanceof Array)) { - pricelist_partnerinfos = [pricelist_partnerinfos]; - } - var partner_info; - while (partner_info = pricelist_partnerinfos.pop()) { - this.pricelist_partnerinfo_by_id[partner_info.id] - = partner_info; - } - }, - add_supplierinfo: function (supplierinfos) { - if (!(supplierinfos instanceof Array)) { - supplierinfos = [supplierinfos]; - } - var supplier_info; - while (supplier_info = supplierinfos.pop()) { - this.supplierinfo_by_id[supplier_info.id] = supplier_info; - } - }, - add_pricelists: function (pricelists) { - if (!(pricelists instanceof Array)) { - pricelists = [pricelists]; - } - var pricelist; - while (pricelist = pricelists.pop()) { - this.pricelist_by_id[pricelist.id] = pricelist; - } - }, - add_pricelist_versions: function (versions) { - if (!(versions instanceof Array)) { - versions = [versions]; - } - var version; - while (version = versions.pop()) { - this.pricelist_version_by_id[version.id] = version; - } - }, - add_pricelist_items: function (items) { - if (!(items instanceof Array)) { - items = [items]; - } - var item; - while (item = items.pop()) { - this.pricelist_item_by_id[item.id] = item; - } - this.pricelist_item_sorted = this._items_sorted(); - }, - add_price_types: function (price_types) { - if (!(price_types instanceof Array)) { - price_types = [price_types]; - } - var ptype; - while (ptype = price_types.pop()) { - this.product_price_type_by_id[ptype.id] = ptype; - } - }, - add_product_categories: function (categories) { - if (!(categories instanceof Array)) { - categories = [categories]; - } - var category; - while (category = categories.pop()) { - this.product_category_by_id[category.id] = category; - this.product_category_children[category.id] = - category.child_id; - } - this._make_ancestors(); - }, - _make_ancestors: function () { - var category, ancestors; - for (var id in this.product_category_by_id) { - category = this.product_category_by_id[id]; - 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; - } - this.product_category_ancestors[parseInt(id)] = ancestors; - } - }, - _items_sorted: function () { - var items = this.pricelist_item_by_id; - var list = []; - for (var key in items) { - list.push(items[key]); - } - list.sort(function (a, b) { - if (a.sequence < b.sequence) return -1; - if (a.sequence > b.sequence) return 1; - if (a.min_quantity > b.min_quantity) return -1; - if (a.min_quantity < b.min_quantity) return 1; - return 0; - }); - return list; - }, - map_tax: function (fiscal_position_id, taxes_ids) { - var taxes = []; - var found_taxes = {}; - for (var id in this.fiscal_position_tax_by_id) { - 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; - } - }) -} diff --git a/pos_pricelist/static/src/js/main.js b/pos_pricelist/static/src/js/main.js deleted file mode 100644 index 143d36e0..00000000 --- a/pos_pricelist/static/src/js/main.js +++ /dev/null @@ -1,24 +0,0 @@ -/****************************************************************************** -* Point Of Sale - Pricelist for POS Odoo -* Copyright (C) 2014 Taktik (http://www.taktik.be) -* @author Adil Houmadi -* -* 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 . -* -******************************************************************************/ -openerp.pos_pricelist = function (instance) { - var module = instance.point_of_sale; - pos_pricelist_db(instance, module); - pos_pricelist_models(instance, module); - pos_pricelist_screens(instance, module); - pos_pricelist_widgets(instance, module); -}; diff --git a/pos_pricelist/static/src/js/models.js b/pos_pricelist/static/src/js/models.js index 5388d1d8..b64834da 100644 --- a/pos_pricelist/static/src/js/models.js +++ b/pos_pricelist/static/src/js/models.js @@ -1,800 +1,272 @@ -/****************************************************************************** - * Point Of Sale - Pricelist for POS Odoo - * Copyright (C) 2014 Taktik (http://www.taktik.be) - * @author Adil Houmadi - * - * 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 . - * - ******************************************************************************/ -function pos_pricelist_models(instance, module) { +/* Copyright 2018 Tecnativa - Jairo Llopis + License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ - var _t = instance.web._t; - var round_pr = instance.web.round_precision; - var round_di = instance.web.round_decimals; +odoo.define("pos_pricelist.models", function (require) { + "use strict"; - /** - * Extend the POS model - */ - var PosModelParent = module.PosModel; - module.PosModel = module.PosModel.extend({ - /** - * @param session - * @param attributes - */ - initialize: function (session, attributes) { - 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); - }, - /** - * find model based on name - * @param model_name - * @returns {{}} - */ - find_model: function (model_name) { - var self = this; - var lookup = {}; - for (var i = 0, len = self.models.length; i < len; i++) { - if (self.models[i].model === model_name) { - lookup[i] = self.models[i] - } - } - return lookup - }, - /** - * @param removed_order - * @param index - * @param reason - */ - on_removed_order: function (removed_order, index, reason) { - 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); - } - } - }); + var core = require("web.core"); + var models = require("point_of_sale.models"); + var utils = require('web.utils'); - /** - * Extend the order - */ - module.Order = module.Order.extend({ - /** - * override this method to merge lines - * TODO : Need some refactoring in the standard POS to Do it better - * TODO : from line 73 till 85, we need to move this to another method - * @param product - * @param options - */ - addProduct: function (product, options) { - options = options || {}; - 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 self = this; - var found = false; + var moment = window.moment; + var round_pr = utils.round_precision; - if (options.quantity !== undefined) { - line.set_quantity(options.quantity); - } - if (options.price !== undefined) { - line.set_unit_price(options.price); - } - if (options.discount !== undefined) { - line.set_discount(options.discount); - } + var exports = {}; - var orderlines = []; - if (self.get('orderLines').models !== undefined) { - orderlines = self.get('orderLines').models; - } - for (var i = 0; i < orderlines.length; i++) { - var _line = orderlines[i]; - if (_line && _line.can_be_merged_with(line) && - options.merge !== false) { - _line.merge(line); - found = true; - break; - } - } - if (!found) { - this.get('orderLines').add(line); - } - this.selectLine(this.getLastOrderline()); - } - }); + // Patch PosModel (it's a Backbone.Model, not a usual odoo model) + var _PosModel_initialize = models.PosModel.prototype.initialize; + models.PosModel.prototype.initialize = function () { + _PosModel_initialize.apply(this, arguments); + this.default_pricelist = null; + }; - /** - * Extend the Order line - */ - var OrderlineParent = module.Orderline; - module.Orderline = module.Orderline.extend({ - /** - * @param attr - * @param options - */ - initialize: function (attr, options) { - 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 - ); - if (price !== false) { - this.price = price; - } - } - }, - /** - * @param state - */ - set_manual_price: function (state) { - this.manual_price = state; - }, - /** - * @param quantity - */ - set_quantity: function (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 - ); - if (price !== false) { - this.price = price; - } - this.trigger('change', this); - }, - /** - * 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: {} - * }} - */ - get_all_prices: function () { - var base = this.get_base_price(); - var totalTax = base; - var totalNoTax = base; - var taxtotal = 0; - var taxdetail = {}; - 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) { - totalNoTax -= tax.amount; - } else { - totalTax += tax.amount; - } - taxtotal += tax.amount; - taxdetail[tax.id] = tax.amount; - }); - totalNoTax = round_pr(totalNoTax, this.pos.currency.rounding); - return { - "priceWithTax": totalTax, - "priceWithoutTax": totalNoTax, - "tax": taxtotal, - "taxDetails": taxdetail - }; - }, - /** - * 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 = OrderlineParent.prototype.can_be_merged_with.apply( - this, arguments - ); - if (!result) { - if (!this.manual_price) { - return ( - this.get_product().id === orderline.get_product().id - ); - } else { - return false; - } - } - return true; - }, - /** - * Override to set price - * @param orderline - */ - merge: function (orderline) { - OrderlineParent.prototype.merge.apply(this, arguments); - this.set_unit_price(orderline.price); - }, - /** - * @param order - * @param product - * @returns {number} - */ - compute_qty: function (order, product) { - var qty = 1; - var orderlines = []; - 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].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; - }, - get_display_unit_price: function(){ - var rounding = this.pos.currency.rounding; - if (this.pos.config.display_price_with_taxes) { - return round_pr(this.get_price_with_tax() / this.get_quantity(), rounding); - } else { - return round_pr(this.get_base_price() / this.get_quantity(), rounding); - } - }, - /** - * @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 - ); - }, + // Patch res.partner + models.load_fields("res.partner", ["property_product_pricelist"]); - 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; + // Patch product.pricelist + models.load_fields("product.pricelist", ['name', 'display_name']); + var _product_pricelist = _.findWhere( + models.PosModel.prototype.models, + {model: "product.pricelist"} + ), + _product_pricelist_domain = _product_pricelist.domain, + _product_pricelist_loaded = _product_pricelist.loaded; + delete _product_pricelist.ids; + _product_pricelist.domain = function (self) { + return _.union( + _product_pricelist_domain, + [['id', 'in', self.config.available_pricelist_ids]] + ); + }; + _product_pricelist.loaded = function (self, pricelists) { + _product_pricelist_loaded.apply(this, arguments); + _.map(pricelists, function (pricelist) { + pricelist.items = []; + }); + self.default_pricelist = _.findWhere( + pricelists, + {id: self.config.pricelist_id[0]} + ); + self.pricelists = pricelists; + self.pricelist = self.default_pricelist; + }; + + // New models to load after product.pricelist + models.load_models( + [ + { + model: 'product.pricelist.item', + domain: function(self) { + return [['pricelist_id', 'in', + _.pluck(self.pricelists, 'id')]]; + }, + loaded: function(self, pricelist_items){ + var pricelist_by_id = {}; + _.each(self.pricelists, function (pricelist) { + pricelist_by_id[pricelist.id] = pricelist; + }); + + _.each(pricelist_items, function (item) { + var pricelist = pricelist_by_id[item.pricelist_id[0]]; + pricelist.items.push(item); + item.base_pricelist = pricelist_by_id[ + item.base_pricelist_id[0] + ]; + }); + }, + }, { + model: 'product.category', + fields: ['name', 'parent_id'], + loaded: function (self, product_categories) { + var category_by_id = {}; + _.each(product_categories, function (category) { + category_by_id[category.id] = category; + }); + _.each(product_categories, function (category) { + category.parent = category_by_id[ + category.parent_id[0] + ]; + }); + self.product_categories = product_categories; + } + }, + ], { + after: "product.pricelist", } - }); + ); - /** - * Pricelist Engine to compute price - */ - module.PricelistEngine = instance.web.Class.extend({ - /** - * @param options - */ - init: function (options) { - options = options || {}; - this.pos = options.pos; - this.db = options.db; - this.pos_widget = options.pos_widget; - }, - /** - * compute price for all price list - * @param db - * @param product - * @param partner - * @param qty - * @returns {*} - */ - compute_price_all: function (db, product, partner, qty) { - var price_list_id = false; - if (partner && partner.property_product_pricelist) { - price_list_id = partner.property_product_pricelist[0]; - } else { - price_list_id = this.pos.config.pricelist_id[0]; - } - return this.compute_price( - db, product, partner, qty, parseInt(price_list_id) + // Patch product.product + models.load_fields( + "product.product", + ['lst_price', 'standard_price', 'categ_id'] + ); + var _product_product = _.findWhere( + models.PosModel.prototype.models, + {model: "product.product"} + ); + _product_product.loaded = function (self, products) { + self.db.add_products(_.map(products, function (product) { + product.categ = _.findWhere( + self.product_categories, + {'id': product.categ_id[0]} ); + return new exports.Product(product); + })); + }; + + // New model Product + exports.Product = core.Class.extend({ + init: function(options) { + _.extend(this, options); }, - /** - * loop find a valid version for the price list id given in param - * @param db - * @param pricelist_id - * @returns {boolean} - */ - find_valid_pricelist_version: function (db, pricelist_id) { - var date = new Date(); - var version = false; - 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))) { - version = v; - break; - } - } - return version; - }, - /** - * compute the price for the given product - * @param database - * @param product - * @param partner - * @param qty - * @param pricelist_id - * @returns {boolean} - */ - compute_price: function (database, product, partner, qty, pricelist_id) { + // Port of get_product_price on product.pricelist. + // + // Anything related to UOM can be ignored, the POS will always use + // the default UOM set on the product and the user cannot change + // it. + // + // Pricelist items do not have to be sorted. All + // product.pricelist.item records are loaded with a search_read + // and were automatically sorted based on their _order by the + // ORM. After that they are added in this order to the pricelists. + get_price: function(pricelist, quantity) { var self = this; - var db = database; + var date = moment().startOf('day'); - // get a valid version - 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.'); - show_error(this, message, comment); - return false; + var category_ids = []; + var category = this.categ; + while (category) { + category_ids.push(category.id); + category = category.parent; } - // get categories - 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]] - ); - } + var pricelist_items = _.filter(pricelist.items, function (item) { + return (! item.product_tmpl_id || item.product_tmpl_id[0] === self.product_tmpl_id) && + (! item.product_id || item.product_id[0] === self.id) && + (! item.categ_id || _.contains(category_ids, item.categ_id[0])) && + (! item.date_start || moment(item.date_start).isSameOrBefore(date)) && + (! item.date_end || moment(item.date_end).isSameOrAfter(date)); + }); - // 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) && - (item.price_version_id[0] === version.id)) { - items.push(item); + var price = self.lst_price; + _.find(pricelist_items, function (rule) { + if (rule.min_quantity && quantity < rule.min_quantity) { + return false; } - } - var results = {}; - results[product.id] = 0.0; - var price_types = {}; - var price = false; - - // loop through items - for (i = 0, len = items.length; i < len; i++) { - var rule = items[i]; - - if (rule.min_quantity && qty < rule.min_quantity) { - continue; - } - if (rule.product_id && rule.product_id[0] - && product.id != rule.product_id[0]) { - continue; + if (rule.base === 'pricelist') { + price = self.get_price(rule.base_pricelist, quantity); + } else if (rule.base === 'standard_price') { + price = self.standard_price; } - if (rule.categ_id) { - var cat_id = product.categ_id[0]; - while (cat_id) { - if (cat_id == rule.categ_id[0]) { - break; - } - cat_id = db.product_category_by_id[cat_id].parent_id[0] - } - if (!(cat_id)) { - continue; - } - } - // Based on field - switch (rule.base) { - case -1: - if (rule.base_pricelist_id) { - price = self.compute_price( - db, product, false, qty, - rule.base_pricelist_id[0] - ); - } - break; - case -2: - var seller = false; - 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)) - continue; - seller = _tmp_seller - } - if (!seller && product.seller_ids) { - 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]; - if (line.min_quantity <= qty) { - price = line.price - } - } - } - break; - default: - if (!price_types.hasOwnProperty(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 (rule.compute_price === 'fixed') { + price = rule.fixed_price; + return true; + } else if (rule.compute_price === 'percentage') { + price -= price * (rule.percent_price / 100); + return true; } - if (price !== false) { - var price_limit = price; - 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 += (rule['price_surcharge'] - ? rule['price_surcharge'] - : 0.0); - if (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'] - ) - } + var price_limit = price; + price -= price * (rule.price_discount / 100); + if (rule.price_round) { + price = round_pr(price, rule.price_round); } - break; - } - return price - }, - /** - * @param partner - */ - 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' - ); - 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 = $.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++) { - if ($.inArray(rules[j].min_quantity, quantities) === -1) { - quantities.push(rules[j].min_quantity); - } + if (rule.price_surcharge) { + price += rule.price_surcharge; } - 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 (rule.price_min_margin) { + price = Math.max( + price, + price_limit + rule.price_min_margin ); - if (price !== false) { - 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 → ' + price + '
'; - } } - 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}} + if (rule.price_max_margin) { + price = Math.min( + price, + price_limit + rule.price_max_margin ); } - } - }, - 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 + return true; }); - orderline.set_quantity(qty); - // reset the sequence - this.pos.pos_session.sequence_number--; - var prices = orderline.get_all_prices(); - return prices; + + // This return value has to be rounded with round_di before + // being used further. Note that this cannot happen here, + // because it would cause inconsistencies with the backend for + // pricelist that have base == 'pricelist'. + return price; }, - /** - * - * @param partner - * @param orderLines - */ - update_ticket: function (partner, orderLines) { - var db = this.db; - for (var i = 0, len = orderLines.length; i < len; i++) { - var line = orderLines[i]; - var product = line.product; - var quantity = line.quantity; - var price = this.compute_price_all( - db, product, partner, quantity - ); - if (price !== false) { - line.price = price; - } - line.trigger('change', line); - } - } }); - /** - * show error - * @param context - * @param message - * @param comment - */ - function show_error(context, message, comment) { - context.pos.pos_widget.screen_selector.show_popup('error', { - 'message': message, - 'comment': comment - }); - } - - /** - * patch models to load some entities - * @param pos_model - */ - function arrange_elements(pos_model) { - 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' + // Patch Orderline + var _Orderline_initialize = models.Orderline.prototype.initialize, + _Orderline_init_from_JSON = models.Orderline.prototype.init_from_JSON, + _Orderline_set_quantity = models.Orderline.prototype.set_quantity; + models.Orderline.prototype.initialize = function (attr, options) { + _Orderline_initialize.apply(this, arguments); + if (options.product) { + this.set_unit_price( + options.product.price || + this.product.get_price( + this.order.pricelist, + this.get_quantity() + ) ); } - - 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] - ); - pos_model.models.splice(++pricelist_index, 0, - { - model: 'account.fiscal.position.tax', - 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 - ); - } - }, - { - model: 'pricelist.partnerinfo', - fields: ['display_name', - 'min_quantity', - 'name', - 'price', - 'suppinfo_id'], - domain: null, - loaded: function (self, 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'], - domain: null, - loaded: function (self, supplierinfos) { - self.db.add_supplierinfo(supplierinfos); - } - }, - { - model: 'product.category', - fields: ['name', - 'display_name', - 'parent_id', - 'child_id'], - domain: null, - loaded: function (self, categories) { - self.db.add_product_categories(categories); - - } - }, - { - model: 'product.pricelist', - fields: ['display_name', - 'name', - 'version_id', - 'currency_id'], - domain: function () { - return [ - ['type', '=', 'sale'] - ] - }, - loaded: function (self, pricelists) { - self.db.add_pricelists(pricelists); - } - }, - { - model: 'product.pricelist.version', - fields: ['name', - 'pricelist_id', - 'date_start', - 'date_end', - 'items'], - domain: null, - loaded: function (self, versions) { - self.db.add_pricelist_versions(versions); - } - }, - { - 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' - ], - domain: null, - loaded: function (self, items) { - self.db.add_pricelist_items(items); - } - }, - { - model: 'product.price.type', - 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++) { - 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 - ); - } - } - } - self.db.add_price_types(price_types); - } - } - ); + }; + models.Orderline.prototype.init_from_JSON = function () { + this.keep_price = 'do not recompute unit price'; + return _Orderline_init_from_JSON.apply(this, arguments); + }; + models.Orderline.prototype.set_quantity = function () { + _Orderline_set_quantity.apply(this, arguments); + var keep_price = this.keep_price; + delete this.keep_price; + // just like in sale.order changing the quantity will recompute the unit price + if (!keep_price) { + this.set_unit_price(this.product.get_price( + this.order.pricelist, + this.get_quantity() + )); + this.order.fix_tax_included_price(this); } + }; - 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' - ); + // Patch Order + var _Order_initialize = models.Order.prototype.initialize, + _Order_init_from_JSON = models.Order.prototype.init_from_JSON, + _Order_export_as_JSON = models.Order.prototype.export_as_JSON; + models.Order.prototype.initialize = function () { + _Order_initialize.apply(this, arguments); + this.set_pricelist(this.pos.default_pricelist); + }; + models.Order.prototype.init_from_JSON = function (json) { + _Order_init_from_JSON.apply(this, arguments); + if (json.pricelist_id) { + this.pricelist = _.find(this.pos.pricelists, function (pricelist) { + return pricelist.id === json.pricelist_id; + }); + } else { + this.pricelist = this.pos.default_pricelist; } + }; + models.Order.prototype.export_as_JSON = function () { + var result = _Order_export_as_JSON.apply(this, arguments); + result.pricelist_id = this.pricelist ? this.pricelist.id : false; + return result; + }; + models.Order.prototype.set_pricelist = function (pricelist) { + var self = this; + this.pricelist = pricelist; + _.each(this.get_orderlines(), function (line) { + line.set_unit_price( + line.product.get_price(self.pricelist, line.get_quantity()) + ); + self.fix_tax_included_price(line); + }); + this.trigger('change'); + }; - } - -} + return exports; +}); diff --git a/pos_pricelist/static/src/js/screens.js b/pos_pricelist/static/src/js/screens.js index 9949ce15..c43342ba 100644 --- a/pos_pricelist/static/src/js/screens.js +++ b/pos_pricelist/static/src/js/screens.js @@ -1,32 +1,167 @@ -/****************************************************************************** -* Point Of Sale - Pricelist for POS Odoo -* Copyright (C) 2014 Taktik (http://www.taktik.be) -* @author Adil Houmadi -* -* 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 . -* -******************************************************************************/ -function pos_pricelist_screens(instance, module) { - - module.ClientListScreenWidget = module.ClientListScreenWidget.extend({ +/* Copyright 2018 Tecnativa - Jairo Llopis + License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +odoo.define("pos_pricelist.screens", function (require) { + "use strict"; + + var core = require("web.core"); + var screens = require("point_of_sale.screens"); + var _t = core._t; + var exports = {}; + + screens.ScaleScreenWidget.include({ + _get_active_pricelist: function() { + var current_order = this.pos.get_order(); + var current_pricelist = this.pos.default_pricelist; + if (current_order) { + current_pricelist = current_order.pricelist; + } + return current_pricelist; + }, + + get_product_price: function () { + var result = this._super.apply(this, arguments); + var product = this.get_product(); + if (product) { + var pricelist = this._get_active_pricelist(); + result = product.get_price(pricelist, this.weight) || result; + } + return result; + }, + }); + + screens.DomCache.include({ + init: function () { + this._super.apply(this, arguments); + this.key_wrapper = function (key) { + return key; + }; + }, + + cache_node: function () { + arguments[0] = this.key_wrapper(arguments[0]); + return this._super.apply(this, arguments); + }, + + clear_node: function() { + arguments[0] = this.key_wrapper(arguments[0]); + return this._super.apply(this, arguments); + }, + + get_node: function() { + arguments[0] = this.key_wrapper(arguments[0]); + return this._super.apply(this, arguments); + }, + }); + + screens.ProductListWidget.include({ + init: function () { + this._super.apply(this, arguments); + this.pos.get('orders').bind( + 'add remove change', + $.proxy(this, "renderElement") + ); + this.pos.bind( + 'change:selectedOrder', + $.proxy(this, "renderElement") + ); + this.product_cache.key_wrapper = $.proxy( + this, + "calculate_cache_key" + ); + }, + + calculate_cache_key: function (product) { + var result = product + ',' + this._get_active_pricelist().id; + return result; + }, + + _get_active_pricelist: function () { + var current_order = this.pos.get_order(); + var current_pricelist = this.pos.default_pricelist; + if (current_order) { + current_pricelist = current_order.pricelist; + } + return current_pricelist; + }, + }); + + screens.ClientListScreenWidget.include({ save_changes: function () { - this._super(); if (this.has_client_changed()) { - var currentOrder = this.pos.get('selectedOrder'); - var orderLines = currentOrder.get('orderLines').models; - var partner = currentOrder.get_client(); - this.pos.pricelist_engine.update_products_ui(partner); - this.pos.pricelist_engine.update_ticket(partner, orderLines); + var order = this.pos.get_order(), + pricelist = false; + if (this.new_client) { + pricelist = _.findWhere( + this.pos.pricelists, + {'id': this.new_client.property_product_pricelist[0]} + ); + } + order.set_pricelist(pricelist || this.pos.default_pricelist); + } + return this._super.apply(this, arguments); + }, + }); + + exports.set_pricelist_button = screens.ActionButtonWidget.extend({ + template: 'SetPricelistButton', + init: function (parent, options) { + this._super(parent, options); + + this.pos.get('orders').bind('add remove change', function () { + this.renderElement(); + }, this); + + this.pos.bind('change:selectedOrder', function () { + this.renderElement(); + }, this); + }, + + button_click: function () { + var self = this; + + var pricelists = _.map(self.pos.pricelists, function (pricelist) { + return { + label: pricelist.name, + item: pricelist + }; + }); + + self.gui.show_popup('selection', { + title: _t('Select pricelist'), + list: pricelists, + confirm: function (pricelist) { + var order = self.pos.get_order(); + order.set_pricelist(pricelist); + }, + is_selected: function (pricelist) { + return pricelist.id === self.pos.get_order().pricelist.id; + } + }); + }, + + get_current_pricelist_name: function () { + var name = _t('Pricelist'); + var order = this.pos.get_order(); + + if (order) { + var pricelist = order.pricelist; + + if (pricelist) { + name = pricelist.display_name; + } } - } + return name; + }, + }); + + screens.define_action_button({ + 'name': 'set_pricelist', + 'widget': exports.set_pricelist_button, + 'condition': function () { + return this.pos.pricelists.length > 1; + }, }); -} + + return exports; +}); diff --git a/pos_pricelist/static/src/js/tests.js b/pos_pricelist/static/src/js/tests.js deleted file mode 100644 index 3f984cca..00000000 --- a/pos_pricelist/static/src/js/tests.js +++ /dev/null @@ -1,184 +0,0 @@ -/****************************************************************************** - * Point Of Sale - Pricelist for POS Odoo - * Copyright (C) 2014 Taktik (http://www.taktik.be) - * @author Adil Houmadi - * - * 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 . - * - ******************************************************************************/ -(function () { - 'use strict'; - - openerp.Tour.register({ - id: 'pos_pricelist_order', - name: 'Complete a order trough the Front-End using POS Pricelist', - path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening', - mode: 'test', - steps: [ - { - wait: 2000, - title: 'Wait for screen to be ready' - }, - { - wait: 2000, - title: 'Load the Session', - waitNot: '.oe_loading:visible', - element: 'span:contains("Resume Session"),' + - 'span:contains("Start Session")' - }, - { - title: 'Loading Screen', - waitFor: '.loader' - }, - { - wait: 2000, - title: 'The Point of Sale', - waitFor: '.pos' - }, - { - title: "We will buy some Products!, let's add (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€', - 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")' - }, - { - wait: 4000, - title: 'We will add another unit', - 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)', - waitFor: '.order .total .value:contains("270.00 €")' - }, - { - wait: 8000, - title: 'We will add another product', - 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")' - }, - { - wait: 4000, - title: "Let's verify the total that we should pay," + - " it's should be equal to : 450€, which means that
" + - "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
" + - "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")' - }, - { - wait: 5000, - title: "Let's check the total (610€)", - waitFor: '.order .total .value:contains("610.00 €")' - }, - { - wait: 5000, - 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")' - }, - { - wait: 10000, - title: 'We will add another unit for ' + - 'this product (POS Product 4)', - element: '.product-list ' + - '.product-name:contains("POS Product 4")' - }, - { - wait: 5000, - title: "Let's check the total again (800€)", - waitFor: '.order .total .value:contains("800.00 €")' - }, - { - wait: 5000, - title: "Let's pay the order", - element: ".paypad-button:contains('Bank')" - }, - { - wait: 1000, - title: "Let's accept the payment", - onload: function () { - window._print = window.print; - window.print = function () { - console.log('Print!') - }; - }, - element: ".button .iconlabel:contains('Validate'):visible" - }, - { - wait: 1000, - title: "Let's finish the order", - element: ".button:not(.disabled) " + - ".iconlabel:contains('Next'):visible" - }, - { - wait: 1000, - onload: function () { - window.print = window._print; - window._print = undefined; - }, - title: "Let's wait for the order posting", - waitFor: ".oe_status.js_synch .js_connected:visible" - }, - { - wait: 1000, - title: "Let's close the Point of Sale", - element: ".header-button:contains('Close')" - }, - { - title: "Let's confirm", - element: ".header-button.confirm:contains('Confirm')" - }, - { - title: "Wait for the backend to ready itself", - element: 'span:contains("Resume Session"),' + - 'span:contains("Start Session")' - } - ] - }); - -})(); diff --git a/pos_pricelist/static/src/js/widgets.js b/pos_pricelist/static/src/js/widgets.js deleted file mode 100644 index 338a4546..00000000 --- a/pos_pricelist/static/src/js/widgets.js +++ /dev/null @@ -1,87 +0,0 @@ -/****************************************************************************** - * Point Of Sale - Pricelist for POS Odoo - * Copyright (C) 2014 Taktik (http://www.taktik.be) - * @author Adil Houmadi - * - * 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 . - * - ******************************************************************************/ -function pos_pricelist_widgets(instance, module) { - - var round_di = instance.web.round_decimals; - - module.OrderWidget = module.OrderWidget.extend({ - set_value: function (val) { - this._super(val); - var order = this.pos.get('selectedOrder'); - if (this.editable && order.getSelectedLine()) { - var mode = this.numpad_state.get('mode'); - if (mode === 'price') { - order.getSelectedLine().set_manual_price(true); - } - } - } - }); - - module.OrderButtonWidget = module.OrderButtonWidget.extend({ - selectOrder: function (event) { - this._super(event); - 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); - } - }); - - module.PosBaseWidget.include({ - format_pr: function(amount, precision) { - // Do not call _super because no addon or XML is using this method - var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2}; - var decimals = currency.decimals; - - if (precision && (typeof this.pos.dp[precision]) !== undefined) { - decimals = this.pos.dp[precision]; - } - - if (typeof amount === 'number') { - amount = round_di(amount,decimals).toFixed(decimals); - amount = openerp.instances[this.session.name].web.format_value(round_di(amount, decimals), { type: 'float', digits: [69, decimals]}); - } - return amount - } - }); -} - diff --git a/pos_pricelist/static/src/xml/pos.xml b/pos_pricelist/static/src/xml/pos.xml index 51fc5099..0b54cd95 100644 --- a/pos_pricelist/static/src/xml/pos.xml +++ b/pos_pricelist/static/src/xml/pos.xml @@ -1,37 +1,51 @@ - - - - -
- - - - - - - - - - + + + + + +
+ +
+
+ + + +
+ Pricelist + +
+
+
+ + + +
+ Pricelist + + - - - - - - - - - + + N/A
- - - - x - + + + + + + + + -
\ No newline at end of file + diff --git a/pos_pricelist/templates/assets.xml b/pos_pricelist/templates/assets.xml new file mode 100644 index 00000000..a5fdc7a6 --- /dev/null +++ b/pos_pricelist/templates/assets.xml @@ -0,0 +1,16 @@ + + + + + - - - - - diff --git a/pos_pricelist/views/pos_pricelist_views.xml b/pos_pricelist/views/pos_pricelist_views.xml deleted file mode 100644 index 084723e7..00000000 --- a/pos_pricelist/views/pos_pricelist_views.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - view.pos.config.form.pos.pricelist - pos.config - - - - - - - - - diff --git a/requirements.txt b/requirements.txt index e69de29b..ff26d683 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +oca.decorators