From 55811d54b15b4a38193fe67f72d8b2d4ae3f0d7d Mon Sep 17 00:00:00 2001 From: snt Date: Thu, 12 Feb 2026 19:23:29 +0100 Subject: [PATCH] [FIX] product_sale_price_from_pricelist: Actualizar tests para Odoo 18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cambiar parámetro qty= a quantity= en llamadas a _compute_price_rule - Eliminar type/detailed_type de product.product creates - Añadir campo name a purchase.order.line - Agregar método _compute_theoritical_price en template - Crear helpers para leer precios teóricos desde variante - Corregir variables no usadas y nombres indefinidos --- .prettierignore | 15 + .prettierrc.yml | 30 ++ ocb | 1 + product_origin/README.rst | 114 +++++ product_origin/__init__.py | 1 + product_origin/__manifest__.py | 18 + product_origin/i18n/fr.po | 72 +++ product_origin/i18n/it.po | 73 +++ product_origin/i18n/product_origin.pot | 66 +++ product_origin/models/__init__.py | 2 + product_origin/models/product_product.py | 60 +++ product_origin/models/product_template.py | 95 ++++ product_origin/pyproject.toml | 3 + product_origin/readme/CONTRIBUTORS.md | 4 + product_origin/readme/DESCRIPTION.md | 4 + product_origin/readme/HISTORY.md | 3 + product_origin/readme/USAGE.md | 5 + product_origin/static/description/icon.png | Bin 0 -> 9455 bytes product_origin/static/description/index.html | 456 ++++++++++++++++++ .../static/description/product_form.png | Bin 0 -> 45968 bytes product_origin/tests/__init__.py | 1 + product_origin/tests/test_module.py | 86 ++++ product_origin/views/product_product.xml | 35 ++ product_origin/views/product_template.xml | 39 ++ .../models/product_template.py | 10 +- .../tests/test_pricelist.py | 269 ++++++----- .../tests/test_product_template.py | 206 ++++---- .../tests/test_stock_move.py | 228 +++++---- 28 files changed, 1569 insertions(+), 327 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.yml create mode 160000 ocb create mode 100644 product_origin/README.rst create mode 100644 product_origin/__init__.py create mode 100644 product_origin/__manifest__.py create mode 100644 product_origin/i18n/fr.po create mode 100644 product_origin/i18n/it.po create mode 100644 product_origin/i18n/product_origin.pot create mode 100644 product_origin/models/__init__.py create mode 100644 product_origin/models/product_product.py create mode 100644 product_origin/models/product_template.py create mode 100644 product_origin/pyproject.toml create mode 100644 product_origin/readme/CONTRIBUTORS.md create mode 100644 product_origin/readme/DESCRIPTION.md create mode 100644 product_origin/readme/HISTORY.md create mode 100644 product_origin/readme/USAGE.md create mode 100644 product_origin/static/description/icon.png create mode 100644 product_origin/static/description/index.html create mode 100644 product_origin/static/description/product_form.png create mode 100644 product_origin/tests/__init__.py create mode 100644 product_origin/tests/test_module.py create mode 100644 product_origin/views/product_product.xml create mode 100644 product_origin/views/product_template.xml diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..bb84fc5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,15 @@ +# Prettier ignore patterns for Odoo addons + +# Ignore XML files - prettier has issues with QWeb mixed content +*.xml + +# Odoo core +ocb/** + +# Build artifacts +*.pyc +__pycache__/ +*.egg-info/ + +# Git +.git/ diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..21a0072 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,30 @@ +# Prettier configuration for Odoo addons +# Note: XML formatting disabled for QWeb templates due to prettier limitations +# with mixed content (text + tags). Use manual formatting for .xml files. + +printWidth: 100 +tabWidth: 4 +useTabs: false + +# XML/HTML specific - disabled, causes readability issues with QWeb +# xmlWhitespaceSensitivity: "strict" +# xmlSelfClosingSpace: true + +# Keep tags more compact - don't break every attribute +overrides: + # Disable prettier for XML files - manual formatting preferred + # - files: "*.xml" + # options: + # printWidth: 120 + # xmlWhitespaceSensitivity: "strict" + # singleAttributePerLine: false + # bracketSameLine: true + + - files: "*.py" + options: + printWidth: 88 + + - files: ["*.json", "*.json5"] + options: + printWidth: 120 + tabWidth: 2 diff --git a/ocb b/ocb new file mode 160000 index 0000000..6fb141f --- /dev/null +++ b/ocb @@ -0,0 +1 @@ +Subproject commit 6fb141fc7547f9de55c9ac702515f1e3a27406d0 diff --git a/product_origin/README.rst b/product_origin/README.rst new file mode 100644 index 0000000..9ff9d21 --- /dev/null +++ b/product_origin/README.rst @@ -0,0 +1,114 @@ +============== +Product Origin +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2db486ad6cf453e31192b518e0e0de9647f6b65545047439e05da8bc413dc410 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/18.0/product_origin + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_origin + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a field to associate the country and state of origin of +a product + +https://en.wikipedia.org/wiki/Country_of_origin + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +- Go to product form +- Fill in the country and/or state of origin of the product under the + 'General Information' tab. + +|image1| + +.. |image1| image:: https://raw.githubusercontent.com/OCA/product-attribute/18.0/product_origin/static/description/product_form.png + +Changelog +========= + +10.0.1.0.0 (2019-01-11) +----------------------- + +- [10.0][ADD] product_origin + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV +* GRAP + +Contributors +------------ + +- Denis Roussel +- Sylvain LE GAL (https://twitter.com/legalsylvain) +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-rousseldenis| image:: https://github.com/rousseldenis.png?size=40px + :target: https://github.com/rousseldenis + :alt: rousseldenis +.. |maintainer-legalsylvain| image:: https://github.com/legalsylvain.png?size=40px + :target: https://github.com/legalsylvain + :alt: legalsylvain + +Current `maintainers `__: + +|maintainer-rousseldenis| |maintainer-legalsylvain| + +This module is part of the `OCA/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_origin/__init__.py b/product_origin/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/product_origin/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_origin/__manifest__.py b/product_origin/__manifest__.py new file mode 100644 index 0000000..9fd0db1 --- /dev/null +++ b/product_origin/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product Origin", + "summary": """Adds the origin of the product""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "development_status": "Beta", + "author": "ACSONE SA/NV,GRAP,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "maintainers": ["rousseldenis", "legalsylvain"], + "depends": ["product"], + "data": [ + "views/product_product.xml", + "views/product_template.xml", + ], +} diff --git a/product_origin/i18n/fr.po b/product_origin/i18n/fr.po new file mode 100644 index 0000000..ba493ef --- /dev/null +++ b/product_origin/i18n/fr.po @@ -0,0 +1,72 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_origin +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-01-11 13:46+0000\n" +"PO-Revision-Date: 2024-01-11 13:46+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__state_id +#: model:ir.model.fields,field_description:product_origin.field_product_template__state_id +msgid "Country State of Origin" +msgstr "Région de fabrication" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__country_id +#: model:ir.model.fields,field_description:product_origin.field_product_template__country_id +#: model_terms:ir.ui.view,arch_db:product_origin.product_template_search_view +msgid "Country of Origin" +msgstr "Pays de fabrication" + +#. module: product_origin +#: model_terms:ir.ui.view,arch_db:product_origin.product_template_form_view +#: model_terms:ir.ui.view,arch_db:product_origin.view_product_product_form +#: model_terms:ir.ui.view,arch_db:product_origin.view_product_product_form_variant +msgid "Origin" +msgstr "Origine" + +#. module: product_origin +#: model:ir.model,name:product_origin.model_product_template +msgid "Product" +msgstr "Produit" + +#. module: product_origin +#: model:ir.model,name:product_origin.model_product_product +msgid "Product Variant" +msgstr "Variante de produit" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__state_id_domain +#: model:ir.model.fields,field_description:product_origin.field_product_template__state_id_domain +msgid "State Id Domain" +msgstr "" + +#. module: product_origin +#: model:ir.model.fields,help:product_origin.field_product_product__state_id_domain +#: model:ir.model.fields,help:product_origin.field_product_template__state_id_domain +msgid "" +"Technical field, used to compute dynamically state domain depending on the " +"country." +msgstr "" +"Champ technique, utilisé pour calculer dynamiquement le domaine de l'état, " +"en fonction du pays." + +#. module: product_origin +#. odoo-python +#: code:addons/product_origin/models/product_product.py:0 +#: code:addons/product_origin/models/product_template.py:0 +#, python-format +msgid "" +"The state '%(state_name)s' doesn't belong to the country '%(country_name)s'" +msgstr "La région '%(state_name)s' n'appartient pas au pays '%(country_name)s'" diff --git a/product_origin/i18n/it.po b/product_origin/i18n/it.po new file mode 100644 index 0000000..1b8e89d --- /dev/null +++ b/product_origin/i18n/it.po @@ -0,0 +1,73 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_origin +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-05-22 17:37+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__state_id +#: model:ir.model.fields,field_description:product_origin.field_product_template__state_id +msgid "Country State of Origin" +msgstr "Nazione paese di orgine" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__country_id +#: model:ir.model.fields,field_description:product_origin.field_product_template__country_id +#: model_terms:ir.ui.view,arch_db:product_origin.product_template_search_view +msgid "Country of Origin" +msgstr "Paese di origine" + +#. module: product_origin +#: model_terms:ir.ui.view,arch_db:product_origin.product_template_form_view +#: model_terms:ir.ui.view,arch_db:product_origin.view_product_product_form +#: model_terms:ir.ui.view,arch_db:product_origin.view_product_product_form_variant +msgid "Origin" +msgstr "Origine" + +#. module: product_origin +#: model:ir.model,name:product_origin.model_product_template +msgid "Product" +msgstr "Prodotto" + +#. module: product_origin +#: model:ir.model,name:product_origin.model_product_product +msgid "Product Variant" +msgstr "Variante prodotto" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__state_id_domain +#: model:ir.model.fields,field_description:product_origin.field_product_template__state_id_domain +msgid "State Id Domain" +msgstr "Dominio ID stato" + +#. module: product_origin +#: model:ir.model.fields,help:product_origin.field_product_product__state_id_domain +#: model:ir.model.fields,help:product_origin.field_product_template__state_id_domain +msgid "" +"Technical field, used to compute dynamically state domain depending on the " +"country." +msgstr "" +"Campo tecnico, utilizzato per calcolare dinamicamente il dominio dello stato " +"in funzione della nazione." + +#. module: product_origin +#. odoo-python +#: code:addons/product_origin/models/product_product.py:0 +#: code:addons/product_origin/models/product_template.py:0 +#, python-format +msgid "" +"The state '%(state_name)s' doesn't belong to the country '%(country_name)s'" +msgstr "" +"Lo stato '%(state_name)s' non appartiene alla nazione '%(country_name)s'" diff --git a/product_origin/i18n/product_origin.pot b/product_origin/i18n/product_origin.pot new file mode 100644 index 0000000..762c9d6 --- /dev/null +++ b/product_origin/i18n/product_origin.pot @@ -0,0 +1,66 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_origin +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__state_id +#: model:ir.model.fields,field_description:product_origin.field_product_template__state_id +msgid "Country State of Origin" +msgstr "" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__country_id +#: model:ir.model.fields,field_description:product_origin.field_product_template__country_id +#: model_terms:ir.ui.view,arch_db:product_origin.product_template_search_view +msgid "Country of Origin" +msgstr "" + +#. module: product_origin +#: model_terms:ir.ui.view,arch_db:product_origin.product_template_form_view +#: model_terms:ir.ui.view,arch_db:product_origin.view_product_product_form +#: model_terms:ir.ui.view,arch_db:product_origin.view_product_product_form_variant +msgid "Origin" +msgstr "" + +#. module: product_origin +#: model:ir.model,name:product_origin.model_product_template +msgid "Product" +msgstr "" + +#. module: product_origin +#: model:ir.model,name:product_origin.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: product_origin +#: model:ir.model.fields,field_description:product_origin.field_product_product__state_id_domain +#: model:ir.model.fields,field_description:product_origin.field_product_template__state_id_domain +msgid "State Id Domain" +msgstr "" + +#. module: product_origin +#: model:ir.model.fields,help:product_origin.field_product_product__state_id_domain +#: model:ir.model.fields,help:product_origin.field_product_template__state_id_domain +msgid "" +"Technical field, used to compute dynamically state domain depending on the " +"country." +msgstr "" + +#. module: product_origin +#. odoo-python +#: code:addons/product_origin/models/product_product.py:0 +#: code:addons/product_origin/models/product_template.py:0 +msgid "" +"The state '%(state_name)s' doesn't belong to the country '%(country_name)s'" +msgstr "" diff --git a/product_origin/models/__init__.py b/product_origin/models/__init__.py new file mode 100644 index 0000000..18b37e8 --- /dev/null +++ b/product_origin/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_product +from . import product_template diff --git a/product_origin/models/product_product.py b/product_origin/models/product_product.py new file mode 100644 index 0000000..a3271d8 --- /dev/null +++ b/product_origin/models/product_product.py @@ -0,0 +1,60 @@ +# Copyright (C) 2023 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api +from odoo import fields +from odoo import models +from odoo.exceptions import ValidationError + + +class ProductProduct(models.Model): + _inherit = "product.product" + + country_id = fields.Many2one( + comodel_name="res.country", + string="Country of Origin", + ondelete="restrict", + ) + + state_id = fields.Many2one( + comodel_name="res.country.state", + string="Country State of Origin", + ondelete="restrict", + ) + + state_id_domain = fields.Binary( + compute="_compute_state_id_domain", + help="Technical field, used to compute dynamically state domain" + " depending on the country.", + ) + + @api.constrains("country_id", "state_id") + def _check_country_id_state_id(self): + for product in self.filtered(lambda x: x.state_id and x.country_id): + if product.country_id != product.state_id.country_id: + raise ValidationError( + self.env._( + "The state '%(state_name)s' doesn't belong to" + " the country '%(country_name)s'", + state_name=product.state_id.name, + country_name=product.country_id.name, + ) + ) + + @api.onchange("country_id") + def onchange_country_id(self): + if self.state_id and self.state_id.country_id != self.country_id: + self.state_id = False + + @api.onchange("state_id") + def onchange_state_id(self): + if self.state_id: + self.country_id = self.state_id.country_id + + @api.depends("country_id") + def _compute_state_id_domain(self): + for product in self.filtered(lambda x: x.country_id): + product.state_id_domain = [("country_id", "=", product.country_id.id)] + for product in self.filtered(lambda x: not x.country_id): + product.state_id_domain = [] diff --git a/product_origin/models/product_template.py b/product_origin/models/product_template.py new file mode 100644 index 0000000..d524a79 --- /dev/null +++ b/product_origin/models/product_template.py @@ -0,0 +1,95 @@ +# Copyright 2018 ACSONE SA/NV +# Copyright (C) 2023 - Today: GRAP (http://www.grap.coop) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api +from odoo import fields +from odoo import models +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + country_id = fields.Many2one( + comodel_name="res.country", + compute="_compute_country_id", + inverse="_inverse_country_id", + string="Country of Origin", + store=True, + ) + + state_id = fields.Many2one( + comodel_name="res.country.state", + compute="_compute_state_id", + inverse="_inverse_state_id", + string="Country State of Origin", + store=True, + ) + + state_id_domain = fields.Binary( + compute="_compute_state_id_domain", + help="Technical field, used to compute dynamically state domain" + " depending on the country.", + ) + + @api.onchange("country_id") + def onchange_country_id(self): + if self.state_id and self.state_id.country_id != self.country_id: + self.state_id = False + + @api.onchange("state_id") + def onchange_state_id(self): + if self.state_id: + self.country_id = self.state_id.country_id + + @api.constrains("country_id", "state_id") + def _check_country_id_state_id(self): + for template in self.filtered(lambda x: x.state_id and x.country_id): + if template.country_id != template.state_id.country_id: + raise ValidationError( + self.env._( + "The state '%(state_name)s' doesn't belong to" + " the country '%(country_name)s'", + state_name=template.state_id.name, + country_name=template.country_id.name, + ) + ) + + @api.depends("product_variant_ids", "product_variant_ids.country_id") + def _compute_country_id(self): + for template in self: + if template.product_variant_count == 1: + template.country_id = template.product_variant_ids.country_id + else: + template.country_id = False + + def _inverse_country_id(self): + for template in self: + if len(template.product_variant_ids) == 1: + template.product_variant_ids.country_id = template.country_id + + @api.depends("product_variant_ids", "product_variant_ids.state_id") + def _compute_state_id(self): + for template in self: + if template.product_variant_count == 1: + template.state_id = template.product_variant_ids.state_id + else: + template.state_id = False + + @api.depends("country_id") + def _compute_state_id_domain(self): + for template in self.filtered(lambda x: x.country_id): + template.state_id_domain = [("country_id", "=", template.country_id.id)] + for template in self.filtered(lambda x: not x.country_id): + template.state_id_domain = [] + + def _inverse_state_id(self): + for template in self: + if len(template.product_variant_ids) == 1: + template.product_variant_ids.country_id = template.country_id + + def _get_related_fields_variant_template(self): + res = super()._get_related_fields_variant_template() + res += ["country_id", "state_id"] + return res diff --git a/product_origin/pyproject.toml b/product_origin/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/product_origin/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_origin/readme/CONTRIBUTORS.md b/product_origin/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..341e1cb --- /dev/null +++ b/product_origin/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Denis Roussel \<\> +- Sylvain LE GAL () +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/product_origin/readme/DESCRIPTION.md b/product_origin/readme/DESCRIPTION.md new file mode 100644 index 0000000..99e128c --- /dev/null +++ b/product_origin/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This module adds a field to associate the country and state of origin of +a product + + diff --git a/product_origin/readme/HISTORY.md b/product_origin/readme/HISTORY.md new file mode 100644 index 0000000..a72018d --- /dev/null +++ b/product_origin/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 10.0.1.0.0 (2019-01-11) + +- \[10.0\]\[ADD\] product_origin diff --git a/product_origin/readme/USAGE.md b/product_origin/readme/USAGE.md new file mode 100644 index 0000000..2b62946 --- /dev/null +++ b/product_origin/readme/USAGE.md @@ -0,0 +1,5 @@ +- Go to product form +- Fill in the country and/or state of origin of the product under the + 'General Information' tab. + +![](../static/description/product_form.png) diff --git a/product_origin/static/description/icon.png b/product_origin/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/product_origin/static/description/index.html b/product_origin/static/description/index.html new file mode 100644 index 0000000..69feda9 --- /dev/null +++ b/product_origin/static/description/index.html @@ -0,0 +1,456 @@ + + + + + +Product Origin + + + +
+

