From c1e739e8758d0e19d27ed1217bf83b212ac59b8a Mon Sep 17 00:00:00 2001 From: "Jose Suniaga [Vauxoo]" Date: Tue, 7 Feb 2017 02:36:21 +0000 Subject: [PATCH 01/15] [ADD] product_cost_usd: handle cost in USD on products The approach is to have the USD as the base currency, so that the sale prices in company currency can be kept up to date through the exchange rate. Module features: - New Cost USD field on the Product form. - Validate Cost USD so that it is not less than the list price of the supplier. - Avoid save a 'Cost in USD' when product does not has assigned a supplier with price in USD. - Allowed pricelist computation based on Cost in USD. - Added unit tests to validate constrains and pricelists computation. - Compatibility with module sale_margin. --- product_cost_usd/README.rst | 38 ++++++ product_cost_usd/__init__.py | 5 + product_cost_usd/__manifest__.py | 27 ++++ product_cost_usd/i18n/es.po | 65 +++++++++ product_cost_usd/i18n/es_MX.po | 17 +++ product_cost_usd/models/__init__.py | 7 + product_cost_usd/models/pricelist.py | 64 +++++++++ product_cost_usd/models/product.py | 46 +++++++ product_cost_usd/models/sale_order.py | 50 +++++++ product_cost_usd/static/description/icon.png | Bin 0 -> 28984 bytes .../static/description/index.html | 67 ++++++++++ product_cost_usd/tests/__init__.py | 5 + .../tests/test_standard_price_usd.py | 125 ++++++++++++++++++ product_cost_usd/views/product_view.xml | 19 +++ 14 files changed, 535 insertions(+) create mode 100644 product_cost_usd/README.rst create mode 100644 product_cost_usd/__init__.py create mode 100644 product_cost_usd/__manifest__.py create mode 100644 product_cost_usd/i18n/es.po create mode 100644 product_cost_usd/i18n/es_MX.po create mode 100644 product_cost_usd/models/__init__.py create mode 100644 product_cost_usd/models/pricelist.py create mode 100644 product_cost_usd/models/product.py create mode 100644 product_cost_usd/models/sale_order.py create mode 100644 product_cost_usd/static/description/icon.png create mode 100644 product_cost_usd/static/description/index.html create mode 100644 product_cost_usd/tests/__init__.py create mode 100644 product_cost_usd/tests/test_standard_price_usd.py create mode 100644 product_cost_usd/views/product_view.xml diff --git a/product_cost_usd/README.rst b/product_cost_usd/README.rst new file mode 100644 index 00000000000..1dbdedaed6f --- /dev/null +++ b/product_cost_usd/README.rst @@ -0,0 +1,38 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +Product Price Cost in USD: +========================== + +This module adds a field to handle cost in USD on products. +The approach is to have the USD as the base currency, +so that the sale prices in company currency can be +kept up to date through the exchange rate. + +Features: +--------- +- New 'Cost in USD' field on the Product form. +- Validate 'Cost in USD' so that it is not less than the list price of the + supplier. +- Avoid save a 'Cost in USD' when product does not has assigned a supplier + with price in USD. +- Allowed pricelist computation based on Cost in USD. +- Added unit tests to validate constrains and pricelists computation. +- Compatibility with the module `sale_margin + `_ + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. + +Credits +======= + +Contributors +------------ + +* Jose Suniaga diff --git a/product_cost_usd/__init__.py b/product_cost_usd/__init__.py new file mode 100644 index 00000000000..2c68d2b1d27 --- /dev/null +++ b/product_cost_usd/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py new file mode 100644 index 00000000000..13f795b5db1 --- /dev/null +++ b/product_cost_usd/__manifest__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Product Price Cost in USD", + "summary": ''' +This module adds the field Cost in USD to the Product form. + ''', + "version": "10.0.0.0.1", + "author": "Vauxoo", + "category": "Rico", + "website": "http://vauxoo.com", + "license": "LGPL-3", + "depends": [ + 'product', + 'sale_margin', + ], + "demo": [ + ], + "data": [ + "views/product_view.xml", + ], + "test": [], + "installable": True, + "auto_install": False, +} diff --git a/product_cost_usd/i18n/es.po b/product_cost_usd/i18n/es.po new file mode 100644 index 00000000000..ac44df8f179 --- /dev/null +++ b/product_cost_usd/i18n/es.po @@ -0,0 +1,65 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_usd +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-02-06 18:52+0000\n" +"PO-Revision-Date: 2017-02-06 18:52+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: product_cost_usd +#: model:ir.model.fields,field_description:product_cost_usd.field_product_template_standard_price_usd +msgid "Cost in USD" +msgstr "Costo en Dólares" + +#. module: product_cost_usd +#: model:ir.model.fields,help:product_cost_usd.field_product_template_standard_price_usd +msgid "Price cost of the product in USD currency" +msgstr "Precio coste del producto en moneda USD" + +#. module: product_cost_usd +#: model:ir.model,name:product_cost_usd.model_product_pricelist +msgid "Pricelist" +msgstr "Tarifa" + +#. module: product_cost_usd +#: model:ir.model,name:product_cost_usd.model_product_pricelist_item +msgid "Pricelist item" +msgstr "Elemento de la tarifa" + +#. module: product_cost_usd +#: model:ir.model,name:product_cost_usd.model_product_template +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: product_cost_usd +#: model:ir.model,name:product_cost_usd.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: product_cost_usd +#: code:addons/product_cost_usd/models/product.py:38 +#, python-format +msgid "You cannot create or modify a product if the cost in USD is less than the supplier list price.\n" +"\n" +"- Supplier list price = %s\n" +"- Cost in USD = %s" +msgstr "No puede crear o modificar un producto si el costo en USD es menor que el precio del proveedores..\n" +"\n" +"- Precio del proveedor = %s\n" +"- Costo en Dólares = %s" + +#. module: product_cost_usd +#: code:addons/product_cost_usd/models/product.py:33 +#, python-format +msgid "You must have at least one supplier with price in USD before assign a Cost in USD" +msgstr "Debe tener al menos un proveedor con precio en Dólares antes de asignar un Costo en Dólares a el producto" + diff --git a/product_cost_usd/i18n/es_MX.po b/product_cost_usd/i18n/es_MX.po new file mode 100644 index 00000000000..65f63379e4a --- /dev/null +++ b/product_cost_usd/i18n/es_MX.po @@ -0,0 +1,17 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_usd +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-02-06 18:52+0000\n" +"PO-Revision-Date: 2017-02-06 18:52+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" + diff --git a/product_cost_usd/models/__init__.py b/product_cost_usd/models/__init__.py new file mode 100644 index 00000000000..15b2e639479 --- /dev/null +++ b/product_cost_usd/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import product +from . import pricelist +from . import sale_order diff --git a/product_cost_usd/models/pricelist.py b/product_cost_usd/models/pricelist.py new file mode 100644 index 00000000000..94fb73018af --- /dev/null +++ b/product_cost_usd/models/pricelist.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from odoo import models, fields, api + + +class Pricelist(models.Model): + _inherit = "product.pricelist" + + @api.multi + def _compute_price_rule( + self, products_qty_partner, date=False, uom_id=False): + """ Inherited to modify price computation when a pricelist item is + based on cost in USD. + + Why this inheritance? + + This method always compute the product price, from product currency + (mostly the same company currency) into pricelist currency. When the + pricelist item is based on cost in USD is necessary that currency + conversion is made from USD currency. By this reason we must go back + the conversion made in super and then made a conversion from USD + currency to the pricelist currency to get the expected price when the + pricelist item is based on cost in USD. + + Returns: dict{product_id: (price, suitable_rule) for given pricelist} + + If date in context: Date of the pricelist (%Y-%m-%d) + + :param products_qty_partner: list of typles products, quantity, partner + :param datetime date: validity date + :param ID uom_id: intermediate unit of measure + """ + results = super(Pricelist, self)._compute_price_rule( + products_qty_partner, date=date, uom_id=uom_id) + usd_currency = self.env.ref('base.USD') + for product_id in results: + # get current price and pricelist item for product_id + price, item_id = results[product_id] + suitable_rule = self.item_ids.filtered(lambda x: x.id == item_id) + # look that pricelist item is based on cost in usd + if not suitable_rule or suitable_rule.base != 'standard_price_usd': + continue + product = self.env['product.product'].browse(product_id) + # go back conversion made in super, moving the price into + # product currency for items based on cost in USD + price = self.currency_id.compute( + price, product.currency_id, round=False) + # now convert from USD into pricelist currency + if self.currency_id != usd_currency: + price = usd_currency.compute( + price, self.currency_id, round=False) + results[product_id] = ( + price, suitable_rule and suitable_rule.id or False) + return results + + +class PricelistItem(models.Model): + _inherit = "product.pricelist.item" + + base = fields.Selection( + selection_add=[('standard_price_usd', 'Cost in USD')]) diff --git a/product_cost_usd/models/product.py b/product_cost_usd/models/product.py new file mode 100644 index 00000000000..ef037464118 --- /dev/null +++ b/product_cost_usd/models/product.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import odoo.addons.decimal_precision as dp +from odoo.tools import float_compare + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + @api.constrains('standard_price_usd', 'seller_ids') + def check_cost_and_price(self): + """ Validate 'Cost in USD' usability. + + Usability conditions: + - Before set a 'Cost in USD' in a product at least one supplier should + have price in USD. + - The Cost in USD cannot be less than supplier price. + """ + usd_currency = self.env.ref('base.USD') + prec = self.env['decimal.precision'].precision_get('Product Price') + usd_seller = self.seller_ids.filtered( + lambda x: x.currency_id == usd_currency) + list_price = usd_seller.price if usd_seller else 0.0 + standard_price_usd = self.standard_price_usd + if not usd_seller and float_compare( + standard_price_usd, 0, precision_digits=prec) > 0: + raise ValidationError( + _('You must have at least one supplier with price in USD' + ' before assign a Cost in USD')) + if float_compare( + list_price, standard_price_usd, precision_digits=prec) > 0: + raise ValidationError( + _('You cannot create or modify a product if the cost in USD' + ' is less than the supplier list price.\n\n' + '- Supplier list price = %s\n' + '- Cost in USD = %s') % (list_price, standard_price_usd)) + + standard_price_usd = fields.Float( + 'Cost in USD', + digits=dp.get_precision('Product Price'), + help="Price cost of the product in USD currency") diff --git a/product_cost_usd/models/sale_order.py b/product_cost_usd/models/sale_order.py new file mode 100644 index 00000000000..e6bda4a6750 --- /dev/null +++ b/product_cost_usd/models/sale_order.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.model + def _get_purchase_price(self, pricelist, product, product_uom, date): + """ Inherited to recalculate purchase price when pricelist item is + based on cost in usd. + """ + res = super(SaleOrderLine, self)._get_purchase_price( + pricelist, product, product_uom, date) + price_rule = pricelist._compute_price_rule([(product, 1, False)]) + price, rule = price_rule[product.id] + suitable_rule = pricelist.item_ids.filtered(lambda x: x.id == rule) + if not suitable_rule or suitable_rule.base != 'standard_price_usd': + return res + frm_cur = self.env.ref('base.USD') + to_cur = pricelist.currency_id + purchase_price = product.standard_price_usd + if product_uom != product.uom_id: + purchase_price = product.uom_id._compute_price( + purchase_price, product_uom) + price = frm_cur.with_context(date=date).compute( + purchase_price, to_cur, round=False) + return {'purchase_price': price} + + @api.model + def _compute_margin(self, order_id, product_id, product_uom_id): + """ Inherited to recalculate purchase price when pricelist item is + based on cost in usd. + + Why this inheritance? + + In spite of the name this method is only used to get the purchase + price, calling the method get_purchase_price we reuse that logic to + calculate the purchase price when pricelist item is based on cost + in usd. + """ + price = super(SaleOrderLine, self)._compute_margin( + order_id, product_id, product_uom_id) + date = order_id.date_order + prices = self._get_purchase_price( + order_id.pricelist_id, product_id, product_uom_id, date) + return prices.get('purchase_price', price) diff --git a/product_cost_usd/static/description/icon.png b/product_cost_usd/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..58dd9665bc24bb487debd12904c5799461acfedb GIT binary patch literal 28984 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 literal 0 HcmV?d00001 diff --git a/product_cost_usd/static/description/index.html b/product_cost_usd/static/description/index.html new file mode 100644 index 00000000000..7a9fcd414f7 --- /dev/null +++ b/product_cost_usd/static/description/index.html @@ -0,0 +1,67 @@ + + + +
+
+

+ Product Price Cost in USD +

+

+ This module adds a field to handle cost in USD on products. + The approach is to have the USD as the base currency, + so that the sale prices in company currency can be + kept up to date through the exchange rate. +

+

Features:

+
    +
  • New 'Cost in USD' field on the Product form.
  • +
  • Validate 'Cost in USD' so that it is not less than the list price + of the supplier.
  • +
  • Avoid save a 'Cost in USD' when product does not has assigned a + supplier with price in USD.
  • +
  • Allowed pricelist computation based on Cost in USD.
  • +
  • Added unit tests to validate constrains and pricelists + computation.
  • +
  • Compatibility with the module `sale_margin`
  • +
