Compare commits
No commits in common. "6d944847109c5ad2dd1cf80f9b498b49777f04e2" and "c308d538a3d9346bfa00618d0951fb46d3d36e5e" have entirely different histories.
6d94484710
...
c308d538a3
34 changed files with 365 additions and 1960 deletions
|
|
@ -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/
|
|
||||||
|
|
@ -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
|
|
||||||
1
ocb
1
ocb
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 6fb141fc7547f9de55c9ac702515f1e3a27406d0
|
|
||||||
|
|
@ -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 <https://github.com/OCA/product-attribute/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 <https://github.com/OCA/product-attribute/issues/new?body=module:%20product_origin%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
|
||||||
|
|
||||||
Do not contact contributors directly about support or help with technical issues.
|
|
||||||
|
|
||||||
Credits
|
|
||||||
=======
|
|
||||||
|
|
||||||
Authors
|
|
||||||
-------
|
|
||||||
|
|
||||||
* ACSONE SA/NV
|
|
||||||
* GRAP
|
|
||||||
|
|
||||||
Contributors
|
|
||||||
------------
|
|
||||||
|
|
||||||
- Denis Roussel <denis.roussel@acsone.eu>
|
|
||||||
- Sylvain LE GAL (https://twitter.com/legalsylvain)
|
|
||||||
- `Heliconia Solutions Pvt. Ltd. <https://www.heliconia.io>`__
|
|
||||||
|
|
||||||
- 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 <https://odoo-community.org/page/maintainer-role>`__:
|
|
||||||
|
|
||||||
|maintainer-rousseldenis| |maintainer-legalsylvain|
|
|
||||||
|
|
||||||
This module is part of the `OCA/product-attribute <https://github.com/OCA/product-attribute/tree/18.0/product_origin>`_ project on GitHub.
|
|
||||||
|
|
||||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from . import models
|
|
||||||
|
|
@ -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",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
@ -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'"
|
|
||||||
|
|
@ -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 <stefano.consolaro@mymage.it>\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'"
|
|
||||||
|
|
@ -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 ""
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
from . import product_product
|
|
||||||
from . import product_template
|
|
||||||
|
|
@ -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 = []
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
[build-system]
|
|
||||||
requires = ["whool"]
|
|
||||||
build-backend = "whool.buildapi"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
- Denis Roussel \<<denis.roussel@acsone.eu>\>
|
|
||||||
- Sylvain LE GAL (<https://twitter.com/legalsylvain>)
|
|
||||||
- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io)
|
|
||||||
- Bhavesh Heliconia
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
This module adds a field to associate the country and state of origin of
|
|
||||||
a product
|
|
||||||
|
|
||||||
<https://en.wikipedia.org/wiki/Country_of_origin>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
## 10.0.1.0.0 (2019-01-11)
|
|
||||||
|
|
||||||
- \[10.0\]\[ADD\] product_origin
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||

|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.2 KiB |
|
|
@ -1,456 +0,0 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
|
||||||
<title>Product Origin</title>
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
/*
|
|
||||||
:Author: David Goodger (goodger@python.org)
|
|
||||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
|
||||||
:Copyright: This stylesheet has been placed in the public domain.
|
|
||||||
|
|
||||||
Default cascading style sheet for the HTML output of Docutils.
|
|
||||||
Despite the name, some widely supported CSS2 features are used.
|
|
||||||
|
|
||||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
|
||||||
customize this style sheet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* used to remove borders from tables and images */
|
|
||||||
.borderless, table.borderless td, table.borderless th {
|
|
||||||
border: 0 }
|
|
||||||
|
|
||||||
table.borderless td, table.borderless th {
|
|
||||||
/* Override padding for "table.docutils td" with "! important".
|
|
||||||
The right padding separates the table cells. */
|
|
||||||
padding: 0 0.5em 0 0 ! important }
|
|
||||||
|
|
||||||
.first {
|
|
||||||
/* Override more specific margin styles with "! important". */
|
|
||||||
margin-top: 0 ! important }
|
|
||||||
|
|
||||||
.last, .with-subtitle {
|
|
||||||
margin-bottom: 0 ! important }
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none }
|
|
||||||
|
|
||||||
.subscript {
|
|
||||||
vertical-align: sub;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
.superscript {
|
|
||||||
vertical-align: super;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
a.toc-backref {
|
|
||||||
text-decoration: none ;
|
|
||||||
color: black }
|
|
||||||
|
|
||||||
blockquote.epigraph {
|
|
||||||
margin: 2em 5em ; }
|
|
||||||
|
|
||||||
dl.docutils dd {
|
|
||||||
margin-bottom: 0.5em }
|
|
||||||
|
|
||||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
|
||||||
dl.docutils dt {
|
|
||||||
font-weight: bold }
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.abstract {
|
|
||||||
margin: 2em 5em }
|
|
||||||
|
|
||||||
div.abstract p.topic-title {
|
|
||||||
font-weight: bold ;
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
|
||||||
div.hint, div.important, div.note, div.tip, div.warning {
|
|
||||||
margin: 2em ;
|
|
||||||
border: medium outset ;
|
|
||||||
padding: 1em }
|
|
||||||
|
|
||||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
|
||||||
div.important p.admonition-title, div.note p.admonition-title,
|
|
||||||
div.tip p.admonition-title {
|
|
||||||
font-weight: bold ;
|
|
||||||
font-family: sans-serif }
|
|
||||||
|
|
||||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
|
||||||
div.danger p.admonition-title, div.error p.admonition-title,
|
|
||||||
div.warning p.admonition-title, .code .error {
|
|
||||||
color: red ;
|
|
||||||
font-weight: bold ;
|
|
||||||
font-family: sans-serif }
|
|
||||||
|
|
||||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
|
||||||
compound paragraphs.
|
|
||||||
div.compound .compound-first, div.compound .compound-middle {
|
|
||||||
margin-bottom: 0.5em }
|
|
||||||
|
|
||||||
div.compound .compound-last, div.compound .compound-middle {
|
|
||||||
margin-top: 0.5em }
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.dedication {
|
|
||||||
margin: 2em 5em ;
|
|
||||||
text-align: center ;
|
|
||||||
font-style: italic }
|
|
||||||
|
|
||||||
div.dedication p.topic-title {
|
|
||||||
font-weight: bold ;
|
|
||||||
font-style: normal }
|
|
||||||
|
|
||||||
div.figure {
|
|
||||||
margin-left: 2em ;
|
|
||||||
margin-right: 2em }
|
|
||||||
|
|
||||||
div.footer, div.header {
|
|
||||||
clear: both;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
div.line-block {
|
|
||||||
display: block ;
|
|
||||||
margin-top: 1em ;
|
|
||||||
margin-bottom: 1em }
|
|
||||||
|
|
||||||
div.line-block div.line-block {
|
|
||||||
margin-top: 0 ;
|
|
||||||
margin-bottom: 0 ;
|
|
||||||
margin-left: 1.5em }
|
|
||||||
|
|
||||||
div.sidebar {
|
|
||||||
margin: 0 0 0.5em 1em ;
|
|
||||||
border: medium outset ;
|
|
||||||
padding: 1em ;
|
|
||||||
background-color: #ffffee ;
|
|
||||||
width: 40% ;
|
|
||||||
float: right ;
|
|
||||||
clear: right }
|
|
||||||
|
|
||||||
div.sidebar p.rubric {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-size: medium }
|
|
||||||
|
|
||||||
div.system-messages {
|
|
||||||
margin: 5em }
|
|
||||||
|
|
||||||
div.system-messages h1 {
|
|
||||||
color: red }
|
|
||||||
|
|
||||||
div.system-message {
|
|
||||||
border: medium outset ;
|
|
||||||
padding: 1em }
|
|
||||||
|
|
||||||
div.system-message p.system-message-title {
|
|
||||||
color: red ;
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
div.topic {
|
|
||||||
margin: 2em }
|
|
||||||
|
|
||||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
|
||||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
|
||||||
margin-top: 0.4em }
|
|
||||||
|
|
||||||
h1.title {
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
h2.subtitle {
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
hr.docutils {
|
|
||||||
width: 75% }
|
|
||||||
|
|
||||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
|
||||||
clear: left ;
|
|
||||||
float: left ;
|
|
||||||
margin-right: 1em }
|
|
||||||
|
|
||||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
|
||||||
clear: right ;
|
|
||||||
float: right ;
|
|
||||||
margin-left: 1em }
|
|
||||||
|
|
||||||
img.align-center, .figure.align-center, object.align-center {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.align-center {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
text-align: left }
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
clear: both ;
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right }
|
|
||||||
|
|
||||||
/* reset inner alignment in figures */
|
|
||||||
div.align-right {
|
|
||||||
text-align: inherit }
|
|
||||||
|
|
||||||
/* div.align-center * { */
|
|
||||||
/* text-align: left } */
|
|
||||||
|
|
||||||
.align-top {
|
|
||||||
vertical-align: top }
|
|
||||||
|
|
||||||
.align-middle {
|
|
||||||
vertical-align: middle }
|
|
||||||
|
|
||||||
.align-bottom {
|
|
||||||
vertical-align: bottom }
|
|
||||||
|
|
||||||
ol.simple, ul.simple {
|
|
||||||
margin-bottom: 1em }
|
|
||||||
|
|
||||||
ol.arabic {
|
|
||||||
list-style: decimal }
|
|
||||||
|
|
||||||
ol.loweralpha {
|
|
||||||
list-style: lower-alpha }
|
|
||||||
|
|
||||||
ol.upperalpha {
|
|
||||||
list-style: upper-alpha }
|
|
||||||
|
|
||||||
ol.lowerroman {
|
|
||||||
list-style: lower-roman }
|
|
||||||
|
|
||||||
ol.upperroman {
|
|
||||||
list-style: upper-roman }
|
|
||||||
|
|
||||||
p.attribution {
|
|
||||||
text-align: right ;
|
|
||||||
margin-left: 50% }
|
|
||||||
|
|
||||||
p.caption {
|
|
||||||
font-style: italic }
|
|
||||||
|
|
||||||
p.credits {
|
|
||||||
font-style: italic ;
|
|
||||||
font-size: smaller }
|
|
||||||
|
|
||||||
p.label {
|
|
||||||
white-space: nowrap }
|
|
||||||
|
|
||||||
p.rubric {
|
|
||||||
font-weight: bold ;
|
|
||||||
font-size: larger ;
|
|
||||||
color: maroon ;
|
|
||||||
text-align: center }
|
|
||||||
|
|
||||||
p.sidebar-title {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-weight: bold ;
|
|
||||||
font-size: larger }
|
|
||||||
|
|
||||||
p.sidebar-subtitle {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
p.topic-title {
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
pre.address {
|
|
||||||
margin-bottom: 0 ;
|
|
||||||
margin-top: 0 ;
|
|
||||||
font: inherit }
|
|
||||||
|
|
||||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
|
||||||
margin-left: 2em ;
|
|
||||||
margin-right: 2em }
|
|
||||||
|
|
||||||
pre.code .ln { color: gray; } /* line numbers */
|
|
||||||
pre.code, code { background-color: #eeeeee }
|
|
||||||
pre.code .comment, code .comment { color: #5C6576 }
|
|
||||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
|
||||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
|
||||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
|
||||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
|
||||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
|
||||||
|
|
||||||
span.classifier {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-style: oblique }
|
|
||||||
|
|
||||||
span.classifier-delimiter {
|
|
||||||
font-family: sans-serif ;
|
|
||||||
font-weight: bold }
|
|
||||||
|
|
||||||
span.interpreted {
|
|
||||||
font-family: sans-serif }
|
|
||||||
|
|
||||||
span.option {
|
|
||||||
white-space: nowrap }
|
|
||||||
|
|
||||||
span.pre {
|
|
||||||
white-space: pre }
|
|
||||||
|
|
||||||
span.problematic, pre.problematic {
|
|
||||||
color: red }
|
|
||||||
|
|
||||||
span.section-subtitle {
|
|
||||||
/* font-size relative to parent (h1..h6 element) */
|
|
||||||
font-size: 80% }
|
|
||||||
|
|
||||||
table.citation {
|
|
||||||
border-left: solid 1px gray;
|
|
||||||
margin-left: 1px }
|
|
||||||
|
|
||||||
table.docinfo {
|
|
||||||
margin: 2em 4em }
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
margin-top: 0.5em ;
|
|
||||||
margin-bottom: 0.5em }
|
|
||||||
|
|
||||||
table.footnote {
|
|
||||||
border-left: solid 1px black;
|
|
||||||
margin-left: 1px }
|
|
||||||
|
|
||||||
table.docutils td, table.docutils th,
|
|
||||||
table.docinfo td, table.docinfo th {
|
|
||||||
padding-left: 0.5em ;
|
|
||||||
padding-right: 0.5em ;
|
|
||||||
vertical-align: top }
|
|
||||||
|
|
||||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
|
||||||
font-weight: bold ;
|
|
||||||
text-align: left ;
|
|
||||||
white-space: nowrap ;
|
|
||||||
padding-left: 0 }
|
|
||||||
|
|
||||||
/* "booktabs" style (no vertical lines) */
|
|
||||||
table.docutils.booktabs {
|
|
||||||
border: 0px;
|
|
||||||
border-top: 2px solid;
|
|
||||||
border-bottom: 2px solid;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
table.docutils.booktabs * {
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
table.docutils.booktabs th {
|
|
||||||
border-bottom: thin solid;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
|
||||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
|
||||||
font-size: 100% }
|
|
||||||
|
|
||||||
ul.auto-toc {
|
|
||||||
list-style-type: none }
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="document" id="product-origin">
|
|
||||||
<h1 class="title">Product Origin</h1>
|
|
||||||
|
|
||||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
!! This file is generated by oca-gen-addon-readme !!
|
|
||||||
!! changes will be overwritten. !!
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
!! source digest: sha256:2db486ad6cf453e31192b518e0e0de9647f6b65545047439e05da8bc413dc410
|
|
||||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
|
||||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/product-attribute/tree/18.0/product_origin"><img alt="OCA/product-attribute" src="https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_origin"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
|
||||||
<p>This module adds a field to associate the country and state of origin of
|
|
||||||
a product</p>
|
|
||||||
<p><a class="reference external" href="https://en.wikipedia.org/wiki/Country_of_origin">https://en.wikipedia.org/wiki/Country_of_origin</a></p>
|
|
||||||
<p><strong>Table of contents</strong></p>
|
|
||||||
<div class="contents local topic" id="contents">
|
|
||||||
<ul class="simple">
|
|
||||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
|
||||||
<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
|
|
||||||
<li><a class="reference internal" href="#section-1" id="toc-entry-3">10.0.1.0.0 (2019-01-11)</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
|
|
||||||
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
|
|
||||||
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
|
|
||||||
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
|
|
||||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="usage">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Go to product form</li>
|
|
||||||
<li>Fill in the country and/or state of origin of the product under the
|
|
||||||
‘General Information’ tab.</li>
|
|
||||||
</ul>
|
|
||||||
<p><img alt="image1" src="https://raw.githubusercontent.com/OCA/product-attribute/18.0/product_origin/static/description/product_form.png" /></p>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="changelog">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
|
|
||||||
<div class="section" id="section-1">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-3">10.0.1.0.0 (2019-01-11)</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>[10.0][ADD] product_origin</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="bug-tracker">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
|
|
||||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/product-attribute/issues">GitHub Issues</a>.
|
|
||||||
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
|
|
||||||
<a class="reference external" href="https://github.com/OCA/product-attribute/issues/new?body=module:%20product_origin%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
|
||||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="credits">
|
|
||||||
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
|
|
||||||
<div class="section" id="authors">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>ACSONE SA/NV</li>
|
|
||||||
<li>GRAP</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="contributors">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li>Denis Roussel <<a class="reference external" href="mailto:denis.roussel@acsone.eu">denis.roussel@acsone.eu</a>></li>
|
|
||||||
<li>Sylvain LE GAL (<a class="reference external" href="https://twitter.com/legalsylvain">https://twitter.com/legalsylvain</a>)</li>
|
|
||||||
<li><a class="reference external" href="https://www.heliconia.io">Heliconia Solutions Pvt. Ltd.</a><ul>
|
|
||||||
<li>Bhavesh Heliconia</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="maintainers">
|
|
||||||
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
|
|
||||||
<p>This module is maintained by the OCA.</p>
|
|
||||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
|
||||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
|
||||||
</a>
|
|
||||||
<p>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.</p>
|
|
||||||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
|
|
||||||
<p><a class="reference external image-reference" href="https://github.com/rousseldenis"><img alt="rousseldenis" src="https://github.com/rousseldenis.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/legalsylvain"><img alt="legalsylvain" src="https://github.com/legalsylvain.png?size=40px" /></a></p>
|
|
||||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/product-attribute/tree/18.0/product_origin">OCA/product-attribute</a> project on GitHub.</p>
|
|
||||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
|
|
@ -1 +0,0 @@
|
||||||
from . import test_module
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!--
|
|
||||||
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).
|
|
||||||
-->
|
|
||||||
<odoo>
|
|
||||||
<record id="view_product_product_form" model="ir.ui.view">
|
|
||||||
<field name="model">product.product</field>
|
|
||||||
<field name="inherit_id" ref="product.product_normal_form_view" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//group[@name='group_lots_and_weight']" position="after">
|
|
||||||
<group string="Origin" name="group_origin">
|
|
||||||
<field name="country_id" />
|
|
||||||
<field name="state_id_domain" invisible="1" />
|
|
||||||
<field name="state_id" domain="state_id_domain" />
|
|
||||||
</group>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_product_product_form_variant" model="ir.ui.view">
|
|
||||||
<field name="model">product.product</field>
|
|
||||||
<field name="inherit_id" ref="product.product_variant_easy_edit_view" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//group[@name='weight']" position="after">
|
|
||||||
<group string="Origin" name="group_origin">
|
|
||||||
<field name="country_id" />
|
|
||||||
<field name="state_id_domain" invisible="1" />
|
|
||||||
<field name="state_id" domain="state_id_domain" />
|
|
||||||
</group>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<!-- Copyright 2018 ACSONE SA/NV
|
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
|
||||||
<odoo>
|
|
||||||
<record model="ir.ui.view" id="product_template_form_view">
|
|
||||||
<field name="model">product.template</field>
|
|
||||||
<field name="inherit_id" ref="product.product_template_only_form_view" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//group[@name='group_lots_and_weight']" position="after">
|
|
||||||
<group
|
|
||||||
string="Origin"
|
|
||||||
name="group_origin"
|
|
||||||
invisible="product_variant_count > 1"
|
|
||||||
>
|
|
||||||
<field name="country_id" />
|
|
||||||
<field name="state_id_domain" invisible="1" />
|
|
||||||
<field name="state_id" domain="state_id_domain" />
|
|
||||||
</group>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record model="ir.ui.view" id="product_template_search_view">
|
|
||||||
<field name="model">product.template</field>
|
|
||||||
<field name="inherit_id" ref="product.product_template_search_view" />
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<filter name="favorites" position="before">
|
|
||||||
<field name="country_id" />
|
|
||||||
</filter>
|
|
||||||
<filter name="categ_id" position="after">
|
|
||||||
<filter
|
|
||||||
string="Country of Origin"
|
|
||||||
name="country_id"
|
|
||||||
context="{'group_by':'country_id'}"
|
|
||||||
/>
|
|
||||||
</filter>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
{ # noqa: B018
|
{ # noqa: B018
|
||||||
"name": "Product Sale Price from Pricelist",
|
"name": "Product Sale Price from Pricelist",
|
||||||
"version": "18.0.2.1.0",
|
"version": "18.0.2.0.0",
|
||||||
"category": "product",
|
"category": "product",
|
||||||
"summary": "Set sale price from pricelist based on last purchase price",
|
"summary": "Set sale price from pricelist based on last purchase price",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"author": "Odoo Community Association (OCA), Criptomart",
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import models
|
from odoo import api, models
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -17,14 +17,17 @@ class ProductPricelist(models.Model):
|
||||||
ProductProduct = self.env["product.product"]
|
ProductProduct = self.env["product.product"]
|
||||||
|
|
||||||
_logger.info(
|
_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.ids,
|
||||||
products._name,
|
|
||||||
quantity,
|
quantity,
|
||||||
)
|
)
|
||||||
|
|
||||||
res = super()._compute_price_rule(
|
res = super()._compute_price_rule(
|
||||||
products, quantity, uom=uom, date=date, **kwargs
|
products,
|
||||||
|
quantity,
|
||||||
|
uom=uom,
|
||||||
|
date=date,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
|
|
@ -46,22 +49,7 @@ class ProductPricelist(models.Model):
|
||||||
item_id,
|
item_id,
|
||||||
)
|
)
|
||||||
if item.base == "last_purchase_price":
|
if item.base == "last_purchase_price":
|
||||||
# product_id could be from product.template or product.product
|
product = ProductProduct.browse(product_id)
|
||||||
# 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)
|
|
||||||
|
|
||||||
price = product.last_purchase_price_received
|
price = product.last_purchase_price_received
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"[PRICELIST DEBUG] Product %s: last_purchase_price_received=%s, item.price_discount=%s",
|
"[PRICELIST DEBUG] Product %s: last_purchase_price_received=%s, item.price_discount=%s",
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ class ProductTemplate(models.Model):
|
||||||
inverse="_inverse_last_purchase_price_updated",
|
inverse="_inverse_last_purchase_price_updated",
|
||||||
search="_search_last_purchase_price_updated",
|
search="_search_last_purchase_price_updated",
|
||||||
store=True,
|
store=True,
|
||||||
company_dependent=False,
|
|
||||||
)
|
)
|
||||||
list_price_theoritical = fields.Float(
|
list_price_theoritical = fields.Float(
|
||||||
string="Theoritical price",
|
string="Theoritical price",
|
||||||
|
|
@ -33,7 +32,6 @@ class ProductTemplate(models.Model):
|
||||||
inverse="_inverse_list_price_theoritical",
|
inverse="_inverse_list_price_theoritical",
|
||||||
search="_search_list_price_theoritical",
|
search="_search_list_price_theoritical",
|
||||||
store=True,
|
store=True,
|
||||||
company_dependent=False,
|
|
||||||
)
|
)
|
||||||
last_purchase_price_received = fields.Float(
|
last_purchase_price_received = fields.Float(
|
||||||
string="Last purchase price",
|
string="Last purchase price",
|
||||||
|
|
@ -41,7 +39,6 @@ class ProductTemplate(models.Model):
|
||||||
inverse="_inverse_last_purchase_price_received",
|
inverse="_inverse_last_purchase_price_received",
|
||||||
search="_search_last_purchase_price_received",
|
search="_search_last_purchase_price_received",
|
||||||
store=True,
|
store=True,
|
||||||
company_dependent=False,
|
|
||||||
)
|
)
|
||||||
last_purchase_price_compute_type = fields.Selection(
|
last_purchase_price_compute_type = fields.Selection(
|
||||||
[
|
[
|
||||||
|
|
@ -56,15 +53,6 @@ class ProductTemplate(models.Model):
|
||||||
inverse="_inverse_last_purchase_price_compute_type",
|
inverse="_inverse_last_purchase_price_compute_type",
|
||||||
search="_search_last_purchase_price_compute_type",
|
search="_search_last_purchase_price_compute_type",
|
||||||
store=True,
|
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")
|
@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)
|
("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):
|
def action_update_list_price(self):
|
||||||
"""Delegate to product variants."""
|
"""Delegate to product variants."""
|
||||||
return self.product_variant_ids.action_update_list_price()
|
return self.product_variant_ids.action_update_list_price()
|
||||||
|
|
|
||||||
|
|
@ -56,23 +56,6 @@ class StockMove(models.Model):
|
||||||
price_updated, move.product_id.uom_id
|
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(
|
if float_compare(
|
||||||
move.product_id.last_purchase_price_received,
|
move.product_id.last_purchase_price_received,
|
||||||
price_updated,
|
price_updated,
|
||||||
|
|
@ -92,20 +75,10 @@ class StockMove(models.Model):
|
||||||
),
|
),
|
||||||
move.product_id.last_purchase_price_compute_type,
|
move.product_id.last_purchase_price_compute_type,
|
||||||
)
|
)
|
||||||
# Use write() to ensure value is persisted before computing price
|
move.product_id.with_company(
|
||||||
product_company = move.product_id.with_company(move.company_id)
|
move.company_id
|
||||||
product_company.write(
|
).last_purchase_price_received = price_updated
|
||||||
{
|
move.product_id.with_company(
|
||||||
"last_purchase_price_received": price_updated,
|
move.company_id
|
||||||
}
|
)._compute_theoritical_price()
|
||||||
)
|
|
||||||
# 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()
|
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -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_product_template
|
||||||
from . import test_stock_move
|
from . import test_stock_move
|
||||||
from . import test_pricelist
|
from . import test_pricelist
|
||||||
from . import test_res_config
|
from . import test_res_config
|
||||||
from . import test_theoretical_price
|
|
||||||
|
|
|
||||||
|
|
@ -12,50 +12,45 @@ class TestPricelist(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create tax
|
# Create tax
|
||||||
cls.tax = cls.env["account.tax"].create(
|
cls.tax = cls.env["account.tax"].create({
|
||||||
{
|
"name": "Test Tax 10%",
|
||||||
"name": "Test Tax 10%",
|
"amount": 10.0,
|
||||||
"amount": 10.0,
|
"amount_type": "percent",
|
||||||
"amount_type": "percent",
|
"type_tax_use": "sale",
|
||||||
"type_tax_use": "sale",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create product
|
# Create product
|
||||||
cls.product = cls.env["product.product"].create(
|
cls.product = cls.env["product.product"].create({
|
||||||
{
|
"name": "Test Product Pricelist",
|
||||||
"name": "Test Product Pricelist",
|
"type": "product",
|
||||||
"list_price": 100.0,
|
"list_price": 100.0,
|
||||||
"standard_price": 50.0,
|
"standard_price": 50.0,
|
||||||
"taxes_id": [(6, 0, [cls.tax.id])],
|
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||||
"last_purchase_price_received": 50.0,
|
"last_purchase_price_received": 50.0,
|
||||||
"last_purchase_price_compute_type": "without_discounts",
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create pricelist with last_purchase_price base
|
# Create pricelist with last_purchase_price base
|
||||||
cls.pricelist = cls.env["product.pricelist"].create(
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
{
|
"name": "Test Pricelist Last Purchase",
|
||||||
"name": "Test Pricelist Last Purchase",
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
"currency_id": cls.env.company.currency_id.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pricelist_item_base_last_purchase_price(self):
|
def test_pricelist_item_base_last_purchase_price(self):
|
||||||
"""Test pricelist item with last_purchase_price base"""
|
"""Test pricelist item with last_purchase_price base"""
|
||||||
self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": self.pricelist.id,
|
||||||
"pricelist_id": self.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_discount": 0,
|
||||||
"price_discount": 0,
|
"price_surcharge": 10.0,
|
||||||
"price_surcharge": 10.0,
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compute price using pricelist
|
# 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
|
# Price should be based on last_purchase_price
|
||||||
self.assertIn(self.product.id, price)
|
self.assertIn(self.product.id, price)
|
||||||
|
|
@ -63,17 +58,17 @@ class TestPricelist(TransactionCase):
|
||||||
|
|
||||||
def test_pricelist_item_with_discount(self):
|
def test_pricelist_item_with_discount(self):
|
||||||
"""Test pricelist item with discount on last_purchase_price"""
|
"""Test pricelist item with discount on last_purchase_price"""
|
||||||
self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": self.pricelist.id,
|
||||||
"pricelist_id": self.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_discount": 20.0, # 20% discount
|
||||||
"price_discount": 20.0, # 20% discount
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
price = self.pricelist._compute_price_rule(self.product, quantity=1, date=False)
|
price = self.pricelist._compute_price_rule(
|
||||||
|
self.product, qty=1, date=False
|
||||||
|
)
|
||||||
|
|
||||||
# Expected: 50.0 - (50.0 * 0.20) = 40.0
|
# Expected: 50.0 - (50.0 * 0.20) = 40.0
|
||||||
self.assertIn(self.product.id, price)
|
self.assertIn(self.product.id, price)
|
||||||
|
|
@ -81,19 +76,21 @@ class TestPricelist(TransactionCase):
|
||||||
|
|
||||||
def test_pricelist_item_with_markup(self):
|
def test_pricelist_item_with_markup(self):
|
||||||
"""Test pricelist item with markup on last_purchase_price"""
|
"""Test pricelist item with markup on last_purchase_price"""
|
||||||
pricelist_item = self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": self.pricelist.id,
|
||||||
"pricelist_id": self.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_markup": 100.0, # 100% markup (double the price)
|
||||||
"price_markup": 100.0, # 100% markup (double the price)
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# _compute_price should return the base price (last_purchase_price_received)
|
# _compute_price should return the base price (last_purchase_price_received)
|
||||||
result = pricelist_item._compute_price(
|
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
|
# Should return the last purchase price as base
|
||||||
|
|
@ -101,18 +98,20 @@ class TestPricelist(TransactionCase):
|
||||||
|
|
||||||
def test_pricelist_item_compute_price_method(self):
|
def test_pricelist_item_compute_price_method(self):
|
||||||
"""Test _compute_price method of pricelist item"""
|
"""Test _compute_price method of pricelist item"""
|
||||||
pricelist_item = self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": self.pricelist.id,
|
||||||
"pricelist_id": self.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_markup": 50.0,
|
||||||
"price_markup": 50.0,
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = pricelist_item._compute_price(
|
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
|
# Should return last_purchase_price_received
|
||||||
|
|
@ -120,25 +119,24 @@ class TestPricelist(TransactionCase):
|
||||||
|
|
||||||
def test_pricelist_item_with_zero_last_purchase_price(self):
|
def test_pricelist_item_with_zero_last_purchase_price(self):
|
||||||
"""Test pricelist behavior when last_purchase_price is zero"""
|
"""Test pricelist behavior when last_purchase_price is zero"""
|
||||||
product_zero = self.env["product.product"].create(
|
product_zero = self.env["product.product"].create({
|
||||||
{
|
"name": "Product Zero Price",
|
||||||
"name": "Product Zero Price",
|
"type": "product",
|
||||||
"list_price": 100.0,
|
"list_price": 100.0,
|
||||||
"last_purchase_price_received": 0.0,
|
"last_purchase_price_received": 0.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": self.pricelist.id,
|
||||||
"pricelist_id": self.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_discount": 10.0,
|
||||||
"price_discount": 10.0,
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
price = self.pricelist._compute_price_rule(product_zero, quantity=1, date=False)
|
price = self.pricelist._compute_price_rule(
|
||||||
|
product_zero, qty=1, date=False
|
||||||
|
)
|
||||||
|
|
||||||
# Should handle zero price gracefully
|
# Should handle zero price gracefully
|
||||||
self.assertIn(product_zero.id, price)
|
self.assertIn(product_zero.id, price)
|
||||||
|
|
@ -146,29 +144,28 @@ class TestPricelist(TransactionCase):
|
||||||
|
|
||||||
def test_pricelist_multiple_products(self):
|
def test_pricelist_multiple_products(self):
|
||||||
"""Test pricelist calculation with multiple products"""
|
"""Test pricelist calculation with multiple products"""
|
||||||
product2 = self.env["product.product"].create(
|
product2 = self.env["product.product"].create({
|
||||||
{
|
"name": "Test Product 2",
|
||||||
"name": "Test Product 2",
|
"type": "product",
|
||||||
"list_price": 200.0,
|
"list_price": 200.0,
|
||||||
"last_purchase_price_received": 100.0,
|
"last_purchase_price_received": 100.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": self.pricelist.id,
|
||||||
"pricelist_id": self.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_discount": 10.0,
|
||||||
"price_discount": 10.0,
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
products = self.product | product2
|
products = self.product | product2
|
||||||
|
|
||||||
# Test with both products
|
# Test with both products
|
||||||
for product in 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)
|
self.assertIn(product.id, price)
|
||||||
|
|
||||||
def test_pricelist_item_selection_add(self):
|
def test_pricelist_item_selection_add(self):
|
||||||
|
|
@ -194,24 +191,22 @@ class TestPricelist(TransactionCase):
|
||||||
if not eur:
|
if not eur:
|
||||||
self.skipTest("EUR currency not available")
|
self.skipTest("EUR currency not available")
|
||||||
|
|
||||||
pricelist_eur = self.env["product.pricelist"].create(
|
pricelist_eur = self.env["product.pricelist"].create({
|
||||||
{
|
"name": "Test Pricelist EUR",
|
||||||
"name": "Test Pricelist EUR",
|
"currency_id": eur.id,
|
||||||
"currency_id": eur.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.env["product.pricelist.item"].create(
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": pricelist_eur.id,
|
||||||
"pricelist_id": pricelist_eur.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_discount": 0,
|
||||||
"price_discount": 0,
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Price calculation should work with different currency
|
# 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)
|
self.assertIn(self.product.id, price)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
# 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 import tagged
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
@tagged("post_install", "-at_install")
|
@tagged("post_install", "-at_install")
|
||||||
|
|
@ -13,94 +13,62 @@ class TestProductTemplate(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create a tax for the product
|
# Create a tax for the product
|
||||||
cls.tax = cls.env["account.tax"].create(
|
cls.tax = cls.env["account.tax"].create({
|
||||||
{
|
"name": "Test Tax 21%",
|
||||||
"name": "Test Tax 21%",
|
"amount": 21.0,
|
||||||
"amount": 21.0,
|
"amount_type": "percent",
|
||||||
"amount_type": "percent",
|
"type_tax_use": "sale",
|
||||||
"type_tax_use": "sale",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a pricelist with formula based on cost
|
# Create a pricelist with formula based on cost
|
||||||
cls.pricelist = cls.env["product.pricelist"].create(
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
{
|
"name": "Test Pricelist Automatic",
|
||||||
"name": "Test Pricelist Automatic",
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
"currency_id": cls.env.company.currency_id.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.pricelist_item = cls.env["product.pricelist.item"].create(
|
cls.pricelist_item = cls.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": cls.pricelist.id,
|
||||||
"pricelist_id": cls.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_discount": 0,
|
||||||
"price_discount": 0,
|
"price_surcharge": 0,
|
||||||
"price_surcharge": 0,
|
"price_markup": 50.0, # 50% markup
|
||||||
"price_markup": 50.0, # 50% markup
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the pricelist in configuration
|
# Set the pricelist in configuration
|
||||||
cls.env["ir.config_parameter"].sudo().set_param(
|
cls.env["ir.config_parameter"].sudo().set_param(
|
||||||
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
str(cls.pricelist.id),
|
str(cls.pricelist.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a product category
|
# Create a product category
|
||||||
cls.category = cls.env["product.category"].create(
|
cls.category = cls.env["product.category"].create({
|
||||||
{
|
"name": "Test Category",
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a product
|
# Create a product
|
||||||
cls.product = cls.env["product.template"].create(
|
cls.product = cls.env["product.template"].create({
|
||||||
{
|
"name": "Test Product",
|
||||||
"name": "Test Product",
|
"type": "product",
|
||||||
"categ_id": cls.category.id,
|
"categ_id": cls.category.id,
|
||||||
"list_price": 10.0,
|
"list_price": 10.0,
|
||||||
"standard_price": 5.0,
|
"standard_price": 5.0,
|
||||||
"taxes_id": [(6, 0, [cls.tax.id])],
|
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||||
}
|
"last_purchase_price_received": 5.0,
|
||||||
)
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
|
})
|
||||||
# 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):
|
def test_compute_theoritical_price_without_tax_error(self):
|
||||||
"""Test that computing theoretical price without taxes raises an error"""
|
"""Test that computing theoretical price without taxes raises an error"""
|
||||||
product_no_tax = self.env["product.template"].create(
|
product_no_tax = self.env["product.template"].create({
|
||||||
{
|
"name": "Product No Tax",
|
||||||
"name": "Product No Tax",
|
"type": "product",
|
||||||
"list_price": 10.0,
|
"list_price": 10.0,
|
||||||
"standard_price": 5.0,
|
"standard_price": 5.0,
|
||||||
"taxes_id": [(5, 0, 0)], # Clear all taxes
|
"last_purchase_price_received": 5.0,
|
||||||
}
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
)
|
})
|
||||||
# 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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
product_no_tax._compute_theoritical_price()
|
product_no_tax._compute_theoritical_price()
|
||||||
|
|
@ -109,65 +77,46 @@ class TestProductTemplate(TransactionCase):
|
||||||
"""Test successful computation of theoretical price"""
|
"""Test successful computation of theoretical price"""
|
||||||
self.product._compute_theoritical_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
|
# Verify that theoretical price was calculated
|
||||||
self.assertGreater(theoretical_price, 0)
|
self.assertGreater(self.product.list_price_theoritical, 0)
|
||||||
|
|
||||||
# Price is calculated WITHOUT tax (taxes added automatically on sales)
|
# Verify that the price includes markup and tax
|
||||||
# With 50% markup on 5.0 = 7.5
|
# With 50% markup on 5.0 = 7.5, plus 21% tax = 9.075, rounded to 9.10
|
||||||
self.assertAlmostEqual(theoretical_price, 7.50, places=2)
|
self.assertAlmostEqual(self.product.list_price_theoritical, 9.10, places=2)
|
||||||
|
|
||||||
def test_compute_theoritical_price_with_rounding(self):
|
def test_compute_theoritical_price_with_rounding(self):
|
||||||
"""Test that prices are properly calculated"""
|
"""Test that prices are rounded to 0.05"""
|
||||||
self.product.product_variant_ids[:1].write(
|
self.product.last_purchase_price_received = 4.0
|
||||||
{
|
|
||||||
"last_purchase_price_received": 4.0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.product._compute_theoritical_price()
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
# Price should be calculated (4.0 * 1.5 = 6.0)
|
# Price should be rounded to nearest 0.05
|
||||||
price = self._get_theoretical_price(self.product)
|
price = self.product.list_price_theoritical
|
||||||
self.assertAlmostEqual(price, 6.0, places=2)
|
self.assertEqual(round(price % 0.05, 2), 0.0)
|
||||||
|
|
||||||
def test_last_purchase_price_updated_flag(self):
|
def test_last_purchase_price_updated_flag(self):
|
||||||
"""Test that the updated flag is set when prices differ"""
|
"""Test that the updated flag is set when prices differ"""
|
||||||
initial_list_price = self.product.list_price
|
initial_list_price = self.product.list_price
|
||||||
self.product.product_variant_ids[:1].write(
|
self.product.last_purchase_price_received = 10.0
|
||||||
{
|
|
||||||
"last_purchase_price_received": 10.0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.product._compute_theoritical_price()
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
if self._get_theoretical_price(self.product) != initial_list_price:
|
if self.product.list_price_theoritical != initial_list_price:
|
||||||
self.assertTrue(self._get_updated_flag(self.product))
|
self.assertTrue(self.product.last_purchase_price_updated)
|
||||||
|
|
||||||
def test_action_update_list_price(self):
|
def test_action_update_list_price(self):
|
||||||
"""Test updating list price from theoretical price"""
|
"""Test updating list price from theoretical price"""
|
||||||
self.product.product_variant_ids[:1].write(
|
self.product.last_purchase_price_received = 8.0
|
||||||
{
|
|
||||||
"last_purchase_price_received": 8.0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.product._compute_theoritical_price()
|
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()
|
self.product.action_update_list_price()
|
||||||
|
|
||||||
# Verify that list price was updated
|
# Verify that list price was updated
|
||||||
self.assertEqual(self.product.list_price, theoretical_price)
|
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):
|
def test_manual_update_type_skips_automatic(self):
|
||||||
"""Test that manual update type prevents automatic price calculation"""
|
"""Test that manual update type prevents automatic price calculation"""
|
||||||
self.product.product_variant_ids[:1].write(
|
self.product.last_purchase_price_compute_type = "manual_update"
|
||||||
{
|
|
||||||
"last_purchase_price_compute_type": "manual_update",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
initial_list_price = self.product.list_price
|
initial_list_price = self.product.list_price
|
||||||
|
|
||||||
self.product.action_update_list_price()
|
self.product.action_update_list_price()
|
||||||
|
|
@ -177,9 +126,7 @@ class TestProductTemplate(TransactionCase):
|
||||||
|
|
||||||
def test_price_compute_with_last_purchase_price(self):
|
def test_price_compute_with_last_purchase_price(self):
|
||||||
"""Test price_compute method with last_purchase_price type"""
|
"""Test price_compute method with last_purchase_price type"""
|
||||||
# price_compute is defined on product.product, not product.template
|
result = self.product.price_compute("last_purchase_price")
|
||||||
variant = self.product.product_variant_ids[:1]
|
|
||||||
result = variant.price_compute("last_purchase_price")
|
|
||||||
|
|
||||||
# Should return dummy prices (1.0) for all product ids
|
# Should return dummy prices (1.0) for all product ids
|
||||||
for product_id in result:
|
for product_id in result:
|
||||||
|
|
@ -189,7 +136,8 @@ class TestProductTemplate(TransactionCase):
|
||||||
"""Test that missing pricelist raises an error"""
|
"""Test that missing pricelist raises an error"""
|
||||||
# Remove pricelist configuration
|
# Remove pricelist configuration
|
||||||
self.env["ir.config_parameter"].sudo().set_param(
|
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):
|
with self.assertRaises(UserError):
|
||||||
|
|
@ -198,7 +146,7 @@ class TestProductTemplate(TransactionCase):
|
||||||
# Restore pricelist configuration for other tests
|
# Restore pricelist configuration for other tests
|
||||||
self.env["ir.config_parameter"].sudo().set_param(
|
self.env["ir.config_parameter"].sudo().set_param(
|
||||||
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
str(self.pricelist.id),
|
str(self.pricelist.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_company_dependent_fields(self):
|
def test_company_dependent_fields(self):
|
||||||
|
|
@ -213,58 +161,55 @@ class TestProductTemplate(TransactionCase):
|
||||||
self.assertTrue(field_theoritical.company_dependent)
|
self.assertTrue(field_theoritical.company_dependent)
|
||||||
self.assertTrue(field_updated.company_dependent)
|
self.assertTrue(field_updated.company_dependent)
|
||||||
self.assertTrue(field_compute_type.company_dependent)
|
self.assertTrue(field_compute_type.company_dependent)
|
||||||
|
|
||||||
def test_compute_theoritical_price_with_actual_purchase_price(self):
|
def test_compute_theoritical_price_with_actual_purchase_price(self):
|
||||||
"""Test that theoretical price is calculated correctly from last purchase price
|
"""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"""
|
This test simulates a real scenario where a product has a purchase price set"""
|
||||||
# Set a realistic purchase price
|
# Set a realistic purchase price
|
||||||
purchase_price = 10.50
|
purchase_price = 10.50
|
||||||
self.product.product_variant_ids[:1].write(
|
self.product.last_purchase_price_received = purchase_price
|
||||||
{
|
self.product.last_purchase_price_compute_type = "without_discounts"
|
||||||
"last_purchase_price_received": purchase_price,
|
|
||||||
"last_purchase_price_compute_type": "without_discounts",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compute theoretical price
|
# Compute theoretical price
|
||||||
self.product._compute_theoritical_price()
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
theoretical_price = self._get_theoretical_price(self.product)
|
|
||||||
|
|
||||||
# Verify price is not zero
|
# Verify price is not zero
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
theoretical_price,
|
self.product.list_price_theoritical,
|
||||||
0.0,
|
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
|
# Verify price calculation is correct
|
||||||
# Expected: 10.50 * 1.50 (50% markup) = 15.75
|
# Expected: 10.50 * 1.50 (50% markup) = 15.75
|
||||||
# Price is WITHOUT tax (taxes added automatically on sales)
|
# Plus 21% tax: 15.75 * 1.21 = 19.0575
|
||||||
expected_price = purchase_price * 1.50 # 15.75
|
# 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
|
# Allow some tolerance for rounding
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
theoretical_price,
|
self.product.list_price_theoritical,
|
||||||
expected_price,
|
expected_with_tax,
|
||||||
delta=0.10,
|
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):
|
def test_compute_price_zero_purchase_price(self):
|
||||||
"""Test behavior when last_purchase_price_received is 0.0"""
|
"""Test behavior when last_purchase_price_received is 0.0"""
|
||||||
self.product.product_variant_ids[:1].write(
|
self.product.last_purchase_price_received = 0.0
|
||||||
{
|
|
||||||
"last_purchase_price_received": 0.0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.product._compute_theoritical_price()
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
# When purchase price is 0, theoretical price should also be 0
|
# When purchase price is 0, theoretical price should also be 0
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self._get_theoretical_price(self.product),
|
self.product.list_price_theoritical,
|
||||||
0.0,
|
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):
|
def test_pricelist_item_base_field(self):
|
||||||
|
|
@ -272,7 +217,7 @@ class TestProductTemplate(TransactionCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.pricelist_item.base,
|
self.pricelist_item.base,
|
||||||
"last_purchase_price",
|
"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
|
# Verify the base field is properly configured
|
||||||
|
|
|
||||||
|
|
@ -12,87 +12,71 @@ class TestStockMove(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create tax
|
# Create tax
|
||||||
cls.tax = cls.env["account.tax"].create(
|
cls.tax = cls.env["account.tax"].create({
|
||||||
{
|
"name": "Test Tax 21%",
|
||||||
"name": "Test Tax 21%",
|
"amount": 21.0,
|
||||||
"amount": 21.0,
|
"amount_type": "percent",
|
||||||
"amount_type": "percent",
|
"type_tax_use": "sale",
|
||||||
"type_tax_use": "sale",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create pricelist
|
# Create pricelist
|
||||||
cls.pricelist = cls.env["product.pricelist"].create(
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
{
|
"name": "Test Pricelist",
|
||||||
"name": "Test Pricelist",
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
"currency_id": cls.env.company.currency_id.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.pricelist_item = cls.env["product.pricelist.item"].create(
|
cls.pricelist_item = cls.env["product.pricelist.item"].create({
|
||||||
{
|
"pricelist_id": cls.pricelist.id,
|
||||||
"pricelist_id": cls.pricelist.id,
|
"compute_price": "formula",
|
||||||
"compute_price": "formula",
|
"base": "last_purchase_price",
|
||||||
"base": "last_purchase_price",
|
"price_markup": 50.0,
|
||||||
"price_markup": 50.0,
|
"applied_on": "3_global",
|
||||||
"applied_on": "3_global",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.env["ir.config_parameter"].sudo().set_param(
|
cls.env["ir.config_parameter"].sudo().set_param(
|
||||||
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
str(cls.pricelist.id),
|
str(cls.pricelist.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create supplier
|
# Create supplier
|
||||||
cls.supplier = cls.env["res.partner"].create(
|
cls.supplier = cls.env["res.partner"].create({
|
||||||
{
|
"name": "Test Supplier",
|
||||||
"name": "Test Supplier",
|
"supplier_rank": 1,
|
||||||
"supplier_rank": 1,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create product with UoM
|
# Create product with UoM
|
||||||
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
||||||
cls.uom_dozen = cls.env.ref("uom.product_uom_dozen")
|
cls.uom_dozen = cls.env.ref("uom.product_uom_dozen")
|
||||||
|
|
||||||
cls.product = cls.env["product.product"].create(
|
cls.product = cls.env["product.product"].create({
|
||||||
{
|
"name": "Test Product Stock",
|
||||||
"name": "Test Product Stock",
|
"type": "product",
|
||||||
"uom_id": cls.uom_unit.id,
|
"uom_id": cls.uom_unit.id,
|
||||||
"uom_po_id": cls.uom_unit.id,
|
"uom_po_id": cls.uom_unit.id,
|
||||||
"list_price": 10.0,
|
"list_price": 10.0,
|
||||||
"standard_price": 5.0,
|
"standard_price": 5.0,
|
||||||
"taxes_id": [(6, 0, [cls.tax.id])],
|
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||||
"last_purchase_price_received": 5.0,
|
"last_purchase_price_received": 5.0,
|
||||||
"last_purchase_price_compute_type": "without_discounts",
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create locations
|
# Create locations
|
||||||
cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
|
cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
|
||||||
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||||
|
|
||||||
def _create_purchase_order(
|
def _create_purchase_order(self, product, qty, price, discount1=0, discount2=0, discount3=0):
|
||||||
self, product, qty, price, discount1=0, discount2=0, discount3=0
|
|
||||||
):
|
|
||||||
"""Helper to create a purchase order"""
|
"""Helper to create a purchase order"""
|
||||||
purchase_order = self.env["purchase.order"].create(
|
purchase_order = self.env["purchase.order"].create({
|
||||||
{
|
"partner_id": self.supplier.id,
|
||||||
"partner_id": self.supplier.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
po_line = self.env["purchase.order.line"].create(
|
po_line = self.env["purchase.order.line"].create({
|
||||||
{
|
"order_id": purchase_order.id,
|
||||||
"order_id": purchase_order.id,
|
"product_id": product.id,
|
||||||
"name": product.name or "Purchase Line",
|
"product_qty": qty,
|
||||||
"product_id": product.id,
|
"price_unit": price,
|
||||||
"product_qty": qty,
|
"product_uom": product.uom_po_id.id,
|
||||||
"price_unit": price,
|
})
|
||||||
"product_uom": product.uom_po_id.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add discounts if module supports it
|
# Add discounts if module supports it
|
||||||
if hasattr(po_line, "discount1"):
|
if hasattr(po_line, "discount1"):
|
||||||
|
|
@ -106,7 +90,9 @@ class TestStockMove(TransactionCase):
|
||||||
|
|
||||||
def test_update_price_without_discounts(self):
|
def test_update_price_without_discounts(self):
|
||||||
"""Test price update without discounts"""
|
"""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()
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
# Get the picking and process it
|
# Get the picking and process it
|
||||||
|
|
@ -119,49 +105,8 @@ class TestStockMove(TransactionCase):
|
||||||
picking.button_validate()
|
picking.button_validate()
|
||||||
|
|
||||||
# Verify price was updated
|
# Verify price was updated
|
||||||
self.product.invalidate_recordset()
|
|
||||||
self.assertEqual(self.product.last_purchase_price_received, 8.0)
|
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):
|
def test_update_price_with_first_discount(self):
|
||||||
"""Test price update with first discount only"""
|
"""Test price update with first discount only"""
|
||||||
if not hasattr(self.env["purchase.order.line"], "discount1"):
|
if not hasattr(self.env["purchase.order.line"], "discount1"):
|
||||||
|
|
@ -216,12 +161,7 @@ class TestStockMove(TransactionCase):
|
||||||
self.product.last_purchase_price_compute_type = "with_three_discounts"
|
self.product.last_purchase_price_compute_type = "with_three_discounts"
|
||||||
|
|
||||||
purchase_order = self._create_purchase_order(
|
purchase_order = self._create_purchase_order(
|
||||||
self.product,
|
self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0, discount3=5.0
|
||||||
qty=10,
|
|
||||||
price=10.0,
|
|
||||||
discount1=20.0,
|
|
||||||
discount2=10.0,
|
|
||||||
discount3=5.0,
|
|
||||||
)
|
)
|
||||||
purchase_order.button_confirm()
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
|
@ -239,23 +179,16 @@ class TestStockMove(TransactionCase):
|
||||||
def test_update_price_with_uom_conversion(self):
|
def test_update_price_with_uom_conversion(self):
|
||||||
"""Test price update with different purchase UoM"""
|
"""Test price update with different purchase UoM"""
|
||||||
# Create product with different purchase UoM
|
# Create product with different purchase UoM
|
||||||
product_dozen = self.product.copy(
|
product_dozen = self.product.copy({
|
||||||
{
|
"name": "Test Product Dozen",
|
||||||
"name": "Test Product Dozen",
|
"uom_po_id": self.uom_dozen.id,
|
||||||
"uom_po_id": self.uom_dozen.id,
|
})
|
||||||
"taxes_id": [(6, 0, [self.tax.id])],
|
|
||||||
"last_purchase_price_compute_type": "without_discounts",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
purchase_order = self._create_purchase_order(
|
purchase_order = self._create_purchase_order(
|
||||||
product_dozen, qty=2, price=120.0 # 2 dozens at 120.0 per dozen
|
product_dozen, qty=2, price=120.0 # 2 dozens at 120.0 per dozen
|
||||||
)
|
)
|
||||||
purchase_order.button_confirm()
|
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 = purchase_order.picking_ids[0]
|
||||||
picking.action_assign()
|
picking.action_assign()
|
||||||
|
|
||||||
|
|
@ -272,7 +205,9 @@ class TestStockMove(TransactionCase):
|
||||||
"""Test that price is not updated with zero quantity done"""
|
"""Test that price is not updated with zero quantity done"""
|
||||||
initial_price = self.product.last_purchase_price_received
|
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()
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
picking = purchase_order.picking_ids[0]
|
picking = purchase_order.picking_ids[0]
|
||||||
|
|
@ -289,8 +224,11 @@ class TestStockMove(TransactionCase):
|
||||||
def test_manual_update_type_no_automatic_update(self):
|
def test_manual_update_type_no_automatic_update(self):
|
||||||
"""Test that manual update type prevents automatic price updates"""
|
"""Test that manual update type prevents automatic price updates"""
|
||||||
self.product.last_purchase_price_compute_type = "manual_update"
|
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()
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
picking = purchase_order.picking_ids[0]
|
picking = purchase_order.picking_ids[0]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue