diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index bb84fc5..0000000 --- a/.prettierignore +++ /dev/null @@ -1,15 +0,0 @@ -# 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 deleted file mode 100644 index 21a0072..0000000 --- a/.prettierrc.yml +++ /dev/null @@ -1,30 +0,0 @@ -# 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 deleted file mode 160000 index 6fb141f..0000000 --- a/ocb +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6fb141fc7547f9de55c9ac702515f1e3a27406d0 diff --git a/product_origin/README.rst b/product_origin/README.rst deleted file mode 100644 index 9ff9d21..0000000 --- a/product_origin/README.rst +++ /dev/null @@ -1,114 +0,0 @@ -============== -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 deleted file mode 100644 index 0650744..0000000 --- a/product_origin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/product_origin/__manifest__.py b/product_origin/__manifest__.py deleted file mode 100644 index 9fd0db1..0000000 --- a/product_origin/__manifest__.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index ba493ef..0000000 --- a/product_origin/i18n/fr.po +++ /dev/null @@ -1,72 +0,0 @@ -# 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 deleted file mode 100644 index 1b8e89d..0000000 --- a/product_origin/i18n/it.po +++ /dev/null @@ -1,73 +0,0 @@ -# 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 deleted file mode 100644 index 762c9d6..0000000 --- a/product_origin/i18n/product_origin.pot +++ /dev/null @@ -1,66 +0,0 @@ -# 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 deleted file mode 100644 index 18b37e8..0000000 --- a/product_origin/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import product_product -from . import product_template diff --git a/product_origin/models/product_product.py b/product_origin/models/product_product.py deleted file mode 100644 index a3271d8..0000000 --- a/product_origin/models/product_product.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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 deleted file mode 100644 index d524a79..0000000 --- a/product_origin/models/product_template.py +++ /dev/null @@ -1,95 +0,0 @@ -# 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 deleted file mode 100644 index 4231d0c..0000000 --- a/product_origin/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["whool"] -build-backend = "whool.buildapi" diff --git a/product_origin/readme/CONTRIBUTORS.md b/product_origin/readme/CONTRIBUTORS.md deleted file mode 100644 index 341e1cb..0000000 --- a/product_origin/readme/CONTRIBUTORS.md +++ /dev/null @@ -1,4 +0,0 @@ -- 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 deleted file mode 100644 index 99e128c..0000000 --- a/product_origin/readme/DESCRIPTION.md +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index a72018d..0000000 --- a/product_origin/readme/HISTORY.md +++ /dev/null @@ -1,3 +0,0 @@ -## 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 deleted file mode 100644 index 2b62946..0000000 --- a/product_origin/readme/USAGE.md +++ /dev/null @@ -1,5 +0,0 @@ -- 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 deleted file mode 100644 index 3a0328b..0000000 Binary files a/product_origin/static/description/icon.png and /dev/null differ diff --git a/product_origin/static/description/index.html b/product_origin/static/description/index.html deleted file mode 100644 index 69feda9..0000000 --- a/product_origin/static/description/index.html +++ /dev/null @@ -1,456 +0,0 @@ - - - - - -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 deleted file mode 100644 index deb4ec6..0000000 Binary files a/product_origin/static/description/product_form.png and /dev/null differ diff --git a/product_origin/tests/__init__.py b/product_origin/tests/__init__.py deleted file mode 100644 index d9b96c4..0000000 --- a/product_origin/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_module diff --git a/product_origin/tests/test_module.py b/product_origin/tests/test_module.py deleted file mode 100644 index 5c01b32..0000000 --- a/product_origin/tests/test_module.py +++ /dev/null @@ -1,86 +0,0 @@ -# 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 deleted file mode 100644 index ff148f1..0000000 --- a/product_origin/views/product_product.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - product.product - - - - - - - - - - - - - - product.product - - - - - - - - - - - - diff --git a/product_origin/views/product_template.xml b/product_origin/views/product_template.xml deleted file mode 100644 index 7c55300..0000000 --- a/product_origin/views/product_template.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - product.template - - - - - - - - - - - - - - product.template - - - - - - - - - - - diff --git a/product_sale_price_from_pricelist/__manifest__.py b/product_sale_price_from_pricelist/__manifest__.py index 760feee..3303455 100644 --- a/product_sale_price_from_pricelist/__manifest__.py +++ b/product_sale_price_from_pricelist/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { # noqa: B018 "name": "Product Sale Price from Pricelist", - "version": "18.0.2.1.0", + "version": "18.0.2.0.0", "category": "product", "summary": "Set sale price from pricelist based on last purchase price", "author": "Odoo Community Association (OCA), Criptomart", diff --git a/product_sale_price_from_pricelist/migrations/18.0.2.1.0/pre-migrate.py b/product_sale_price_from_pricelist/migrations/18.0.2.1.0/pre-migrate.py deleted file mode 100644 index 447a730..0000000 --- a/product_sale_price_from_pricelist/migrations/18.0.2.1.0/pre-migrate.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import logging - -_logger = logging.getLogger(__name__) - - -def migrate(cr, version): - """Migrate price fields from product.template to product.product. - - In version 18.0.2.1.0, these fields were moved from product.template - to product.product for proper variant handling: - - last_purchase_price_received - - list_price_theoritical - - last_purchase_price_updated - - last_purchase_price_compute_type - """ - if not version: - return - - _logger.info("Migrating price fields from product.template to product.product...") - - # Migrate last_purchase_price_received - cr.execute(""" - UPDATE product_product pp - SET last_purchase_price_received = pt.last_purchase_price_received - FROM product_template pt - WHERE pp.product_tmpl_id = pt.id - AND pt.last_purchase_price_received IS NOT NULL - AND (pp.last_purchase_price_received IS NULL - OR pp.last_purchase_price_received = '{}') - """) - _logger.info("Migrated last_purchase_price_received: %d rows", cr.rowcount) - - # Migrate list_price_theoritical - cr.execute(""" - UPDATE product_product pp - SET list_price_theoritical = pt.list_price_theoritical - FROM product_template pt - WHERE pp.product_tmpl_id = pt.id - AND pt.list_price_theoritical IS NOT NULL - AND (pp.list_price_theoritical IS NULL - OR pp.list_price_theoritical = '{}') - """) - _logger.info("Migrated list_price_theoritical: %d rows", cr.rowcount) - - # Migrate last_purchase_price_updated - cr.execute(""" - UPDATE product_product pp - SET last_purchase_price_updated = pt.last_purchase_price_updated - FROM product_template pt - WHERE pp.product_tmpl_id = pt.id - AND pt.last_purchase_price_updated IS NOT NULL - AND (pp.last_purchase_price_updated IS NULL - OR pp.last_purchase_price_updated = '{}') - """) - _logger.info("Migrated last_purchase_price_updated: %d rows", cr.rowcount) - - # Migrate last_purchase_price_compute_type - cr.execute(""" - UPDATE product_product pp - SET last_purchase_price_compute_type = pt.last_purchase_price_compute_type - FROM product_template pt - WHERE pp.product_tmpl_id = pt.id - AND pt.last_purchase_price_compute_type IS NOT NULL - AND (pp.last_purchase_price_compute_type IS NULL - OR pp.last_purchase_price_compute_type = '{}') - """) - _logger.info("Migrated last_purchase_price_compute_type: %d rows", cr.rowcount) - - _logger.info("Migration of price fields completed.") diff --git a/product_sale_price_from_pricelist/models/product_pricelist.py b/product_sale_price_from_pricelist/models/product_pricelist.py index 14c2ba2..6668aad 100644 --- a/product_sale_price_from_pricelist/models/product_pricelist.py +++ b/product_sale_price_from_pricelist/models/product_pricelist.py @@ -4,7 +4,7 @@ import logging -from odoo import models +from odoo import api, models _logger = logging.getLogger(__name__) @@ -15,23 +15,26 @@ class ProductPricelist(models.Model): def _compute_price_rule(self, products, quantity, uom=None, date=False, **kwargs): ProductPricelistItem = self.env["product.pricelist.item"] ProductProduct = self.env["product.product"] - + _logger.info( - "[PRICELIST DEBUG] _compute_price_rule called with products=%s (model=%s), quantity=%s", + "[PRICELIST DEBUG] _compute_price_rule called with products=%s, quantity=%s", products.ids, - products._name, quantity, ) - + res = super()._compute_price_rule( - products, quantity, uom=uom, date=date, **kwargs + products, + quantity, + uom=uom, + date=date, + **kwargs ) - + _logger.info( "[PRICELIST DEBUG] super()._compute_price_rule returned: %s", res, ) - + new_res = res.copy() item_id = [] for product_id, values in res.items(): @@ -46,22 +49,7 @@ class ProductPricelist(models.Model): item_id, ) if item.base == "last_purchase_price": - # product_id could be from product.template or product.product - # Check which model we're working with - if products._name == "product.template": - # Get the variant from the template - template = products.browse(product_id) - if template.exists(): - product = template.product_variant_id - else: - _logger.warning( - "[PRICELIST] Template ID %s not found in products", - product_id, - ) - continue - else: - product = ProductProduct.browse(product_id) - + product = ProductProduct.browse(product_id) price = product.last_purchase_price_received _logger.info( "[PRICELIST DEBUG] Product %s: last_purchase_price_received=%s, item.price_discount=%s", diff --git a/product_sale_price_from_pricelist/models/product_template.py b/product_sale_price_from_pricelist/models/product_template.py index 5a13a45..09ef270 100644 --- a/product_sale_price_from_pricelist/models/product_template.py +++ b/product_sale_price_from_pricelist/models/product_template.py @@ -25,7 +25,6 @@ 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", @@ -33,7 +32,6 @@ 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", @@ -41,7 +39,6 @@ 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( [ @@ -56,15 +53,6 @@ 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( - compute="_compute_last_purchase_price", - search="_search_last_purchase_price", - store=True, - company_dependent=False, ) @api.depends("product_variant_ids.last_purchase_price_updated") @@ -151,19 +139,6 @@ class ProductTemplate(models.Model): ("product_variant_ids.last_purchase_price_compute_type", operator, value) ] - @api.depends("last_purchase_price_received") - def _compute_last_purchase_price(self): - """Alias for backward compatibility with pricelist computations.""" - for template in self: - template.last_purchase_price = template.last_purchase_price_received - - 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/models/stock_move.py b/product_sale_price_from_pricelist/models/stock_move.py index ca933c9..26521be 100644 --- a/product_sale_price_from_pricelist/models/stock_move.py +++ b/product_sale_price_from_pricelist/models/stock_move.py @@ -56,23 +56,6 @@ class StockMove(models.Model): price_updated, move.product_id.uom_id ) - _logger.info( - "[PRICE DEBUG] Product %s [%s]: price_updated=%.2f, current_price=%.2f, quantity=%.2f, will_update=%s", - move.product_id.default_code or move.product_id.name, - move.product_id.id, - price_updated, - move.product_id.last_purchase_price_received, - move.quantity, - bool( - float_compare( - move.product_id.last_purchase_price_received, - price_updated, - precision_digits=2, - ) - and not float_is_zero(move.quantity, precision_digits=3) - ), - ) - if float_compare( move.product_id.last_purchase_price_received, price_updated, @@ -92,20 +75,10 @@ class StockMove(models.Model): ), move.product_id.last_purchase_price_compute_type, ) - # Use write() to ensure value is persisted before computing price - product_company = move.product_id.with_company(move.company_id) - product_company.write( - { - "last_purchase_price_received": price_updated, - } - ) - # Verify write was successful - product_company.invalidate_recordset() - _logger.info( - "[PRICE DEBUG] Product %s [%s]: After write, last_purchase_price_received=%.2f", - product_company.default_code or product_company.name, - product_company.id, - product_company.last_purchase_price_received, - ) - product_company._compute_theoritical_price() + move.product_id.with_company( + move.company_id + ).last_purchase_price_received = price_updated + move.product_id.with_company( + move.company_id + )._compute_theoritical_price() return res diff --git a/product_sale_price_from_pricelist/tests/__init__.py b/product_sale_price_from_pricelist/tests/__init__.py index 89313bf..b14ee57 100644 --- a/product_sale_price_from_pricelist/tests/__init__.py +++ b/product_sale_price_from_pricelist/tests/__init__.py @@ -1,7 +1,4 @@ -# flake8: noqa: F401 -# Imports are used by Odoo test framework to register tests from . import test_product_template from . import test_stock_move from . import test_pricelist from . import test_res_config -from . import test_theoretical_price diff --git a/product_sale_price_from_pricelist/tests/test_pricelist.py b/product_sale_price_from_pricelist/tests/test_pricelist.py index b879ae3..b37c726 100644 --- a/product_sale_price_from_pricelist/tests/test_pricelist.py +++ b/product_sale_price_from_pricelist/tests/test_pricelist.py @@ -10,172 +10,169 @@ 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", - "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", + "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", + }) + # 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""" - 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", - } - ) - + 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, quantity=1, date=False) - + price = self.pricelist._compute_price_rule( + self.product, qty=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""" - 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", - } + 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 ) - - 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", - } - ) - + 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 + 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", - "list_price": 100.0, - "last_purchase_price_received": 0.0, - } + 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 ) - - 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", - "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", - } - ) - + 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", + }) + products = self.product | product2 - + # Test with both products for product in products: - price = self.pricelist._compute_price_rule(product, quantity=1, date=False) + price = self.pricelist._compute_price_rule( + product, qty=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) @@ -183,7 +180,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) @@ -193,25 +190,23 @@ 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, - } - ) - - 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", - } - ) - + + 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, quantity=1, date=False) - + price = pricelist_eur._compute_price_rule( + self.product, qty=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 6f3419c..bfc2420 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,176 +11,123 @@ 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", - "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 + 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", + }) 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", - "list_price": 10.0, - "standard_price": 5.0, - "taxes_id": [(5, 0, 0)], # Clear all taxes - } - ) - # Write to variant directly - product_no_tax.product_variant_ids[:1].write( - { - "last_purchase_price_received": 5.0, - "last_purchase_price_compute_type": "without_discounts", - "taxes_id": [(5, 0, 0)], # Ensure variant also has no taxes - } - ) - + 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", + }) + 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(theoretical_price, 0) - - # Price is calculated WITHOUT tax (taxes added automatically on sales) - # With 50% markup on 5.0 = 7.5 - self.assertAlmostEqual(theoretical_price, 7.50, places=2) + self.assertGreater(self.product.list_price_theoritical, 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) def test_compute_theoritical_price_with_rounding(self): - """Test that prices are properly calculated""" - self.product.product_variant_ids[:1].write( - { - "last_purchase_price_received": 4.0, - } - ) + """Test that prices are rounded to 0.05""" + self.product.last_purchase_price_received = 4.0 self.product._compute_theoritical_price() - - # Price should be calculated (4.0 * 1.5 = 6.0) - price = self._get_theoretical_price(self.product) - self.assertAlmostEqual(price, 6.0, places=2) + + # Price should be rounded to nearest 0.05 + price = self.product.list_price_theoritical + self.assertEqual(round(price % 0.05, 2), 0.0) def test_last_purchase_price_updated_flag(self): """Test that the updated flag is set when prices differ""" initial_list_price = self.product.list_price - self.product.product_variant_ids[:1].write( - { - "last_purchase_price_received": 10.0, - } - ) + self.product.last_purchase_price_received = 10.0 self.product._compute_theoritical_price() - - if self._get_theoretical_price(self.product) != initial_list_price: - self.assertTrue(self._get_updated_flag(self.product)) + + if self.product.list_price_theoritical != initial_list_price: + self.assertTrue(self.product.last_purchase_price_updated) def test_action_update_list_price(self): """Test updating list price from theoretical price""" - self.product.product_variant_ids[:1].write( - { - "last_purchase_price_received": 8.0, - } - ) + self.product.last_purchase_price_received = 8.0 self.product._compute_theoritical_price() - - theoretical_price = self._get_theoretical_price(self.product) + + theoretical_price = self.product.list_price_theoritical self.product.action_update_list_price() - + # Verify that list price was updated self.assertEqual(self.product.list_price, theoretical_price) - self.assertFalse(self._get_updated_flag(self.product)) + self.assertFalse(self.product.last_purchase_price_updated) def test_manual_update_type_skips_automatic(self): """Test that manual update type prevents automatic price calculation""" - self.product.product_variant_ids[:1].write( - { - "last_purchase_price_compute_type": "manual_update", - } - ) + 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""" - # price_compute is defined on product.product, not product.template - variant = self.product.product_variant_ids[:1] - result = variant.price_compute("last_purchase_price") - + 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) @@ -189,16 +136,17 @@ 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): @@ -208,63 +156,60 @@ 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""" # Set a realistic purchase price purchase_price = 10.50 - self.product.product_variant_ids[:1].write( - { - "last_purchase_price_received": purchase_price, - "last_purchase_price_compute_type": "without_discounts", - } - ) - + 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( - theoretical_price, + self.product.list_price_theoritical, 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 - # Price is WITHOUT tax (taxes added automatically on sales) - expected_price = purchase_price * 1.50 # 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, + expected_base, + "Theoretical price should include taxes" + ) + # Allow some tolerance for rounding self.assertAlmostEqual( - theoretical_price, - expected_price, + self.product.list_price_theoritical, + expected_with_tax, delta=0.10, - msg=f"Expected around {expected_price:.2f}, got {theoretical_price:.2f}", + msg=f"Expected around {expected_with_tax:.2f}, got {self.product.list_price_theoritical:.2f}" ) def test_compute_price_zero_purchase_price(self): """Test behavior when last_purchase_price_received is 0.0""" - self.product.product_variant_ids[:1].write( - { - "last_purchase_price_received": 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._get_theoretical_price(self.product), + self.product.list_price_theoritical, 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): @@ -272,9 +217,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) + self.assertEqual(self.pricelist_item.price_markup, 50.0) \ No newline at end of file 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 16b87e8..d423d85 100644 --- a/product_sale_price_from_pricelist/tests/test_stock_move.py +++ b/product_sale_price_from_pricelist/tests/test_stock_move.py @@ -10,90 +10,74 @@ 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", - "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", + "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", + }) + # 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, - "name": product.name or "Purchase Line", - "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, + "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 @@ -101,87 +85,48 @@ 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.product.invalidate_recordset() self.assertEqual(self.product.last_purchase_price_received, 8.0) - def test_full_flow_updates_theoretical_price(self): - """Test that validating a purchase receipt updates theoretical price.""" - # Initial state - self.product.write( - { - "last_purchase_price_received": 0.0, - "list_price_theoritical": 0.0, - } - ) - self.product.invalidate_recordset() - - # Create and confirm purchase order at 100.0 - purchase_order = self._create_purchase_order(self.product, qty=5, price=100.0) - purchase_order.button_confirm() - - # Validate the picking - picking = purchase_order.picking_ids[0] - picking.action_assign() - for move in picking.move_ids: - move.quantity = move.product_uom_qty - picking.button_validate() - - # Re-read from DB - self.product.invalidate_recordset() - - # Verify last_purchase_price_received was updated - self.assertEqual( - self.product.last_purchase_price_received, - 100.0, - "last_purchase_price_received should be 100.0 after receipt", - ) - - # Verify theoretical price was calculated (100.0 * 1.5 = 150.0 with 50% markup) - self.assertAlmostEqual( - self.product.list_price_theoritical, - 150.0, - places=2, - msg=f"Theoretical price should be 150.0, got {self.product.list_price_theoritical}", - ) - def test_update_price_with_first_discount(self): """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) @@ -189,22 +134,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) @@ -212,58 +157,46 @@ 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, - "taxes_id": [(6, 0, [self.tax.id])], - "last_purchase_price_compute_type": "without_discounts", - } - ) - + 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() - - if not purchase_order.picking_ids: - self.skipTest("Purchase order did not generate picking") - + 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) @@ -271,17 +204,19 @@ 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) @@ -289,18 +224,21 @@ 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" - - purchase_order = self._create_purchase_order(self.product, qty=10, price=15.0) + initial_price = self.product.last_purchase_price_received + + 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 diff --git a/product_sale_price_from_pricelist/tests/test_theoretical_price.py b/product_sale_price_from_pricelist/tests/test_theoretical_price.py deleted file mode 100644 index c5baff7..0000000 --- a/product_sale_price_from_pricelist/tests/test_theoretical_price.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 2026: Criptomart (https://criptomart.net) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo.tests import tagged -from odoo.tests.common import TransactionCase - - -@tagged("post_install", "-at_install") -class TestTheoreticalPriceCalculation(TransactionCase): - """Test the theoretical price calculation to diagnose issues.""" - - @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", - } - ) - - # Create pricelist with last_purchase_price base and 10% markup - cls.pricelist = cls.env["product.pricelist"].create( - { - "name": "Test Auto 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_discount": -10.0, # 10% markup (negative discount) - "applied_on": "3_global", - } - ) - - # Configure the pricelist in settings - cls.env["ir.config_parameter"].sudo().set_param( - "product_sale_price_from_pricelist.product_pricelist_automatic", - str(cls.pricelist.id), - ) - - # Create product.product directly - cls.product = cls.env["product.product"].create( - { - "name": "Test Product Direct", - "list_price": 10.0, - "standard_price": 5.0, - "taxes_id": [(6, 0, [cls.tax.id])], - } - ) - - def test_write_last_purchase_price_on_product(self): - """Test that writing last_purchase_price_received on product.product works.""" - # Write directly to product.product - self.product.write({"last_purchase_price_received": 100.0}) - - # Re-read from DB to ensure value is persisted - self.product.invalidate_recordset() - self.assertEqual( - self.product.last_purchase_price_received, - 100.0, - "last_purchase_price_received should be 100.0 after write", - ) - - def test_compute_theoretical_price_from_product(self): - """Test computing theoretical price when called on product.product.""" - # Set purchase price - self.product.write( - { - "last_purchase_price_received": 100.0, - "last_purchase_price_compute_type": "without_discounts", - } - ) - self.product.invalidate_recordset() - - # Verify price was written - self.assertEqual(self.product.last_purchase_price_received, 100.0) - - # Compute theoretical price - self.product._compute_theoritical_price() - self.product.invalidate_recordset() - - # Expected: 100.0 * 1.10 = 110.0 (10% markup) - self.assertAlmostEqual( - self.product.list_price_theoritical, - 110.0, - places=2, - msg=f"Theoretical price should be 110.0, got {self.product.list_price_theoritical}", - ) - - def test_compute_theoretical_price_from_template(self): - """Test computing theoretical price when called on product.template.""" - template = self.product.product_tmpl_id - - # Set purchase price on variant - self.product.write( - { - "last_purchase_price_received": 100.0, - "last_purchase_price_compute_type": "without_discounts", - } - ) - self.product.invalidate_recordset() - - # Verify price was written - self.assertEqual(self.product.last_purchase_price_received, 100.0) - - # Compute theoretical price via template - template._compute_theoritical_price() - self.product.invalidate_recordset() - - # Expected: 100.0 * 1.10 = 110.0 (10% markup) - self.assertAlmostEqual( - self.product.list_price_theoritical, - 110.0, - places=2, - msg=f"Theoretical price should be 110.0, got {self.product.list_price_theoritical}", - ) - - def test_field_storage_location(self): - """Test that fields are stored on product.product, not product.template.""" - # Check field definitions - product_fields = self.env["product.product"]._fields - template_fields = self.env["product.template"]._fields - - # These should be stored fields on product.product - self.assertFalse( - product_fields["last_purchase_price_received"].compute, - "last_purchase_price_received should NOT be computed on product.product", - ) - self.assertFalse( - product_fields["list_price_theoritical"].compute, - "list_price_theoritical should NOT be computed on product.product", - ) - - # These should be computed fields on product.template - self.assertTrue( - template_fields["last_purchase_price_received"].compute, - "last_purchase_price_received should be computed on product.template", - ) - self.assertTrue( - template_fields["list_price_theoritical"].compute, - "list_price_theoritical should be computed on product.template", - )