Product Origin

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

This module adds a field to associate the country and state of origin of +a product

+

https://en.wikipedia.org/wiki/Country_of_origin

+

Table of contents

+ +
+

Usage

+
    +
  • Go to product form
  • +
  • Fill in the country and/or state of origin of the product under the +‘General Information’ tab.
  • +
+

image1

+
+
+

Changelog

+
+

10.0.1.0.0 (2019-01-11)

+
    +
  • [10.0][ADD] product_origin
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
  • GRAP
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

rousseldenis legalsylvain

+

This module is part of the OCA/product-attribute project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_origin/static/description/product_form.png b/product_origin/static/description/product_form.png new file mode 100644 index 0000000000000000000000000000000000000000..deb4ec638e42a0baa920451cf511fbf8e65a9f29 GIT binary patch literal 45968 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 mbNv6`;BMyURa@%Rq&>6%b8>``CMS&;AWZ^{{;a literal 0 HcmV?d00001 diff --git a/product_origin/tests/__init__.py b/product_origin/tests/__init__.py new file mode 100644 index 0000000..d9b96c4 --- /dev/null +++ b/product_origin/tests/__init__.py @@ -0,0 +1 @@ +from . import test_module diff --git a/product_origin/tests/test_module.py b/product_origin/tests/test_module.py new file mode 100644 index 0000000..5c01b32 --- /dev/null +++ b/product_origin/tests/test_module.py @@ -0,0 +1,86 @@ +# Copyright (C) 2024 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from odoo.addons.base.tests.common import BaseCommon + + +class TestModule(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.country_us = cls.env.ref("base.us") + cls.state_tarapaca = cls.env.ref("base.state_cl_01") + cls.state_us = cls.env.ref("base.state_us_1") + cls.country_chile = cls.env.ref("base.cl") + + def test_product_product(self): + self._test_compute_and_constrains(self.env["product.product"]) + self._test_onchange_methods(self.env["product.product"]) + + def test_product_template(self): + self._test_compute_and_constrains(self.env["product.template"]) + self._test_onchange_methods(self.env["product.template"]) + + def _test_compute_and_constrains(self, model): + # Set state should set country + product = model.create({"name": "Test Name"}) + self.assertEqual(product.state_id_domain, []) + + product.country_id = self.country_us + self.assertEqual( + product.state_id_domain, [("country_id", "=", self.country_us.id)] + ) + + with self.assertRaises(ValidationError): + product.state_id = self.state_tarapaca + + def _test_onchange_methods(self, model): + # Test 1: onchange_country_id - When country changes, + # mismatched state is cleared + product = model.create({"name": "Test Onchange"}) + + # Set state first + product.state_id = self.state_us + # Set matching country + product.country_id = self.country_us + + # Create a new product with a temporary state/country combination + # that we'll modify through onchange + product_for_onchange = model.new( + { + "name": "Test Onchange Product", + "state_id": self.state_us.id, + "country_id": self.country_us.id, + } + ) + + # Verify initial state + self.assertEqual(product_for_onchange.state_id.id, self.state_us.id) + self.assertEqual(product_for_onchange.country_id.id, self.country_us.id) + + # Change country and trigger onchange + product_for_onchange.country_id = self.country_chile + product_for_onchange.onchange_country_id() + + # State should be cleared because it doesn't match country + self.assertFalse(product_for_onchange.state_id) + + # Test 2: onchange_state_id - When state changes, country should update + product_for_state_change = model.new( + { + "name": "Test Onchange State", + } + ) + + # Initially no country + self.assertFalse(product_for_state_change.country_id) + + # Set state and trigger onchange + product_for_state_change.state_id = self.state_us + product_for_state_change.onchange_state_id() + + # Country should be set to match state's country + self.assertEqual(product_for_state_change.country_id.id, self.country_us.id) diff --git a/product_origin/views/product_product.xml b/product_origin/views/product_product.xml new file mode 100644 index 0000000..ff148f1 --- /dev/null +++ b/product_origin/views/product_product.xml @@ -0,0 +1,35 @@ + + + + + product.product + + + + + + + + + + + + + + product.product + + + + + + + + + + + + diff --git a/product_origin/views/product_template.xml b/product_origin/views/product_template.xml new file mode 100644 index 0000000..7c55300 --- /dev/null +++ b/product_origin/views/product_template.xml @@ -0,0 +1,39 @@ + + + + + product.template + + + + + + + + + + + + + + product.template + + + + + + + + + + + diff --git a/product_sale_price_from_pricelist/models/product_template.py b/product_sale_price_from_pricelist/models/product_template.py index 8e80b4e..5a13a45 100644 --- a/product_sale_price_from_pricelist/models/product_template.py +++ b/product_sale_price_from_pricelist/models/product_template.py @@ -25,6 +25,7 @@ class ProductTemplate(models.Model): inverse="_inverse_last_purchase_price_updated", search="_search_last_purchase_price_updated", store=True, + company_dependent=False, ) list_price_theoritical = fields.Float( string="Theoritical price", @@ -32,6 +33,7 @@ class ProductTemplate(models.Model): inverse="_inverse_list_price_theoritical", search="_search_list_price_theoritical", store=True, + company_dependent=False, ) last_purchase_price_received = fields.Float( string="Last purchase price", @@ -39,6 +41,7 @@ class ProductTemplate(models.Model): inverse="_inverse_last_purchase_price_received", search="_search_last_purchase_price_received", store=True, + company_dependent=False, ) last_purchase_price_compute_type = fields.Selection( [ @@ -53,14 +56,15 @@ class ProductTemplate(models.Model): inverse="_inverse_last_purchase_price_compute_type", search="_search_last_purchase_price_compute_type", store=True, + company_dependent=False, ) # Alias for backward compatibility with pricelist base price computation last_purchase_price = fields.Float( - string="Last Purchase Price", compute="_compute_last_purchase_price", search="_search_last_purchase_price", store=True, + company_dependent=False, ) @api.depends("product_variant_ids.last_purchase_price_updated") @@ -156,6 +160,10 @@ class ProductTemplate(models.Model): def _search_last_purchase_price(self, operator, value): return [("last_purchase_price_received", operator, value)] + def _compute_theoritical_price(self): + """Delegate to product variants.""" + return self.product_variant_ids._compute_theoritical_price() + def action_update_list_price(self): """Delegate to product variants.""" return self.product_variant_ids.action_update_list_price() diff --git a/product_sale_price_from_pricelist/tests/test_pricelist.py b/product_sale_price_from_pricelist/tests/test_pricelist.py index b37c726..b879ae3 100644 --- a/product_sale_price_from_pricelist/tests/test_pricelist.py +++ b/product_sale_price_from_pricelist/tests/test_pricelist.py @@ -10,169 +10,172 @@ class TestPricelist(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create tax - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 10%", - "amount": 10.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - + cls.tax = cls.env["account.tax"].create( + { + "name": "Test Tax 10%", + "amount": 10.0, + "amount_type": "percent", + "type_tax_use": "sale", + } + ) + # Create product - cls.product = cls.env["product.product"].create({ - "name": "Test Product Pricelist", - "type": "product", - "list_price": 100.0, - "standard_price": 50.0, - "taxes_id": [(6, 0, [cls.tax.id])], - "last_purchase_price_received": 50.0, - "last_purchase_price_compute_type": "without_discounts", - }) - + cls.product = cls.env["product.product"].create( + { + "name": "Test Product Pricelist", + "list_price": 100.0, + "standard_price": 50.0, + "taxes_id": [(6, 0, [cls.tax.id])], + "last_purchase_price_received": 50.0, + "last_purchase_price_compute_type": "without_discounts", + } + ) + # Create pricelist with last_purchase_price base - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Pricelist Last Purchase", - "currency_id": cls.env.company.currency_id.id, - }) + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test Pricelist Last Purchase", + "currency_id": cls.env.company.currency_id.id, + } + ) def test_pricelist_item_base_last_purchase_price(self): """Test pricelist item with last_purchase_price base""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 0, - "price_surcharge": 10.0, - "applied_on": "3_global", - }) - - # Compute price using pricelist - price = self.pricelist._compute_price_rule( - self.product, qty=1, date=False + self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_discount": 0, + "price_surcharge": 10.0, + "applied_on": "3_global", + } ) - + + # Compute price using pricelist + price = self.pricelist._compute_price_rule(self.product, quantity=1, date=False) + # Price should be based on last_purchase_price self.assertIn(self.product.id, price) # The exact price depends on the formula calculation def test_pricelist_item_with_discount(self): """Test pricelist item with discount on last_purchase_price""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 20.0, # 20% discount - "applied_on": "3_global", - }) - - price = self.pricelist._compute_price_rule( - self.product, qty=1, date=False + self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_discount": 20.0, # 20% discount + "applied_on": "3_global", + } ) - + + price = self.pricelist._compute_price_rule(self.product, quantity=1, date=False) + # Expected: 50.0 - (50.0 * 0.20) = 40.0 self.assertIn(self.product.id, price) self.assertEqual(price[self.product.id][0], 40.0) def test_pricelist_item_with_markup(self): """Test pricelist item with markup on last_purchase_price""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_markup": 100.0, # 100% markup (double the price) - "applied_on": "3_global", - }) - + pricelist_item = self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_markup": 100.0, # 100% markup (double the price) + "applied_on": "3_global", + } + ) + # _compute_price should return the base price (last_purchase_price_received) result = pricelist_item._compute_price( - self.product, - qty=1, - uom=self.product.uom_id, - date=False, - currency=None + self.product, qty=1, uom=self.product.uom_id, date=False, currency=None ) - + # Should return the last purchase price as base self.assertEqual(result, 50.0) def test_pricelist_item_compute_price_method(self): """Test _compute_price method of pricelist item""" - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_markup": 50.0, - "applied_on": "3_global", - }) - - result = pricelist_item._compute_price( - self.product, - qty=1, - uom=self.product.uom_id, - date=False, - currency=None + pricelist_item = self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_markup": 50.0, + "applied_on": "3_global", + } ) - + + result = pricelist_item._compute_price( + self.product, qty=1, uom=self.product.uom_id, date=False, currency=None + ) + # Should return last_purchase_price_received self.assertEqual(result, self.product.last_purchase_price_received) def test_pricelist_item_with_zero_last_purchase_price(self): """Test pricelist behavior when last_purchase_price is zero""" - product_zero = self.env["product.product"].create({ - "name": "Product Zero Price", - "type": "product", - "list_price": 100.0, - "last_purchase_price_received": 0.0, - }) - - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 10.0, - "applied_on": "3_global", - }) - - price = self.pricelist._compute_price_rule( - product_zero, qty=1, date=False + product_zero = self.env["product.product"].create( + { + "name": "Product Zero Price", + "list_price": 100.0, + "last_purchase_price_received": 0.0, + } ) - + + self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_discount": 10.0, + "applied_on": "3_global", + } + ) + + price = self.pricelist._compute_price_rule(product_zero, quantity=1, date=False) + # Should handle zero price gracefully self.assertIn(product_zero.id, price) self.assertEqual(price[product_zero.id][0], 0.0) def test_pricelist_multiple_products(self): """Test pricelist calculation with multiple products""" - product2 = self.env["product.product"].create({ - "name": "Test Product 2", - "type": "product", - "list_price": 200.0, - "last_purchase_price_received": 100.0, - }) - - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": self.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 10.0, - "applied_on": "3_global", - }) - + product2 = self.env["product.product"].create( + { + "name": "Test Product 2", + "list_price": 200.0, + "last_purchase_price_received": 100.0, + } + ) + + self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_discount": 10.0, + "applied_on": "3_global", + } + ) + products = self.product | product2 - + # Test with both products for product in products: - price = self.pricelist._compute_price_rule( - product, qty=1, date=False - ) + price = self.pricelist._compute_price_rule(product, quantity=1, date=False) self.assertIn(product.id, price) def test_pricelist_item_selection_add(self): """Test that last_purchase_price is added to base selection""" pricelist_item_model = self.env["product.pricelist.item"] base_field = pricelist_item_model._fields["base"] - + # Check that last_purchase_price is in the selection selection_values = [item[0] for item in base_field.selection] self.assertIn("last_purchase_price", selection_values) @@ -180,7 +183,7 @@ class TestPricelist(TransactionCase): def test_product_price_compute_fallback(self): """Test price_compute method fallback for last_purchase_price""" result = self.product.price_compute("last_purchase_price") - + # Should return dummy value 1.0 for all products self.assertEqual(result[self.product.id], 1.0) @@ -190,23 +193,25 @@ class TestPricelist(TransactionCase): eur = self.env.ref("base.EUR", raise_if_not_found=False) if not eur: self.skipTest("EUR currency not available") - - pricelist_eur = self.env["product.pricelist"].create({ - "name": "Test Pricelist EUR", - "currency_id": eur.id, - }) - - pricelist_item = self.env["product.pricelist.item"].create({ - "pricelist_id": pricelist_eur.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 0, - "applied_on": "3_global", - }) - - # Price calculation should work with different currency - price = pricelist_eur._compute_price_rule( - self.product, qty=1, date=False + + pricelist_eur = self.env["product.pricelist"].create( + { + "name": "Test Pricelist EUR", + "currency_id": eur.id, + } ) - + + self.env["product.pricelist.item"].create( + { + "pricelist_id": pricelist_eur.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_discount": 0, + "applied_on": "3_global", + } + ) + + # Price calculation should work with different currency + price = pricelist_eur._compute_price_rule(self.product, quantity=1, date=False) + self.assertIn(self.product.id, price) diff --git a/product_sale_price_from_pricelist/tests/test_product_template.py b/product_sale_price_from_pricelist/tests/test_product_template.py index bfc2420..ed397c2 100644 --- a/product_sale_price_from_pricelist/tests/test_product_template.py +++ b/product_sale_price_from_pricelist/tests/test_product_template.py @@ -1,9 +1,9 @@ # Copyright (C) 2020: Criptomart (https://criptomart.net) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.exceptions import UserError from odoo.tests import tagged from odoo.tests.common import TransactionCase -from odoo.exceptions import UserError @tagged("post_install", "-at_install") @@ -11,86 +11,114 @@ class TestProductTemplate(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create a tax for the product - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 21%", - "amount": 21.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - + cls.tax = cls.env["account.tax"].create( + { + "name": "Test Tax 21%", + "amount": 21.0, + "amount_type": "percent", + "type_tax_use": "sale", + } + ) + # Create a pricelist with formula based on cost - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Pricelist Automatic", - "currency_id": cls.env.company.currency_id.id, - }) - - cls.pricelist_item = cls.env["product.pricelist.item"].create({ - "pricelist_id": cls.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_discount": 0, - "price_surcharge": 0, - "price_markup": 50.0, # 50% markup - "applied_on": "3_global", - }) - + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test Pricelist Automatic", + "currency_id": cls.env.company.currency_id.id, + } + ) + + cls.pricelist_item = cls.env["product.pricelist.item"].create( + { + "pricelist_id": cls.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_discount": 0, + "price_surcharge": 0, + "price_markup": 50.0, # 50% markup + "applied_on": "3_global", + } + ) + # Set the pricelist in configuration cls.env["ir.config_parameter"].sudo().set_param( "product_sale_price_from_pricelist.product_pricelist_automatic", - str(cls.pricelist.id) + str(cls.pricelist.id), ) - + # Create a product category - cls.category = cls.env["product.category"].create({ - "name": "Test Category", - }) - + cls.category = cls.env["product.category"].create( + { + "name": "Test Category", + } + ) + # Create a product - cls.product = cls.env["product.template"].create({ - "name": "Test Product", - "type": "product", - "categ_id": cls.category.id, - "list_price": 10.0, - "standard_price": 5.0, - "taxes_id": [(6, 0, [cls.tax.id])], - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - }) + cls.product = cls.env["product.template"].create( + { + "name": "Test Product", + "categ_id": cls.category.id, + "list_price": 10.0, + "standard_price": 5.0, + "taxes_id": [(6, 0, [cls.tax.id])], + } + ) + + # Set price fields directly on variant to ensure they're set + # (computed fields on template don't always propagate during create) + cls.product.product_variant_ids[:1].write( + { + "last_purchase_price_received": 5.0, + "last_purchase_price_compute_type": "without_discounts", + } + ) + + def _get_theoretical_price(self, product_template): + """Helper to get theoretical price from variant, avoiding cache issues.""" + return product_template.product_variant_ids[:1].list_price_theoritical + + def _get_updated_flag(self, product_template): + """Helper to get updated flag from variant, avoiding cache issues.""" + return product_template.product_variant_ids[:1].last_purchase_price_updated def test_compute_theoritical_price_without_tax_error(self): """Test that computing theoretical price without taxes raises an error""" - product_no_tax = self.env["product.template"].create({ - "name": "Product No Tax", - "type": "product", - "list_price": 10.0, - "standard_price": 5.0, - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - }) - + product_no_tax = self.env["product.template"].create( + { + "name": "Product No Tax", + "list_price": 10.0, + "standard_price": 5.0, + "last_purchase_price_received": 5.0, + "last_purchase_price_compute_type": "without_discounts", + } + ) + with self.assertRaises(UserError): product_no_tax._compute_theoritical_price() def test_compute_theoritical_price_success(self): """Test successful computation of theoretical price""" self.product._compute_theoritical_price() - + + # Read from variant directly to avoid cache issues + theoretical_price = self.product.product_variant_ids[:1].list_price_theoritical + # Verify that theoretical price was calculated - self.assertGreater(self.product.list_price_theoritical, 0) - + self.assertGreater(theoretical_price, 0) + # Verify that the price includes markup and tax # With 50% markup on 5.0 = 7.5, plus 21% tax = 9.075, rounded to 9.10 - self.assertAlmostEqual(self.product.list_price_theoritical, 9.10, places=2) + self.assertAlmostEqual(theoretical_price, 9.10, places=2) def test_compute_theoritical_price_with_rounding(self): """Test that prices are rounded to 0.05""" self.product.last_purchase_price_received = 4.0 self.product._compute_theoritical_price() - + # Price should be rounded to nearest 0.05 - price = self.product.list_price_theoritical + price = self._get_theoretical_price(self.product) self.assertEqual(round(price % 0.05, 2), 0.0) def test_last_purchase_price_updated_flag(self): @@ -98,36 +126,36 @@ class TestProductTemplate(TransactionCase): initial_list_price = self.product.list_price self.product.last_purchase_price_received = 10.0 self.product._compute_theoritical_price() - - if self.product.list_price_theoritical != initial_list_price: - self.assertTrue(self.product.last_purchase_price_updated) + + if self._get_theoretical_price(self.product) != initial_list_price: + self.assertTrue(self._get_updated_flag(self.product)) def test_action_update_list_price(self): """Test updating list price from theoretical price""" self.product.last_purchase_price_received = 8.0 self.product._compute_theoritical_price() - - theoretical_price = self.product.list_price_theoritical + + theoretical_price = self._get_theoretical_price(self.product) self.product.action_update_list_price() - + # Verify that list price was updated self.assertEqual(self.product.list_price, theoretical_price) - self.assertFalse(self.product.last_purchase_price_updated) + self.assertFalse(self._get_updated_flag(self.product)) def test_manual_update_type_skips_automatic(self): """Test that manual update type prevents automatic price calculation""" self.product.last_purchase_price_compute_type = "manual_update" initial_list_price = self.product.list_price - + self.product.action_update_list_price() - + # List price should not change for manual update type self.assertEqual(self.product.list_price, initial_list_price) def test_price_compute_with_last_purchase_price(self): """Test price_compute method with last_purchase_price type""" result = self.product.price_compute("last_purchase_price") - + # Should return dummy prices (1.0) for all product ids for product_id in result: self.assertEqual(result[product_id], 1.0) @@ -136,17 +164,16 @@ class TestProductTemplate(TransactionCase): """Test that missing pricelist raises an error""" # Remove pricelist configuration self.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - "" + "product_sale_price_from_pricelist.product_pricelist_automatic", "" ) - + with self.assertRaises(UserError): self.product._compute_theoritical_price() - + # Restore pricelist configuration for other tests self.env["ir.config_parameter"].sudo().set_param( "product_sale_price_from_pricelist.product_pricelist_automatic", - str(self.pricelist.id) + str(self.pricelist.id), ) def test_company_dependent_fields(self): @@ -156,11 +183,12 @@ class TestProductTemplate(TransactionCase): field_theoritical = self.product._fields["list_price_theoritical"] field_updated = self.product._fields["last_purchase_price_updated"] field_compute_type = self.product._fields["last_purchase_price_compute_type"] - + self.assertTrue(field_last_purchase.company_dependent) self.assertTrue(field_theoritical.company_dependent) self.assertTrue(field_updated.company_dependent) self.assertTrue(field_compute_type.company_dependent) + def test_compute_theoritical_price_with_actual_purchase_price(self): """Test that theoretical price is calculated correctly from last purchase price This test simulates a real scenario where a product has a purchase price set""" @@ -168,48 +196,50 @@ class TestProductTemplate(TransactionCase): purchase_price = 10.50 self.product.last_purchase_price_received = purchase_price self.product.last_purchase_price_compute_type = "without_discounts" - + # Compute theoretical price self.product._compute_theoritical_price() - + + theoretical_price = self._get_theoretical_price(self.product) + # Verify price is not zero self.assertNotEqual( - self.product.list_price_theoritical, + theoretical_price, 0.0, - "Theoretical price should not be 0.0 when last_purchase_price_received is set" + "Theoretical price should not be 0.0 when last_purchase_price_received is set", ) - + # Verify price calculation is correct # Expected: 10.50 * 1.50 (50% markup) = 15.75 # Plus 21% tax: 15.75 * 1.21 = 19.0575 # Rounded to 0.05: 19.05 or 19.10 expected_base = purchase_price * 1.50 # 15.75 expected_with_tax = expected_base * 1.21 # 19.0575 - + self.assertGreater( - self.product.list_price_theoritical, + theoretical_price, expected_base, - "Theoretical price should include taxes" + "Theoretical price should include taxes", ) - + # Allow some tolerance for rounding self.assertAlmostEqual( - self.product.list_price_theoritical, + theoretical_price, expected_with_tax, delta=0.10, - msg=f"Expected around {expected_with_tax:.2f}, got {self.product.list_price_theoritical:.2f}" + msg=f"Expected around {expected_with_tax:.2f}, got {theoretical_price:.2f}", ) def test_compute_price_zero_purchase_price(self): """Test behavior when last_purchase_price_received is 0.0""" self.product.last_purchase_price_received = 0.0 self.product._compute_theoritical_price() - + # When purchase price is 0, theoretical price should also be 0 self.assertEqual( - self.product.list_price_theoritical, + self._get_theoretical_price(self.product), 0.0, - "Theoretical price should be 0.0 when last_purchase_price_received is 0.0" + "Theoretical price should be 0.0 when last_purchase_price_received is 0.0", ) def test_pricelist_item_base_field(self): @@ -217,9 +247,9 @@ class TestProductTemplate(TransactionCase): self.assertEqual( self.pricelist_item.base, "last_purchase_price", - "Pricelist item should use last_purchase_price as base" + "Pricelist item should use last_purchase_price as base", ) - + # Verify the base field is properly configured self.assertEqual(self.pricelist_item.compute_price, "formula") - self.assertEqual(self.pricelist_item.price_markup, 50.0) \ No newline at end of file + self.assertEqual(self.pricelist_item.price_markup, 50.0) diff --git a/product_sale_price_from_pricelist/tests/test_stock_move.py b/product_sale_price_from_pricelist/tests/test_stock_move.py index d423d85..bf478a5 100644 --- a/product_sale_price_from_pricelist/tests/test_stock_move.py +++ b/product_sale_price_from_pricelist/tests/test_stock_move.py @@ -10,74 +10,90 @@ class TestStockMove(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create tax - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 21%", - "amount": 21.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - + cls.tax = cls.env["account.tax"].create( + { + "name": "Test Tax 21%", + "amount": 21.0, + "amount_type": "percent", + "type_tax_use": "sale", + } + ) + # Create pricelist - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Pricelist", - "currency_id": cls.env.company.currency_id.id, - }) - - cls.pricelist_item = cls.env["product.pricelist.item"].create({ - "pricelist_id": cls.pricelist.id, - "compute_price": "formula", - "base": "last_purchase_price", - "price_markup": 50.0, - "applied_on": "3_global", - }) - + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test Pricelist", + "currency_id": cls.env.company.currency_id.id, + } + ) + + cls.pricelist_item = cls.env["product.pricelist.item"].create( + { + "pricelist_id": cls.pricelist.id, + "compute_price": "formula", + "base": "last_purchase_price", + "price_markup": 50.0, + "applied_on": "3_global", + } + ) + cls.env["ir.config_parameter"].sudo().set_param( "product_sale_price_from_pricelist.product_pricelist_automatic", - str(cls.pricelist.id) + str(cls.pricelist.id), ) - + # Create supplier - cls.supplier = cls.env["res.partner"].create({ - "name": "Test Supplier", - "supplier_rank": 1, - }) - + cls.supplier = cls.env["res.partner"].create( + { + "name": "Test Supplier", + "supplier_rank": 1, + } + ) + # Create product with UoM cls.uom_unit = cls.env.ref("uom.product_uom_unit") cls.uom_dozen = cls.env.ref("uom.product_uom_dozen") - - cls.product = cls.env["product.product"].create({ - "name": "Test Product Stock", - "type": "product", - "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, - "list_price": 10.0, - "standard_price": 5.0, - "taxes_id": [(6, 0, [cls.tax.id])], - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - }) - + + cls.product = cls.env["product.product"].create( + { + "name": "Test Product Stock", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + "list_price": 10.0, + "standard_price": 5.0, + "taxes_id": [(6, 0, [cls.tax.id])], + "last_purchase_price_received": 5.0, + "last_purchase_price_compute_type": "without_discounts", + } + ) + # Create locations cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") cls.stock_location = cls.env.ref("stock.stock_location_stock") - def _create_purchase_order(self, product, qty, price, discount1=0, discount2=0, discount3=0): + def _create_purchase_order( + self, product, qty, price, discount1=0, discount2=0, discount3=0 + ): """Helper to create a purchase order""" - purchase_order = self.env["purchase.order"].create({ - "partner_id": self.supplier.id, - }) - - po_line = self.env["purchase.order.line"].create({ - "order_id": purchase_order.id, - "product_id": product.id, - "product_qty": qty, - "price_unit": price, - "product_uom": product.uom_po_id.id, - }) - + purchase_order = self.env["purchase.order"].create( + { + "partner_id": self.supplier.id, + } + ) + + po_line = self.env["purchase.order.line"].create( + { + "order_id": purchase_order.id, + "name": product.name or "Purchase Line", + "product_id": product.id, + "product_qty": qty, + "price_unit": price, + "product_uom": product.uom_po_id.id, + } + ) + # Add discounts if module supports it if hasattr(po_line, "discount1"): po_line.discount1 = discount1 @@ -85,25 +101,23 @@ class TestStockMove(TransactionCase): po_line.discount2 = discount2 if hasattr(po_line, "discount3"): po_line.discount3 = discount3 - + return purchase_order def test_update_price_without_discounts(self): """Test price update without discounts""" - purchase_order = self._create_purchase_order( - self.product, qty=10, price=8.0 - ) + purchase_order = self._create_purchase_order(self.product, qty=10, price=8.0) purchase_order.button_confirm() - + # Get the picking and process it picking = purchase_order.picking_ids[0] picking.action_assign() - + for move in picking.move_ids: move.quantity = move.product_uom_qty - + picking.button_validate() - + # Verify price was updated self.assertEqual(self.product.last_purchase_price_received, 8.0) @@ -111,22 +125,22 @@ class TestStockMove(TransactionCase): """Test price update with first discount only""" if not hasattr(self.env["purchase.order.line"], "discount1"): self.skipTest("Purchase discount module not installed") - + self.product.last_purchase_price_compute_type = "with_discount" - + purchase_order = self._create_purchase_order( self.product, qty=10, price=10.0, discount1=20.0 ) purchase_order.button_confirm() - + picking = purchase_order.picking_ids[0] picking.action_assign() - + for move in picking.move_ids: move.quantity = move.product_uom_qty - + picking.button_validate() - + # Expected: 10.0 * (1 - 0.20) = 8.0 self.assertEqual(self.product.last_purchase_price_received, 8.0) @@ -134,22 +148,22 @@ class TestStockMove(TransactionCase): """Test price update with two discounts""" if not hasattr(self.env["purchase.order.line"], "discount2"): self.skipTest("Purchase double discount module not installed") - + self.product.last_purchase_price_compute_type = "with_two_discounts" - + purchase_order = self._create_purchase_order( self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0 ) purchase_order.button_confirm() - + picking = purchase_order.picking_ids[0] picking.action_assign() - + for move in picking.move_ids: move.quantity = move.product_uom_qty - + picking.button_validate() - + # Expected: 10.0 * (1 - 0.20) * (1 - 0.10) = 7.2 self.assertEqual(self.product.last_purchase_price_received, 7.2) @@ -157,46 +171,53 @@ class TestStockMove(TransactionCase): """Test price update with three discounts""" if not hasattr(self.env["purchase.order.line"], "discount3"): self.skipTest("Purchase triple discount module not installed") - + self.product.last_purchase_price_compute_type = "with_three_discounts" - + purchase_order = self._create_purchase_order( - self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0, discount3=5.0 + self.product, + qty=10, + price=10.0, + discount1=20.0, + discount2=10.0, + discount3=5.0, ) purchase_order.button_confirm() - + picking = purchase_order.picking_ids[0] picking.action_assign() - + for move in picking.move_ids: move.quantity = move.product_uom_qty - + picking.button_validate() - + # Price should be calculated from subtotal / qty # Subtotal with all discounts applied def test_update_price_with_uom_conversion(self): """Test price update with different purchase UoM""" # Create product with different purchase UoM - product_dozen = self.product.copy({ - "name": "Test Product Dozen", - "uom_po_id": self.uom_dozen.id, - }) - + product_dozen = self.product.copy( + { + "name": "Test Product Dozen", + "uom_po_id": self.uom_dozen.id, + } + ) + purchase_order = self._create_purchase_order( product_dozen, qty=2, price=120.0 # 2 dozens at 120.0 per dozen ) purchase_order.button_confirm() - + picking = purchase_order.picking_ids[0] picking.action_assign() - + for move in picking.move_ids: move.quantity = move.product_uom_qty - + picking.button_validate() - + # Price should be converted to base UoM (unit) # 120.0 per dozen = 10.0 per unit self.assertEqual(product_dozen.last_purchase_price_received, 10.0) @@ -204,19 +225,17 @@ class TestStockMove(TransactionCase): def test_no_update_with_zero_quantity(self): """Test that price is not updated with zero quantity done""" initial_price = self.product.last_purchase_price_received - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=15.0 - ) + + purchase_order = self._create_purchase_order(self.product, qty=10, price=15.0) purchase_order.button_confirm() - + picking = purchase_order.picking_ids[0] picking.action_assign() - + # Set quantity done to 0 for move in picking.move_ids: move.quantity = 0 - + # This should not update the price # Price should remain unchanged self.assertEqual(self.product.last_purchase_price_received, initial_price) @@ -224,21 +243,18 @@ class TestStockMove(TransactionCase): def test_manual_update_type_no_automatic_update(self): """Test that manual update type prevents automatic price updates""" self.product.last_purchase_price_compute_type = "manual_update" - initial_price = self.product.last_purchase_price_received - - purchase_order = self._create_purchase_order( - self.product, qty=10, price=15.0 - ) + + purchase_order = self._create_purchase_order(self.product, qty=10, price=15.0) purchase_order.button_confirm() - + picking = purchase_order.picking_ids[0] picking.action_assign() - + for move in picking.move_ids: move.quantity = move.product_uom_qty - + picking.button_validate() - + # For manual update, the standard Odoo behavior applies # which may or may not update standard_price, but our custom # last_purchase_price_received logic still runs