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