+
+
+
+
+
+

Do you need help?

+

+ Let's offer you the best services! +

+

+ Contact us by our official channels. +

+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ + + + +
+
+
+
diff --git a/product_cost_usd/tests/__init__.py b/product_cost_usd/tests/__init__.py new file mode 100644 index 00000000000..4564e70ed0c --- /dev/null +++ b/product_cost_usd/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Vauxoo +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_standard_price_usd diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py new file mode 100644 index 00000000000..5b57f30bed6 --- /dev/null +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -0,0 +1,125 @@ +# coding: utf-8 + +from __future__ import division + +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from odoo.tools import float_round +from odoo import fields + + +class TestStandardPriceUsd(TransactionCase): + + def setUp(self): + super(TestStandardPriceUsd, self).setUp() + self.mxn = self.env.ref('base.MXN') + self.usd = self.env.ref('base.USD') + self.sale_order = self.env['sale.order'] + self.partner = self.env.ref('base.res_partner_4') + self.product_uom = self.env.ref('product.product_uom_unit') + self.product = self.env.ref('product.product_product_24') + self.pricelist_15_usd = self.env['product.pricelist'].create({ + 'name': 'Pricelist 15% USD', + 'currency_id': self.usd.id, + 'item_ids': [(0, 0, { + 'compute_price': 'formula', + 'base': 'standard_price_usd', # based on cost in usd + 'price_discount': -15, + })] + }) + self.pricelist_15_mxn = self.pricelist_15_usd.copy({ + 'name': 'Pricelist 15% USD', + 'currency_id': self.mxn.id}) + self.pricelist_id = self.ref('product.list0') + + def set_standard_price_usd(self, price): + self.assertTrue(self.product.seller_ids) + self.product.seller_ids[0].write({'currency_id': self.usd.id}) + self.product.write({'standard_price_usd': price}) + + def test_01(self): + """ Test USD pricelist based on cost in usd. """ + self.set_standard_price_usd(880) + product = self.product.with_context(pricelist=self.pricelist_15_usd.id) + expected_price = float_round( + product.standard_price_usd * 1.15, + precision_rounding=self.usd.rounding) + self.assertEqual(product.price, expected_price) + + def test_02(self): + """ Test a MXN pricelist based on cost in usd. """ + self.set_standard_price_usd(880) + product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) + mxn_rate = (self.mxn.rate / self.usd.rate) + expected_price = float_round( + (product.standard_price_usd * 1.15) * mxn_rate, + precision_rounding=self.mxn.rounding) + self.assertEqual(product.price, expected_price) + + def test_03(self): + """ Test constraint check_cost_and_price. """ + with self.assertRaisesRegexp( + ValidationError, + 'You must have at least one supplier with price in USD'): + self.product.write({'standard_price_usd': 880}) + + def test_04(self): + """ Test constraint check_cost_and_price. """ + with self.assertRaisesRegexp( + ValidationError, + 'You cannot create or modify a product if the cost in USD'): + self.set_standard_price_usd(1) + + def test_sale_margin(self): + """ Test the sale margin module using a pricelist with cost in usd. """ + self.set_standard_price_usd(880) + product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) + # Create a sale order for product Graphics Card. + sale_order = self.sale_order.create({ + 'date_order': fields.Datetime.now(), + 'name': 'Test', + 'order_line': [(0, 0, { + 'name': '[CARD] Graphics Card', + 'product_uom': self.product_uom.id, + 'product_uom_qty': 1, + 'state': 'draft', + 'product_id': product.id})], + 'partner_id': self.partner.id, + 'pricelist_id': self.pricelist_15_mxn.id}) + # Confirm the sale order. + sale_order.action_confirm() + # Verify that margin field gets bind with the value. + mxn_rate = (self.mxn.rate / self.usd.rate) + expected_price = float_round( + (product.standard_price_usd * 1.15) * mxn_rate, + precision_rounding=self.mxn.rounding) + expected_cost = float_round( + product.standard_price_usd * mxn_rate, + precision_rounding=self.mxn.rounding) + margin = float_round( + expected_price - expected_cost, + precision_rounding=self.mxn.rounding) + msg = "Sale order margin should be %s" % margin + self.assertEqual(sale_order.margin, margin, msg) + + def test_sale_margin_normal(self): + """ Test the sale margin module using a pricelist without cost in + usd. + """ + # Create a sale order for product Graphics Card. + sale_order = self.sale_order.create({ + 'date_order': fields.Datetime.now(), + 'name': 'Test', + 'order_line': [(0, 0, { + 'name': '[CARD] Graphics Card', + 'product_uom': self.product_uom.id, + 'product_uom_qty': 1, + 'state': 'draft', + 'product_id': self.product.id})], + 'partner_id': self.partner.id, + 'pricelist_id': self.pricelist_id}) + # Confirm the sale order. + sale_order.action_confirm() + # Verify that margin field gets bind with the value. + msg = "Sale order margin should be 9.0" + self.assertEqual(sale_order.margin, 9.0, msg) diff --git a/product_cost_usd/views/product_view.xml b/product_cost_usd/views/product_view.xml new file mode 100644 index 00000000000..eb02e3499d2 --- /dev/null +++ b/product_cost_usd/views/product_view.xml @@ -0,0 +1,19 @@ + + + + + + product.template.inhrt.tanner + product.template + form + + + + + + + + + + + From bad8c8aab912eedb078e24c6990571ac07605118 Mon Sep 17 00:00:00 2001 From: "Jose Suniaga [Vauxoo]" Date: Tue, 17 Oct 2017 19:08:26 +0000 Subject: [PATCH 02/15] [FIX] product_cost_usd: replace pricelist in unit test The Default Pricelist might be modified in other modules by this reason is not convinient use it in unit tests. --- product_cost_usd/tests/test_standard_price_usd.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index 5b57f30bed6..68370339ab6 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -28,9 +28,10 @@ def setUp(self): })] }) self.pricelist_15_mxn = self.pricelist_15_usd.copy({ - 'name': 'Pricelist 15% USD', + 'name': 'Pricelist 15% MXN', 'currency_id': self.mxn.id}) - self.pricelist_id = self.ref('product.list0') + self.pricelist = self.env['product.pricelist'].create({ + 'name': 'Pricelist Demo'}) def set_standard_price_usd(self, price): self.assertTrue(self.product.seller_ids) @@ -117,7 +118,7 @@ def test_sale_margin_normal(self): 'state': 'draft', 'product_id': self.product.id})], 'partner_id': self.partner.id, - 'pricelist_id': self.pricelist_id}) + 'pricelist_id': self.pricelist.id}) # Confirm the sale order. sale_order.action_confirm() # Verify that margin field gets bind with the value. From 91d9fa01922a84f47ecb650d0cb450dd89798683 Mon Sep 17 00:00:00 2001 From: "Jose Suniaga [Vauxoo]" Date: Tue, 17 Oct 2017 20:15:43 +0000 Subject: [PATCH 03/15] [FIX] product_cost_usd: use float_compare in unit test --- product_cost_usd/tests/test_standard_price_usd.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index 68370339ab6..9792a6a05fc 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -4,7 +4,7 @@ from odoo.tests.common import TransactionCase from odoo.exceptions import ValidationError -from odoo.tools import float_round +from odoo.tools import float_round, float_compare from odoo import fields @@ -45,7 +45,9 @@ def test_01(self): expected_price = float_round( product.standard_price_usd * 1.15, precision_rounding=self.usd.rounding) - self.assertEqual(product.price, expected_price) + self.assertEqual( + float_compare(product.price, expected_price, precision_digits=2), + 0, "Product price should be %s" % product.price) def test_02(self): """ Test a MXN pricelist based on cost in usd. """ @@ -55,7 +57,9 @@ def test_02(self): expected_price = float_round( (product.standard_price_usd * 1.15) * mxn_rate, precision_rounding=self.mxn.rounding) - self.assertEqual(product.price, expected_price) + self.assertEqual( + float_compare(product.price, expected_price, precision_digits=2), + 0, "Product price should be %s" % product.price) def test_03(self): """ Test constraint check_cost_and_price. """ @@ -100,8 +104,9 @@ def test_sale_margin(self): margin = float_round( expected_price - expected_cost, precision_rounding=self.mxn.rounding) - msg = "Sale order margin should be %s" % margin - self.assertEqual(sale_order.margin, margin, msg) + self.assertEqual( + float_compare(sale_order.margin, margin, precision_digits=2), + 0, "Sale order margin should be %s" % margin) def test_sale_margin_normal(self): """ Test the sale margin module using a pricelist without cost in From 8c1ecad235f02cb96ad9ec1cb356c326fabc0c05 Mon Sep 17 00:00:00 2001 From: Alejandro Garza Date: Fri, 12 Aug 2022 19:33:39 +0000 Subject: [PATCH 04/15] [IMP] product_cost_usd: black and isort to the module --- product_cost_usd/__manifest__.py | 11 +- product_cost_usd/models/pricelist.py | 28 ++- product_cost_usd/models/product.py | 43 +++-- product_cost_usd/models/sale_order.py | 27 ++- .../tests/test_standard_price_usd.py | 178 ++++++++++-------- 5 files changed, 146 insertions(+), 141 deletions(-) diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 13f795b5db1..724bcbb1e3f 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -4,20 +4,19 @@ { "name": "Product Price Cost in USD", - "summary": ''' + "summary": """ This module adds the field Cost in USD to the Product form. - ''', + """, "version": "10.0.0.0.1", "author": "Vauxoo", "category": "Rico", "website": "http://vauxoo.com", "license": "LGPL-3", "depends": [ - 'product', - 'sale_margin', - ], - "demo": [ + "product", + "sale_margin", ], + "demo": [], "data": [ "views/product_view.xml", ], diff --git a/product_cost_usd/models/pricelist.py b/product_cost_usd/models/pricelist.py index 94fb73018af..c2dd18ab31e 100644 --- a/product_cost_usd/models/pricelist.py +++ b/product_cost_usd/models/pricelist.py @@ -3,16 +3,15 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import models, fields, api +from odoo import api, fields, models class Pricelist(models.Model): _inherit = "product.pricelist" @api.multi - def _compute_price_rule( - self, products_qty_partner, date=False, uom_id=False): - """ Inherited to modify price computation when a pricelist item is + def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): + """Inherited to modify price computation when a pricelist item is based on cost in USD. Why this inheritance? @@ -33,32 +32,27 @@ def _compute_price_rule( :param datetime date: validity date :param ID uom_id: intermediate unit of measure """ - results = super(Pricelist, self)._compute_price_rule( - products_qty_partner, date=date, uom_id=uom_id) - usd_currency = self.env.ref('base.USD') + results = super(Pricelist, self)._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) + usd_currency = self.env.ref("base.USD") for product_id in results: # get current price and pricelist item for product_id price, item_id = results[product_id] suitable_rule = self.item_ids.filtered(lambda x: x.id == item_id) # look that pricelist item is based on cost in usd - if not suitable_rule or suitable_rule.base != 'standard_price_usd': + if not suitable_rule or suitable_rule.base != "standard_price_usd": continue - product = self.env['product.product'].browse(product_id) + product = self.env["product.product"].browse(product_id) # go back conversion made in super, moving the price into # product currency for items based on cost in USD - price = self.currency_id.compute( - price, product.currency_id, round=False) + price = self.currency_id.compute(price, product.currency_id, round=False) # now convert from USD into pricelist currency if self.currency_id != usd_currency: - price = usd_currency.compute( - price, self.currency_id, round=False) - results[product_id] = ( - price, suitable_rule and suitable_rule.id or False) + price = usd_currency.compute(price, self.currency_id, round=False) + results[product_id] = (price, suitable_rule and suitable_rule.id or False) return results class PricelistItem(models.Model): _inherit = "product.pricelist.item" - base = fields.Selection( - selection_add=[('standard_price_usd', 'Cost in USD')]) + base = fields.Selection(selection_add=[("standard_price_usd", "Cost in USD")]) diff --git a/product_cost_usd/models/product.py b/product_cost_usd/models/product.py index ef037464118..d7f332526fb 100644 --- a/product_cost_usd/models/product.py +++ b/product_cost_usd/models/product.py @@ -3,44 +3,43 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import odoo.addons.decimal_precision as dp -from odoo.tools import float_compare - -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.tools import float_compare class ProductTemplate(models.Model): _inherit = "product.template" - @api.constrains('standard_price_usd', 'seller_ids') + @api.constrains("standard_price_usd", "seller_ids") def check_cost_and_price(self): - """ Validate 'Cost in USD' usability. + """Validate 'Cost in USD' usability. Usability conditions: - Before set a 'Cost in USD' in a product at least one supplier should have price in USD. - The Cost in USD cannot be less than supplier price. """ - usd_currency = self.env.ref('base.USD') - prec = self.env['decimal.precision'].precision_get('Product Price') - usd_seller = self.seller_ids.filtered( - lambda x: x.currency_id == usd_currency) + usd_currency = self.env.ref("base.USD") + prec = self.env["decimal.precision"].precision_get("Product Price") + usd_seller = self.seller_ids.filtered(lambda x: x.currency_id == usd_currency) list_price = usd_seller.price if usd_seller else 0.0 standard_price_usd = self.standard_price_usd - if not usd_seller and float_compare( - standard_price_usd, 0, precision_digits=prec) > 0: + if not usd_seller and float_compare(standard_price_usd, 0, precision_digits=prec) > 0: raise ValidationError( - _('You must have at least one supplier with price in USD' - ' before assign a Cost in USD')) - if float_compare( - list_price, standard_price_usd, precision_digits=prec) > 0: + _("You must have at least one supplier with price in USD" " before assign a Cost in USD") + ) + if float_compare(list_price, standard_price_usd, precision_digits=prec) > 0: raise ValidationError( - _('You cannot create or modify a product if the cost in USD' - ' is less than the supplier list price.\n\n' - '- Supplier list price = %s\n' - '- Cost in USD = %s') % (list_price, standard_price_usd)) + _( + "You cannot create or modify a product if the cost in USD" + " is less than the supplier list price.\n\n" + "- Supplier list price = %s\n" + "- Cost in USD = %s" + ) + % (list_price, standard_price_usd) + ) standard_price_usd = fields.Float( - 'Cost in USD', - digits=dp.get_precision('Product Price'), - help="Price cost of the product in USD currency") + "Cost in USD", digits=dp.get_precision("Product Price"), help="Price cost of the product in USD currency" + ) diff --git a/product_cost_usd/models/sale_order.py b/product_cost_usd/models/sale_order.py index e6bda4a6750..bcb5a0ab260 100644 --- a/product_cost_usd/models/sale_order.py +++ b/product_cost_usd/models/sale_order.py @@ -10,29 +10,26 @@ class SaleOrderLine(models.Model): @api.model def _get_purchase_price(self, pricelist, product, product_uom, date): - """ Inherited to recalculate purchase price when pricelist item is + """Inherited to recalculate purchase price when pricelist item is based on cost in usd. """ - res = super(SaleOrderLine, self)._get_purchase_price( - pricelist, product, product_uom, date) + res = super(SaleOrderLine, self)._get_purchase_price(pricelist, product, product_uom, date) price_rule = pricelist._compute_price_rule([(product, 1, False)]) price, rule = price_rule[product.id] suitable_rule = pricelist.item_ids.filtered(lambda x: x.id == rule) - if not suitable_rule or suitable_rule.base != 'standard_price_usd': + if not suitable_rule or suitable_rule.base != "standard_price_usd": return res - frm_cur = self.env.ref('base.USD') + frm_cur = self.env.ref("base.USD") to_cur = pricelist.currency_id purchase_price = product.standard_price_usd if product_uom != product.uom_id: - purchase_price = product.uom_id._compute_price( - purchase_price, product_uom) - price = frm_cur.with_context(date=date).compute( - purchase_price, to_cur, round=False) - return {'purchase_price': price} + purchase_price = product.uom_id._compute_price(purchase_price, product_uom) + price = frm_cur.with_context(date=date).compute(purchase_price, to_cur, round=False) + return {"purchase_price": price} @api.model def _compute_margin(self, order_id, product_id, product_uom_id): - """ Inherited to recalculate purchase price when pricelist item is + """Inherited to recalculate purchase price when pricelist item is based on cost in usd. Why this inheritance? @@ -42,9 +39,7 @@ def _compute_margin(self, order_id, product_id, product_uom_id): calculate the purchase price when pricelist item is based on cost in usd. """ - price = super(SaleOrderLine, self)._compute_margin( - order_id, product_id, product_uom_id) + price = super(SaleOrderLine, self)._compute_margin(order_id, product_id, product_uom_id) date = order_id.date_order - prices = self._get_purchase_price( - order_id.pricelist_id, product_id, product_uom_id, date) - return prices.get('purchase_price', price) + prices = self._get_purchase_price(order_id.pricelist_id, product_id, product_uom_id, date) + return prices.get("purchase_price", price) diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index 9792a6a05fc..705b31af140 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -2,128 +2,146 @@ from __future__ import division -from odoo.tests.common import TransactionCase -from odoo.exceptions import ValidationError -from odoo.tools import float_round, float_compare from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase +from odoo.tools import float_compare, float_round class TestStandardPriceUsd(TransactionCase): - def setUp(self): super(TestStandardPriceUsd, self).setUp() - self.mxn = self.env.ref('base.MXN') - self.usd = self.env.ref('base.USD') - self.sale_order = self.env['sale.order'] - self.partner = self.env.ref('base.res_partner_4') - self.product_uom = self.env.ref('product.product_uom_unit') - self.product = self.env.ref('product.product_product_24') - self.pricelist_15_usd = self.env['product.pricelist'].create({ - 'name': 'Pricelist 15% USD', - 'currency_id': self.usd.id, - 'item_ids': [(0, 0, { - 'compute_price': 'formula', - 'base': 'standard_price_usd', # based on cost in usd - 'price_discount': -15, - })] - }) - self.pricelist_15_mxn = self.pricelist_15_usd.copy({ - 'name': 'Pricelist 15% MXN', - 'currency_id': self.mxn.id}) - self.pricelist = self.env['product.pricelist'].create({ - 'name': 'Pricelist Demo'}) + self.mxn = self.env.ref("base.MXN") + self.usd = self.env.ref("base.USD") + self.sale_order = self.env["sale.order"] + self.partner = self.env.ref("base.res_partner_4") + self.product_uom = self.env.ref("product.product_uom_unit") + self.product = self.env.ref("product.product_product_24") + self.pricelist_15_usd = self.env["product.pricelist"].create( + { + "name": "Pricelist 15% USD", + "currency_id": self.usd.id, + "item_ids": [ + ( + 0, + 0, + { + "compute_price": "formula", + "base": "standard_price_usd", # based on cost in usd + "price_discount": -15, + }, + ) + ], + } + ) + self.pricelist_15_mxn = self.pricelist_15_usd.copy({"name": "Pricelist 15% MXN", "currency_id": self.mxn.id}) + self.pricelist = self.env["product.pricelist"].create({"name": "Pricelist Demo"}) def set_standard_price_usd(self, price): self.assertTrue(self.product.seller_ids) - self.product.seller_ids[0].write({'currency_id': self.usd.id}) - self.product.write({'standard_price_usd': price}) + self.product.seller_ids[0].write({"currency_id": self.usd.id}) + self.product.write({"standard_price_usd": price}) def test_01(self): - """ Test USD pricelist based on cost in usd. """ + """Test USD pricelist based on cost in usd.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_usd.id) - expected_price = float_round( - product.standard_price_usd * 1.15, - precision_rounding=self.usd.rounding) + expected_price = float_round(product.standard_price_usd * 1.15, precision_rounding=self.usd.rounding) self.assertEqual( float_compare(product.price, expected_price, precision_digits=2), - 0, "Product price should be %s" % product.price) + 0, + "Product price should be %s" % product.price, + ) def test_02(self): - """ Test a MXN pricelist based on cost in usd. """ + """Test a MXN pricelist based on cost in usd.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) - mxn_rate = (self.mxn.rate / self.usd.rate) + mxn_rate = self.mxn.rate / self.usd.rate expected_price = float_round( - (product.standard_price_usd * 1.15) * mxn_rate, - precision_rounding=self.mxn.rounding) + (product.standard_price_usd * 1.15) * mxn_rate, precision_rounding=self.mxn.rounding + ) self.assertEqual( float_compare(product.price, expected_price, precision_digits=2), - 0, "Product price should be %s" % product.price) + 0, + "Product price should be %s" % product.price, + ) def test_03(self): - """ Test constraint check_cost_and_price. """ - with self.assertRaisesRegexp( - ValidationError, - 'You must have at least one supplier with price in USD'): - self.product.write({'standard_price_usd': 880}) + """Test constraint check_cost_and_price.""" + with self.assertRaisesRegexp(ValidationError, "You must have at least one supplier with price in USD"): + self.product.write({"standard_price_usd": 880}) def test_04(self): - """ Test constraint check_cost_and_price. """ - with self.assertRaisesRegexp( - ValidationError, - 'You cannot create or modify a product if the cost in USD'): + """Test constraint check_cost_and_price.""" + with self.assertRaisesRegexp(ValidationError, "You cannot create or modify a product if the cost in USD"): self.set_standard_price_usd(1) def test_sale_margin(self): - """ Test the sale margin module using a pricelist with cost in usd. """ + """Test the sale margin module using a pricelist with cost in usd.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) # Create a sale order for product Graphics Card. - sale_order = self.sale_order.create({ - 'date_order': fields.Datetime.now(), - 'name': 'Test', - 'order_line': [(0, 0, { - 'name': '[CARD] Graphics Card', - 'product_uom': self.product_uom.id, - 'product_uom_qty': 1, - 'state': 'draft', - 'product_id': product.id})], - 'partner_id': self.partner.id, - 'pricelist_id': self.pricelist_15_mxn.id}) + sale_order = self.sale_order.create( + { + "date_order": fields.Datetime.now(), + "name": "Test", + "order_line": [ + ( + 0, + 0, + { + "name": "[CARD] Graphics Card", + "product_uom": self.product_uom.id, + "product_uom_qty": 1, + "state": "draft", + "product_id": product.id, + }, + ) + ], + "partner_id": self.partner.id, + "pricelist_id": self.pricelist_15_mxn.id, + } + ) # Confirm the sale order. sale_order.action_confirm() # Verify that margin field gets bind with the value. - mxn_rate = (self.mxn.rate / self.usd.rate) + mxn_rate = self.mxn.rate / self.usd.rate expected_price = float_round( - (product.standard_price_usd * 1.15) * mxn_rate, - precision_rounding=self.mxn.rounding) - expected_cost = float_round( - product.standard_price_usd * mxn_rate, - precision_rounding=self.mxn.rounding) - margin = float_round( - expected_price - expected_cost, - precision_rounding=self.mxn.rounding) + (product.standard_price_usd * 1.15) * mxn_rate, precision_rounding=self.mxn.rounding + ) + expected_cost = float_round(product.standard_price_usd * mxn_rate, precision_rounding=self.mxn.rounding) + margin = float_round(expected_price - expected_cost, precision_rounding=self.mxn.rounding) self.assertEqual( - float_compare(sale_order.margin, margin, precision_digits=2), - 0, "Sale order margin should be %s" % margin) + float_compare(sale_order.margin, margin, precision_digits=2), 0, "Sale order margin should be %s" % margin + ) def test_sale_margin_normal(self): - """ Test the sale margin module using a pricelist without cost in + """Test the sale margin module using a pricelist without cost in usd. """ # Create a sale order for product Graphics Card. - sale_order = self.sale_order.create({ - 'date_order': fields.Datetime.now(), - 'name': 'Test', - 'order_line': [(0, 0, { - 'name': '[CARD] Graphics Card', - 'product_uom': self.product_uom.id, - 'product_uom_qty': 1, - 'state': 'draft', - 'product_id': self.product.id})], - 'partner_id': self.partner.id, - 'pricelist_id': self.pricelist.id}) + sale_order = self.sale_order.create( + { + "date_order": fields.Datetime.now(), + "name": "Test", + "order_line": [ + ( + 0, + 0, + { + "name": "[CARD] Graphics Card", + "product_uom": self.product_uom.id, + "product_uom_qty": 1, + "state": "draft", + "product_id": self.product.id, + }, + ) + ], + "partner_id": self.partner.id, + "pricelist_id": self.pricelist.id, + } + ) # Confirm the sale order. sale_order.action_confirm() # Verify that margin field gets bind with the value. From 3009ae4de2c852477e765310870d01be77ec50be Mon Sep 17 00:00:00 2001 From: agarzaarvizu Date: Fri, 12 Aug 2022 21:24:48 +0000 Subject: [PATCH 05/15] [FIX] product_cost_usd: fix models, views and manifest - Rename model files and remove parameters in super - Rename views files and views IDs - Change category and fix version in manifest - Remove headers on files --- product_cost_usd/__init__.py | 4 ---- product_cost_usd/__manifest__.py | 14 ++++---------- product_cost_usd/i18n/es_MX.po | 17 ----------------- product_cost_usd/models/__init__.py | 11 ++++------- .../{pricelist.py => product_pricelist.py} | 13 +------------ .../models/product_pricelist_item.py | 7 +++++++ .../{product.py => product_template.py} | 12 ++++-------- .../{sale_order.py => sale_order_line.py} | 8 ++------ product_cost_usd/tests/__init__.py | 4 ---- .../tests/test_standard_price_usd.py | 4 +--- .../views/product_template_views.xml | 14 ++++++++++++++ product_cost_usd/views/product_view.xml | 19 ------------------- 12 files changed, 37 insertions(+), 90 deletions(-) delete mode 100644 product_cost_usd/i18n/es_MX.po rename product_cost_usd/models/{pricelist.py => product_pricelist.py} (85%) create mode 100644 product_cost_usd/models/product_pricelist_item.py rename product_cost_usd/models/{product.py => product_template.py} (94%) rename product_cost_usd/models/{sale_order.py => sale_order_line.py} (84%) create mode 100644 product_cost_usd/views/product_template_views.xml delete mode 100644 product_cost_usd/views/product_view.xml diff --git a/product_cost_usd/__init__.py b/product_cost_usd/__init__.py index 2c68d2b1d27..0650744f6bc 100644 --- a/product_cost_usd/__init__.py +++ b/product_cost_usd/__init__.py @@ -1,5 +1 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - from . import models diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 724bcbb1e3f..cc6f62d18c2 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -1,26 +1,20 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - { "name": "Product Price Cost in USD", "summary": """ This module adds the field Cost in USD to the Product form. """, - "version": "10.0.0.0.1", + "version": "11.0.1.0.0", "author": "Vauxoo", - "category": "Rico", - "website": "http://vauxoo.com", + "category": "Sales/Sales", + "website": "https://vauxoo.com", "license": "LGPL-3", "depends": [ - "product", "sale_margin", ], "demo": [], "data": [ - "views/product_view.xml", + "views/product_template_views.xml", ], - "test": [], "installable": True, "auto_install": False, } diff --git a/product_cost_usd/i18n/es_MX.po b/product_cost_usd/i18n/es_MX.po deleted file mode 100644 index 65f63379e4a..00000000000 --- a/product_cost_usd/i18n/es_MX.po +++ /dev/null @@ -1,17 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * product_cost_usd -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-02-06 18:52+0000\n" -"PO-Revision-Date: 2017-02-06 18:52+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" - diff --git a/product_cost_usd/models/__init__.py b/product_cost_usd/models/__init__.py index 15b2e639479..7e0aced5d03 100644 --- a/product_cost_usd/models/__init__.py +++ b/product_cost_usd/models/__init__.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -from . import product -from . import pricelist -from . import sale_order +from . import product_template +from . import product_pricelist +from . import product_pricelist_item +from . import sale_order_line diff --git a/product_cost_usd/models/pricelist.py b/product_cost_usd/models/product_pricelist.py similarity index 85% rename from product_cost_usd/models/pricelist.py rename to product_cost_usd/models/product_pricelist.py index c2dd18ab31e..577781e2ef1 100644 --- a/product_cost_usd/models/pricelist.py +++ b/product_cost_usd/models/product_pricelist.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - - from odoo import api, fields, models @@ -32,7 +27,7 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): :param datetime date: validity date :param ID uom_id: intermediate unit of measure """ - results = super(Pricelist, self)._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) + results = super()._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) usd_currency = self.env.ref("base.USD") for product_id in results: # get current price and pricelist item for product_id @@ -50,9 +45,3 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): price = usd_currency.compute(price, self.currency_id, round=False) results[product_id] = (price, suitable_rule and suitable_rule.id or False) return results - - -class PricelistItem(models.Model): - _inherit = "product.pricelist.item" - - base = fields.Selection(selection_add=[("standard_price_usd", "Cost in USD")]) diff --git a/product_cost_usd/models/product_pricelist_item.py b/product_cost_usd/models/product_pricelist_item.py new file mode 100644 index 00000000000..dceceb4999c --- /dev/null +++ b/product_cost_usd/models/product_pricelist_item.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class PricelistItem(models.Model): + _inherit = "product.pricelist.item" + + base = fields.Selection(selection_add=[("standard_price_usd", "Cost in USD")]) diff --git a/product_cost_usd/models/product.py b/product_cost_usd/models/product_template.py similarity index 94% rename from product_cost_usd/models/product.py rename to product_cost_usd/models/product_template.py index d7f332526fb..05571269533 100644 --- a/product_cost_usd/models/product.py +++ b/product_cost_usd/models/product_template.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - import odoo.addons.decimal_precision as dp from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -11,6 +7,10 @@ class ProductTemplate(models.Model): _inherit = "product.template" + standard_price_usd = fields.Float( + "Cost in USD", digits=dp.get_precision("Product Price"), help="Price cost of the product in USD currency" + ) + @api.constrains("standard_price_usd", "seller_ids") def check_cost_and_price(self): """Validate 'Cost in USD' usability. @@ -39,7 +39,3 @@ def check_cost_and_price(self): ) % (list_price, standard_price_usd) ) - - standard_price_usd = fields.Float( - "Cost in USD", digits=dp.get_precision("Product Price"), help="Price cost of the product in USD currency" - ) diff --git a/product_cost_usd/models/sale_order.py b/product_cost_usd/models/sale_order_line.py similarity index 84% rename from product_cost_usd/models/sale_order.py rename to product_cost_usd/models/sale_order_line.py index bcb5a0ab260..6e31871a57f 100644 --- a/product_cost_usd/models/sale_order.py +++ b/product_cost_usd/models/sale_order_line.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - from odoo import api, models @@ -13,7 +9,7 @@ def _get_purchase_price(self, pricelist, product, product_uom, date): """Inherited to recalculate purchase price when pricelist item is based on cost in usd. """ - res = super(SaleOrderLine, self)._get_purchase_price(pricelist, product, product_uom, date) + res = super()._get_purchase_price(pricelist, product, product_uom, date) price_rule = pricelist._compute_price_rule([(product, 1, False)]) price, rule = price_rule[product.id] suitable_rule = pricelist.item_ids.filtered(lambda x: x.id == rule) @@ -39,7 +35,7 @@ def _compute_margin(self, order_id, product_id, product_uom_id): calculate the purchase price when pricelist item is based on cost in usd. """ - price = super(SaleOrderLine, self)._compute_margin(order_id, product_id, product_uom_id) + price = super()._compute_margin(order_id, product_id, product_uom_id) date = order_id.date_order prices = self._get_purchase_price(order_id.pricelist_id, product_id, product_uom_id, date) return prices.get("purchase_price", price) diff --git a/product_cost_usd/tests/__init__.py b/product_cost_usd/tests/__init__.py index 4564e70ed0c..c7ce23a0291 100644 --- a/product_cost_usd/tests/__init__.py +++ b/product_cost_usd/tests/__init__.py @@ -1,5 +1 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Vauxoo -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - from . import test_standard_price_usd diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index 705b31af140..e6c116f07f1 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -1,5 +1,3 @@ -# coding: utf-8 - from __future__ import division from odoo import fields @@ -10,7 +8,7 @@ class TestStandardPriceUsd(TransactionCase): def setUp(self): - super(TestStandardPriceUsd, self).setUp() + super().setUp() self.mxn = self.env.ref("base.MXN") self.usd = self.env.ref("base.USD") self.sale_order = self.env["sale.order"] diff --git a/product_cost_usd/views/product_template_views.xml b/product_cost_usd/views/product_template_views.xml new file mode 100644 index 00000000000..1c45357b988 --- /dev/null +++ b/product_cost_usd/views/product_template_views.xml @@ -0,0 +1,14 @@ + + + + product.template.form.view.inherit + product.template + + + + + + + + + diff --git a/product_cost_usd/views/product_view.xml b/product_cost_usd/views/product_view.xml deleted file mode 100644 index eb02e3499d2..00000000000 --- a/product_cost_usd/views/product_view.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - product.template.inhrt.tanner - product.template - form - - - - - - - - - - - From 48eb9477d03a8f9a4ddeeef1168dff92ee0ad162 Mon Sep 17 00:00:00 2001 From: agarzaarvizu Date: Thu, 25 Aug 2022 06:24:07 +0000 Subject: [PATCH 06/15] [IMP] product_cost_usd: fixing translations and adding demo data - Fixing translations - Fixing code comments on models - Adding demo data for product_pricelist --- product_cost_usd/__manifest__.py | 6 +++-- .../demo/product_pricelist_demo.xml | 14 ++++++++++ product_cost_usd/i18n/es.po | 9 +++---- product_cost_usd/models/product_pricelist.py | 6 ++--- product_cost_usd/models/product_template.py | 7 ++--- product_cost_usd/models/sale_order_line.py | 4 +-- .../tests/test_standard_price_usd.py | 26 ++++--------------- .../views/product_template_views.xml | 7 +++-- 8 files changed, 39 insertions(+), 40 deletions(-) create mode 100644 product_cost_usd/demo/product_pricelist_demo.xml diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index cc6f62d18c2..13f1ed5b797 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -3,7 +3,7 @@ "summary": """ This module adds the field Cost in USD to the Product form. """, - "version": "11.0.1.0.0", + "version": "11.0.1.0.1", "author": "Vauxoo", "category": "Sales/Sales", "website": "https://vauxoo.com", @@ -11,7 +11,9 @@ "depends": [ "sale_margin", ], - "demo": [], + "demo": [ + "demo/product_pricelist_demo.xml", + ], "data": [ "views/product_template_views.xml", ], diff --git a/product_cost_usd/demo/product_pricelist_demo.xml b/product_cost_usd/demo/product_pricelist_demo.xml new file mode 100644 index 00000000000..1445ad6b941 --- /dev/null +++ b/product_cost_usd/demo/product_pricelist_demo.xml @@ -0,0 +1,14 @@ + + + + Pricelist 15% USD + + + + + formula + standard_price_usd + -15 + + + diff --git a/product_cost_usd/i18n/es.po b/product_cost_usd/i18n/es.po index ac44df8f179..077c18691a6 100644 --- a/product_cost_usd/i18n/es.po +++ b/product_cost_usd/i18n/es.po @@ -18,7 +18,7 @@ msgstr "" #. module: product_cost_usd #: model:ir.model.fields,field_description:product_cost_usd.field_product_template_standard_price_usd msgid "Cost in USD" -msgstr "Costo en Dólares" +msgstr "Costo en USD" #. module: product_cost_usd #: model:ir.model.fields,help:product_cost_usd.field_product_template_standard_price_usd @@ -52,14 +52,13 @@ msgid "You cannot create or modify a product if the cost in USD is less than the "\n" "- Supplier list price = %s\n" "- Cost in USD = %s" -msgstr "No puede crear o modificar un producto si el costo en USD es menor que el precio del proveedores..\n" +msgstr "No puede crear o modificar un producto si el costo en USD es menor que el precio del proveedor.\n" "\n" "- Precio del proveedor = %s\n" -"- Costo en Dólares = %s" +"- Costo en USD = %s" #. module: product_cost_usd #: code:addons/product_cost_usd/models/product.py:33 #, python-format msgid "You must have at least one supplier with price in USD before assign a Cost in USD" -msgstr "Debe tener al menos un proveedor con precio en Dólares antes de asignar un Costo en Dólares a el producto" - +msgstr "Debe tener al menos un proveedor con precio en USD antes de asignar un Costo en USD al producto" diff --git a/product_cost_usd/models/product_pricelist.py b/product_cost_usd/models/product_pricelist.py index 577781e2ef1..713d9e337f5 100644 --- a/product_cost_usd/models/product_pricelist.py +++ b/product_cost_usd/models/product_pricelist.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models +from odoo import api, models class Pricelist(models.Model): @@ -11,10 +11,10 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): Why this inheritance? - This method always compute the product price, from product currency + This method always computes the product price, from product currency (mostly the same company currency) into pricelist currency. When the pricelist item is based on cost in USD is necessary that currency - conversion is made from USD currency. By this reason we must go back + conversion is made from USD currency. For this reason, we must go back the conversion made in super and then made a conversion from USD currency to the pricelist currency to get the expected price when the pricelist item is based on cost in USD. diff --git a/product_cost_usd/models/product_template.py b/product_cost_usd/models/product_template.py index 05571269533..b9c1829baf0 100644 --- a/product_cost_usd/models/product_template.py +++ b/product_cost_usd/models/product_template.py @@ -1,8 +1,9 @@ -import odoo.addons.decimal_precision as dp from odoo import _, api, fields, models from odoo.exceptions import ValidationError from odoo.tools import float_compare +import odoo.addons.decimal_precision as dp + class ProductTemplate(models.Model): _inherit = "product.template" @@ -23,11 +24,11 @@ def check_cost_and_price(self): usd_currency = self.env.ref("base.USD") prec = self.env["decimal.precision"].precision_get("Product Price") usd_seller = self.seller_ids.filtered(lambda x: x.currency_id == usd_currency) - list_price = usd_seller.price if usd_seller else 0.0 + list_price = usd_seller.price standard_price_usd = self.standard_price_usd if not usd_seller and float_compare(standard_price_usd, 0, precision_digits=prec) > 0: raise ValidationError( - _("You must have at least one supplier with price in USD" " before assign a Cost in USD") + _("You must have at least one supplier with price in USD before assigning a Cost in USD") ) if float_compare(list_price, standard_price_usd, precision_digits=prec) > 0: raise ValidationError( diff --git a/product_cost_usd/models/sale_order_line.py b/product_cost_usd/models/sale_order_line.py index 6e31871a57f..a23d6e5930a 100644 --- a/product_cost_usd/models/sale_order_line.py +++ b/product_cost_usd/models/sale_order_line.py @@ -26,14 +26,14 @@ def _get_purchase_price(self, pricelist, product, product_uom, date): @api.model def _compute_margin(self, order_id, product_id, product_uom_id): """Inherited to recalculate purchase price when pricelist item is - based on cost in usd. + based on cost in USD. Why this inheritance? In spite of the name this method is only used to get the purchase price, calling the method get_purchase_price we reuse that logic to calculate the purchase price when pricelist item is based on cost - in usd. + in USD. """ price = super()._compute_margin(order_id, product_id, product_uom_id) date = order_id.date_order diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index e6c116f07f1..a576fe84a44 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -15,23 +15,7 @@ def setUp(self): self.partner = self.env.ref("base.res_partner_4") self.product_uom = self.env.ref("product.product_uom_unit") self.product = self.env.ref("product.product_product_24") - self.pricelist_15_usd = self.env["product.pricelist"].create( - { - "name": "Pricelist 15% USD", - "currency_id": self.usd.id, - "item_ids": [ - ( - 0, - 0, - { - "compute_price": "formula", - "base": "standard_price_usd", # based on cost in usd - "price_discount": -15, - }, - ) - ], - } - ) + self.pricelist_15_usd = self.env.ref("product_cost_usd.pricelist_15_usd") self.pricelist_15_mxn = self.pricelist_15_usd.copy({"name": "Pricelist 15% MXN", "currency_id": self.mxn.id}) self.pricelist = self.env["product.pricelist"].create({"name": "Pricelist Demo"}) @@ -41,7 +25,7 @@ def set_standard_price_usd(self, price): self.product.write({"standard_price_usd": price}) def test_01(self): - """Test USD pricelist based on cost in usd.""" + """Test USD pricelist based on cost in USD.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_usd.id) expected_price = float_round(product.standard_price_usd * 1.15, precision_rounding=self.usd.rounding) @@ -52,7 +36,7 @@ def test_01(self): ) def test_02(self): - """Test a MXN pricelist based on cost in usd.""" + """Test a MXN pricelist based on cost in USD.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) mxn_rate = self.mxn.rate / self.usd.rate @@ -76,7 +60,7 @@ def test_04(self): self.set_standard_price_usd(1) def test_sale_margin(self): - """Test the sale margin module using a pricelist with cost in usd.""" + """Test the sale margin module using a pricelist with cost in USD.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) # Create a sale order for product Graphics Card. @@ -116,7 +100,7 @@ def test_sale_margin(self): def test_sale_margin_normal(self): """Test the sale margin module using a pricelist without cost in - usd. + USD. """ # Create a sale order for product Graphics Card. sale_order = self.sale_order.create( diff --git a/product_cost_usd/views/product_template_views.xml b/product_cost_usd/views/product_template_views.xml index 1c45357b988..56e06d6d711 100644 --- a/product_cost_usd/views/product_template_views.xml +++ b/product_cost_usd/views/product_template_views.xml @@ -1,14 +1,13 @@ - + product.template.form.view.inherit product.template - + - + - From 39b6c8e2d72b1487049680a500b797d359aa6301 Mon Sep 17 00:00:00 2001 From: agarzaarvizu Date: Tue, 23 Aug 2022 21:53:43 +0000 Subject: [PATCH 07/15] [FIX] product_cost_usd: inheritance errors related to the old view id - add pre-migration in order to remove fix errors on inherited views of_product_template_tann_inhrt view --- product_cost_usd/__manifest__.py | 2 +- .../migrations/11.0.1.0.2/pre-migration.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 product_cost_usd/migrations/11.0.1.0.2/pre-migration.py diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 13f1ed5b797..30114750953 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -3,7 +3,7 @@ "summary": """ This module adds the field Cost in USD to the Product form. """, - "version": "11.0.1.0.1", + "version": "11.0.1.0.2", "author": "Vauxoo", "category": "Sales/Sales", "website": "https://vauxoo.com", diff --git a/product_cost_usd/migrations/11.0.1.0.2/pre-migration.py b/product_cost_usd/migrations/11.0.1.0.2/pre-migration.py new file mode 100644 index 00000000000..ebe6cf80949 --- /dev/null +++ b/product_cost_usd/migrations/11.0.1.0.2/pre-migration.py @@ -0,0 +1,13 @@ +def delete_inherited_views(cr): + cr.execute(""" + DELETE FROM ir_ui_view AS iuv + USING ir_model_data AS imd + WHERE iuv.inherit_id=imd.res_id + AND imd.name='view_product_template_tann_inhrt' + AND imd.module='product_cost_usd' + AND imd.model='ir.ui.view' + """) + + +def migrate(cr, version): + delete_inherited_views(cr) From 331f9d71e0c94da5e3162b94a50b17ecbb83ab1e Mon Sep 17 00:00:00 2001 From: Alejandro Garza Date: Thu, 25 Aug 2022 22:37:07 +0000 Subject: [PATCH 08/15] [MIG] product_cost_usd: module migrated to v15.0 --- product_cost_usd/__manifest__.py | 2 +- product_cost_usd/i18n/es.po | 64 +++++++++--- .../migrations/11.0.1.0.2/pre-migration.py | 13 --- product_cost_usd/models/product_pricelist.py | 8 +- .../models/product_pricelist_item.py | 4 +- product_cost_usd/models/product_template.py | 38 +++---- product_cost_usd/models/sale_order_line.py | 52 ++++------ .../tests/test_standard_price_usd.py | 98 +++++++------------ .../views/product_template_views.xml | 4 +- 9 files changed, 131 insertions(+), 152 deletions(-) delete mode 100644 product_cost_usd/migrations/11.0.1.0.2/pre-migration.py diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 30114750953..2064a486e0e 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -3,7 +3,7 @@ "summary": """ This module adds the field Cost in USD to the Product form. """, - "version": "11.0.1.0.2", + "version": "15.0.1.0.0", "author": "Vauxoo", "category": "Sales/Sales", "website": "https://vauxoo.com", diff --git a/product_cost_usd/i18n/es.po b/product_cost_usd/i18n/es.po index 077c18691a6..bfe781fb635 100644 --- a/product_cost_usd/i18n/es.po +++ b/product_cost_usd/i18n/es.po @@ -1,14 +1,14 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * product_cost_usd +# * product_cost_usd # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 10.0+e\n" +"Project-Id-Version: Odoo Server 15.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-02-06 18:52+0000\n" -"PO-Revision-Date: 2017-02-06 18:52+0000\n" -"Last-Translator: <>\n" +"POT-Creation-Date: 2022-08-25 22:26+0000\n" +"PO-Revision-Date: 2022-08-25 22:26+0000\n" +"Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -16,24 +16,50 @@ msgstr "" "Plural-Forms: \n" #. module: product_cost_usd -#: model:ir.model.fields,field_description:product_cost_usd.field_product_template_standard_price_usd +#: model:ir.model.fields,help:product_cost_usd.field_product_pricelist_item__base +msgid "" +"Base price for computation.\n" +"Sales Price: The base price will be the Sales Price.\n" +"Cost Price : The base price will be the cost price.\n" +"Other Pricelist : Computation of the base price based on another Pricelist." +msgstr "" +"Precio base para el cálculo.\n" +"Precio de venta: El precio base será el precio de venta.\n" +"Precio de coste: El precio base será el precio de coste.\n" +"Otra lista de precios: Cálculo del precio base de acuerdo con otra lista de precios." + +#. module: product_cost_usd +#: model:ir.model.fields,field_description:product_cost_usd.field_product_pricelist_item__base +msgid "Based on" +msgstr "Basado en" + +#. module: product_cost_usd +#: model:ir.model.fields,field_description:product_cost_usd.field_product_product__standard_price_usd +#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__standard_price_usd +#: model:ir.model.fields.selection,name:product_cost_usd.selection__product_pricelist_item__base__standard_price_usd msgid "Cost in USD" msgstr "Costo en USD" #. module: product_cost_usd -#: model:ir.model.fields,help:product_cost_usd.field_product_template_standard_price_usd +#: model:ir.model.fields,help:product_cost_usd.field_product_product__standard_price_usd +#: model:ir.model.fields,help:product_cost_usd.field_product_template__standard_price_usd msgid "Price cost of the product in USD currency" msgstr "Precio coste del producto en moneda USD" #. module: product_cost_usd #: model:ir.model,name:product_cost_usd.model_product_pricelist msgid "Pricelist" -msgstr "Tarifa" +msgstr "Lista de precios" + +#. module: product_cost_usd +#: model:product.pricelist,name:product_cost_usd.pricelist_15_usd +msgid "Pricelist 15% USD" +msgstr "Lista de precios 15% USD" #. module: product_cost_usd #: model:ir.model,name:product_cost_usd.model_product_pricelist_item -msgid "Pricelist item" -msgstr "Elemento de la tarifa" +msgid "Pricelist Rule" +msgstr "Regla de la lista de precios" #. module: product_cost_usd #: model:ir.model,name:product_cost_usd.model_product_template @@ -46,19 +72,25 @@ msgid "Sales Order Line" msgstr "Línea de pedido de venta" #. module: product_cost_usd -#: code:addons/product_cost_usd/models/product.py:38 +#: code:addons/product_cost_usd/models/product_template.py:0 #, python-format -msgid "You cannot create or modify a product if the cost in USD is less than the supplier list price.\n" +msgid "" +"You cannot create or modify a product if the cost in USD is less than the supplier list price.\n" "\n" "- Supplier list price = %s\n" "- Cost in USD = %s" -msgstr "No puede crear o modificar un producto si el costo en USD es menor que el precio del proveedor.\n" +msgstr "" +"No puede crear o modificar un producto si el costo en USD es menor que el precio del proveedor.\n" "\n" "- Precio del proveedor = %s\n" "- Costo en USD = %s" #. module: product_cost_usd -#: code:addons/product_cost_usd/models/product.py:33 +#: code:addons/product_cost_usd/models/product_template.py:0 #, python-format -msgid "You must have at least one supplier with price in USD before assign a Cost in USD" -msgstr "Debe tener al menos un proveedor con precio en USD antes de asignar un Costo en USD al producto" +msgid "" +"You must have at least one supplier with price in USD before assigning a " +"Cost in USD" +msgstr "" +"Debe tener al menos un proveedor con precio en USD antes de asignar un Costo" +" en USD al producto" diff --git a/product_cost_usd/migrations/11.0.1.0.2/pre-migration.py b/product_cost_usd/migrations/11.0.1.0.2/pre-migration.py deleted file mode 100644 index ebe6cf80949..00000000000 --- a/product_cost_usd/migrations/11.0.1.0.2/pre-migration.py +++ /dev/null @@ -1,13 +0,0 @@ -def delete_inherited_views(cr): - cr.execute(""" - DELETE FROM ir_ui_view AS iuv - USING ir_model_data AS imd - WHERE iuv.inherit_id=imd.res_id - AND imd.name='view_product_template_tann_inhrt' - AND imd.module='product_cost_usd' - AND imd.model='ir.ui.view' - """) - - -def migrate(cr, version): - delete_inherited_views(cr) diff --git a/product_cost_usd/models/product_pricelist.py b/product_cost_usd/models/product_pricelist.py index 713d9e337f5..745dba37557 100644 --- a/product_cost_usd/models/product_pricelist.py +++ b/product_cost_usd/models/product_pricelist.py @@ -1,10 +1,9 @@ -from odoo import api, models +from odoo import fields, models class Pricelist(models.Model): _inherit = "product.pricelist" - @api.multi def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): """Inherited to modify price computation when a pricelist item is based on cost in USD. @@ -29,6 +28,7 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): """ results = super()._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) usd_currency = self.env.ref("base.USD") + date = fields.Date.context_today(self) for product_id in results: # get current price and pricelist item for product_id price, item_id = results[product_id] @@ -39,9 +39,9 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): product = self.env["product.product"].browse(product_id) # go back conversion made in super, moving the price into # product currency for items based on cost in USD - price = self.currency_id.compute(price, product.currency_id, round=False) + price = self.currency_id._convert(price, product.currency_id, self.env.company, date, round=False) # now convert from USD into pricelist currency if self.currency_id != usd_currency: - price = usd_currency.compute(price, self.currency_id, round=False) + price = usd_currency._convert(price, self.currency_id, self.env.company, date, round=False) results[product_id] = (price, suitable_rule and suitable_rule.id or False) return results diff --git a/product_cost_usd/models/product_pricelist_item.py b/product_cost_usd/models/product_pricelist_item.py index dceceb4999c..508e0ab6892 100644 --- a/product_cost_usd/models/product_pricelist_item.py +++ b/product_cost_usd/models/product_pricelist_item.py @@ -4,4 +4,6 @@ class PricelistItem(models.Model): _inherit = "product.pricelist.item" - base = fields.Selection(selection_add=[("standard_price_usd", "Cost in USD")]) + base = fields.Selection( + selection_add=[("standard_price_usd", "Cost in USD")], ondelete={"standard_price_usd": "set default"} + ) diff --git a/product_cost_usd/models/product_template.py b/product_cost_usd/models/product_template.py index b9c1829baf0..25e6eaccde9 100644 --- a/product_cost_usd/models/product_template.py +++ b/product_cost_usd/models/product_template.py @@ -2,14 +2,12 @@ from odoo.exceptions import ValidationError from odoo.tools import float_compare -import odoo.addons.decimal_precision as dp - class ProductTemplate(models.Model): _inherit = "product.template" standard_price_usd = fields.Float( - "Cost in USD", digits=dp.get_precision("Product Price"), help="Price cost of the product in USD currency" + "Cost in USD", digits="Product Price", help="Price cost of the product in USD currency" ) @api.constrains("standard_price_usd", "seller_ids") @@ -23,20 +21,22 @@ def check_cost_and_price(self): """ usd_currency = self.env.ref("base.USD") prec = self.env["decimal.precision"].precision_get("Product Price") - usd_seller = self.seller_ids.filtered(lambda x: x.currency_id == usd_currency) - list_price = usd_seller.price - standard_price_usd = self.standard_price_usd - if not usd_seller and float_compare(standard_price_usd, 0, precision_digits=prec) > 0: - raise ValidationError( - _("You must have at least one supplier with price in USD before assigning a Cost in USD") - ) - if float_compare(list_price, standard_price_usd, precision_digits=prec) > 0: - raise ValidationError( - _( - "You cannot create or modify a product if the cost in USD" - " is less than the supplier list price.\n\n" - "- Supplier list price = %s\n" - "- Cost in USD = %s" + for product in self: + usd_seller = product.seller_ids.filtered(lambda x: x.currency_id == usd_currency)[:1] + list_price = usd_seller.price + standard_price_usd = product.standard_price_usd + if not usd_seller and float_compare(standard_price_usd, 0, precision_digits=prec) > 0: + raise ValidationError( + _("You must have at least one supplier with price in USD before assigning a Cost in USD") + ) + if float_compare(list_price, standard_price_usd, precision_digits=prec) > 0: + raise ValidationError( + _( + "You cannot create or modify a product if the cost in USD" + " is less than the supplier list price.\n\n" + "- Supplier list price = %s\n" + "- Cost in USD = %s", + list_price, + standard_price_usd, + ) ) - % (list_price, standard_price_usd) - ) diff --git a/product_cost_usd/models/sale_order_line.py b/product_cost_usd/models/sale_order_line.py index a23d6e5930a..97672e52be8 100644 --- a/product_cost_usd/models/sale_order_line.py +++ b/product_cost_usd/models/sale_order_line.py @@ -4,38 +4,26 @@ class SaleOrderLine(models.Model): _inherit = "sale.order.line" - @api.model - def _get_purchase_price(self, pricelist, product, product_uom, date): - """Inherited to recalculate purchase price when pricelist item is - based on cost in usd. - """ - res = super()._get_purchase_price(pricelist, product, product_uom, date) - price_rule = pricelist._compute_price_rule([(product, 1, False)]) - price, rule = price_rule[product.id] - suitable_rule = pricelist.item_ids.filtered(lambda x: x.id == rule) - if not suitable_rule or suitable_rule.base != "standard_price_usd": - return res - frm_cur = self.env.ref("base.USD") - to_cur = pricelist.currency_id - purchase_price = product.standard_price_usd - if product_uom != product.uom_id: - purchase_price = product.uom_id._compute_price(purchase_price, product_uom) - price = frm_cur.with_context(date=date).compute(purchase_price, to_cur, round=False) - return {"purchase_price": price} - - @api.model - def _compute_margin(self, order_id, product_id, product_uom_id): + @api.depends("product_id", "company_id", "currency_id", "product_uom") + def _compute_purchase_price(self): """Inherited to recalculate purchase price when pricelist item is based on cost in USD. - - Why this inheritance? - - In spite of the name this method is only used to get the purchase - price, calling the method get_purchase_price we reuse that logic to - calculate the purchase price when pricelist item is based on cost - in USD. """ - price = super()._compute_margin(order_id, product_id, product_uom_id) - date = order_id.date_order - prices = self._get_purchase_price(order_id.pricelist_id, product_id, product_uom_id, date) - return prices.get("purchase_price", price) + res = super()._compute_purchase_price() + for line in self: + pricelist = line.order_id.pricelist_id + date = line.order_id.date_order + if not line.product_id: + continue + price_rule = pricelist._compute_price_rule([(line.product_id, 1, False)]) + _price, rule = price_rule[line.product_id.id] + suitable_rule = pricelist.item_ids.filtered(lambda x: x.id == rule) + if not suitable_rule or suitable_rule.base != "standard_price_usd": + continue + currency_usd = self.env.ref("base.USD") + to_cur = pricelist.currency_id + purchase_price = line.product_id.standard_price_usd + if line.product_uom != line.product_id.uom_id: + purchase_price = line.product_id.uom_id._compute_price(purchase_price, line.product_uom) + line.purchase_price = currency_usd._convert(purchase_price, to_cur, line.company_id, date, round=False) + return res diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index a576fe84a44..eda637a1bb8 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -2,128 +2,98 @@ from odoo import fields from odoo.exceptions import ValidationError -from odoo.tests.common import TransactionCase -from odoo.tools import float_compare, float_round +from odoo.tests import Form, TransactionCase, tagged +from odoo.tools import float_compare +@tagged("post_install", "-at_install", "sale") class TestStandardPriceUsd(TransactionCase): def setUp(self): super().setUp() self.mxn = self.env.ref("base.MXN") self.usd = self.env.ref("base.USD") - self.sale_order = self.env["sale.order"] self.partner = self.env.ref("base.res_partner_4") - self.product_uom = self.env.ref("product.product_uom_unit") + self.product_uom = self.env.ref("uom.product_uom_unit") self.product = self.env.ref("product.product_product_24") self.pricelist_15_usd = self.env.ref("product_cost_usd.pricelist_15_usd") self.pricelist_15_mxn = self.pricelist_15_usd.copy({"name": "Pricelist 15% MXN", "currency_id": self.mxn.id}) self.pricelist = self.env["product.pricelist"].create({"name": "Pricelist Demo"}) + def create_sale_order(self, product=None, partner=None, pricelist=None, **line_kwargs): + if partner is None: + partner = self.partner + order = Form(self.env["sale.order"]) + order.date_order = fields.Datetime.now() + order.partner_id = partner + order.pricelist_id = pricelist + with order.order_line.new() as line: + line.product_id = product + line.product_uom = self.product_uom + line.product_uom_qty = 1 + order = order.save() + return order + def set_standard_price_usd(self, price): self.assertTrue(self.product.seller_ids) self.product.seller_ids[0].write({"currency_id": self.usd.id}) self.product.write({"standard_price_usd": price}) - def test_01(self): + def test_01_usd_pricelist(self): """Test USD pricelist based on cost in USD.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_usd.id) - expected_price = float_round(product.standard_price_usd * 1.15, precision_rounding=self.usd.rounding) + expected_price = self.usd.round(product.standard_price_usd * 1.15) self.assertEqual( float_compare(product.price, expected_price, precision_digits=2), 0, "Product price should be %s" % product.price, ) - def test_02(self): + def test_02_mxn_pricelist(self): """Test a MXN pricelist based on cost in USD.""" self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) mxn_rate = self.mxn.rate / self.usd.rate - expected_price = float_round( - (product.standard_price_usd * 1.15) * mxn_rate, precision_rounding=self.mxn.rounding - ) + expected_price = self.mxn.round((product.standard_price_usd * 1.15) * mxn_rate) self.assertEqual( float_compare(product.price, expected_price, precision_digits=2), 0, "Product price should be %s" % product.price, ) - def test_03(self): + def test_03_constraint_check_cost_no_seller(self): """Test constraint check_cost_and_price.""" - with self.assertRaisesRegexp(ValidationError, "You must have at least one supplier with price in USD"): + self.product.seller_ids = False + with self.assertRaisesRegex(ValidationError, "You must have at least one supplier with price in USD"): self.product.write({"standard_price_usd": 880}) - def test_04(self): + def test_04_constraint_check_cost(self): """Test constraint check_cost_and_price.""" - with self.assertRaisesRegexp(ValidationError, "You cannot create or modify a product if the cost in USD"): + with self.assertRaisesRegex(ValidationError, "You cannot create or modify a product if the cost in USD"): self.set_standard_price_usd(1) - def test_sale_margin(self): + def test_05_sale_margin(self): """Test the sale margin module using a pricelist with cost in USD.""" self.set_standard_price_usd(880) - product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) # Create a sale order for product Graphics Card. - sale_order = self.sale_order.create( - { - "date_order": fields.Datetime.now(), - "name": "Test", - "order_line": [ - ( - 0, - 0, - { - "name": "[CARD] Graphics Card", - "product_uom": self.product_uom.id, - "product_uom_qty": 1, - "state": "draft", - "product_id": product.id, - }, - ) - ], - "partner_id": self.partner.id, - "pricelist_id": self.pricelist_15_mxn.id, - } - ) + sale_order = self.create_sale_order(product=self.product, pricelist=self.pricelist_15_mxn) # Confirm the sale order. sale_order.action_confirm() # Verify that margin field gets bind with the value. mxn_rate = self.mxn.rate / self.usd.rate - expected_price = float_round( - (product.standard_price_usd * 1.15) * mxn_rate, precision_rounding=self.mxn.rounding - ) - expected_cost = float_round(product.standard_price_usd * mxn_rate, precision_rounding=self.mxn.rounding) - margin = float_round(expected_price - expected_cost, precision_rounding=self.mxn.rounding) + expected_price = self.mxn.round((self.product.standard_price_usd * 1.15) * mxn_rate) + expected_cost = self.mxn.round(self.product.standard_price_usd * mxn_rate) + margin = self.mxn.round(expected_price - expected_cost) self.assertEqual( float_compare(sale_order.margin, margin, precision_digits=2), 0, "Sale order margin should be %s" % margin ) - def test_sale_margin_normal(self): + def test_06_sale_margin_normal(self): """Test the sale margin module using a pricelist without cost in USD. """ # Create a sale order for product Graphics Card. - sale_order = self.sale_order.create( - { - "date_order": fields.Datetime.now(), - "name": "Test", - "order_line": [ - ( - 0, - 0, - { - "name": "[CARD] Graphics Card", - "product_uom": self.product_uom.id, - "product_uom_qty": 1, - "state": "draft", - "product_id": self.product.id, - }, - ) - ], - "partner_id": self.partner.id, - "pricelist_id": self.pricelist.id, - } - ) + sale_order = self.create_sale_order(product=self.product, pricelist=self.pricelist) # Confirm the sale order. sale_order.action_confirm() # Verify that margin field gets bind with the value. diff --git a/product_cost_usd/views/product_template_views.xml b/product_cost_usd/views/product_template_views.xml index 56e06d6d711..a400965a000 100644 --- a/product_cost_usd/views/product_template_views.xml +++ b/product_cost_usd/views/product_template_views.xml @@ -5,9 +5,9 @@ product.template - + - + From 435f11af36547ecc17c41fa7743a237618c99a5e Mon Sep 17 00:00:00 2001 From: "Andy Quijada [Vauxoo]" Date: Mon, 9 Jan 2023 20:18:51 -0400 Subject: [PATCH 09/15] [IMP] product_cost_usd: Use product.template in the price calculation Modification to avoid errors coming from the store when listing the products, due to the fact that the the ID of the record used may belong to the product.template model, therefore change the iteration object to the variable `products_qty_partner` to more directly take the value of the product used in the process --- product_cost_usd/models/product_pricelist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/product_cost_usd/models/product_pricelist.py b/product_cost_usd/models/product_pricelist.py index 745dba37557..c15bbdef5a8 100644 --- a/product_cost_usd/models/product_pricelist.py +++ b/product_cost_usd/models/product_pricelist.py @@ -29,19 +29,19 @@ def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): results = super()._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) usd_currency = self.env.ref("base.USD") date = fields.Date.context_today(self) - for product_id in results: + for product_qty_partner in products_qty_partner: + product = product_qty_partner[0] # get current price and pricelist item for product_id - price, item_id = results[product_id] + price, item_id = results[product.id] suitable_rule = self.item_ids.filtered(lambda x: x.id == item_id) # look that pricelist item is based on cost in usd if not suitable_rule or suitable_rule.base != "standard_price_usd": continue - product = self.env["product.product"].browse(product_id) # go back conversion made in super, moving the price into # product currency for items based on cost in USD price = self.currency_id._convert(price, product.currency_id, self.env.company, date, round=False) # now convert from USD into pricelist currency if self.currency_id != usd_currency: price = usd_currency._convert(price, self.currency_id, self.env.company, date, round=False) - results[product_id] = (price, suitable_rule and suitable_rule.id or False) + results[product.id] = (price, suitable_rule and suitable_rule.id or False) return results From b3194de56ecdb1164183dd1b535ee6a49840997c Mon Sep 17 00:00:00 2001 From: Estefania Date: Mon, 23 Jan 2023 21:06:29 -0300 Subject: [PATCH 10/15] [FIX] product_cost_usd: pre-commit-vauxoo error Fix lint: - (implicit-str-concat) --- product_cost_usd/tests/test_standard_price_usd.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index eda637a1bb8..374fe302393 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -1,5 +1,3 @@ -from __future__ import division - from odoo import fields from odoo.exceptions import ValidationError from odoo.tests import Form, TransactionCase, tagged From b9805d4fb442f67c246c0a72f2ceafc984f55894 Mon Sep 17 00:00:00 2001 From: rolandojduartem Date: Mon, 8 May 2023 17:34:35 +0000 Subject: [PATCH 11/15] [FIX] product_cost_usd: Show standard_price_usd field as USD When the currency of the company is different to USD, the field is shown with the currency of the company instead of USD, the field is set to be shown as USD. --- product_cost_usd/__manifest__.py | 3 ++- product_cost_usd/data/res_currency_data.xml | 6 ++++++ product_cost_usd/i18n/es.po | 13 +++++++++++++ product_cost_usd/models/product_template.py | 15 ++++++++++++++- product_cost_usd/views/product_template_views.xml | 7 ++++++- 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 product_cost_usd/data/res_currency_data.xml diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 2064a486e0e..fd81f73f014 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -3,7 +3,7 @@ "summary": """ This module adds the field Cost in USD to the Product form. """, - "version": "15.0.1.0.0", + "version": "15.0.1.0.1", "author": "Vauxoo", "category": "Sales/Sales", "website": "https://vauxoo.com", @@ -15,6 +15,7 @@ "demo/product_pricelist_demo.xml", ], "data": [ + "data/res_currency_data.xml", "views/product_template_views.xml", ], "installable": True, diff --git a/product_cost_usd/data/res_currency_data.xml b/product_cost_usd/data/res_currency_data.xml new file mode 100644 index 00000000000..d92a0d495cd --- /dev/null +++ b/product_cost_usd/data/res_currency_data.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/product_cost_usd/i18n/es.po b/product_cost_usd/i18n/es.po index bfe781fb635..8ba05f2a47e 100644 --- a/product_cost_usd/i18n/es.po +++ b/product_cost_usd/i18n/es.po @@ -40,6 +40,12 @@ msgstr "Basado en" msgid "Cost in USD" msgstr "Costo en USD" +#. module: product_cost_usd +#: model:ir.model.fields,field_description:product_cost_usd.field_product_product__currency_usd_id +#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__currency_usd_id +msgid "Currency USD" +msgstr "Moneda USD" + #. module: product_cost_usd #: model:ir.model.fields,help:product_cost_usd.field_product_product__standard_price_usd #: model:ir.model.fields,help:product_cost_usd.field_product_template__standard_price_usd @@ -71,6 +77,13 @@ msgstr "Plantilla de producto" msgid "Sales Order Line" msgstr "Línea de pedido de venta" +#. module: product_cost_usd +#: model:ir.model.fields,help:product_cost_usd.field_product_product__currency_usd_id +#: model:ir.model.fields,help:product_cost_usd.field_product_template__currency_usd_id +msgid "Technical field to show the price fields as USD in the products" +msgstr "" +"Campo técnico para mostrar los campos de precio como USD en los productos" + #. module: product_cost_usd #: code:addons/product_cost_usd/models/product_template.py:0 #, python-format diff --git a/product_cost_usd/models/product_template.py b/product_cost_usd/models/product_template.py index 25e6eaccde9..077f11835f8 100644 --- a/product_cost_usd/models/product_template.py +++ b/product_cost_usd/models/product_template.py @@ -6,10 +6,23 @@ class ProductTemplate(models.Model): _inherit = "product.template" + currency_usd_id = fields.Many2one( + "res.currency", + string="Currency USD", + compute="_compute_currency_usd_id", + help="Technical field to show the price fields as USD in the products", + ) standard_price_usd = fields.Float( - "Cost in USD", digits="Product Price", help="Price cost of the product in USD currency" + "Cost in USD", + digits="Product Price", + help="Price cost of the product in USD currency", ) + def _compute_currency_usd_id(self): + currency_usd = self.env.ref("base.USD") + for product in self: + product.currency_usd_id = currency_usd + @api.constrains("standard_price_usd", "seller_ids") def check_cost_and_price(self): """Validate 'Cost in USD' usability. diff --git a/product_cost_usd/views/product_template_views.xml b/product_cost_usd/views/product_template_views.xml index a400965a000..3f53a30c5cc 100644 --- a/product_cost_usd/views/product_template_views.xml +++ b/product_cost_usd/views/product_template_views.xml @@ -6,7 +6,12 @@ - + + From bb32914089c0b0a626c0c7fada59ebcfea655b6c Mon Sep 17 00:00:00 2001 From: "German Loredo [Vauxoo]" Date: Mon, 8 Jul 2024 21:11:35 +0000 Subject: [PATCH 12/15] [FIX] product_cost_usd: order in recompute purchase price The `sale_stock_margin` module recomputes the purchase price in sale order lines and calls `super()` only over the lines without a stock move related to it. This causes this module to not recompute the lines with stock moves related, leading to incorrect computation of the purchase price. This commit adds the `sale_stock_margin` module as a dependency in order to first run this module compute. --- product_cost_usd/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index fd81f73f014..197931334af 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -10,6 +10,7 @@ "license": "LGPL-3", "depends": [ "sale_margin", + "sale_stock_margin", ], "demo": [ "demo/product_pricelist_demo.xml", From 2fb42a82820848746496870b02ec52e341a7859c Mon Sep 17 00:00:00 2001 From: "German Loredo [Vauxoo]" Date: Tue, 27 Aug 2024 02:42:09 +0000 Subject: [PATCH 13/15] [FIX] product_cost_usd: correctly set purchase price in multicompany Since each company has its own purchase price, we must explicitly set the company we are working with (the one in the sale order line) to correctly set the purchase price. --- product_cost_usd/models/sale_order_line.py | 1 + 1 file changed, 1 insertion(+) diff --git a/product_cost_usd/models/sale_order_line.py b/product_cost_usd/models/sale_order_line.py index 97672e52be8..a896344a7aa 100644 --- a/product_cost_usd/models/sale_order_line.py +++ b/product_cost_usd/models/sale_order_line.py @@ -11,6 +11,7 @@ def _compute_purchase_price(self): """ res = super()._compute_purchase_price() for line in self: + line = line.with_company(line.company_id) pricelist = line.order_id.pricelist_id date = line.order_id.date_order if not line.product_id: From b5502f45ea9cd95b9264ac48c4182b18f56fa85c Mon Sep 17 00:00:00 2001 From: emtz10 Date: Sun, 17 May 2026 16:38:38 +0000 Subject: [PATCH 14/15] [IMP] product_cost_usd: remove superfluous manifest key Following the same approach as Odoo [1], the `auto_install` key is removed because its value is the same as the default. [1]: https://github.com/odoo/odoo/pull/90209 --- product_cost_usd/__manifest__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 197931334af..00f50ae3e82 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -20,5 +20,4 @@ "views/product_template_views.xml", ], "installable": True, - "auto_install": False, } From a07fe926d2034dd04b340307c11f848f4a4d1911 Mon Sep 17 00:00:00 2001 From: emtz10 Date: Tue, 17 Feb 2026 19:40:29 +0000 Subject: [PATCH 15/15] [MIG] product_cost_usd: migration to 19.0 Changes include: - Rename `product_uom` to `product_uom_id` in `sale.order.line` dependencies and computations according to [1]. - Update `_compute_price_rule` method call and dictionary extraction to match the new API signature according to [2]. - Optimize `product.pricelist.item` search by using direct `browse` instead of filtering `item_ids`. - Create test records (Partner, Product, Pricelist) dynamically as demo data is no longer loaded by default. - Set `price_discount` to `-15` in the test pricelist to comply with Odoo's native markup calculation standard. - Replace usage of the deprecated `price` field on product templates with `get_contextual_price()` according to [3]. - Replace usage of the deprecated `pricing` div on product template views with `list_price_uom` according to [4]. - Deprecate the usage of `invisible` in views according to [5]. - Removed deprecated inheritance in `product.pricelist` as it was causing silent fallbacks to `list_price`, breaking margin computations in sales orders. - Injected the raw USD cost directly via the official `_compute_base_price` hook in `product.pricelist.item`. - Added native support for UoM (Unit of Measure) conversions. - Grant `product.group_product_pricelist` explicitly in tests because demo data is no longer loaded by default, removing the implicit activation that previously made pricelist_id visible in the Form. [1]: https://github.com/odoo/odoo/commit/c8461a5 [2]: https://github.com/odoo/odoo/commit/57ced81 [3]: https://github.com/odoo/odoo/commit/9e99a9d [4]: https://github.com/odoo/odoo/commit/65652c7 [5]: https://github.com/odoo/odoo/pull/137031 --- product_cost_usd/__manifest__.py | 2 +- product_cost_usd/i18n/es.po | 42 +++++---- product_cost_usd/models/__init__.py | 1 - product_cost_usd/models/product_pricelist.py | 47 ---------- .../models/product_pricelist_item.py | 33 +++++++ product_cost_usd/models/product_template.py | 10 +-- product_cost_usd/models/sale_order_line.py | 27 +++--- .../tests/test_standard_price_usd.py | 86 ++++++++++++++----- .../views/product_template_views.xml | 4 +- 9 files changed, 147 insertions(+), 105 deletions(-) delete mode 100644 product_cost_usd/models/product_pricelist.py diff --git a/product_cost_usd/__manifest__.py b/product_cost_usd/__manifest__.py index 00f50ae3e82..28c6c1447d6 100644 --- a/product_cost_usd/__manifest__.py +++ b/product_cost_usd/__manifest__.py @@ -3,7 +3,7 @@ "summary": """ This module adds the field Cost in USD to the Product form. """, - "version": "15.0.1.0.1", + "version": "19.0.1.0.0", "author": "Vauxoo", "category": "Sales/Sales", "website": "https://vauxoo.com", diff --git a/product_cost_usd/i18n/es.po b/product_cost_usd/i18n/es.po index 8ba05f2a47e..b7c9b80fa2b 100644 --- a/product_cost_usd/i18n/es.po +++ b/product_cost_usd/i18n/es.po @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 15.0+e\n" +"Project-Id-Version: Odoo Server 19.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-25 22:26+0000\n" -"PO-Revision-Date: 2022-08-25 22:26+0000\n" +"POT-Creation-Date: 2026-05-06 21:00+0000\n" +"PO-Revision-Date: 2026-05-06 21:00+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -20,8 +20,8 @@ msgstr "" msgid "" "Base price for computation.\n" "Sales Price: The base price will be the Sales Price.\n" -"Cost Price : The base price will be the cost price.\n" -"Other Pricelist : Computation of the base price based on another Pricelist." +"Cost Price: The base price will be the cost price.\n" +"Other Pricelist: Computation of the base price based on another Pricelist." msgstr "" "Precio base para el cálculo.\n" "Precio de venta: El precio base será el precio de venta.\n" @@ -47,20 +47,24 @@ msgid "Currency USD" msgstr "Moneda USD" #. module: product_cost_usd -#: model:ir.model.fields,help:product_cost_usd.field_product_product__standard_price_usd -#: model:ir.model.fields,help:product_cost_usd.field_product_template__standard_price_usd -msgid "Price cost of the product in USD currency" -msgstr "Precio coste del producto en moneda USD" +#: model:ir.model.fields,field_description:product_cost_usd.field_product_pricelist_item__display_name +#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__display_name +#: model:ir.model.fields,field_description:product_cost_usd.field_sale_order_line__display_name +msgid "Display Name" +msgstr "Nombre mostrado" #. module: product_cost_usd -#: model:ir.model,name:product_cost_usd.model_product_pricelist -msgid "Pricelist" -msgstr "Lista de precios" +#: model:ir.model.fields,field_description:product_cost_usd.field_product_pricelist_item__id +#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__id +#: model:ir.model.fields,field_description:product_cost_usd.field_sale_order_line__id +msgid "ID" +msgstr "" #. module: product_cost_usd -#: model:product.pricelist,name:product_cost_usd.pricelist_15_usd -msgid "Pricelist 15% USD" -msgstr "Lista de precios 15% USD" +#: model:ir.model.fields,help:product_cost_usd.field_product_product__standard_price_usd +#: model:ir.model.fields,help:product_cost_usd.field_product_template__standard_price_usd +msgid "Price cost of the product in USD currency" +msgstr "Precio coste del producto en moneda USD" #. module: product_cost_usd #: model:ir.model,name:product_cost_usd.model_product_pricelist_item @@ -69,8 +73,8 @@ msgstr "Regla de la lista de precios" #. module: product_cost_usd #: model:ir.model,name:product_cost_usd.model_product_template -msgid "Product Template" -msgstr "Plantilla de producto" +msgid "Product" +msgstr "Producto" #. module: product_cost_usd #: model:ir.model,name:product_cost_usd.model_sale_order_line @@ -85,8 +89,8 @@ msgstr "" "Campo técnico para mostrar los campos de precio como USD en los productos" #. module: product_cost_usd +#. odoo-python #: code:addons/product_cost_usd/models/product_template.py:0 -#, python-format msgid "" "You cannot create or modify a product if the cost in USD is less than the supplier list price.\n" "\n" @@ -99,8 +103,8 @@ msgstr "" "- Costo en USD = %s" #. module: product_cost_usd +#. odoo-python #: code:addons/product_cost_usd/models/product_template.py:0 -#, python-format msgid "" "You must have at least one supplier with price in USD before assigning a " "Cost in USD" diff --git a/product_cost_usd/models/__init__.py b/product_cost_usd/models/__init__.py index 7e0aced5d03..33a5ae818c4 100644 --- a/product_cost_usd/models/__init__.py +++ b/product_cost_usd/models/__init__.py @@ -1,4 +1,3 @@ from . import product_template -from . import product_pricelist from . import product_pricelist_item from . import sale_order_line diff --git a/product_cost_usd/models/product_pricelist.py b/product_cost_usd/models/product_pricelist.py deleted file mode 100644 index c15bbdef5a8..00000000000 --- a/product_cost_usd/models/product_pricelist.py +++ /dev/null @@ -1,47 +0,0 @@ -from odoo import fields, models - - -class Pricelist(models.Model): - _inherit = "product.pricelist" - - def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False): - """Inherited to modify price computation when a pricelist item is - based on cost in USD. - - Why this inheritance? - - This method always computes the product price, from product currency - (mostly the same company currency) into pricelist currency. When the - pricelist item is based on cost in USD is necessary that currency - conversion is made from USD currency. For this reason, we must go back - the conversion made in super and then made a conversion from USD - currency to the pricelist currency to get the expected price when the - pricelist item is based on cost in USD. - - Returns: dict{product_id: (price, suitable_rule) for given pricelist} - - If date in context: Date of the pricelist (%Y-%m-%d) - - :param products_qty_partner: list of typles products, quantity, partner - :param datetime date: validity date - :param ID uom_id: intermediate unit of measure - """ - results = super()._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id) - usd_currency = self.env.ref("base.USD") - date = fields.Date.context_today(self) - for product_qty_partner in products_qty_partner: - product = product_qty_partner[0] - # get current price and pricelist item for product_id - price, item_id = results[product.id] - suitable_rule = self.item_ids.filtered(lambda x: x.id == item_id) - # look that pricelist item is based on cost in usd - if not suitable_rule or suitable_rule.base != "standard_price_usd": - continue - # go back conversion made in super, moving the price into - # product currency for items based on cost in USD - price = self.currency_id._convert(price, product.currency_id, self.env.company, date, round=False) - # now convert from USD into pricelist currency - if self.currency_id != usd_currency: - price = usd_currency._convert(price, self.currency_id, self.env.company, date, round=False) - results[product.id] = (price, suitable_rule and suitable_rule.id or False) - return results diff --git a/product_cost_usd/models/product_pricelist_item.py b/product_cost_usd/models/product_pricelist_item.py index 508e0ab6892..b95b72abf88 100644 --- a/product_cost_usd/models/product_pricelist_item.py +++ b/product_cost_usd/models/product_pricelist_item.py @@ -7,3 +7,36 @@ class PricelistItem(models.Model): base = fields.Selection( selection_add=[("standard_price_usd", "Cost in USD")], ondelete={"standard_price_usd": "set default"} ) + + def _compute_base_price(self, product, quantity, uom, date, currency, **kwargs): + """Compute the base price for a pricelist item based on the USD standard price. + + This method overrides the native base price computation to inject our custom + 'standard_price_usd' base. When this specific base is selected in the pricelist + item, the method performs the following pipeline: + 1. Retrieves the raw USD cost from the product (`standard_price_usd`). + 2. Adjusts the price according to the requested Unit of Measure (UoM) to ensure + correct pricing for different quantities (e.g., dozens vs. units). + 3. Converts the adjusted USD price into the pricelist's target currency. + + By returning the exact converted base price at this stage, we allow Odoo's + native pricing engine to seamlessly handle subsequent operations like discounts, + surcharges, and financial rounding rules. + """ + if self.base != "standard_price_usd": + return super()._compute_base_price(product, quantity, uom, date, currency, **kwargs) + target_currency = currency or self.currency_id + usd_currency = self.env.ref("base.USD", raise_if_not_found=False) + + # 1. Retrieve the raw USD cost from the product + price = product.standard_price_usd + + # 2. Unit of Measure conversion + if product.uom_id != uom: + price = product.uom_id._compute_price(price, uom) + + # 3. Convert from USD to the target pricelist currency + # We set round=False to allow Odoo's native engine to apply its own rounding rules at the end + if usd_currency and target_currency != usd_currency: + price = usd_currency._convert(price, target_currency, self.env.company, date, round=False) + return price diff --git a/product_cost_usd/models/product_template.py b/product_cost_usd/models/product_template.py index 077f11835f8..91642e9451b 100644 --- a/product_cost_usd/models/product_template.py +++ b/product_cost_usd/models/product_template.py @@ -1,4 +1,4 @@ -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import ValidationError from odoo.tools import float_compare @@ -13,7 +13,7 @@ class ProductTemplate(models.Model): help="Technical field to show the price fields as USD in the products", ) standard_price_usd = fields.Float( - "Cost in USD", + string="Cost in USD", digits="Product Price", help="Price cost of the product in USD currency", ) @@ -40,11 +40,11 @@ def check_cost_and_price(self): standard_price_usd = product.standard_price_usd if not usd_seller and float_compare(standard_price_usd, 0, precision_digits=prec) > 0: raise ValidationError( - _("You must have at least one supplier with price in USD before assigning a Cost in USD") + self.env._("You must have at least one supplier with price in USD before assigning a Cost in USD") ) - if float_compare(list_price, standard_price_usd, precision_digits=prec) > 0: + if usd_seller and float_compare(list_price, standard_price_usd, precision_digits=prec) > 0: raise ValidationError( - _( + self.env._( "You cannot create or modify a product if the cost in USD" " is less than the supplier list price.\n\n" "- Supplier list price = %s\n" diff --git a/product_cost_usd/models/sale_order_line.py b/product_cost_usd/models/sale_order_line.py index a896344a7aa..e8ec8d607d3 100644 --- a/product_cost_usd/models/sale_order_line.py +++ b/product_cost_usd/models/sale_order_line.py @@ -4,7 +4,7 @@ class SaleOrderLine(models.Model): _inherit = "sale.order.line" - @api.depends("product_id", "company_id", "currency_id", "product_uom") + @api.depends("product_id", "company_id", "currency_id", "product_uom_id") def _compute_purchase_price(self): """Inherited to recalculate purchase price when pricelist item is based on cost in USD. @@ -12,19 +12,26 @@ def _compute_purchase_price(self): res = super()._compute_purchase_price() for line in self: line = line.with_company(line.company_id) + product = line.product_id + if not product: + continue pricelist = line.order_id.pricelist_id date = line.order_id.date_order - if not line.product_id: - continue - price_rule = pricelist._compute_price_rule([(line.product_id, 1, False)]) - _price, rule = price_rule[line.product_id.id] - suitable_rule = pricelist.item_ids.filtered(lambda x: x.id == rule) - if not suitable_rule or suitable_rule.base != "standard_price_usd": + price_rule = pricelist._compute_price_rule( + products=product, + quantity=1.0, + currency=pricelist.currency_id, + uom=line.product_uom_id, + date=date, + ) + _price, rule = price_rule.get(product.id, (0.0, False)) + suitable_rule_id = self.env["product.pricelist.item"].browse(rule) + if suitable_rule_id.base != "standard_price_usd": continue currency_usd = self.env.ref("base.USD") to_cur = pricelist.currency_id - purchase_price = line.product_id.standard_price_usd - if line.product_uom != line.product_id.uom_id: - purchase_price = line.product_id.uom_id._compute_price(purchase_price, line.product_uom) + purchase_price = product.standard_price_usd + if line.product_uom_id != product.uom_id: + purchase_price = product.uom_id._compute_price(purchase_price, line.product_uom_id) line.purchase_price = currency_usd._convert(purchase_price, to_cur, line.company_id, date, round=False) return res diff --git a/product_cost_usd/tests/test_standard_price_usd.py b/product_cost_usd/tests/test_standard_price_usd.py index 374fe302393..985d3642803 100644 --- a/product_cost_usd/tests/test_standard_price_usd.py +++ b/product_cost_usd/tests/test_standard_price_usd.py @@ -1,4 +1,4 @@ -from odoo import fields +from odoo import Command from odoo.exceptions import ValidationError from odoo.tests import Form, TransactionCase, tagged from odoo.tools import float_compare @@ -6,30 +6,74 @@ @tagged("post_install", "-at_install", "sale") class TestStandardPriceUsd(TransactionCase): - def setUp(self): - super().setUp() - self.mxn = self.env.ref("base.MXN") - self.usd = self.env.ref("base.USD") - self.partner = self.env.ref("base.res_partner_4") - self.product_uom = self.env.ref("uom.product_uom_unit") - self.product = self.env.ref("product.product_product_24") - self.pricelist_15_usd = self.env.ref("product_cost_usd.pricelist_15_usd") - self.pricelist_15_mxn = self.pricelist_15_usd.copy({"name": "Pricelist 15% MXN", "currency_id": self.mxn.id}) - self.pricelist = self.env["product.pricelist"].create({"name": "Pricelist Demo"}) + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.mxn = cls.env.ref("base.MXN") + cls.usd = cls.env.ref("base.USD") + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.env.user.write({"group_ids": [Command.link(cls.env.ref("product.group_product_pricelist").id)]}) + # Get or create the unit UOM + cls.product_uom = cls.env.ref("uom.product_uom_unit") + if not cls.product_uom: + cls.product_uom = cls.env["uom.uom"].create( + { + "name": "Unit", + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "uom_id": cls.product_uom.id, + "type": "consu", + "standard_price": 876.0, + "list_price": 885.0, + } + ) + + # Create supplier for product + cls.env["product.supplierinfo"].create( + { + "product_tmpl_id": cls.product.product_tmpl_id.id, + "partner_id": cls.partner.id, + "price": 876.0, + "currency_id": cls.usd.id, + } + ) + + # Create pricelist with USD base cost strategy + cls.pricelist_15_usd = cls.env["product.pricelist"].create( + { + "name": "Pricelist 15% USD", + "currency_id": cls.usd.id, + "item_ids": [ + Command.create( + { + "applied_on": "1_product", + "product_id": cls.product.id, + "base": "standard_price_usd", + "compute_price": "formula", + "price_discount": -15, + } + ) + ], + } + ) + cls.pricelist_15_mxn = cls.pricelist_15_usd.copy({"name": "Pricelist 15% MXN", "currency_id": cls.mxn.id}) + cls.pricelist = cls.env["product.pricelist"].create({"name": "Pricelist Demo"}) def create_sale_order(self, product=None, partner=None, pricelist=None, **line_kwargs): if partner is None: partner = self.partner + order = Form(self.env["sale.order"]) - order.date_order = fields.Datetime.now() order.partner_id = partner - order.pricelist_id = pricelist + if pricelist: + order.pricelist_id = pricelist with order.order_line.new() as line: line.product_id = product - line.product_uom = self.product_uom line.product_uom_qty = 1 - order = order.save() - return order + return order.save() def set_standard_price_usd(self, price): self.assertTrue(self.product.seller_ids) @@ -41,10 +85,11 @@ def test_01_usd_pricelist(self): self.set_standard_price_usd(880) product = self.product.with_context(pricelist=self.pricelist_15_usd.id) expected_price = self.usd.round(product.standard_price_usd * 1.15) + product_price = product.get_contextual_price() self.assertEqual( - float_compare(product.price, expected_price, precision_digits=2), + float_compare(product_price, expected_price, precision_digits=2), 0, - "Product price should be %s" % product.price, + "Product price should be %s" % product_price, ) def test_02_mxn_pricelist(self): @@ -53,10 +98,11 @@ def test_02_mxn_pricelist(self): product = self.product.with_context(pricelist=self.pricelist_15_mxn.id) mxn_rate = self.mxn.rate / self.usd.rate expected_price = self.mxn.round((product.standard_price_usd * 1.15) * mxn_rate) + product_price = product.get_contextual_price() self.assertEqual( - float_compare(product.price, expected_price, precision_digits=2), + float_compare(product_price, expected_price, precision_digits=2), 0, - "Product price should be %s" % product.price, + "Product price should be %s" % product_price, ) def test_03_constraint_check_cost_no_seller(self): diff --git a/product_cost_usd/views/product_template_views.xml b/product_cost_usd/views/product_template_views.xml index 3f53a30c5cc..8bf39de0ef6 100644 --- a/product_cost_usd/views/product_template_views.xml +++ b/product_cost_usd/views/product_template_views.xml @@ -5,8 +5,8 @@ product.template - - + +