diff --git a/uom_rounding_coherence/README.rst b/uom_rounding_coherence/README.rst new file mode 100644 index 000000000..ea6faf83c --- /dev/null +++ b/uom_rounding_coherence/README.rst @@ -0,0 +1,111 @@ +====================== +UoM Rounding Coherence +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d646eddf3ef1d6ff89719e1651d153ab196194e731190ebbcaa2e135d9ff4a8e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-NuoBiT%2Fodoo--addons-lightgray.png?logo=github + :target: https://github.com/NuoBiT/odoo-addons/tree/18.0/uom_rounding_coherence + :alt: NuoBiT/odoo-addons + +|badge1| |badge2| |badge3| + +This module adds validation to ensure that Unit of Measure (UoM) +rounding precision is coherent with the conversion ratio to the +reference unit, preventing precision loss during conversions. + +When converting quantities between UoMs in the same category, a +non-reference UoM whose rounding is too coarse relative to its +conversion factor will lose precision beyond the reference UoM's +rounding. + +**Example Problem:** + +If the reference UoM has rounding 0.001 and a secondary UoM with ratio +1.141 has rounding 0.01, each conversion can introduce up to ±0.004 +error in reference units — enough to accumulate visible discrepancies +over multiple transactions. + +**The Validation:** + +The module checks that converting the UoM's rounding step to reference +units (rounding / factor) does not exceed the reference UoM's rounding. +This ensures data consistency and prevents precision-related errors. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +No configuration is needed. The module automatically validates UoM +rounding coherence when you create or modify UoMs. + +Usage +===== + +When creating or modifying Units of Measure: + +1. Go to *Inventory > Configuration > UoM Categories* +2. Create or edit a UoM +3. If the rounding precision is too coarse for the conversion ratio, the + system will show a validation error +4. The error message will indicate: + + - The UoM with the problem + - Its current rounding value + - The conversion factor + - The effective rounding in reference units + - The reference unit and its rounding + +5. To fix the error, either: + + - Decrease the rounding of the problematic UoM, or + - Increase the rounding of the reference UoM (if appropriate) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* NuoBiT Solutions SL + +Contributors +------------ + +- `NuoBiT `__: + + - Eric Antones eantones@nuobit.com + - Deniz Gallo dgallo@nuobit.com + +Maintainers +----------- + +This module is part of the `NuoBiT/odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/uom_rounding_coherence/__init__.py b/uom_rounding_coherence/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/uom_rounding_coherence/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/uom_rounding_coherence/__manifest__.py b/uom_rounding_coherence/__manifest__.py new file mode 100644 index 000000000..0116d20cd --- /dev/null +++ b/uom_rounding_coherence/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 NuoBiT Solutions SL - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "UoM Rounding Coherence", + "summary": "Validates that UoM rounding precision is coherent" + " with the conversion ratio to prevent precision loss", + "version": "18.0.1.0.0", + "author": "NuoBiT Solutions SL", + "website": "https://github.com/NuoBiT/odoo-addons", + "category": "Product", + "depends": ["uom"], + "license": "AGPL-3", +} diff --git a/uom_rounding_coherence/i18n/ca.po b/uom_rounding_coherence/i18n/ca.po new file mode 100644 index 000000000..667152d26 --- /dev/null +++ b/uom_rounding_coherence/i18n/ca.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * uom_rounding_coherence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-17 00:00+0000\n" +"PO-Revision-Date: 2026-02-17 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: uom_rounding_coherence +#: code:addons/uom_rounding_coherence/models/uom_uom.py:0 +#, python-format +msgid "" +"Cannot validate rounding coherence for category '%(category)s': expected " +"exactly one active reference unit of measure but found %(count)s." +msgstr "" +"No es pot validar la coherència d'arrodoniment per a la categoria " +"'%(category)s': s'esperava exactament una unitat de mesura de referència " +"activa però se n'han trobat %(count)s." + +#. module: uom_rounding_coherence +#: code:addons/uom_rounding_coherence/models/uom_uom.py:0 +#, python-format +msgid "" +"The rounding precision of '%(uom)s' (%(uom_rounding)s) is too coarse for " +"its conversion ratio (%(factor)s). When converted to the reference unit " +"'%(ref)s', the effective rounding becomes %(effective)s, which exceeds the " +"reference rounding of %(ref_rounding)s.\n" +"\n" +"To fix this, either decrease the rounding of '%(uom)s' or increase the " +"rounding of '%(ref)s'." +msgstr "" +"La precisió d'arrodoniment de '%(uom)s' (%(uom_rounding)s) és massa " +"gruixuda per al seu ràtio de conversió (%(factor)s). En convertir a la " +"unitat de referència '%(ref)s', l'arrodoniment efectiu és %(effective)s, " +"que excedeix l'arrodoniment de referència de %(ref_rounding)s.\n" +"\n" +"Per solucionar-ho, disminuïu l'arrodoniment de '%(uom)s' o augmenteu " +"l'arrodoniment de '%(ref)s'." diff --git a/uom_rounding_coherence/i18n/es.po b/uom_rounding_coherence/i18n/es.po new file mode 100644 index 000000000..474ff88d0 --- /dev/null +++ b/uom_rounding_coherence/i18n/es.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * uom_rounding_coherence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-17 00:00+0000\n" +"PO-Revision-Date: 2026-02-17 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: uom_rounding_coherence +#: code:addons/uom_rounding_coherence/models/uom_uom.py:0 +#, python-format +msgid "" +"Cannot validate rounding coherence for category '%(category)s': expected " +"exactly one active reference unit of measure but found %(count)s." +msgstr "" +"No se puede validar la coherencia de redondeo para la categoría " +"'%(category)s': se esperaba exactamente una unidad de medida de referencia " +"activa pero se encontraron %(count)s." + +#. module: uom_rounding_coherence +#: code:addons/uom_rounding_coherence/models/uom_uom.py:0 +#, python-format +msgid "" +"The rounding precision of '%(uom)s' (%(uom_rounding)s) is too coarse for " +"its conversion ratio (%(factor)s). When converted to the reference unit " +"'%(ref)s', the effective rounding becomes %(effective)s, which exceeds the " +"reference rounding of %(ref_rounding)s.\n" +"\n" +"To fix this, either decrease the rounding of '%(uom)s' or increase the " +"rounding of '%(ref)s'." +msgstr "" +"La precisión de redondeo de '%(uom)s' (%(uom_rounding)s) es demasiado " +"gruesa para su ratio de conversión (%(factor)s). Al convertir a la unidad " +"de referencia '%(ref)s', el redondeo efectivo es %(effective)s, que excede " +"el redondeo de referencia de %(ref_rounding)s.\n" +"\n" +"Para solucionarlo, disminuya el redondeo de '%(uom)s' o aumente el " +"redondeo de '%(ref)s'." diff --git a/uom_rounding_coherence/models/__init__.py b/uom_rounding_coherence/models/__init__.py new file mode 100644 index 000000000..7753e8e76 --- /dev/null +++ b/uom_rounding_coherence/models/__init__.py @@ -0,0 +1 @@ +from . import uom_uom diff --git a/uom_rounding_coherence/models/uom_uom.py b/uom_rounding_coherence/models/uom_uom.py new file mode 100644 index 000000000..f8c15d249 --- /dev/null +++ b/uom_rounding_coherence/models/uom_uom.py @@ -0,0 +1,83 @@ +# Copyright 2026 NuoBiT Solutions SL - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, models, tools +from odoo.exceptions import ValidationError + + +class UoM(models.Model): + _inherit = "uom.uom" + + @api.constrains("rounding", "factor", "uom_type", "category_id") + def _check_rounding_factor_coherence(self): + """Ensure UoM rounding is fine enough relative to the conversion ratio. + + When converting quantities between UoMs in the same category, + a non-reference UoM whose rounding is too coarse relative to + its conversion factor will lose precision beyond the reference + UoM's rounding. For example, if the reference UoM has rounding + 0.001 and a secondary UoM with ratio 1.141 has rounding 0.01, + each conversion can introduce up to ±0.004 error in reference + units — enough to accumulate visible discrepancies over + multiple transactions. + + The check: converting the UoM's rounding step to reference units + (rounding / factor) must not exceed the reference UoM's rounding. + """ + categories = self.mapped("category_id") + for category in categories: + uoms = self.env["uom.uom"].search( + [ + ("category_id", "=", category.id), + ("active", "=", True), + ] + ) + ref_uom = uoms.filtered(lambda u: u.uom_type == "reference") + if len(ref_uom) != 1: + raise ValidationError( + _( + "Cannot validate rounding coherence for " + "category '%(category)s': expected exactly one " + "active reference unit of measure but found " + "%(count)s." + ) + % { + "category": category.name, + "count": len(ref_uom), + } + ) + for uom in uoms - ref_uom: + # Convert this UoM's rounding to reference units + # Using the same formula as _compute_quantity: + # amount = qty / self.factor * to_unit.factor + # Since ref_uom.factor = 1.0: + # rounding_in_ref = uom.rounding / uom.factor + rounding_in_ref = uom.rounding / uom.factor + if ( + tools.float_compare( + rounding_in_ref, + ref_uom.rounding, + precision_rounding=ref_uom.rounding, + ) + > 0 + ): + raise ValidationError( + _( + "The rounding precision of '%(uom)s' (%(uom_rounding)s) " + "is too coarse for its conversion ratio (%(factor)s). " + "When converted to the reference unit '%(ref)s', " + "the effective rounding becomes %(effective)s, " + "which exceeds the reference rounding of " + "%(ref_rounding)s.\n\n" + "To fix this, either decrease the rounding of " + "'%(uom)s' or increase the rounding of '%(ref)s'." + ) + % { + "uom": uom.name, + "uom_rounding": uom.rounding, + "factor": uom.factor, + "ref": ref_uom.name, + "effective": rounding_in_ref, + "ref_rounding": ref_uom.rounding, + } + ) diff --git a/uom_rounding_coherence/pyproject.toml b/uom_rounding_coherence/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/uom_rounding_coherence/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/uom_rounding_coherence/readme/CONFIGURE.md b/uom_rounding_coherence/readme/CONFIGURE.md new file mode 100644 index 000000000..09d8c0726 --- /dev/null +++ b/uom_rounding_coherence/readme/CONFIGURE.md @@ -0,0 +1,2 @@ +No configuration is needed. The module automatically validates UoM +rounding coherence when you create or modify UoMs. diff --git a/uom_rounding_coherence/readme/CONTRIBUTORS.md b/uom_rounding_coherence/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..2f88e0877 --- /dev/null +++ b/uom_rounding_coherence/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [NuoBiT](https://www.nuobit.com): + - Eric Antones + - Deniz Gallo diff --git a/uom_rounding_coherence/readme/DESCRIPTION.md b/uom_rounding_coherence/readme/DESCRIPTION.md new file mode 100644 index 000000000..f35a92e54 --- /dev/null +++ b/uom_rounding_coherence/readme/DESCRIPTION.md @@ -0,0 +1,21 @@ +This module adds validation to ensure that Unit of Measure (UoM) +rounding precision is coherent with the conversion ratio to the +reference unit, preventing precision loss during conversions. + +When converting quantities between UoMs in the same category, a +non-reference UoM whose rounding is too coarse relative to its +conversion factor will lose precision beyond the reference UoM's +rounding. + +**Example Problem:** + +If the reference UoM has rounding 0.001 and a secondary UoM with ratio +1.141 has rounding 0.01, each conversion can introduce up to ±0.004 +error in reference units — enough to accumulate visible discrepancies +over multiple transactions. + +**The Validation:** + +The module checks that converting the UoM's rounding step to reference +units (rounding / factor) does not exceed the reference UoM's rounding. +This ensures data consistency and prevents precision-related errors. diff --git a/uom_rounding_coherence/readme/USAGE.md b/uom_rounding_coherence/readme/USAGE.md new file mode 100644 index 000000000..904cd2d9a --- /dev/null +++ b/uom_rounding_coherence/readme/USAGE.md @@ -0,0 +1,15 @@ +When creating or modifying Units of Measure: + +1. Go to *Inventory \> Configuration \> UoM Categories* +2. Create or edit a UoM +3. If the rounding precision is too coarse for the conversion ratio, + the system will show a validation error +4. The error message will indicate: + - The UoM with the problem + - Its current rounding value + - The conversion factor + - The effective rounding in reference units + - The reference unit and its rounding +5. To fix the error, either: + - Decrease the rounding of the problematic UoM, or + - Increase the rounding of the reference UoM (if appropriate) diff --git a/uom_rounding_coherence/static/description/icon.png b/uom_rounding_coherence/static/description/icon.png new file mode 100644 index 000000000..1cd641e79 Binary files /dev/null and b/uom_rounding_coherence/static/description/icon.png differ diff --git a/uom_rounding_coherence/static/description/index.html b/uom_rounding_coherence/static/description/index.html new file mode 100644 index 000000000..de3dc3734 --- /dev/null +++ b/uom_rounding_coherence/static/description/index.html @@ -0,0 +1,465 @@ + + + + + +UoM Rounding Coherence + + + +
+

UoM Rounding Coherence

+ + +

Beta License: AGPL-3 NuoBiT/odoo-addons

+

This module adds validation to ensure that Unit of Measure (UoM) +rounding precision is coherent with the conversion ratio to the +reference unit, preventing precision loss during conversions.

+

When converting quantities between UoMs in the same category, a +non-reference UoM whose rounding is too coarse relative to its +conversion factor will lose precision beyond the reference UoM’s +rounding.

+

Example Problem:

+

If the reference UoM has rounding 0.001 and a secondary UoM with ratio +1.141 has rounding 0.01, each conversion can introduce up to ±0.004 +error in reference units — enough to accumulate visible discrepancies +over multiple transactions.

+

The Validation:

+

The module checks that converting the UoM’s rounding step to reference +units (rounding / factor) does not exceed the reference UoM’s rounding. +This ensures data consistency and prevents precision-related errors.

+

Table of contents

+ +
+

Configuration

+

No configuration is needed. The module automatically validates UoM +rounding coherence when you create or modify UoMs.

+
+
+

Usage

+

When creating or modifying Units of Measure:

+
    +
  1. Go to Inventory > Configuration > UoM Categories
  2. +
  3. Create or edit a UoM
  4. +
  5. If the rounding precision is too coarse for the conversion ratio, the +system will show a validation error
  6. +
  7. The error message will indicate:
      +
    • The UoM with the problem
    • +
    • Its current rounding value
    • +
    • The conversion factor
    • +
    • The effective rounding in reference units
    • +
    • The reference unit and its rounding
    • +
    +
  8. +
  9. To fix the error, either:
      +
    • Decrease the rounding of the problematic UoM, or
    • +
    • Increase the rounding of the reference UoM (if appropriate)
    • +
    +
  10. +
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • NuoBiT Solutions SL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the NuoBiT/odoo-addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/uom_rounding_coherence/tests/__init__.py b/uom_rounding_coherence/tests/__init__.py new file mode 100644 index 000000000..5a50ffdb8 --- /dev/null +++ b/uom_rounding_coherence/tests/__init__.py @@ -0,0 +1 @@ +from . import test_uom_rounding_coherence diff --git a/uom_rounding_coherence/tests/test_uom_rounding_coherence.py b/uom_rounding_coherence/tests/test_uom_rounding_coherence.py new file mode 100644 index 000000000..5134025f8 --- /dev/null +++ b/uom_rounding_coherence/tests/test_uom_rounding_coherence.py @@ -0,0 +1,304 @@ +# Copyright 2026 NuoBiT Solutions SL- Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase + + +class TestUomRoundingCoherence(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.category = cls.env["uom.category"].create( + {"name": "Test Coherence Category"} + ) + cls.ref_uom = cls.env["uom.uom"].create( + { + "name": "Ref Unit", + "category_id": cls.category.id, + "uom_type": "reference", + "rounding": 0.001, + } + ) + + def test_coherent_rounding_passes(self): + """ + PRE: - A reference UoM exists with rounding 0.001 + ACT: - Create a smaller UoM with factor=1.141 and rounding=0.001 + POST: - The UoM is created without error because + effective rounding (0.001 / 1.141 ≈ 0.000877) <= ref rounding (0.001) + """ + # ARRANGE & ACT + self.env["uom.uom"].create( + { + "name": "Fine Unit", + "category_id": self.category.id, + "uom_type": "smaller", + "factor": 1.141, + "rounding": 0.001, + } + ) + + def test_coarse_rounding_fails(self): + """ + PRE: - A reference UoM exists with rounding 0.001 + ACT: - Create a smaller UoM with factor=1.141 and rounding=0.01 + POST: - ValidationError is raised because + effective rounding (0.01 / 1.141 ≈ 0.00877) > ref rounding (0.001) + """ + # ARRANGE & ACT & ASSERT + with self.assertRaises(ValidationError): + self.env["uom.uom"].create( + { + "name": "Coarse Unit", + "category_id": self.category.id, + "uom_type": "smaller", + "factor": 1.141, + "rounding": 0.01, + } + ) + + def test_borderline_rounding_passes(self): + """ + PRE: - A reference UoM exists with rounding 0.0001 + ACT: - Create a smaller UoM with factor=0.8086 and rounding=0.0001 + POST: - The UoM is created without error because + effective rounding (0.0001 / 0.8086 ≈ 0.0001237) is within + float_compare tolerance of ref rounding (0.0001) + """ + # ARRANGE + category = self.env["uom.category"].create({"name": "Test Borderline Category"}) + self.env["uom.uom"].create( + { + "name": "Ref Borderline", + "category_id": category.id, + "uom_type": "reference", + "rounding": 0.0001, + } + ) + # ACT + self.env["uom.uom"].create( + { + "name": "Borderline Unit", + "category_id": category.id, + "uom_type": "smaller", + "factor": 0.8086, + "rounding": 0.0001, + } + ) + + def test_update_rounding_to_coarse_fails(self): + """ + PRE: - A reference UoM exists with rounding 0.001 + - A smaller UoM exists with factor=1.141 and rounding=0.001 + ACT: - Update the smaller UoM rounding to 0.01 + POST: - ValidationError is raised because + effective rounding (0.01 / 1.141 ≈ 0.00877) > ref rounding (0.001) + """ + # ARRANGE + uom = self.env["uom.uom"].create( + { + "name": "Update Test Unit", + "category_id": self.category.id, + "uom_type": "smaller", + "factor": 1.141, + "rounding": 0.001, + } + ) + # ACT & ASSERT + with self.assertRaises(ValidationError): + uom.write({"rounding": 0.01}) + + def test_update_ref_rounding_to_finer_fails(self): + """ + PRE: - A reference UoM exists with rounding 0.01 + - A smaller UoM exists with factor=1.141 and rounding=0.01 + (effective rounding 0.01 / 1.141 ≈ 0.00877 <= 0.01: OK) + ACT: - Update the reference UoM rounding to 0.001 + POST: - ValidationError is raised because + effective rounding (0.01 / 1.141 ≈ 0.00877) > new ref rounding (0.001) + """ + # ARRANGE + category = self.env["uom.category"].create({"name": "Test Ref Update Category"}) + ref = self.env["uom.uom"].create( + { + "name": "Ref Updatable", + "category_id": category.id, + "uom_type": "reference", + "rounding": 0.01, + } + ) + self.env["uom.uom"].create( + { + "name": "Secondary", + "category_id": category.id, + "uom_type": "smaller", + "factor": 1.141, + "rounding": 0.01, + } + ) + # ACT & ASSERT + with self.assertRaises(ValidationError): + ref.write({"rounding": 0.001}) + + def test_bigger_uom_coarse_rounding_fails(self): + """ + PRE: - A reference UoM exists with rounding 0.001 + ACT: - Create a bigger UoM with factor=0.001 and rounding=1.0 + POST: - ValidationError is raised because + effective rounding (1.0 / 0.001 = 1000) >> ref rounding (0.001) + """ + # ARRANGE & ACT & ASSERT + with self.assertRaises(ValidationError): + self.env["uom.uom"].create( + { + "name": "Big Coarse Unit", + "category_id": self.category.id, + "uom_type": "bigger", + "factor": 0.001, + "rounding": 1.0, + } + ) + + def test_bigger_uom_coherent_rounding_passes(self): + """ + PRE: - A reference UoM exists with rounding 0.001 + ACT: - Create a bigger UoM with factor=0.001 and rounding=0.000001 + POST: - The UoM is created without error because + effective rounding (0.000001 / 0.001 = 0.001) <= ref rounding (0.001) + """ + # ARRANGE & ACT + self.env["uom.uom"].create( + { + "name": "Big Fine Unit", + "category_id": self.category.id, + "uom_type": "bigger", + "factor": 0.001, + "rounding": 0.000001, + } + ) + + def test_bigger_uom_integer_ratio_passes(self): + """ + PRE: - A reference UoM exists with rounding 0.001 + ACT: - Create a bigger UoM with factor=1/12 (dozen) and rounding=0.00001 + POST: - The UoM is created without error because + effective rounding + (0.00001 / 0.08333 ≈ 0.00012) <= ref rounding (0.001) + """ + # ARRANGE & ACT + self.env["uom.uom"].create( + { + "name": "Dozen", + "category_id": self.category.id, + "uom_type": "bigger", + "factor": 0.08333333333, # 1/12 + "rounding": 0.00001, + } + ) + + def test_reference_only_category_passes(self): + """ + PRE: - A category exists with only a reference UoM + ACT: - Update the reference UoM rounding + POST: - No error is raised because there are no non-reference + UoMs to validate against + """ + # ARRANGE + category = self.env["uom.category"].create( + {"name": "Test Reference Only Category"} + ) + ref = self.env["uom.uom"].create( + { + "name": "Ref Only", + "category_id": category.id, + "uom_type": "reference", + "rounding": 0.01, + } + ) + # ACT + ref.write({"rounding": 0.001}) + + def test_no_active_reference_uom_fails(self): + """ + PRE: - A category exists with a reference UoM and a smaller UoM + ACT: - Archive the reference UoM via SQL (bypassing ORM constraints) + and update the non-reference UoM rounding to trigger validation + POST: - ValidationError is raised because no active reference UoM + is found to validate against + """ + # ARRANGE + category = self.env["uom.category"].create( + {"name": "Test No Active Ref Category"} + ) + ref = self.env["uom.uom"].create( + { + "name": "Ref To Deactivate", + "category_id": category.id, + "uom_type": "reference", + "rounding": 0.001, + } + ) + non_ref = self.env["uom.uom"].create( + { + "name": "Orphan Unit", + "category_id": category.id, + "uom_type": "smaller", + "factor": 2.0, + "rounding": 0.001, + } + ) + self.env.cr.execute( + "UPDATE uom_uom SET active = FALSE WHERE id = %s", (ref.id,) + ) + self.env["uom.uom"].invalidate_model() + # ACT & ASSERT + with self.assertRaises(ValidationError): + non_ref.write({"rounding": 0.0001}) + + def test_multiple_active_reference_uoms_fails(self): + """ + PRE: - A category exists with a reference UoM and two smaller UoMs + ACT: - Change one smaller UoM to reference type via SQL + (bypassing ORM constraints) and update the other smaller + UoM rounding to trigger validation + POST: - ValidationError is raised because multiple active reference + UoMs are found + """ + # ARRANGE + category = self.env["uom.category"].create({"name": "Test Multi Ref Category"}) + self.env["uom.uom"].create( + { + "name": "Ref Original", + "category_id": category.id, + "uom_type": "reference", + "rounding": 0.001, + } + ) + to_corrupt = self.env["uom.uom"].create( + { + "name": "Unit To Corrupt", + "category_id": category.id, + "uom_type": "smaller", + "factor": 2.0, + "rounding": 0.001, + } + ) + another = self.env["uom.uom"].create( + { + "name": "Another Unit", + "category_id": category.id, + "uom_type": "smaller", + "factor": 3.0, + "rounding": 0.001, + } + ) + self.env.cr.execute( + "UPDATE uom_uom SET uom_type = 'reference', factor = 1.0 WHERE id = %s", + (to_corrupt.id,), + ) + self.env["uom.uom"].invalidate_model() + # ACT & ASSERT + with self.assertRaises(ValidationError): + another.write({"rounding": 0.0001})