diff --git a/.gitignore b/.gitignore index e03074a..a3957ae 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,3 @@ dmypy.json # Pyre type checker .pyre/ - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index beb2928..3e97523 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,142 +1,142 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: | - (?x) - # NOT INSTALLABLE ADDONS - # END NOT INSTALLABLE ADDONS - # Files and folders generated by bots, to avoid loops - /setup/|/README\.rst$|/static/description/index\.html$| - # Maybe reactivate this when all README files include prettier ignore tags? - ^README\.md$| - # Library files can have extraneous formatting (even minimized) - /static/(src/)?lib/| - # Repos using Sphinx to generate docs don't need prettying - ^docs/_templates/.*\.html$| - # You don't usually want a bot to modify your legal texts - (LICENSE.*|COPYING.*) + (?x) + # NOT INSTALLABLE ADDONS + # END NOT INSTALLABLE ADDONS + # Files and folders generated by bots, to avoid loops + /setup/|/README\.rst$|/static/description/index\.html$| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Library files can have extraneous formatting (even minimized) + /static/(src/)?lib/| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) default_language_version: - python: python3 - node: "16.17.0" + python: python3 + node: "16.17.0" repos: - - repo: local - hooks: - # These files are most likely copier diff rejection junks; if found, - # review them manually, fix the problem (if needed) and remove them - - id: forbidden-files - name: forbidden files - entry: found forbidden files; remove them - language: fail - files: "\\.rej$" - - repo: https://github.com/oca/maintainer-tools - rev: 71aa4caec15e8c1456b4da19e9f39aa0aa7377a9 - hooks: - # update the NOT INSTALLABLE ADDONS section above - - id: oca-update-pre-commit-excluded-addons - - repo: https://github.com/myint/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: ["-i", "--ignore-init-module-imports"] - - repo: https://github.com/psf/black - rev: 26.1.0 - hooks: - - id: black - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 - hooks: - - id: prettier - name: prettier + plugin-xml - additional_dependencies: - - "prettier@2.7.1" - - "@prettier/plugin-xml@2.2.0" - args: - - --plugin=@prettier/plugin-xml - files: \.(css|htm|html|js|json|json5|scss|toml|xml|yaml|yml)$ - - repo: https://github.com/pre-commit/mirrors-eslint - rev: v10.0.0 - hooks: - - id: eslint - verbose: true - args: - - --color - - --fix - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: trailing-whitespace - # exclude autogenerated files - exclude: /README\.rst$|\.pot?$ - - id: end-of-file-fixer - # exclude autogenerated files - exclude: /README\.rst$|\.pot?$ - - id: debug-statements - - id: check-case-conflict - - id: check-docstring-first - - id: check-executables-have-shebangs - - id: check-merge-conflict - # exclude files where underlines are not distinguishable from merge conflicts - exclude: /README\.rst$|^docs/.*\.rst$ - - id: check-symlinks - - id: check-xml - - id: mixed-line-ending - args: ["--fix=lf"] - - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 - hooks: - - id: pyupgrade - args: ["--py38-plus"] - - repo: https://github.com/PyCQA/isort - rev: 7.0.0 - hooks: - - id: isort - name: isort except __init__.py - args: - - --settings=. - exclude: /__init__\.py$ - # setuptools-odoo deshabilitado temporalmente (no soporta Odoo 18.0) - # - repo: https://github.com/acsone/setuptools-odoo - # rev: 3.3.2 - # hooks: - # - id: setuptools-odoo-make-default - # - id: setuptools-odoo-get-requirements - # args: - # - --output - # - requirements.txt - # - --header - # - "# generated from manifests external_dependencies" - - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - - id: flake8 - name: flake8 - additional_dependencies: ["flake8-bugbear==23.12.2"] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 - hooks: - - id: mypy - # do not run on test files or __init__ files (mypy does not support - # namespace packages) - exclude: (/tests/|/__init__\.py$) - additional_dependencies: - - "lxml" - - "odoo-stubs" - - "types-python-dateutil" - - "types-pytz" - - "types-requests" - - "types-setuptools" - - repo: https://github.com/PyCQA/pylint - rev: v4.0.4 - hooks: - - id: pylint - name: pylint with optional checks - args: - - --rcfile=.pylintrc - - --exit-zero - verbose: true - additional_dependencies: &pylint_deps - - pylint-odoo==10.0.0 - - id: pylint - name: pylint with mandatory checks - args: - - --rcfile=.pylintrc-mandatory - additional_dependencies: *pylint_deps + - repo: local + hooks: + # These files are most likely copier diff rejection junks; if found, + # review them manually, fix the problem (if needed) and remove them + - id: forbidden-files + name: forbidden files + entry: found forbidden files; remove them + language: fail + files: "\\.rej$" + - repo: https://github.com/oca/maintainer-tools + rev: 71aa4caec15e8c1456b4da19e9f39aa0aa7377a9 + hooks: + # update the NOT INSTALLABLE ADDONS section above + - id: oca-update-pre-commit-excluded-addons + - repo: https://github.com/myint/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + args: ["-i", "--ignore-init-module-imports"] + - repo: https://github.com/psf/black + rev: 26.1.0 + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + name: prettier + plugin-xml + additional_dependencies: + - "prettier@2.7.1" + - "@prettier/plugin-xml@2.2.0" + args: + - --plugin=@prettier/plugin-xml + files: \.(css|htm|html|js|json|json5|scss|toml|xml|yaml|yml)$ + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v10.0.0 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + # exclude autogenerated files + exclude: /README\.rst$|\.pot?$ + - id: end-of-file-fixer + # exclude autogenerated files + exclude: /README\.rst$|\.pot?$ + - id: debug-statements + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + # exclude files where underlines are not distinguishable from merge conflicts + exclude: /README\.rst$|^docs/.*\.rst$ + - id: check-symlinks + - id: check-xml + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ["--py38-plus"] + - repo: https://github.com/PyCQA/isort + rev: 7.0.0 + hooks: + - id: isort + name: isort except __init__.py + args: + - --settings=. + exclude: /__init__\.py$ + # setuptools-odoo deshabilitado temporalmente (no soporta Odoo 18.0) + # - repo: https://github.com/acsone/setuptools-odoo + # rev: 3.3.2 + # hooks: + # - id: setuptools-odoo-make-default + # - id: setuptools-odoo-get-requirements + # args: + # - --output + # - requirements.txt + # - --header + # - "# generated from manifests external_dependencies" + - repo: https://github.com/PyCQA/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + name: flake8 + additional_dependencies: ["flake8-bugbear==23.12.2"] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.19.1 + hooks: + - id: mypy + # do not run on test files or __init__ files (mypy does not support + # namespace packages) + exclude: (/tests/|/__init__\.py$) + additional_dependencies: + - "lxml" + - "odoo-stubs" + - "types-python-dateutil" + - "types-pytz" + - "types-requests" + - "types-setuptools" + - repo: https://github.com/PyCQA/pylint + rev: v4.0.4 + hooks: + - id: pylint + name: pylint with optional checks + args: + - --rcfile=.pylintrc + - --exit-zero + verbose: true + additional_dependencies: &pylint_deps + - pylint-odoo==10.0.0 + - id: pylint + name: pylint with mandatory checks + args: + - --rcfile=.pylintrc-mandatory + additional_dependencies: *pylint_deps diff --git a/account_invoice_triple_discount_readonly/tests/test_account_move.py b/account_invoice_triple_discount_readonly/tests/test_account_move.py index 4b86fa5..a84ff41 100644 --- a/account_invoice_triple_discount_readonly/tests/test_account_move.py +++ b/account_invoice_triple_discount_readonly/tests/test_account_move.py @@ -10,72 +10,88 @@ class TestAccountMove(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create a partner - cls.partner = cls.env["res.partner"].create({ - "name": "Test Customer", - "email": "customer@test.com", - }) - + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Customer", + "email": "customer@test.com", + } + ) + # Create a product - cls.product = cls.env["product.product"].create({ - "name": "Test Product Invoice", - "type": "consu", - "list_price": 200.0, - "standard_price": 100.0, - }) - + cls.product = cls.env["product.product"].create( + { + "name": "Test Product Invoice", + "type": "consu", + "list_price": 200.0, + "standard_price": 100.0, + } + ) + # Create tax - cls.tax = cls.env["account.tax"].create({ - "name": "Test Tax 10%", - "amount": 10.0, - "amount_type": "percent", - "type_tax_use": "sale", - }) - + cls.tax = cls.env["account.tax"].create( + { + "name": "Test Tax 10%", + "amount": 10.0, + "amount_type": "percent", + "type_tax_use": "sale", + } + ) + # Create an invoice - cls.invoice = cls.env["account.move"].create({ - "move_type": "out_invoice", - "partner_id": cls.partner.id, - "invoice_date": "2026-01-01", - }) - + cls.invoice = cls.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": cls.partner.id, + "invoice_date": "2026-01-01", + } + ) + # Create invoice line - cls.invoice_line = cls.env["account.move.line"].create({ - "move_id": cls.invoice.id, - "product_id": cls.product.id, - "quantity": 5, - "price_unit": 200.0, - "discount1": 10.0, - "discount2": 5.0, - "discount3": 2.0, - "tax_ids": [(6, 0, [cls.tax.id])], - }) + cls.invoice_line = cls.env["account.move.line"].create( + { + "move_id": cls.invoice.id, + "product_id": cls.product.id, + "quantity": 5, + "price_unit": 200.0, + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + "tax_ids": [(6, 0, [cls.tax.id])], + } + ) def test_invoice_line_discount_readonly(self): """Test that discount field is readonly in invoice lines""" field = self.invoice_line._fields["discount"] - self.assertTrue(field.readonly, "Discount field should be readonly in invoice lines") + self.assertTrue( + field.readonly, "Discount field should be readonly in invoice lines" + ) def test_invoice_line_write_with_explicit_discounts(self): """Test writing invoice line with explicit discounts""" - self.invoice_line.write({ - "discount": 30.0, # Should be ignored - "discount1": 15.0, - "discount2": 10.0, - "discount3": 5.0, - }) - + self.invoice_line.write( + { + "discount": 30.0, # Should be ignored + "discount1": 15.0, + "discount2": 10.0, + "discount3": 5.0, + } + ) + self.assertEqual(self.invoice_line.discount1, 15.0) self.assertEqual(self.invoice_line.discount2, 10.0) self.assertEqual(self.invoice_line.discount3, 5.0) def test_invoice_line_legacy_discount(self): """Test legacy discount behavior in invoice lines""" - self.invoice_line.write({ - "discount": 20.0, - }) - + self.invoice_line.write( + { + "discount": 20.0, + } + ) + # Should map to discount1 and reset others self.assertEqual(self.invoice_line.discount1, 20.0) self.assertEqual(self.invoice_line.discount2, 0.0) @@ -83,12 +99,14 @@ class TestAccountMove(TransactionCase): def test_invoice_line_price_calculation(self): """Test that price subtotal is calculated correctly with triple discount""" - self.invoice_line.write({ - "discount1": 10.0, - "discount2": 5.0, - "discount3": 0.0, - }) - + self.invoice_line.write( + { + "discount1": 10.0, + "discount2": 5.0, + "discount3": 0.0, + } + ) + # Base: 5 * 200 = 1000 # After 10% discount: 900 # After 5% discount: 855 @@ -99,17 +117,19 @@ class TestAccountMove(TransactionCase): def test_multiple_invoice_lines(self): """Test multiple invoice lines with different discounts""" - line2 = self.env["account.move.line"].create({ - "move_id": self.invoice.id, - "product_id": self.product.id, - "quantity": 3, - "price_unit": 150.0, - "discount1": 20.0, - "discount2": 10.0, - "discount3": 5.0, - "tax_ids": [(6, 0, [self.tax.id])], - }) - + line2 = self.env["account.move.line"].create( + { + "move_id": self.invoice.id, + "product_id": self.product.id, + "quantity": 3, + "price_unit": 150.0, + "discount1": 20.0, + "discount2": 10.0, + "discount3": 5.0, + "tax_ids": [(6, 0, [self.tax.id])], + } + ) + # Verify both lines have correct discounts self.assertEqual(self.invoice_line.discount1, 10.0) self.assertEqual(line2.discount1, 20.0) @@ -120,11 +140,13 @@ class TestAccountMove(TransactionCase): """Test updating quantity doesn't affect discounts""" initial_discount1 = self.invoice_line.discount1 initial_discount2 = self.invoice_line.discount2 - - self.invoice_line.write({ - "quantity": 10, - }) - + + self.invoice_line.write( + { + "quantity": 10, + } + ) + # Discounts should remain unchanged self.assertEqual(self.invoice_line.discount1, initial_discount1) self.assertEqual(self.invoice_line.discount2, initial_discount2) @@ -134,11 +156,13 @@ class TestAccountMove(TransactionCase): def test_invoice_line_update_price(self): """Test updating price doesn't affect discounts""" initial_discount1 = self.invoice_line.discount1 - - self.invoice_line.write({ - "price_unit": 250.0, - }) - + + self.invoice_line.write( + { + "price_unit": 250.0, + } + ) + # Discount should remain unchanged self.assertEqual(self.invoice_line.discount1, initial_discount1) # Price should be updated @@ -146,18 +170,20 @@ class TestAccountMove(TransactionCase): def test_invoice_with_zero_discounts(self): """Test invoice line with all zero discounts""" - self.invoice_line.write({ - "discount1": 0.0, - "discount2": 0.0, - "discount3": 0.0, - }) - + self.invoice_line.write( + { + "discount1": 0.0, + "discount2": 0.0, + "discount3": 0.0, + } + ) + # All discounts should be zero self.assertEqual(self.invoice_line.discount, 0.0) self.assertEqual(self.invoice_line.discount1, 0.0) self.assertEqual(self.invoice_line.discount2, 0.0) self.assertEqual(self.invoice_line.discount3, 0.0) - + # Subtotal should be quantity * price expected = 5 * 200 self.assertEqual(self.invoice_line.price_subtotal, expected) @@ -165,23 +191,23 @@ class TestAccountMove(TransactionCase): def test_invoice_line_combined_operations(self): """Test combined operations on invoice line""" # Update multiple fields at once - self.invoice_line.write({ - "quantity": 8, - "price_unit": 180.0, - "discount1": 12.0, - "discount2": 6.0, - "discount3": 0.0, # Reset discount3 explicitly - }) - + self.invoice_line.write( + { + "quantity": 8, + "price_unit": 180.0, + "discount1": 12.0, + "discount2": 6.0, + "discount3": 0.0, # Reset discount3 explicitly + } + ) + # All fields should be updated correctly self.assertEqual(self.invoice_line.quantity, 8) self.assertEqual(self.invoice_line.price_unit, 180.0) self.assertEqual(self.invoice_line.discount1, 12.0) self.assertEqual(self.invoice_line.discount2, 6.0) self.assertEqual(self.invoice_line.discount3, 0.0) - + # Calculate expected subtotal: 8 * 180 * (1-0.12) * (1-0.06) expected = 8 * 180 * 0.88 * 0.94 - self.assertAlmostEqual( - self.invoice_line.price_subtotal, expected, places=2 - ) + self.assertAlmostEqual(self.invoice_line.price_subtotal, expected, places=2) diff --git a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py index fc22dd0..b3ecc47 100644 --- a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py +++ b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py @@ -10,37 +10,45 @@ class TestPurchaseOrder(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create a supplier - cls.supplier = cls.env["res.partner"].create({ - "name": "Test Supplier", - "email": "supplier@test.com", - "supplier_rank": 1, - }) - + cls.supplier = cls.env["res.partner"].create( + { + "name": "Test Supplier", + "email": "supplier@test.com", + "supplier_rank": 1, + } + ) + # Create a product - cls.product = cls.env["product.product"].create({ - "name": "Test Product PO", - "type": "product", - "list_price": 150.0, - "standard_price": 80.0, - }) - + cls.product = cls.env["product.product"].create( + { + "name": "Test Product PO", + "type": "product", + "list_price": 150.0, + "standard_price": 80.0, + } + ) + # Create a purchase order - cls.purchase_order = cls.env["purchase.order"].create({ - "partner_id": cls.supplier.id, - }) - + cls.purchase_order = cls.env["purchase.order"].create( + { + "partner_id": cls.supplier.id, + } + ) + # Create purchase order line - cls.po_line = cls.env["purchase.order.line"].create({ - "order_id": cls.purchase_order.id, - "product_id": cls.product.id, - "product_qty": 10, - "price_unit": 150.0, - "discount1": 10.0, - "discount2": 5.0, - "discount3": 2.0, - }) + cls.po_line = cls.env["purchase.order.line"].create( + { + "order_id": cls.purchase_order.id, + "product_id": cls.product.id, + "product_qty": 10, + "price_unit": 150.0, + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + } + ) def test_po_line_discount_readonly(self): """Test that discount field is readonly in PO lines""" @@ -49,23 +57,27 @@ class TestPurchaseOrder(TransactionCase): def test_po_line_write_with_explicit_discounts(self): """Test writing PO line with explicit discounts""" - self.po_line.write({ - "discount": 25.0, # Should be ignored - "discount1": 12.0, - "discount2": 8.0, - "discount3": 4.0, - }) - + self.po_line.write( + { + "discount": 25.0, # Should be ignored + "discount1": 12.0, + "discount2": 8.0, + "discount3": 4.0, + } + ) + self.assertEqual(self.po_line.discount1, 12.0) self.assertEqual(self.po_line.discount2, 8.0) self.assertEqual(self.po_line.discount3, 4.0) def test_po_line_legacy_discount(self): """Test legacy discount behavior in PO lines""" - self.po_line.write({ - "discount": 18.0, - }) - + self.po_line.write( + { + "discount": 18.0, + } + ) + # Should map to discount1 and reset others self.assertEqual(self.po_line.discount1, 18.0) self.assertEqual(self.po_line.discount2, 0.0) @@ -73,33 +85,35 @@ class TestPurchaseOrder(TransactionCase): def test_po_line_price_calculation(self): """Test that price subtotal is calculated correctly with triple discount""" - self.po_line.write({ - "discount1": 15.0, - "discount2": 10.0, - "discount3": 5.0, - }) - + self.po_line.write( + { + "discount1": 15.0, + "discount2": 10.0, + "discount3": 5.0, + } + ) + # Base: 10 * 150 = 1500 # After 15% discount: 1275 # After 10% discount: 1147.5 # After 5% discount: 1090.125 expected_subtotal = 10 * 150 * 0.85 * 0.90 * 0.95 - self.assertAlmostEqual( - self.po_line.price_subtotal, expected_subtotal, places=2 - ) + self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2) def test_multiple_po_lines(self): """Test multiple PO lines with different discounts""" - line2 = self.env["purchase.order.line"].create({ - "order_id": self.purchase_order.id, - "product_id": self.product.id, - "product_qty": 5, - "price_unit": 120.0, - "discount1": 20.0, - "discount2": 15.0, - "discount3": 10.0, - }) - + line2 = self.env["purchase.order.line"].create( + { + "order_id": self.purchase_order.id, + "product_id": self.product.id, + "product_qty": 5, + "price_unit": 120.0, + "discount1": 20.0, + "discount2": 15.0, + "discount3": 10.0, + } + ) + # Verify both lines have correct discounts self.assertEqual(self.po_line.discount1, 15.0) self.assertEqual(line2.discount1, 20.0) @@ -110,11 +124,13 @@ class TestPurchaseOrder(TransactionCase): """Test updating quantity doesn't affect discounts""" initial_discount1 = self.po_line.discount1 initial_discount2 = self.po_line.discount2 - - self.po_line.write({ - "product_qty": 20, - }) - + + self.po_line.write( + { + "product_qty": 20, + } + ) + # Discounts should remain unchanged self.assertEqual(self.po_line.discount1, initial_discount1) self.assertEqual(self.po_line.discount2, initial_discount2) @@ -124,11 +140,13 @@ class TestPurchaseOrder(TransactionCase): def test_po_line_update_price(self): """Test updating price doesn't affect discounts""" initial_discount1 = self.po_line.discount1 - - self.po_line.write({ - "price_unit": 200.0, - }) - + + self.po_line.write( + { + "price_unit": 200.0, + } + ) + # Discount should remain unchanged self.assertEqual(self.po_line.discount1, initial_discount1) # Price should be updated @@ -136,18 +154,20 @@ class TestPurchaseOrder(TransactionCase): def test_po_with_zero_discounts(self): """Test PO line with all zero discounts""" - self.po_line.write({ - "discount1": 0.0, - "discount2": 0.0, - "discount3": 0.0, - }) - + self.po_line.write( + { + "discount1": 0.0, + "discount2": 0.0, + "discount3": 0.0, + } + ) + # All discounts should be zero self.assertEqual(self.po_line.discount, 0.0) self.assertEqual(self.po_line.discount1, 0.0) self.assertEqual(self.po_line.discount2, 0.0) self.assertEqual(self.po_line.discount3, 0.0) - + # Subtotal should be quantity * price expected = 10 * 150 self.assertEqual(self.po_line.price_subtotal, expected) @@ -155,38 +175,40 @@ class TestPurchaseOrder(TransactionCase): def test_po_line_combined_operations(self): """Test combined operations on PO line""" # Update multiple fields at once - self.po_line.write({ - "product_qty": 15, - "price_unit": 175.0, - "discount1": 18.0, - "discount2": 12.0, - "discount3": 6.0, - }) - + self.po_line.write( + { + "product_qty": 15, + "price_unit": 175.0, + "discount1": 18.0, + "discount2": 12.0, + "discount3": 6.0, + } + ) + # All fields should be updated correctly self.assertEqual(self.po_line.product_qty, 15) self.assertEqual(self.po_line.price_unit, 175.0) self.assertEqual(self.po_line.discount1, 18.0) self.assertEqual(self.po_line.discount2, 12.0) self.assertEqual(self.po_line.discount3, 6.0) - + # Calculate expected subtotal expected = 15 * 175 * 0.82 * 0.88 * 0.94 - self.assertAlmostEqual( - self.po_line.price_subtotal, expected, places=2 - ) + self.assertAlmostEqual(self.po_line.price_subtotal, expected, places=2) def test_po_confirm_with_discounts(self): """Test confirming PO doesn't alter discounts""" - self.po_line.write({ - "discount1": 10.0, - "discount2": 5.0, - "discount3": 2.0, - }) - + self.po_line.write( + { + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + } + ) + # Confirm the purchase order self.purchase_order.button_confirm() - + # Discounts should remain unchanged after confirmation self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.po_line.discount2, 5.0) diff --git a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py index f4600a2..7e0ea7c 100644 --- a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py +++ b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py @@ -10,35 +10,43 @@ class TestTripleDiscountMixin(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create a partner - cls.partner = cls.env["res.partner"].create({ - "name": "Test Partner", - }) - + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Partner", + } + ) + # Create a product - cls.product = cls.env["product.product"].create({ - "name": "Test Product", - "type": "product", - "list_price": 100.0, - "standard_price": 50.0, - }) - + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "type": "product", + "list_price": 100.0, + "standard_price": 50.0, + } + ) + # Create a purchase order - cls.purchase_order = cls.env["purchase.order"].create({ - "partner_id": cls.partner.id, - }) - + cls.purchase_order = cls.env["purchase.order"].create( + { + "partner_id": cls.partner.id, + } + ) + # Create a purchase order line - cls.po_line = cls.env["purchase.order.line"].create({ - "order_id": cls.purchase_order.id, - "product_id": cls.product.id, - "product_qty": 10, - "price_unit": 100.0, - "discount1": 10.0, - "discount2": 5.0, - "discount3": 2.0, - }) + cls.po_line = cls.env["purchase.order.line"].create( + { + "order_id": cls.purchase_order.id, + "product_id": cls.product.id, + "product_qty": 10, + "price_unit": 100.0, + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + } + ) def test_discount_field_is_readonly(self): """Test that the discount field is readonly""" @@ -48,18 +56,20 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_with_explicit_discounts(self): """Test writing with explicit discount1, discount2, discount3""" # Write with explicit discounts - self.po_line.write({ - "discount": 20.0, # This should be ignored - "discount1": 15.0, - "discount2": 10.0, - "discount3": 5.0, - }) - + self.po_line.write( + { + "discount": 20.0, # This should be ignored + "discount1": 15.0, + "discount2": 10.0, + "discount3": 5.0, + } + ) + # Verify explicit discounts were applied self.assertEqual(self.po_line.discount1, 15.0) self.assertEqual(self.po_line.discount2, 10.0) self.assertEqual(self.po_line.discount3, 5.0) - + # The computed discount field should reflect the combined discounts # Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05)) expected_discount = 100 - (100 * 0.85 * 0.90 * 0.95) @@ -67,11 +77,13 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_only_discount1(self): """Test writing only discount1 explicitly""" - self.po_line.write({ - "discount": 25.0, # This should be ignored - "discount1": 20.0, - }) - + self.po_line.write( + { + "discount": 25.0, # This should be ignored + "discount1": 20.0, + } + ) + # Only discount1 should change self.assertEqual(self.po_line.discount1, 20.0) # Others should remain unchanged @@ -80,11 +92,13 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_only_discount2(self): """Test writing only discount2 explicitly""" - self.po_line.write({ - "discount": 30.0, # This should be ignored - "discount2": 12.0, - }) - + self.po_line.write( + { + "discount": 30.0, # This should be ignored + "discount2": 12.0, + } + ) + # Only discount2 should change self.assertEqual(self.po_line.discount2, 12.0) # Others should remain unchanged from previous test @@ -93,11 +107,13 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_only_discount3(self): """Test writing only discount3 explicitly""" - self.po_line.write({ - "discount": 35.0, # This should be ignored - "discount3": 8.0, - }) - + self.po_line.write( + { + "discount": 35.0, # This should be ignored + "discount3": 8.0, + } + ) + # Only discount3 should change self.assertEqual(self.po_line.discount3, 8.0) # Others should remain unchanged from previous tests @@ -107,17 +123,21 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_legacy_discount_only(self): """Test legacy behavior: writing only discount field""" # Reset to known state first - self.po_line.write({ - "discount1": 10.0, - "discount2": 5.0, - "discount3": 2.0, - }) - + self.po_line.write( + { + "discount1": 10.0, + "discount2": 5.0, + "discount3": 2.0, + } + ) + # Write only discount (legacy behavior) - self.po_line.write({ - "discount": 25.0, - }) - + self.po_line.write( + { + "discount": 25.0, + } + ) + # Should map to discount1 and reset others self.assertEqual(self.po_line.discount1, 25.0) self.assertEqual(self.po_line.discount2, 0.0) @@ -126,20 +146,24 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_multiple_times(self): """Test writing multiple times to ensure consistency""" # First write - self.po_line.write({ - "discount1": 10.0, - "discount2": 10.0, - }) - + self.po_line.write( + { + "discount1": 10.0, + "discount2": 10.0, + } + ) + self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.po_line.discount2, 10.0) - + # Second write - self.po_line.write({ - "discount": 5.0, - "discount3": 5.0, - }) - + self.po_line.write( + { + "discount": 5.0, + "discount3": 5.0, + } + ) + # discount3 should change, others remain self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.po_line.discount2, 10.0) @@ -147,12 +171,14 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_zero_discounts(self): """Test writing zero discounts""" - self.po_line.write({ - "discount1": 0.0, - "discount2": 0.0, - "discount3": 0.0, - }) - + self.po_line.write( + { + "discount1": 0.0, + "discount2": 0.0, + "discount3": 0.0, + } + ) + self.assertEqual(self.po_line.discount1, 0.0) self.assertEqual(self.po_line.discount2, 0.0) self.assertEqual(self.po_line.discount3, 0.0) @@ -161,18 +187,22 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_combined_scenario(self): """Test a realistic combined scenario""" # Initial state - self.po_line.write({ - "discount1": 15.0, - "discount2": 5.0, - "discount3": 0.0, - }) - + self.po_line.write( + { + "discount1": 15.0, + "discount2": 5.0, + "discount3": 0.0, + } + ) + # User tries to update discount field (should be ignored if explicit discounts present) - self.po_line.write({ - "discount": 50.0, - "discount1": 20.0, - }) - + self.po_line.write( + { + "discount": 50.0, + "discount1": 20.0, + } + ) + # discount1 should be updated, others unchanged self.assertEqual(self.po_line.discount1, 20.0) self.assertEqual(self.po_line.discount2, 5.0) @@ -180,12 +210,14 @@ class TestTripleDiscountMixin(TransactionCase): def test_discount_calculation_accuracy(self): """Test that discount calculation is accurate""" - self.po_line.write({ - "discount1": 10.0, - "discount2": 10.0, - "discount3": 10.0, - }) - + self.po_line.write( + { + "discount1": 10.0, + "discount2": 10.0, + "discount3": 10.0, + } + ) + # Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1 expected = 100 - (100 * 0.9 * 0.9 * 0.9) self.assertAlmostEqual(self.po_line.discount, expected, places=2) @@ -193,13 +225,15 @@ class TestTripleDiscountMixin(TransactionCase): def test_write_without_discount_field(self): """Test writing other fields without touching discount fields""" initial_discount1 = self.po_line.discount1 - + # Write other fields - self.po_line.write({ - "product_qty": 20, - "price_unit": 150.0, - }) - + self.po_line.write( + { + "product_qty": 20, + "price_unit": 150.0, + } + ) + # Discounts should remain unchanged self.assertEqual(self.po_line.discount1, initial_discount1) # But other fields should be updated diff --git a/product_price_category_supplier/BEFORE_AND_AFTER.md b/product_price_category_supplier/BEFORE_AND_AFTER.md index 5f3372c..cc4a1e5 100644 --- a/product_price_category_supplier/BEFORE_AND_AFTER.md +++ b/product_price_category_supplier/BEFORE_AND_AFTER.md @@ -1,7 +1,7 @@ # BEFORE & AFTER - Error Fixes -**Document**: Visual comparison of all changes made to fix installation errors -**Date**: 10 de febrero de 2026 +**Document**: Visual comparison of all changes made to fix installation errors +**Date**: 10 de febrero de 2026 **Status**: ✅ All fixed and working --- @@ -146,7 +146,7 @@ class ResPartner(models.Model): product_count = self.env['product.template'].search_count([ ('default_supplier_id', '=', self.id) ]) - + # ... rest of method ``` @@ -179,7 +179,7 @@ class ResPartner(models.Model): product_count = self.env['product.template'].search_count([ ('default_supplier_id', '=', self.id) ]) - + # ... rest of method ``` @@ -315,8 +315,8 @@ class WizardUpdateProductCategory(models.TransientModel): | `models/res_partner.py` | `_()` in field def | 2 `_()` calls | Removed | ✅ Fixed | | `models/wizard_update_product_category.py` | `_()` in field defs | 4 `_()` calls | Removed | ✅ Fixed | -**Total Changes**: 8 modifications across 3 files -**Total Errors Fixed**: 2 categories (XPath + Translation) +**Total Changes**: 8 modifications across 3 files +**Total Errors Fixed**: 2 categories (XPath + Translation) **Result**: ✅ **All fixed, addon working** --- @@ -325,11 +325,11 @@ class WizardUpdateProductCategory(models.TransientModel): ### Before (with errors): ``` -2026-02-10 16:17:56,252 47 INFO odoo odoo.modules.registry: module product_price_category_supplier: creating or updating database tables -2026-02-10 16:17:56,344 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/security/ir.model.access.csv -2026-02-10 16:17:56,351 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/views/res_partner_views.xml -2026-02-10 16:17:56,362 47 WARNING odoo odoo.modules.loading: Transient module states were reset -2026-02-10 16:17:56,362 47 ERROR odoo odoo.modules.registry: Failed to load registry +2026-02-10 16:17:56,252 47 INFO odoo odoo.modules.registry: module product_price_category_supplier: creating or updating database tables +2026-02-10 16:17:56,344 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/security/ir.model.access.csv +2026-02-10 16:17:56,351 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/views/res_partner_views.xml +2026-02-10 16:17:56,362 47 WARNING odoo odoo.modules.loading: Transient module states were reset +2026-02-10 16:17:56,362 47 ERROR odoo odoo.modules.registry: Failed to load registry 2026-02-10 16:17:56,362 47 CRITICAL odoo odoo.service.server: Failed to initialize database `odoo`. ❌ ParseError: while parsing /mnt/extra-addons/product_price_category_supplier/views/res_partner_views.xml:4 ``` @@ -365,6 +365,6 @@ In Odoo 18.0 partner form: --- -**Document Status**: ✅ Complete -**Last Updated**: 10 de febrero de 2026 +**Document Status**: ✅ Complete +**Last Updated**: 10 de febrero de 2026 **License**: AGPL-3.0 diff --git a/product_price_category_supplier/ERROR_FIX_REPORT.md b/product_price_category_supplier/ERROR_FIX_REPORT.md index 2e9c0de..f53306f 100644 --- a/product_price_category_supplier/ERROR_FIX_REPORT.md +++ b/product_price_category_supplier/ERROR_FIX_REPORT.md @@ -1,7 +1,7 @@ # ERROR FIX REPORT - product_price_category_supplier -**Date**: 10 de febrero de 2026 -**Status**: ✅ FIXED & VERIFIED +**Date**: 10 de febrero de 2026 +**Status**: ✅ FIXED & VERIFIED **Author**: GitHub Copilot --- @@ -57,7 +57,7 @@ Element '' cannot be located in **Warning Message**: ``` -2026-02-10 16:17:56,165 47 WARNING odoo odoo.tools.translate: no translation language detected, +2026-02-10 16:17:56,165 47 WARNING odoo odoo.tools.translate: no translation language detected, skipping translation ``` @@ -262,9 +262,9 @@ docker-compose exec -T odoo odoo -d odoo -i product_price_category_supplier --st ## Summary of Changes -**Total Files Modified**: 3 -**Total Changes**: 8 -**Status**: ✅ All Fixed & Tested +**Total Files Modified**: 3 +**Total Changes**: 8 +**Status**: ✅ All Fixed & Tested The addon is now **ready for production use** with proper: - ✅ View inheritance (correct XPath paths) @@ -275,5 +275,5 @@ The addon is now **ready for production use** with proper: --- -**Maintained by**: Criptomart | **License**: AGPL-3.0 +**Maintained by**: Criptomart | **License**: AGPL-3.0 **Last Updated**: 10 de febrero de 2026 diff --git a/product_price_category_supplier/INSTALLATION_COMPLETE.md b/product_price_category_supplier/INSTALLATION_COMPLETE.md index efb6cc5..ae72534 100644 --- a/product_price_category_supplier/INSTALLATION_COMPLETE.md +++ b/product_price_category_supplier/INSTALLATION_COMPLETE.md @@ -1,8 +1,8 @@ # INSTALLATION COMPLETE - product_price_category_supplier -**Status**: ✅ **ADDON SUCCESSFULLY INSTALLED** -**Date**: 10 de febrero de 2026 -**Version**: 18.0.1.0.0 +**Status**: ✅ **ADDON SUCCESSFULLY INSTALLED** +**Date**: 10 de febrero de 2026 +**Version**: 18.0.1.0.0 **License**: AGPL-3.0 --- @@ -11,11 +11,11 @@ El addon `product_price_category_supplier` ha sido creado, corregido y **instalado exitosamente** en tu instancia Odoo 18.0. -✅ **21 files created** -✅ **3 files fixed** (XPath errors & translation issues) -✅ **0 remaining errors** -✅ **Database tables created** -✅ **Translations loaded** (Spanish + Euskera) +✅ **21 files created** +✅ **3 files fixed** (XPath errors & translation issues) +✅ **0 remaining errors** +✅ **Database tables created** +✅ **Translations loaded** (Spanish + Euskera) --- @@ -29,7 +29,7 @@ El addon `product_price_category_supplier` ha sido creado, corregido y **instala ### Problem 2: Translation Warnings - **Issue**: Uso de `_()` en definiciones de campos causaba warnings al importar módulo - **Solution**: Removidos `_()` de field definitions (se extraen automáticamente) -- **Files**: +- **Files**: - `models/res_partner.py` (1 cambio) - `models/wizard_update_product_category.py` (4 cambios) @@ -177,22 +177,22 @@ python3 -m py_compile product_price_category_supplier/models/*.py ## Installation Output ``` -2026-02-10 16:21:04,843 69 INFO odoo odoo.modules.loading: +2026-02-10 16:21:04,843 69 INFO odoo odoo.modules.loading: loading product_price_category_supplier/security/ir.model.access.csv -2026-02-10 16:21:04,868 69 INFO odoo odoo.modules.loading: +2026-02-10 16:21:04,868 69 INFO odoo odoo.modules.loading: loading product_price_category_supplier/views/res_partner_views.xml -2026-02-10 16:21:04,875 69 INFO odoo odoo.modules.loading: +2026-02-10 16:21:04,875 69 INFO odoo odoo.modules.loading: loading product_price_category_supplier/views/wizard_update_product_category.xml -2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module: +2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module: module product_price_category_supplier: loading translation file ...eu.po -2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module: +2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module: module product_price_category_supplier: loading translation file ...es.po -2026-02-10 16:21:04,912 69 INFO odoo odoo.modules.loading: +2026-02-10 16:21:04,912 69 INFO odoo odoo.modules.loading: Module product_price_category_supplier loaded in 0.68s, 179 queries ✅ No errors @@ -294,7 +294,7 @@ If you need to: --- -**Status**: ✅ Production Ready -**Created**: 10 de febrero de 2026 -**License**: AGPL-3.0 +**Status**: ✅ Production Ready +**Created**: 10 de febrero de 2026 +**License**: AGPL-3.0 **Author**: Criptomart diff --git a/product_price_category_supplier/INSTALLATION_STATUS.md b/product_price_category_supplier/INSTALLATION_STATUS.md index 731436c..c53f792 100644 --- a/product_price_category_supplier/INSTALLATION_STATUS.md +++ b/product_price_category_supplier/INSTALLATION_STATUS.md @@ -1,8 +1,8 @@ # ✅ ADDON INSTALLATION STATUS REPORT -**Addon**: `product_price_category_supplier` -**Status**: ✅ **INSTALLED & WORKING** -**Date**: 10 de febrero de 2026 +**Addon**: `product_price_category_supplier` +**Status**: ✅ **INSTALLED & WORKING** +**Date**: 10 de febrero de 2026 **Installation Time**: 2 cycles (fixed errors on 2nd attempt) --- @@ -11,7 +11,7 @@ El addon `product_price_category_supplier` fue creado exitosamente para extender Odoo 18.0 con funcionalidad de categorías de precio por proveedor. -**Ciclo 1**: Error ParseError en XPath (vista XML) +**Ciclo 1**: Error ParseError en XPath (vista XML) **Ciclo 2**: ✅ Errores corregidos, addon instalado correctamente --- @@ -90,7 +90,7 @@ Error: Element '' cannot be loca - + ``` -**File**: `views/res_partner_views.xml` line 11 +**File**: `views/res_partner_views.xml` line 11 **Reason**: Odoo 18 partner form uses `sales_purchases` page name ### Fix 2: Field Name in Tree View @@ -98,7 +98,7 @@ Error: Element '' cannot be loca - + ``` -**File**: `views/res_partner_views.xml` line 27 +**File**: `views/res_partner_views.xml` line 27 **Reason**: Tree view uses `complete_name` as first field ### Fix 3: Remove _() from Partner Field @@ -108,7 +108,7 @@ Error: Element '' cannot be loca + string='Default Price Category', + help='Default price category for products from this supplier', ``` -**File**: `models/res_partner.py` lines 13-15 +**File**: `models/res_partner.py` lines 13-15 **Reason**: Automatic extraction, `_()` causes translation warnings ### Fix 4: Remove _() from Wizard Fields @@ -122,7 +122,7 @@ Error: Element '' cannot be loca + string='Price Category', + string='Number of Products', ``` -**File**: `models/wizard_update_product_category.py` lines 15, 21, 27, 34 +**File**: `models/wizard_update_product_category.py` lines 15, 21, 27, 34 **Reason**: Same as Fix 3 - automatic extraction by Odoo --- @@ -307,7 +307,7 @@ The addon is **production-ready** and fully functional. --- -**Created**: 10 de febrero de 2026 -**Status**: ✅ Installation Complete -**License**: AGPL-3.0 +**Created**: 10 de febrero de 2026 +**Status**: ✅ Installation Complete +**License**: AGPL-3.0 **Maintainer**: Criptomart diff --git a/product_price_category_supplier/QUICK_FIX_REFERENCE.md b/product_price_category_supplier/QUICK_FIX_REFERENCE.md index 5971b2f..9670cb6 100644 --- a/product_price_category_supplier/QUICK_FIX_REFERENCE.md +++ b/product_price_category_supplier/QUICK_FIX_REFERENCE.md @@ -1,7 +1,7 @@ # QUICK REFERENCE - Fixes Applied -**Date**: 10 de febrero de 2026 -**Addon**: product_price_category_supplier +**Date**: 10 de febrero de 2026 +**Addon**: product_price_category_supplier **Status**: ✅ All fixed --- diff --git a/product_price_category_supplier/TEST_REPORT.md b/product_price_category_supplier/TEST_REPORT.md index 29e2bda..a6eafa2 100644 --- a/product_price_category_supplier/TEST_REPORT.md +++ b/product_price_category_supplier/TEST_REPORT.md @@ -1,17 +1,17 @@ # TEST REPORT - product_price_category_supplier -**Date**: 10 de febrero de 2026 -**Status**: ✅ ALL TESTS PASSING -**Test Framework**: Odoo TransactionCase +**Date**: 10 de febrero de 2026 +**Status**: ✅ ALL TESTS PASSING +**Test Framework**: Odoo TransactionCase **Test Count**: 10 comprehensive tests --- ## Executive Summary -✅ **10/10 tests passing** (0 failures, 0 errors) -⏱️ **Execution time**: 0.35 seconds -📊 **Database queries**: 379 queries +✅ **10/10 tests passing** (0 failures, 0 errors) +⏱️ **Execution time**: 0.35 seconds +📊 **Database queries**: 379 queries 🎯 **Coverage**: All critical features tested --- @@ -33,8 +33,8 @@ ## Test Cases ### ✅ Test 01: Supplier Has Default Price Category Field -**Purpose**: Verify field existence and assignment -**Status**: PASSED +**Purpose**: Verify field existence and assignment +**Status**: PASSED **Verifies**: - `default_price_category_id` field exists on res.partner - Supplier can have category assigned @@ -43,8 +43,8 @@ --- ### ✅ Test 02: Action Opens Wizard -**Purpose**: Test wizard opening action -**Status**: PASSED +**Purpose**: Test wizard opening action +**Status**: PASSED **Verifies**: - Action type is `ir.actions.act_window` - Opens `wizard.update.product.category` model @@ -54,8 +54,8 @@ --- ### ✅ Test 03: Wizard Counts Products Correctly -**Purpose**: Verify product counting logic -**Status**: PASSED +**Purpose**: Verify product counting logic +**Status**: PASSED **Verifies**: - Wizard shows correct product count (3 for Supplier A) - Partner name displays correctly @@ -64,8 +64,8 @@ --- ### ✅ Test 04: Wizard Updates All Products -**Purpose**: Test bulk update functionality -**Status**: PASSED +**Purpose**: Test bulk update functionality +**Status**: PASSED **Verifies**: - All products from supplier get updated - Products from other suppliers remain unchanged @@ -83,8 +83,8 @@ Result: Products 1,2,3 now have Premium category --- ### ✅ Test 05: Wizard Handles No Products -**Purpose**: Test edge case - supplier with no products -**Status**: PASSED +**Purpose**: Test edge case - supplier with no products +**Status**: PASSED **Verifies**: - Warning notification displayed - No database errors @@ -93,8 +93,8 @@ Result: Products 1,2,3 now have Premium category --- ### ✅ Test 06: Customer Field Visibility -**Purpose**: Verify customers don't see price category -**Status**: PASSED +**Purpose**: Verify customers don't see price category +**Status**: PASSED **Verifies**: - Customer has `supplier_rank = 0` - No price category assigned to customer @@ -103,8 +103,8 @@ Result: Products 1,2,3 now have Premium category --- ### ✅ Test 07: Wizard Overwrites Existing Categories -**Purpose**: Test update behavior on pre-existing categories -**Status**: PASSED +**Purpose**: Test update behavior on pre-existing categories +**Status**: PASSED **Verifies**: - Existing categories get overwritten - No data loss or corruption @@ -120,8 +120,8 @@ Result: All products now Premium (overwritten) --- ### ✅ Test 08: Multiple Suppliers Independent Updates -**Purpose**: Test isolation between suppliers -**Status**: PASSED +**Purpose**: Test isolation between suppliers +**Status**: PASSED **Verifies**: - Updating Supplier A doesn't affect Supplier B products - Each supplier maintains independent category @@ -137,8 +137,8 @@ Both remain independent after updates --- ### ✅ Test 09: Wizard Readonly Fields -**Purpose**: Verify display field computations -**Status**: PASSED +**Purpose**: Verify display field computations +**Status**: PASSED **Verifies**: - `partner_name` computed from `partner_id.name` - Related fields work correctly @@ -147,8 +147,8 @@ Both remain independent after updates --- ### ✅ Test 10: Action Counts Products Correctly -**Purpose**: Verify product count accuracy -**Status**: PASSED +**Purpose**: Verify product count accuracy +**Status**: PASSED **Verifies**: - Manual count matches wizard count - Search logic is correct @@ -159,10 +159,10 @@ Both remain independent after updates ## Test Execution Results ``` -2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.stats: +2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.stats: product_price_category_supplier: 12 tests 0.35s 379 queries -2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.result: +2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.result: 0 failed, 0 error(s) of 10 tests when loading database 'odoo' ✅ Result: ALL TESTS PASSED @@ -267,11 +267,11 @@ All tests use `TransactionCase` which ensures: ## Code Quality Indicators -✅ **No test flakiness** - All tests pass consistently -✅ **Fast execution** - 0.35s for full suite -✅ **Good coverage** - All major features tested -✅ **Edge cases handled** - Empty suppliers, overwrites, isolation -✅ **Clear assertions** - Descriptive error messages +✅ **No test flakiness** - All tests pass consistently +✅ **Fast execution** - 0.35s for full suite +✅ **Good coverage** - All major features tested +✅ **Edge cases handled** - Empty suppliers, overwrites, isolation +✅ **Clear assertions** - Descriptive error messages --- @@ -307,6 +307,6 @@ All 10 tests passing with 0 failures and 0 errors confirms the addon is stable a --- -**Maintained by**: Criptomart -**License**: AGPL-3.0 +**Maintained by**: Criptomart +**License**: AGPL-3.0 **Last Updated**: 10 de febrero de 2026 diff --git a/product_price_category_supplier/VALIDATION.md b/product_price_category_supplier/VALIDATION.md index d3c5fd4..cdac99a 100644 --- a/product_price_category_supplier/VALIDATION.md +++ b/product_price_category_supplier/VALIDATION.md @@ -304,6 +304,6 @@ docker-compose exec -T odoo odoo -d odoo \ --- -**Status**: ✅ **IMPLEMENTACIÓN COMPLETA** -**Fecha**: 10 de febrero de 2026 +**Status**: ✅ **IMPLEMENTACIÓN COMPLETA** +**Fecha**: 10 de febrero de 2026 **Licencia**: AGPL-3.0 diff --git a/product_price_category_supplier/models/res_partner.py b/product_price_category_supplier/models/res_partner.py index e1e9e08..0eec36f 100644 --- a/product_price_category_supplier/models/res_partner.py +++ b/product_price_category_supplier/models/res_partner.py @@ -1,18 +1,21 @@ # Copyright 2026 Your Company # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models +from odoo import _ +from odoo import api +from odoo import fields +from odoo import models class ResPartner(models.Model): """Extend res.partner with default price category for suppliers.""" - _inherit = 'res.partner' + _inherit = "res.partner" default_price_category_id = fields.Many2one( - comodel_name='product.price.category', - string='Default Price Category', - help='Default price category for products from this supplier', + comodel_name="product.price.category", + string="Default Price Category", + help="Default price category for products from this supplier", domain=[], ) @@ -21,24 +24,26 @@ class ResPartner(models.Model): self.ensure_one() # Count products where this partner is the default supplier - product_count = self.env['product.template'].search_count([ - ('main_seller_id', '=', self.id) - ]) + product_count = self.env["product.template"].search_count( + [("main_seller_id", "=", self.id)] + ) # Create wizard record with context data - wizard = self.env['wizard.update.product.category'].create({ - 'partner_id': self.id, - 'partner_name': self.name, - 'price_category_id': self.default_price_category_id.id, - 'product_count': product_count, - }) + wizard = self.env["wizard.update.product.category"].create( + { + "partner_id": self.id, + "partner_name": self.name, + "price_category_id": self.default_price_category_id.id, + "product_count": product_count, + } + ) # Return action to open wizard modal return { - 'type': 'ir.actions.act_window', - 'name': _('Update Product Price Category'), - 'res_model': 'wizard.update.product.category', - 'res_id': wizard.id, - 'view_mode': 'form', - 'target': 'new', + "type": "ir.actions.act_window", + "name": _("Update Product Price Category"), + "res_model": "wizard.update.product.category", + "res_id": wizard.id, + "view_mode": "form", + "target": "new", } diff --git a/product_price_category_supplier/models/wizard_update_product_category.py b/product_price_category_supplier/models/wizard_update_product_category.py index d49c421..d7ca3f0 100644 --- a/product_price_category_supplier/models/wizard_update_product_category.py +++ b/product_price_category_supplier/models/wizard_update_product_category.py @@ -1,37 +1,40 @@ # Copyright 2026 Your Company # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models +from odoo import _ +from odoo import api +from odoo import fields +from odoo import models class WizardUpdateProductCategory(models.TransientModel): """Wizard to confirm and bulk update product price categories.""" - _name = 'wizard.update.product.category' - _description = 'Update Product Price Category' + _name = "wizard.update.product.category" + _description = "Update Product Price Category" partner_id = fields.Many2one( - comodel_name='res.partner', - string='Supplier', + comodel_name="res.partner", + string="Supplier", readonly=True, required=True, ) partner_name = fields.Char( - string='Supplier Name', + string="Supplier Name", readonly=True, - related='partner_id.name', + related="partner_id.name", ) price_category_id = fields.Many2one( - comodel_name='product.price.category', - string='Price Category', + comodel_name="product.price.category", + string="Price Category", readonly=True, required=True, ) product_count = fields.Integer( - string='Number of Products', + string="Number of Products", readonly=True, required=True, ) @@ -41,36 +44,33 @@ class WizardUpdateProductCategory(models.TransientModel): self.ensure_one() # Search all products where this partner is the default supplier - products = self.env['product.template'].search([ - ('main_seller_id', '=', self.partner_id.id) - ]) + products = self.env["product.template"].search( + [("main_seller_id", "=", self.partner_id.id)] + ) if not products: return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('No Products'), - 'message': _('No products found with this supplier.'), - 'type': 'warning', - 'sticky': False, - } + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("No Products"), + "message": _("No products found with this supplier."), + "type": "warning", + "sticky": False, + }, } # Bulk update all products - products.write({ - 'price_category_id': self.price_category_id.id - }) + products.write({"price_category_id": self.price_category_id.id}) return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Success'), - 'message': _( - '%d products updated with category "%s".' - ) % (len(products), self.price_category_id.display_name), - 'type': 'success', - 'sticky': False, - } + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Success"), + "message": _('%d products updated with category "%s".') + % (len(products), self.price_category_id.display_name), + "type": "success", + "sticky": False, + }, } diff --git a/product_price_category_supplier/tests/test_product_price_category_supplier.py b/product_price_category_supplier/tests/test_product_price_category_supplier.py index c65df85..52e7717 100644 --- a/product_price_category_supplier/tests/test_product_price_category_supplier.py +++ b/product_price_category_supplier/tests/test_product_price_category_supplier.py @@ -1,8 +1,8 @@ # Copyright 2026 Your Company # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo.tests.common import TransactionCase from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase class TestProductPriceCategorySupplier(TransactionCase): @@ -14,68 +14,88 @@ class TestProductPriceCategorySupplier(TransactionCase): super().setUpClass() # Create price categories - cls.category_premium = cls.env['product.price.category'].create({ - 'name': 'Premium', - }) - cls.category_standard = cls.env['product.price.category'].create({ - 'name': 'Standard', - }) + cls.category_premium = cls.env["product.price.category"].create( + { + "name": "Premium", + } + ) + cls.category_standard = cls.env["product.price.category"].create( + { + "name": "Standard", + } + ) # Create suppliers - cls.supplier_a = cls.env['res.partner'].create({ - 'name': 'Supplier A', - 'supplier_rank': 1, - 'default_price_category_id': cls.category_premium.id, - }) - cls.supplier_b = cls.env['res.partner'].create({ - 'name': 'Supplier B', - 'supplier_rank': 1, - 'default_price_category_id': cls.category_standard.id, - }) + cls.supplier_a = cls.env["res.partner"].create( + { + "name": "Supplier A", + "supplier_rank": 1, + "default_price_category_id": cls.category_premium.id, + } + ) + cls.supplier_b = cls.env["res.partner"].create( + { + "name": "Supplier B", + "supplier_rank": 1, + "default_price_category_id": cls.category_standard.id, + } + ) # Create a non-supplier partner - cls.customer = cls.env['res.partner'].create({ - 'name': 'Customer A', - 'customer_rank': 1, - 'supplier_rank': 0, - }) + cls.customer = cls.env["res.partner"].create( + { + "name": "Customer A", + "customer_rank": 1, + "supplier_rank": 0, + } + ) # Create products with supplier A as default - cls.product_1 = cls.env['product.template'].create({ - 'name': 'Product 1', - 'default_supplier_id': cls.supplier_a.id, - }) - cls.product_2 = cls.env['product.template'].create({ - 'name': 'Product 2', - 'default_supplier_id': cls.supplier_a.id, - }) - cls.product_3 = cls.env['product.template'].create({ - 'name': 'Product 3', - 'default_supplier_id': cls.supplier_a.id, - }) + cls.product_1 = cls.env["product.template"].create( + { + "name": "Product 1", + "default_supplier_id": cls.supplier_a.id, + } + ) + cls.product_2 = cls.env["product.template"].create( + { + "name": "Product 2", + "default_supplier_id": cls.supplier_a.id, + } + ) + cls.product_3 = cls.env["product.template"].create( + { + "name": "Product 3", + "default_supplier_id": cls.supplier_a.id, + } + ) # Create product with supplier B - cls.product_4 = cls.env['product.template'].create({ - 'name': 'Product 4', - 'default_supplier_id': cls.supplier_b.id, - }) + cls.product_4 = cls.env["product.template"].create( + { + "name": "Product 4", + "default_supplier_id": cls.supplier_b.id, + } + ) # Create product without supplier - cls.product_5 = cls.env['product.template'].create({ - 'name': 'Product 5', - 'default_supplier_id': False, - }) + cls.product_5 = cls.env["product.template"].create( + { + "name": "Product 5", + "default_supplier_id": False, + } + ) def test_01_supplier_has_default_price_category_field(self): """Test that supplier has default_price_category_id field.""" self.assertTrue( - hasattr(self.supplier_a, 'default_price_category_id'), - 'Supplier should have default_price_category_id field' + hasattr(self.supplier_a, "default_price_category_id"), + "Supplier should have default_price_category_id field", ) self.assertEqual( self.supplier_a.default_price_category_id.id, self.category_premium.id, - 'Supplier should have Premium category assigned' + "Supplier should have Premium category assigned", ) def test_02_action_update_products_opens_wizard(self): @@ -83,41 +103,38 @@ class TestProductPriceCategorySupplier(TransactionCase): action = self.supplier_a.action_update_products_price_category() self.assertEqual( - action['type'], 'ir.actions.act_window', - 'Action should be a window action' + action["type"], "ir.actions.act_window", "Action should be a window action" ) self.assertEqual( - action['res_model'], 'wizard.update.product.category', - 'Action should open wizard model' + action["res_model"], + "wizard.update.product.category", + "Action should open wizard model", ) self.assertEqual( - action['target'], 'new', - 'Action should open in modal (target=new)' + action["target"], "new", "Action should open in modal (target=new)" ) - self.assertIn('res_id', action, 'Action should have res_id') + self.assertIn("res_id", action, "Action should have res_id") self.assertTrue( - action['res_id'] > 0, - 'res_id should be a valid wizard record ID' + action["res_id"] > 0, "res_id should be a valid wizard record ID" ) def test_03_wizard_counts_products_correctly(self): """Test that wizard counts products from supplier correctly.""" action = self.supplier_a.action_update_products_price_category() - + # Get the wizard record that was created - wizard = self.env['wizard.update.product.category'].browse(action['res_id']) + wizard = self.env["wizard.update.product.category"].browse(action["res_id"]) self.assertEqual( - wizard.product_count, 3, - 'Wizard should count 3 products from Supplier A' + wizard.product_count, 3, "Wizard should count 3 products from Supplier A" ) self.assertEqual( - wizard.partner_name, 'Supplier A', - 'Wizard should display supplier name' + wizard.partner_name, "Supplier A", "Wizard should display supplier name" ) self.assertEqual( - wizard.price_category_id.id, self.category_premium.id, - 'Wizard should have Premium category from supplier' + wizard.price_category_id.id, + self.category_premium.id, + "Wizard should have Premium category from supplier", ) def test_04_wizard_updates_all_products_from_supplier(self): @@ -125,75 +142,82 @@ class TestProductPriceCategorySupplier(TransactionCase): # Verify initial state - no categories assigned self.assertFalse( self.product_1.price_category_id, - 'Product 1 should not have category initially' + "Product 1 should not have category initially", ) self.assertFalse( self.product_2.price_category_id, - 'Product 2 should not have category initially' + "Product 2 should not have category initially", ) # Create and execute wizard - wizard = self.env['wizard.update.product.category'].create({ - 'partner_id': self.supplier_a.id, - 'price_category_id': self.category_premium.id, - 'product_count': 3, - }) + wizard = self.env["wizard.update.product.category"].create( + { + "partner_id": self.supplier_a.id, + "price_category_id": self.category_premium.id, + "product_count": 3, + } + ) result = wizard.action_confirm() # Verify products were updated self.assertEqual( - self.product_1.price_category_id.id, self.category_premium.id, - 'Product 1 should have Premium category' + self.product_1.price_category_id.id, + self.category_premium.id, + "Product 1 should have Premium category", ) self.assertEqual( - self.product_2.price_category_id.id, self.category_premium.id, - 'Product 2 should have Premium category' + self.product_2.price_category_id.id, + self.category_premium.id, + "Product 2 should have Premium category", ) self.assertEqual( - self.product_3.price_category_id.id, self.category_premium.id, - 'Product 3 should have Premium category' + self.product_3.price_category_id.id, + self.category_premium.id, + "Product 3 should have Premium category", ) # Verify product from other supplier was NOT updated self.assertFalse( self.product_4.price_category_id, - 'Product 4 (from Supplier B) should not be updated' + "Product 4 (from Supplier B) should not be updated", ) # Verify success notification self.assertEqual( - result['type'], 'ir.actions.client', - 'Result should be a client action' + result["type"], "ir.actions.client", "Result should be a client action" ) self.assertEqual( - result['tag'], 'display_notification', - 'Result should display a notification' + result["tag"], + "display_notification", + "Result should display a notification", ) def test_05_wizard_handles_supplier_with_no_products(self): """Test wizard behavior when supplier has no products.""" # Create supplier without products - supplier_no_products = self.env['res.partner'].create({ - 'name': 'Supplier No Products', - 'supplier_rank': 1, - 'default_price_category_id': self.category_standard.id, - }) + supplier_no_products = self.env["res.partner"].create( + { + "name": "Supplier No Products", + "supplier_rank": 1, + "default_price_category_id": self.category_standard.id, + } + ) - wizard = self.env['wizard.update.product.category'].create({ - 'partner_id': supplier_no_products.id, - 'price_category_id': self.category_standard.id, - 'product_count': 0, - }) + wizard = self.env["wizard.update.product.category"].create( + { + "partner_id": supplier_no_products.id, + "price_category_id": self.category_standard.id, + "product_count": 0, + } + ) result = wizard.action_confirm() # Verify warning notification self.assertEqual( - result['type'], 'ir.actions.client', - 'Result should be a client action' + result["type"], "ir.actions.client", "Result should be a client action" ) self.assertEqual( - result['params']['type'], 'warning', - 'Should display warning notification' + result["params"]["type"], "warning", "Should display warning notification" ) def test_06_customer_does_not_show_price_category_field(self): @@ -201,11 +225,10 @@ class TestProductPriceCategorySupplier(TransactionCase): # This is a view-level test - we verify the field exists but logic is correct self.assertFalse( self.customer.default_price_category_id, - 'Customer should not have price category set' + "Customer should not have price category set", ) self.assertEqual( - self.customer.supplier_rank, 0, - 'Customer should have supplier_rank = 0' + self.customer.supplier_rank, 0, "Customer should have supplier_rank = 0" ) def test_07_wizard_overwrites_existing_categories(self): @@ -215,87 +238,99 @@ class TestProductPriceCategorySupplier(TransactionCase): self.product_2.price_category_id = self.category_standard.id self.assertEqual( - self.product_1.price_category_id.id, self.category_standard.id, - 'Product 1 should have Standard category initially' + self.product_1.price_category_id.id, + self.category_standard.id, + "Product 1 should have Standard category initially", ) # Execute wizard to change to Premium - wizard = self.env['wizard.update.product.category'].create({ - 'partner_id': self.supplier_a.id, - 'price_category_id': self.category_premium.id, - 'product_count': 3, - }) + wizard = self.env["wizard.update.product.category"].create( + { + "partner_id": self.supplier_a.id, + "price_category_id": self.category_premium.id, + "product_count": 3, + } + ) wizard.action_confirm() # Verify categories were overwritten self.assertEqual( - self.product_1.price_category_id.id, self.category_premium.id, - 'Product 1 category should be overwritten to Premium' + self.product_1.price_category_id.id, + self.category_premium.id, + "Product 1 category should be overwritten to Premium", ) self.assertEqual( - self.product_2.price_category_id.id, self.category_premium.id, - 'Product 2 category should be overwritten to Premium' + self.product_2.price_category_id.id, + self.category_premium.id, + "Product 2 category should be overwritten to Premium", ) def test_08_multiple_suppliers_independent_updates(self): """Test that updating one supplier doesn't affect other suppliers' products.""" # Update Supplier A products - wizard_a = self.env['wizard.update.product.category'].create({ - 'partner_id': self.supplier_a.id, - 'price_category_id': self.category_premium.id, - 'product_count': 3, - }) + wizard_a = self.env["wizard.update.product.category"].create( + { + "partner_id": self.supplier_a.id, + "price_category_id": self.category_premium.id, + "product_count": 3, + } + ) wizard_a.action_confirm() # Update Supplier B products - wizard_b = self.env['wizard.update.product.category'].create({ - 'partner_id': self.supplier_b.id, - 'price_category_id': self.category_standard.id, - 'product_count': 1, - }) + wizard_b = self.env["wizard.update.product.category"].create( + { + "partner_id": self.supplier_b.id, + "price_category_id": self.category_standard.id, + "product_count": 1, + } + ) wizard_b.action_confirm() # Verify each supplier's products have correct category self.assertEqual( - self.product_1.price_category_id.id, self.category_premium.id, - 'Supplier A products should have Premium' + self.product_1.price_category_id.id, + self.category_premium.id, + "Supplier A products should have Premium", ) self.assertEqual( - self.product_4.price_category_id.id, self.category_standard.id, - 'Supplier B products should have Standard' + self.product_4.price_category_id.id, + self.category_standard.id, + "Supplier B products should have Standard", ) def test_09_wizard_readonly_fields(self): """Test that wizard display fields are readonly.""" - wizard = self.env['wizard.update.product.category'].create({ - 'partner_id': self.supplier_a.id, - 'price_category_id': self.category_premium.id, - 'product_count': 3, - }) + wizard = self.env["wizard.update.product.category"].create( + { + "partner_id": self.supplier_a.id, + "price_category_id": self.category_premium.id, + "product_count": 3, + } + ) # Verify partner_name is computed from partner_id self.assertEqual( - wizard.partner_name, 'Supplier A', - 'partner_name should be related to partner_id.name' + wizard.partner_name, + "Supplier A", + "partner_name should be related to partner_id.name", ) def test_10_action_counts_products_correctly(self): """Test that action_update_products_price_category counts products correctly.""" action = self.supplier_a.action_update_products_price_category() - + # Get the wizard that was created - wizard = self.env['wizard.update.product.category'].browse(action['res_id']) + wizard = self.env["wizard.update.product.category"].browse(action["res_id"]) # Count products manually - actual_count = self.env['product.template'].search_count([ - ('default_supplier_id', '=', self.supplier_a.id) - ]) + actual_count = self.env["product.template"].search_count( + [("default_supplier_id", "=", self.supplier_a.id)] + ) self.assertEqual( - wizard.product_count, actual_count, - f'Wizard should count {actual_count} products' - ) - self.assertEqual( - wizard.product_count, 3, - 'Supplier A should have 3 products' + wizard.product_count, + actual_count, + f"Wizard should count {actual_count} products", ) + self.assertEqual(wizard.product_count, 3, "Supplier A should have 3 products") diff --git a/product_sale_price_from_pricelist/models/res_config.py b/product_sale_price_from_pricelist/models/res_config.py index 699d53c..596c8c4 100644 --- a/product_sale_price_from_pricelist/models/res_config.py +++ b/product_sale_price_from_pricelist/models/res_config.py @@ -1,4 +1,6 @@ -from odoo import models, fields, api +from odoo import api +from odoo import fields +from odoo import models class ResConfigSettings(models.TransientModel): diff --git a/product_sale_price_from_pricelist/tests/test_res_config.py b/product_sale_price_from_pricelist/tests/test_res_config.py index 7c48926..25669b0 100644 --- a/product_sale_price_from_pricelist/tests/test_res_config.py +++ b/product_sale_price_from_pricelist/tests/test_res_config.py @@ -10,25 +10,31 @@ class TestResConfigSettings(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - - cls.pricelist = cls.env["product.pricelist"].create({ - "name": "Test Config Pricelist", - "currency_id": cls.env.company.currency_id.id, - }) + + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test Config Pricelist", + "currency_id": cls.env.company.currency_id.id, + } + ) def test_config_parameter_set_and_get(self): """Test setting and getting pricelist configuration""" - config = self.env["res.config.settings"].create({ - "product_pricelist_automatic": self.pricelist.id, - }) - - config.execute() - - # Verify parameter was saved - saved_id = self.env["ir.config_parameter"].sudo().get_param( - "product_sale_price_from_pricelist.product_pricelist_automatic" + config = self.env["res.config.settings"].create( + { + "product_pricelist_automatic": self.pricelist.id, + } ) - + + config.execute() + + # Verify parameter was saved + saved_id = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("product_sale_price_from_pricelist.product_pricelist_automatic") + ) + self.assertEqual(int(saved_id), self.pricelist.id) def test_config_load_from_parameter(self): @@ -36,43 +42,51 @@ class TestResConfigSettings(TransactionCase): # Set parameter directly self.env["ir.config_parameter"].sudo().set_param( "product_sale_price_from_pricelist.product_pricelist_automatic", - str(self.pricelist.id) + str(self.pricelist.id), ) - + # Create config and check if value is loaded config = self.env["res.config.settings"].create({}) - + self.assertEqual(config.product_pricelist_automatic.id, self.pricelist.id) def test_config_update_pricelist(self): """Test updating pricelist configuration""" # Set initial pricelist - config = self.env["res.config.settings"].create({ - "product_pricelist_automatic": self.pricelist.id, - }) - config.execute() - - # Create new pricelist and update - new_pricelist = self.env["product.pricelist"].create({ - "name": "New Config Pricelist", - "currency_id": self.env.company.currency_id.id, - }) - - config2 = self.env["res.config.settings"].create({ - "product_pricelist_automatic": new_pricelist.id, - }) - config2.execute() - - # Verify new value - saved_id = self.env["ir.config_parameter"].sudo().get_param( - "product_sale_price_from_pricelist.product_pricelist_automatic" + config = self.env["res.config.settings"].create( + { + "product_pricelist_automatic": self.pricelist.id, + } ) - + config.execute() + + # Create new pricelist and update + new_pricelist = self.env["product.pricelist"].create( + { + "name": "New Config Pricelist", + "currency_id": self.env.company.currency_id.id, + } + ) + + config2 = self.env["res.config.settings"].create( + { + "product_pricelist_automatic": new_pricelist.id, + } + ) + config2.execute() + + # Verify new value + saved_id = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("product_sale_price_from_pricelist.product_pricelist_automatic") + ) + self.assertEqual(int(saved_id), new_pricelist.id) def test_config_without_pricelist(self): """Test configuration can be saved without pricelist""" config = self.env["res.config.settings"].create({}) - + # Should not raise error config.execute() diff --git a/product_sale_price_from_pricelist/views/actions.xml b/product_sale_price_from_pricelist/views/actions.xml index 4a4a372..813df8a 100644 --- a/product_sale_price_from_pricelist/views/actions.xml +++ b/product_sale_price_from_pricelist/views/actions.xml @@ -11,7 +11,7 @@ Update Sales Price - + code records.action_update_list_price() @@ -19,4 +19,3 @@ - diff --git a/purchase_triple_discount/models/purchase_order.py b/purchase_triple_discount/models/purchase_order.py index de23b23..5ddc2cb 100644 --- a/purchase_triple_discount/models/purchase_order.py +++ b/purchase_triple_discount/models/purchase_order.py @@ -10,9 +10,6 @@ class PurchaseOrder(models.Model): def _prepare_supplier_info(self, partner, line, price, currency): res = super()._prepare_supplier_info(partner, line, price, currency) res.update( - { - fname: line[fname] - for fname in line._get_multiple_discount_field_names() - } + {fname: line[fname] for fname in line._get_multiple_discount_field_names()} ) return res diff --git a/purchase_triple_discount/models/purchase_order_line.py b/purchase_triple_discount/models/purchase_order_line.py index 0dd635e..5a48af5 100644 --- a/purchase_triple_discount/models/purchase_order_line.py +++ b/purchase_triple_discount/models/purchase_order_line.py @@ -43,10 +43,7 @@ class PurchaseOrderLine(models.Model): self.ensure_one() res = super()._prepare_account_move_line(move) res.update( - { - fname: self[fname] - for fname in self._get_multiple_discount_field_names() - } + {fname: self[fname] for fname in self._get_multiple_discount_field_names()} ) return res diff --git a/website_sale_aplicoop/.codeclimate.yml b/website_sale_aplicoop/.codeclimate.yml index 83d75d5..31b6e57 100644 --- a/website_sale_aplicoop/.codeclimate.yml +++ b/website_sale_aplicoop/.codeclimate.yml @@ -1,29 +1,29 @@ -version: '2' +version: "2" checks: - similar-code: - enabled: true - config: - threshold: 3 - duplicate-code: - enabled: true - config: - threshold: 3 + similar-code: + enabled: true + config: + threshold: 3 + duplicate-code: + enabled: true + config: + threshold: 3 exclude-patterns: - - tests/ - - migrations/ + - tests/ + - migrations/ python-targets: - - 3.10 - - 3.11 - - 3.12 + - 3.10 + - 3.11 + - 3.12 plugins: - pylint: - enabled: true - config: - load-plugins: - - pylint_odoo - pydocstyle: - enabled: false + pylint: + enabled: true + config: + load-plugins: + - pylint_odoo + pydocstyle: + enabled: false diff --git a/website_sale_aplicoop/.pre-commit-config.yaml b/website_sale_aplicoop/.pre-commit-config.yaml index 5d2060c..029d3ea 100644 --- a/website_sale_aplicoop/.pre-commit-config.yaml +++ b/website_sale_aplicoop/.pre-commit-config.yaml @@ -1,33 +1,33 @@ repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: check-merge-conflict + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict - - repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - language_version: python3.10 + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3.10 - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - args: ["--profile", "black"] + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black"] - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - args: ["--max-line-length=88", "--extend-ignore=E203"] + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + args: ["--max-line-length=88", "--extend-ignore=E203"] - - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 - hooks: - - id: pyupgrade - args: ["--py310-plus"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.4.0 + hooks: + - id: pyupgrade + args: ["--py310-plus"] diff --git a/website_sale_aplicoop/controllers/portal.py b/website_sale_aplicoop/controllers/portal.py index 40ae0cd..d9457bd 100644 --- a/website_sale_aplicoop/controllers/portal.py +++ b/website_sale_aplicoop/controllers/portal.py @@ -1,61 +1,72 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _ -from odoo.http import request, route -from odoo.addons.sale.controllers import portal as sale_portal import logging +from odoo import _ +from odoo.http import request +from odoo.http import route + +from odoo.addons.sale.controllers import portal as sale_portal + _logger = logging.getLogger(__name__) class CustomerPortal(sale_portal.CustomerPortal): - '''Extend sale portal to include draft orders.''' + """Extend sale portal to include draft orders.""" def _prepare_orders_domain(self, partner): - '''Override to include draft and done orders.''' + """Override to include draft and done orders.""" return [ - ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), - ('state', 'in', ['draft', 'sale', 'done']), # Include draft orders + ("message_partner_ids", "child_of", [partner.commercial_partner_id.id]), + ("state", "in", ["draft", "sale", "done"]), # Include draft orders ] - @route(['/my/orders', '/my/orders/page/'], - type='http', auth='user', website=True) + @route( + ["/my/orders", "/my/orders/page/"], + type="http", + auth="user", + website=True, + ) def portal_my_orders(self, **kwargs): - '''Override to add translated day names to context.''' + """Override to add translated day names to context.""" # Get values from parent - values = self._prepare_sale_portal_rendering_values(quotation_page=False, **kwargs) - + values = self._prepare_sale_portal_rendering_values( + quotation_page=False, **kwargs + ) + # Add translated day names for pickup_day display - values['day_names'] = [ - _('Monday'), - _('Tuesday'), - _('Wednesday'), - _('Thursday'), - _('Friday'), - _('Saturday'), - _('Sunday'), + values["day_names"] = [ + _("Monday"), + _("Tuesday"), + _("Wednesday"), + _("Thursday"), + _("Friday"), + _("Saturday"), + _("Sunday"), ] - - request.session['my_orders_history'] = values['orders'].ids[:100] + + request.session["my_orders_history"] = values["orders"].ids[:100] return request.render("sale.portal_my_orders", values) - @route(['/my/orders/'], type='http', auth='public', website=True) + @route(["/my/orders/"], type="http", auth="public", website=True) def portal_order_page(self, order_id, access_token=None, **kwargs): - '''Override to add translated day names for order detail page.''' + """Override to add translated day names for order detail page.""" # Call parent to get response - response = super().portal_order_page(order_id, access_token=access_token, **kwargs) - + response = super().portal_order_page( + order_id, access_token=access_token, **kwargs + ) + # If it's a template render (not a redirect), add day_names to the context - if hasattr(response, 'qcontext'): - response.qcontext['day_names'] = [ - _('Monday'), - _('Tuesday'), - _('Wednesday'), - _('Thursday'), - _('Friday'), - _('Saturday'), - _('Sunday'), + if hasattr(response, "qcontext"): + response.qcontext["day_names"] = [ + _("Monday"), + _("Tuesday"), + _("Wednesday"), + _("Thursday"), + _("Friday"), + _("Saturday"), + _("Sunday"), ] - + return response diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index da0971a..93c0cf6 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -1809,10 +1809,8 @@ class AplicoopWebsiteSale(WebsiteSale): ) _logger.warning("========================================") - # Get delivery product ID and name (translated to user's language) - delivery_product = request.env.ref( - "website_sale_aplicoop.product_home_delivery", raise_if_not_found=False - ) + # Get delivery product from group_order (configured per group order) + delivery_product = group_order.delivery_product_id delivery_product_id = delivery_product.id if delivery_product else None # Get translated product name based on current language if delivery_product: @@ -2804,9 +2802,8 @@ class AplicoopWebsiteSale(WebsiteSale): return request.redirect("/eskaera/%d" % sale_order.group_order_id.id) # Extract items from the order (skip delivery product) - delivery_product = request.env.ref( - "website_sale_aplicoop.product_home_delivery", raise_if_not_found=False - ) + # Use the delivery_product_id from the group_order + delivery_product = sale_order.group_order_id.delivery_product_id delivery_product_id = delivery_product.id if delivery_product else None items = [] diff --git a/website_sale_aplicoop/i18n/README.md b/website_sale_aplicoop/i18n/README.md index e203387..0dc25b6 100644 --- a/website_sale_aplicoop/i18n/README.md +++ b/website_sale_aplicoop/i18n/README.md @@ -21,7 +21,7 @@ Each `.po` file contains **66 translations** for: - **Selection Field Options** (Days of week, Recurrence periods) - Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday - Daily, Weekly, Biweekly, Monthly - + - **Order States** - Draft, Open, Closed, Cancelled @@ -36,7 +36,7 @@ Each `.po` file contains **66 translations** for: When users switch their Odoo interface language to any of the supported languages, all UI strings will automatically display in that language. ### Example -- English: "Group Order" +- English: "Group Order" - Spanish: "Pedido de Grupo" - Portuguese: "Pedido de Grupo" - French: "Commande de Groupe" diff --git a/website_sale_aplicoop/migrations/18.0.1.0.0/post-migrate.py b/website_sale_aplicoop/migrations/18.0.1.0.0/post-migrate.py index 23a9ef7..93245bd 100644 --- a/website_sale_aplicoop/migrations/18.0.1.0.0/post-migrate.py +++ b/website_sale_aplicoop/migrations/18.0.1.0.0/post-migrate.py @@ -1,36 +1,40 @@ """Fill pickup_day and pickup_date for existing group orders.""" -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta def migrate(cr, version): """ Fill pickup_day and pickup_date for existing group orders. - + This ensures that existing group orders show delivery information. """ - from odoo import api, SUPERUSER_ID - + from odoo import SUPERUSER_ID + from odoo import api + env = api.Environment(cr, SUPERUSER_ID, {}) - + # Get all group orders that don't have pickup_day set - group_orders = env['group.order'].search([('pickup_day', '=', False)]) - + group_orders = env["group.order"].search([("pickup_day", "=", False)]) + if not group_orders: return - + # Set default values: Friday (4) and one week from now today = datetime.now().date() - + # Find Friday of next week (day 4) days_until_friday = (4 - today.weekday()) % 7 # 4 = Friday if days_until_friday == 0: days_until_friday = 7 friday = today + timedelta(days=days_until_friday) - + for order in group_orders: - order.write({ - 'pickup_day': 4, # Friday - 'pickup_date': friday, - 'delivery_notice': 'Home delivery available.', - }) + order.write( + { + "pickup_day": 4, # Friday + "pickup_date": friday, + "delivery_notice": "Home delivery available.", + } + ) diff --git a/website_sale_aplicoop/migrations/18.0.1.0.2/post-migrate.py b/website_sale_aplicoop/migrations/18.0.1.0.2/post-migrate.py index 6095a69..29f7523 100644 --- a/website_sale_aplicoop/migrations/18.0.1.0.2/post-migrate.py +++ b/website_sale_aplicoop/migrations/18.0.1.0.2/post-migrate.py @@ -1,7 +1,8 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import api, SUPERUSER_ID +from odoo import SUPERUSER_ID +from odoo import api def migrate(cr, version): @@ -13,7 +14,7 @@ def migrate(cr, version): env = api.Environment(cr, SUPERUSER_ID, {}) # Obtener la compañía por defecto - default_company = env['res.company'].search([], limit=1) + default_company = env["res.company"].search([], limit=1) if default_company: # Actualizar todos los registros de group.order que no tengan company_id @@ -23,7 +24,7 @@ def migrate(cr, version): SET company_id = %s WHERE company_id IS NULL """, - (default_company.id,) + (default_company.id,), ) cr.commit() diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py index 2bfbd9e..2e90b0d 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -4,7 +4,6 @@ import logging from datetime import timedelta -from odoo import _ from odoo import api from odoo import fields from odoo import models @@ -19,52 +18,47 @@ class GroupOrder(models.Model): _inherit = ["mail.thread", "mail.activity.mixin"] _order = "start_date desc" - @staticmethod - def _get_order_type_selection(records): + def _get_order_type_selection(self): """Return order type selection options with translations.""" return [ - ("regular", _("Regular Order")), - ("special", _("Special Order")), - ("promotional", _("Promotional Order")), + ("regular", self.env._("Regular Order")), + ("special", self.env._("Special Order")), + ("promotional", self.env._("Promotional Order")), ] - @staticmethod - def _get_period_selection(records): + def _get_period_selection(self): """Return period selection options with translations.""" return [ - ("once", _("One-time")), - ("weekly", _("Weekly")), - ("biweekly", _("Biweekly")), - ("monthly", _("Monthly")), + ("once", self.env._("One-time")), + ("weekly", self.env._("Weekly")), + ("biweekly", self.env._("Biweekly")), + ("monthly", self.env._("Monthly")), ] - @staticmethod - def _get_day_selection(records): + def _get_day_selection(self): """Return day of week selection options with translations.""" return [ - ("0", _("Monday")), - ("1", _("Tuesday")), - ("2", _("Wednesday")), - ("3", _("Thursday")), - ("4", _("Friday")), - ("5", _("Saturday")), - ("6", _("Sunday")), + ("0", self.env._("Monday")), + ("1", self.env._("Tuesday")), + ("2", self.env._("Wednesday")), + ("3", self.env._("Thursday")), + ("4", self.env._("Friday")), + ("5", self.env._("Saturday")), + ("6", self.env._("Sunday")), ] - @staticmethod - def _get_state_selection(records): + def _get_state_selection(self): """Return state selection options with translations.""" return [ - ("draft", _("Draft")), - ("open", _("Open")), - ("closed", _("Closed")), - ("cancelled", _("Cancelled")), + ("draft", self.env._("Draft")), + ("open", self.env._("Open")), + ("closed", self.env._("Closed")), + ("cancelled", self.env._("Cancelled")), ] # === Multicompañía === company_id = fields.Many2one( "res.company", - string="Company", required=True, default=lambda self: self.env.company, tracking=True, @@ -73,7 +67,6 @@ class GroupOrder(models.Model): # === Campos básicos === name = fields.Char( - string="Name", required=True, tracking=True, translate=True, @@ -84,7 +77,6 @@ class GroupOrder(models.Model): "group_order_group_rel", "order_id", "group_id", - string="Consumer Groups", required=True, domain=[("is_group", "=", True)], tracking=True, @@ -92,7 +84,6 @@ class GroupOrder(models.Model): ) type = fields.Selection( selection=_get_order_type_selection, - string="Order Type", required=True, default="regular", tracking=True, @@ -101,13 +92,11 @@ class GroupOrder(models.Model): # === Fechas === start_date = fields.Date( - string="Start Date", required=False, tracking=True, help="Day when the consumer group order opens for purchases", ) end_date = fields.Date( - string="End Date", required=False, tracking=True, help="If empty, the consumer group order is permanent", @@ -116,7 +105,6 @@ class GroupOrder(models.Model): # === Período y días === period = fields.Selection( selection=_get_period_selection, - string="Recurrence Period", required=True, default="weekly", tracking=True, @@ -124,14 +112,12 @@ class GroupOrder(models.Model): ) pickup_day = fields.Selection( selection=_get_day_selection, - string="Pickup Day", required=False, tracking=True, help="Day of the week when members pick up their orders", ) cutoff_day = fields.Selection( selection=_get_day_selection, - string="Cutoff Day", required=False, tracking=True, help="Day when purchases stop and the consumer group order is locked for this week.", @@ -139,20 +125,17 @@ class GroupOrder(models.Model): # === Home delivery === home_delivery = fields.Boolean( - string="Home Delivery", default=False, tracking=True, help="Whether this consumer group order includes home delivery service", ) delivery_product_id = fields.Many2one( "product.product", - string="Delivery Product", domain=[("type", "=", "service")], tracking=True, help="Product to use for home delivery (service type)", ) delivery_date = fields.Date( - string="Delivery Date", compute="_compute_delivery_date", store=True, readonly=True, @@ -161,14 +144,12 @@ class GroupOrder(models.Model): # === Computed date fields === pickup_date = fields.Date( - string="Pickup Date", compute="_compute_pickup_date", store=True, readonly=True, help="Calculated next occurrence of pickup day", ) cutoff_date = fields.Date( - string="Cutoff Date", compute="_compute_cutoff_date", store=True, readonly=True, @@ -181,7 +162,6 @@ class GroupOrder(models.Model): "group_order_supplier_rel", "order_id", "supplier_id", - string="Suppliers", domain=[("supplier_rank", ">", 0)], tracking=True, help="Products from these suppliers will be available.", @@ -191,7 +171,6 @@ class GroupOrder(models.Model): "group_order_product_rel", "order_id", "product_id", - string="Products", tracking=True, help="Directly assigned products.", ) @@ -200,7 +179,6 @@ class GroupOrder(models.Model): "group_order_category_rel", "order_id", "category_id", - string="Categories", tracking=True, help="Products in these categories will be available", ) @@ -208,29 +186,24 @@ class GroupOrder(models.Model): # === Estado === state = fields.Selection( selection=_get_state_selection, - string="State", default="draft", tracking=True, ) # === Descripción e imagen === description = fields.Text( - string="Description", translate=True, help="Free text description for this consumer group order", ) delivery_notice = fields.Text( - string="Delivery Notice", translate=True, help="Notice about home delivery displayed to users (shown when home delivery is enabled)", ) image = fields.Binary( - string="Image", help="Image displayed alongside the consumer group order name", attachment=True, ) display_image = fields.Binary( - string="Display Image", compute="_compute_display_image", store=True, help="Image to display: uses consumer group order image if set, otherwise group image", @@ -249,7 +222,6 @@ class GroupOrder(models.Model): record.display_image = False available_products_count = fields.Integer( - string="Available Products Count", compute="_compute_available_products_count", store=False, help="Total count of available products from all sources", @@ -270,13 +242,14 @@ class GroupOrder(models.Model): if group.company_id and group.company_id != record.company_id: raise ValidationError( self.env._( - "Group {group} belongs to company {group_company}, " - "not to {record_company}." - ).format( - group=group.name, - group_company=group.company_id.name, - record_company=record.company_id.name, + "Group %(group)s belongs to company %(group_company)s, " + "not to %(record_company)s." ) + % { + "group": group.name, + "group_company": group.company_id.name, + "record_company": record.company_id.name, + } ) @api.constrains("start_date", "end_date") @@ -569,11 +542,12 @@ class GroupOrder(models.Model): pickup_name = dict(self._get_day_selection())[str(pickup)] cutoff_name = dict(self._get_day_selection())[str(cutoff)] raise ValidationError( - _( - "For weekly orders, pickup day ({pickup}) must be after or equal to " - "cutoff day ({cutoff}) in the same week. Current configuration would " + self.env._( + "For weekly orders, pickup day (%(pickup)s) must be after or equal to " + "cutoff day (%(cutoff)s) in the same week. Current configuration would " "put pickup before cutoff, which is illogical." - ).format(pickup=pickup_name, cutoff=cutoff_name) + ) + % {"pickup": pickup_name, "cutoff": cutoff_name} ) # === Onchange Methods === diff --git a/website_sale_aplicoop/models/js_translations.py b/website_sale_aplicoop/models/js_translations.py index 686c0fa..01e24ad 100644 --- a/website_sale_aplicoop/models/js_translations.py +++ b/website_sale_aplicoop/models/js_translations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. """ @@ -20,151 +19,157 @@ from odoo import _ def _register_translations(): """ Register all JavaScript translation strings. - + Called by Odoo's translation extraction system. These calls populate the POT/PO files for translation. """ # ======================== # Action Labels # ======================== - _('Save Cart') - _('Reload Cart') - _('Browse Product Categories') - _('Proceed to Checkout') - _('Confirm Order') - _('Back to Cart') - _('Remove Item') - _('Add to Cart') - _('Save as Draft') - _('Load Draft') - _('Browse Product Categories') + _("Save Cart") + _("Reload Cart") + _("Browse Product Categories") + _("Proceed to Checkout") + _("Confirm Order") + _("Back to Cart") + _("Remove Item") + _("Add to Cart") + _("Save as Draft") + _("Load Draft") + _("Browse Product Categories") # ======================== # Draft Modal Labels # ======================== - _('Draft Already Exists') - _('A saved draft already exists for this week.') - _('You have two options:') - _('Option 1: Merge with Existing Draft') - _('Combine your current cart with the existing draft.') - _('Existing draft has') - _('Current cart has') - _('item(s)') - _('Products will be merged by adding quantities. If a product exists in both, quantities will be combined.') - _('Option 2: Replace with Current Cart') - _('Delete the old draft and save only the current cart items.') - _('The existing draft will be permanently deleted.') - _('Merge') - _('Replace') + _("Draft Already Exists") + _("A saved draft already exists for this week.") + _("You have two options:") + _("Option 1: Merge with Existing Draft") + _("Combine your current cart with the existing draft.") + _("Existing draft has") + _("Current cart has") + _("item(s)") + _( + "Products will be merged by adding quantities. If a product exists in both, quantities will be combined." + ) + _("Option 2: Replace with Current Cart") + _("Delete the old draft and save only the current cart items.") + _("The existing draft will be permanently deleted.") + _("Merge") + _("Replace") # ======================== # Draft Save/Load Confirmations # ======================== - _('Are you sure you want to save this cart as draft? Items to save: ') - _('You will be able to reload this cart later.') - _('Are you sure you want to load your last saved draft?') - _('This will replace the current items in your cart') - _('with the saved draft.') + _("Are you sure you want to save this cart as draft? Items to save: ") + _("You will be able to reload this cart later.") + _("Are you sure you want to load your last saved draft?") + _("This will replace the current items in your cart") + _("with the saved draft.") # ======================== # Cart Messages (All Variations) # ======================== - _('Your cart is empty') - _('This order\'s cart is empty.') - _('This order\'s cart is empty') - _('added to cart') - _('items') - _('Your cart has been restored') + _("Your cart is empty") + _("This order's cart is empty.") + _("This order's cart is empty") + _("added to cart") + _("items") + _("Your cart has been restored") # ======================== # Confirmation & Validation # ======================== - _('Confirmation') - _('Confirm') - _('Cancel') - _('Please enter a valid quantity') + _("Confirmation") + _("Confirm") + _("Cancel") + _("Please enter a valid quantity") # ======================== # Error Messages # ======================== - _('Error: Order ID not found') - _('No draft orders found for this week') - _('Connection error') - _('Error loading order') - _('Error loading draft') - _('Unknown error') - _('Error saving cart') - _('Error processing response') + _("Error: Order ID not found") + _("No draft orders found for this week") + _("Connection error") + _("Error loading order") + _("Error loading draft") + _("Unknown error") + _("Error saving cart") + _("Error processing response") # ======================== # Success Messages # ======================== - _('Cart saved as draft successfully') - _('Draft order loaded successfully') - _('Draft merged successfully') - _('Draft replaced successfully') - _('Order loaded') - _('Thank you! Your order has been confirmed.') - _('Quantity updated') + _("Cart saved as draft successfully") + _("Draft order loaded successfully") + _("Draft merged successfully") + _("Draft replaced successfully") + _("Order loaded") + _("Thank you! Your order has been confirmed.") + _("Quantity updated") # ======================== # Field Labels # ======================== - _('Product') - _('Supplier') - _('Price') - _('Quantity') - _('Subtotal') - _('Total') + _("Product") + _("Supplier") + _("Price") + _("Quantity") + _("Subtotal") + _("Total") # ======================== # Checkout Page Labels # ======================== - _('Home Delivery') - _('Delivery Information') - _('Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}') - _('Your order will be delivered the day after pickup between 11:00 - 14:00') - _('Important') - _('Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.') + _("Home Delivery") + _("Delivery Information") + _( + "Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}" + ) + _("Your order will be delivered the day after pickup between 11:00 - 14:00") + _("Important") + _( + "Once you confirm this order, you will not be able to modify it. Please review carefully before confirming." + ) # ======================== # Search & Filter Labels # ======================== - _('Search') - _('Search products...') - _('No products found') - _('Categories') - _('All categories') + _("Search") + _("Search products...") + _("No products found") + _("Categories") + _("All categories") # ======================== # Category Labels # ======================== - _('Order Type') - _('Order Period') - _('Cutoff Day') - _('Pickup Day') - _('Store Pickup Day') - _('Open until') + _("Order Type") + _("Order Period") + _("Cutoff Day") + _("Pickup Day") + _("Store Pickup Day") + _("Open until") # ======================== # Portal Page Labels (New) # ======================== - _('Load in Cart') - _('Consumer Group') - _('Delivery Information') - _('Delivery Date:') - _('Pickup Date:') - _('Delivery Notice:') - _('No special delivery instructions') - _('Pickup Location:') + _("Load in Cart") + _("Consumer Group") + _("Delivery Information") + _("Delivery Date:") + _("Pickup Date:") + _("Delivery Notice:") + _("No special delivery instructions") + _("Pickup Location:") # ======================== # Day Names (Required for translations) # ======================== - _('Monday') - _('Tuesday') - _('Wednesday') - _('Thursday') - _('Friday') - _('Saturday') - _('Sunday') + _("Monday") + _("Tuesday") + _("Wednesday") + _("Thursday") + _("Friday") + _("Saturday") + _("Sunday") diff --git a/website_sale_aplicoop/models/product_extension.py b/website_sale_aplicoop/models/product_extension.py index 90d896a..d02fdbd 100644 --- a/website_sale_aplicoop/models/product_extension.py +++ b/website_sale_aplicoop/models/product_extension.py @@ -1,20 +1,23 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _, api, fields, models +from odoo import _ +from odoo import api +from odoo import fields +from odoo import models class ProductProduct(models.Model): - _inherit = 'product.product' + _inherit = "product.product" group_order_ids = fields.Many2many( - 'group.order', - 'group_order_product_rel', - 'product_id', - 'order_id', - string='Group Orders', + "group.order", + "group_order_product_rel", + "product_id", + "order_id", + string="Group Orders", readonly=True, - help='Group orders where this product is available', + help="Group orders where this product is available", ) @api.model @@ -25,26 +28,25 @@ class ProductProduct(models.Model): responsibilities together. Keep this wrapper so existing callers on `product.product` keep working. """ - order = self.env['group.order'].browse(order_id) + order = self.env["group.order"].browse(order_id) if not order.exists(): return self.browse() return order._get_products_for_group_order(order.id) class ProductTemplate(models.Model): - _inherit = 'product.template' + _inherit = "product.template" group_order_ids = fields.Many2many( - 'group.order', - compute='_compute_group_order_ids', - string='Consumer Group Orders', + "group.order", + compute="_compute_group_order_ids", + string="Consumer Group Orders", readonly=True, - help='Consumer group orders where variants of this product are available', + help="Consumer group orders where variants of this product are available", ) - @api.depends('product_variant_ids.group_order_ids') + @api.depends("product_variant_ids.group_order_ids") def _compute_group_order_ids(self): for template in self: variants = template.product_variant_ids - template.group_order_ids = variants.mapped('group_order_ids') - + template.group_order_ids = variants.mapped("group_order_ids") diff --git a/website_sale_aplicoop/models/res_partner_extension.py b/website_sale_aplicoop/models/res_partner_extension.py index 4095037..0168c9e 100644 --- a/website_sale_aplicoop/models/res_partner_extension.py +++ b/website_sale_aplicoop/models/res_partner_extension.py @@ -1,37 +1,39 @@ # Copyright 2025-Today Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _, fields, models +from odoo import _ +from odoo import fields +from odoo import models class ResPartner(models.Model): - _inherit = 'res.partner' + _inherit = "res.partner" # Campo para identificar si un partner es un grupo is_group = fields.Boolean( - string='Is a Consumer Group?', - help='Check this box if the partner represents a group of users', + string="Is a Consumer Group?", + help="Check this box if the partner represents a group of users", default=False, ) # Relación para los miembros de un grupo (si is_group es True) member_ids = fields.Many2many( - 'res.partner', - 'res_partner_group_members_rel', - 'group_id', - 'member_id', - domain=[('is_group', '=', True)], - string='Consumer Groups', - help='Consumer Groups this partner belongs to', + "res.partner", + "res_partner_group_members_rel", + "group_id", + "member_id", + domain=[("is_group", "=", True)], + string="Consumer Groups", + help="Consumer Groups this partner belongs to", ) # Inverse relation: group orders this group participates in group_order_ids = fields.Many2many( - 'group.order', - 'group_order_group_rel', - 'group_id', - 'order_id', - string='Consumer Group Orders', - help='Group orders this consumer group participates in', + "group.order", + "group_order_group_rel", + "group_id", + "order_id", + string="Consumer Group Orders", + help="Group orders this consumer group participates in", readonly=True, ) diff --git a/website_sale_aplicoop/models/sale_order_extension.py b/website_sale_aplicoop/models/sale_order_extension.py index 72a16be..873b9a9 100644 --- a/website_sale_aplicoop/models/sale_order_extension.py +++ b/website_sale_aplicoop/models/sale_order_extension.py @@ -1,56 +1,52 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _, fields, models +from odoo import fields +from odoo import models class SaleOrder(models.Model): - _inherit = 'sale.order' + _inherit = "sale.order" - @staticmethod - def _get_pickup_day_selection(records): + def _get_pickup_day_selection(self): """Return pickup day selection options with translations.""" return [ - ('0', _('Monday')), - ('1', _('Tuesday')), - ('2', _('Wednesday')), - ('3', _('Thursday')), - ('4', _('Friday')), - ('5', _('Saturday')), - ('6', _('Sunday')), + ("0", self.env._("Monday")), + ("1", self.env._("Tuesday")), + ("2", self.env._("Wednesday")), + ("3", self.env._("Thursday")), + ("4", self.env._("Friday")), + ("5", self.env._("Saturday")), + ("6", self.env._("Sunday")), ] pickup_day = fields.Selection( selection=_get_pickup_day_selection, - string='Pickup Day', - help='Day of week when this order will be picked up (inherited from group order)', + help="Day of week when this order will be picked up (inherited from group order)", ) group_order_id = fields.Many2one( - 'group.order', - string='Consumer Group Order', - help='Reference to the consumer group order that originated this sale order', + "group.order", + help="Reference to the consumer group order that originated this sale order", ) pickup_date = fields.Date( - string='Pickup Date', - help='Calculated pickup/delivery date (inherited from consumer group order)', + help="Calculated pickup/delivery date (inherited from consumer group order)", ) home_delivery = fields.Boolean( - string='Home Delivery', default=False, - help='Whether this order includes home delivery (inherited from consumer group order)', + help="Whether this order includes home delivery (inherited from consumer group order)", ) def _get_name_portal_content_view(self): """Override to return custom portal content template with group order info. - + This method is called by the portal template to determine which content template to render. We return our custom template that includes the group order information (Consumer Group, Delivery/Pickup info, etc.) """ self.ensure_one() if self.group_order_id: - return 'website_sale_aplicoop.sale_order_portal_content_aplicoop' + return "website_sale_aplicoop.sale_order_portal_content_aplicoop" return super()._get_name_portal_content_view() diff --git a/website_sale_aplicoop/readme/CREDITS.md b/website_sale_aplicoop/readme/CREDITS.md index d1e1972..70ad028 100644 --- a/website_sale_aplicoop/readme/CREDITS.md +++ b/website_sale_aplicoop/readme/CREDITS.md @@ -6,4 +6,3 @@ The implementation follows OCA standards for: - Code quality and testing (26 passing tests) - Documentation structure and multilingual support - Security and access control - diff --git a/website_sale_aplicoop/readme/USAGE.md b/website_sale_aplicoop/readme/USAGE.md index 4dcd7a5..db09130 100644 --- a/website_sale_aplicoop/readme/USAGE.md +++ b/website_sale_aplicoop/readme/USAGE.md @@ -48,4 +48,3 @@ - `start_date` must be ≤ `end_date` (when both filled) - Empty end_date = permanent order - diff --git a/website_sale_aplicoop/security/ir.model.access.csv b/website_sale_aplicoop/security/ir.model.access.csv index 484ee9b..10027e4 100644 --- a/website_sale_aplicoop/security/ir.model.access.csv +++ b/website_sale_aplicoop/security/ir.model.access.csv @@ -4,4 +4,3 @@ access_group_order_user,group.order user,model_group_order,website_sale_aplicoop access_group_order_manager,group.order manager,model_group_order,website_sale_aplicoop.group_group_order_manager,1,1,1,1 access_group_order_portal,group.order portal,model_group_order,base.group_portal,1,0,0,0 access_product_supplierinfo_portal,product.supplierinfo portal,product.model_product_supplierinfo,base.group_portal,1,0,0,0 - diff --git a/website_sale_aplicoop/setup.py b/website_sale_aplicoop/setup.py index 43c6b0c..ee38ad1 100644 --- a/website_sale_aplicoop/setup.py +++ b/website_sale_aplicoop/setup.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from setuptools import setup, find_packages +from setuptools import find_packages +from setuptools import setup -with open("README.rst", "r", encoding="utf-8") as fh: +with open("README.rst", encoding="utf-8") as fh: long_description = fh.read() setup( diff --git a/website_sale_aplicoop/static/src/css/README.md b/website_sale_aplicoop/static/src/css/README.md index fc15249..938b4a0 100644 --- a/website_sale_aplicoop/static/src/css/README.md +++ b/website_sale_aplicoop/static/src/css/README.md @@ -1,8 +1,8 @@ # CSS Architecture - Website Sale Aplicoop -**Refactoring Date**: 7 de febrero de 2026 -**Status**: ✅ Complete -**Previous Size**: 2,986 líneas en 1 archivo +**Refactoring Date**: 7 de febrero de 2026 +**Status**: ✅ Complete +**Previous Size**: 2,986 líneas en 1 archivo **New Size**: ~400 líneas distribuidas en 15 archivos modulares --- @@ -59,43 +59,43 @@ website_sale_aplicoop/static/src/css/ ## 📊 Desglose de Archivos ### **base/** - Fundamentos -- **variables.css** (~80 líneas) +- **variables.css** (~80 líneas) Colores, tipografía, espaciados, sombras, transiciones, z-index -- **utilities.css** (~15 líneas) +- **utilities.css** (~15 líneas) Clases utilitarias reutilizables (.sr-only, .text-muted, etc) ### **layout/** - Estructura Global -- **pages.css** (~70 líneas) +- **pages.css** (~70 líneas) Fondos de página, gradientes, pseudo-elementos (::before) -- **header.css** (~100 líneas) +- **header.css** (~100 líneas) Headers, navegación, títulos, información de pedidos -- **responsive.css** (~200 líneas) +- **responsive.css** (~200 líneas) Todas las media queries (4 breakpoints: 992px, 768px, 576px, etc) ### **components/** - Elementos Reutilizables -- **product-card.css** (~80 líneas) +- **product-card.css** (~80 líneas) Tarjetas de producto con hover, imagen, título, precio -- **order-card.css** (~100 líneas) +- **order-card.css** (~100 líneas) Tarjetas de orden (Eskaera) con metadatos, badges -- **cart.css** (~150 líneas) +- **cart.css** (~150 líneas) Carrito lateral, items, total, botones save/reload -- **buttons.css** (~80 líneas) +- **buttons.css** (~80 líneas) Botones primarios, checkout, acciones -- **quantity-control.css** (~100 líneas) +- **quantity-control.css** (~100 líneas) Control de cantidad (spinners + input numérico) -- **forms.css** (~70 líneas) +- **forms.css** (~70 líneas) Inputs, selects, checkboxes, labels -- **alerts.css** (~50 líneas) +- **alerts.css** (~50 líneas) Alertas, notificaciones, toasts ### **sections/** - Layouts Específicos de Página -- **products-grid.css** (~25 líneas) +- **products-grid.css** (~25 líneas) Grid de productos con responsive -- **order-list.css** (~40 líneas) +- **order-list.css** (~40 líneas) Lista de órdenes (Eskaera page) -- **checkout.css** (~100 líneas) +- **checkout.css** (~100 líneas) Tabla de checkout, totales, summary -- **info-cards.css** (~50 líneas) +- **info-cards.css** (~50 líneas) Tarjetas de información, metadatos --- @@ -183,7 +183,7 @@ Permitiría mejor nesting y variables más poderosas. ## 📈 Cambios Visuales -✅ **NINGUNO** - La refactorización es solo organizacional +✅ **NINGUNO** - La refactorización es solo organizacional El CSS compilado genera **exactamente el mismo output** que antes. --- @@ -231,6 +231,6 @@ grep -r "components/\|sections/\|base/\|layout/" css/website_sale.css --- -**Mantenido por**: Equipo de Frontend -**Última actualización**: 7 de febrero de 2026 +**Mantenido por**: Equipo de Frontend +**Última actualización**: 7 de febrero de 2026 **Licencia**: AGPL-3.0 diff --git a/website_sale_aplicoop/static/src/css/base/variables.css b/website_sale_aplicoop/static/src/css/base/variables.css index 7289115..9152e76 100644 --- a/website_sale_aplicoop/static/src/css/base/variables.css +++ b/website_sale_aplicoop/static/src/css/base/variables.css @@ -16,26 +16,27 @@ --info-color: #17a2b8; --light-color: #f8f9fa; --dark-color: #2d3748; - + /* Text colors */ --text-primary: #1a202c; --text-secondary: #4a5568; --text-muted: #6b7280; - + /* Border colors */ --border-light: #e2e8f0; --border-medium: #cbd5e0; --border-dark: #718096; - + /* ========== TYPOGRAPHY ========== */ - --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", + Arial, sans-serif; --font-weight-light: 300; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-extrabold: 800; - + /* ========== SPACING ========== */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; @@ -43,23 +44,23 @@ --spacing-lg: 1.5rem; --spacing-xl: 2rem; --spacing-2xl: 3rem; - + /* ========== BORDER RADIUS ========== */ --radius-sm: 0.25rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; - + /* ========== SHADOWS ========== */ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12); - + /* ========== TRANSITIONS ========== */ --transition-fast: 200ms ease; - --transition-normal: 320ms cubic-bezier(.2, .9, .2, 1); + --transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1); --transition-slow: 500ms ease; - + /* ========== Z-INDEX ========== */ --z-dropdown: 1000; --z-sticky: 1020; diff --git a/website_sale_aplicoop/static/src/css/components/order-card.css b/website_sale_aplicoop/static/src/css/components/order-card.css index 699798f..781e166 100644 --- a/website_sale_aplicoop/static/src/css/components/order-card.css +++ b/website_sale_aplicoop/static/src/css/components/order-card.css @@ -17,7 +17,8 @@ border: 1px solid rgba(90, 103, 216, 0.12); border-radius: 0.75rem; box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06); - transition: transform 320ms cubic-bezier(.2, .9, .2, 1), box-shadow 320ms, border-color 320ms, background 320ms; + transition: transform 320ms cubic-bezier(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms, + background 320ms; overflow: hidden; display: flex; flex-direction: column; @@ -139,7 +140,7 @@ } .eskaera-order-card .btn::before { - content: ''; + content: ""; position: absolute; top: 50%; left: 50%; diff --git a/website_sale_aplicoop/static/src/css/components/product-card.css b/website_sale_aplicoop/static/src/css/components/product-card.css index 5d5b73e..c6eef5c 100644 --- a/website_sale_aplicoop/static/src/css/components/product-card.css +++ b/website_sale_aplicoop/static/src/css/components/product-card.css @@ -51,7 +51,11 @@ } .product-card:hover .card-body { - background: linear-gradient(135deg, rgba(108, 117, 125, 0.10) 0%, rgba(108, 117, 125, 0.08) 100%); + background: linear-gradient( + 135deg, + rgba(108, 117, 125, 0.1) 0%, + rgba(108, 117, 125, 0.08) 100% + ); } .product-card .card-title { diff --git a/website_sale_aplicoop/static/src/css/components/quantity-control.css b/website_sale_aplicoop/static/src/css/components/quantity-control.css index 78e8ed1..64a0087 100644 --- a/website_sale_aplicoop/static/src/css/components/quantity-control.css +++ b/website_sale_aplicoop/static/src/css/components/quantity-control.css @@ -21,7 +21,7 @@ .add-to-cart-form .input-group { width: 100%; gap: 0; - padding: 0; + padding: 0; display: flex; align-items: center; justify-content: center; diff --git a/website_sale_aplicoop/static/src/css/components/tag-filter.css b/website_sale_aplicoop/static/src/css/components/tag-filter.css index 0ad42be..45022d5 100644 --- a/website_sale_aplicoop/static/src/css/components/tag-filter.css +++ b/website_sale_aplicoop/static/src/css/components/tag-filter.css @@ -5,7 +5,7 @@ /** * Tag Filter Badges Component - * + * * Styles for interactive tag filter badges in the product search/filter bar. * Badges toggle between secondary (unselected) and primary (selected) states. */ @@ -65,7 +65,7 @@ .tag-filter-badges { gap: 0.375rem; } - + .tag-filter-badge { padding: 0.375rem 0.625rem; font-size: 0.8125rem; diff --git a/website_sale_aplicoop/static/src/css/layout/pages.css b/website_sale_aplicoop/static/src/css/layout/pages.css index 34ce83a..267e560 100644 --- a/website_sale_aplicoop/static/src/css/layout/pages.css +++ b/website_sale_aplicoop/static/src/css/layout/pages.css @@ -4,13 +4,15 @@ * Page backgrounds and main layout structures */ -html, body { +html, +body { background-color: transparent !important; background: transparent !important; } body.website_published { - background: linear-gradient(135deg, + background: linear-gradient( + 135deg, color-mix(in srgb, var(--primary-color) 30%, white), color-mix(in srgb, var(--primary-color) 60%, black) ) !important; @@ -32,21 +34,24 @@ body.website_published .eskaera-checkout-page { .eskaera-page, .eskaera-generic-page { - background: linear-gradient(180deg, + background: linear-gradient( + 180deg, color-mix(in srgb, var(--primary-color) 10%, white), color-mix(in srgb, var(--primary-color) 70%, black) ) !important; } .eskaera-shop-page { - background: linear-gradient(135deg, + background: linear-gradient( + 135deg, color-mix(in srgb, var(--primary-color) 10%, white), color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135)) ) !important; } .eskaera-checkout-page { - background: linear-gradient(-135deg, + background: linear-gradient( + -135deg, color-mix(in srgb, var(--primary-color) 0%, white), color-mix(in srgb, var(--primary-color) 60%, black) ) !important; @@ -54,29 +59,54 @@ body.website_published .eskaera-checkout-page { .eskaera-page::before, .eskaera-generic-page::before { - background-image: - radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%), - radial-gradient(circle at 40% 20%, color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%, transparent 50%); + background-image: radial-gradient( + circle at 20% 50%, + color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 80%, + color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 40% 20%, + color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%, + transparent 50% + ); } .eskaera-shop-page::before { - background-image: - radial-gradient(circle at 15% 30%, color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%, transparent 50%), - radial-gradient(circle at 85% 70%, color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, transparent 50%); + background-image: radial-gradient( + circle at 15% 30%, + color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 85% 70%, + color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, + transparent 50% + ); } .eskaera-checkout-page::before { - background-image: - radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%); + background-image: radial-gradient( + circle at 20% 50%, + color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 80%, + color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, + transparent 50% + ); } .eskaera-page::before, .eskaera-shop-page::before, .eskaera-generic-page::before, .eskaera-checkout-page::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; diff --git a/website_sale_aplicoop/static/src/css/layout/responsive.css b/website_sale_aplicoop/static/src/css/layout/responsive.css index a0d0fd0..1e62b6e 100644 --- a/website_sale_aplicoop/static/src/css/layout/responsive.css +++ b/website_sale_aplicoop/static/src/css/layout/responsive.css @@ -17,20 +17,20 @@ .cart-items { max-height: 400px; } - + #cart-items-container { width: 100%; padding: 0.75rem; } - + .list-group-item { padding: 0.75rem; } - + .list-group-item h6 { font-size: 0.95rem; } - + .list-group-item strong { min-width: 70px; } @@ -43,7 +43,7 @@ .cart-header h5 { font-size: 1.25rem; } - + .cart-title-lg { font-size: 1.25rem; } @@ -476,13 +476,13 @@ .product-tags { font-size: 1.1rem !important; } - + /* Scale down quantity input for 6-column layout */ .add-to-cart-form .product-qty { font-size: 0.85rem; max-width: 55px; } - + .add-to-cart-form .qty-decrease, .add-to-cart-form .qty-increase { font-size: 0.75rem; @@ -495,13 +495,13 @@ .product-tags { font-size: 1.25rem !important; } - + /* Scale down quantity input for 5-column layout */ .add-to-cart-form .product-qty { font-size: 0.9rem; max-width: 60px; } - + .add-to-cart-form .qty-decrease, .add-to-cart-form .qty-increase { font-size: 0.8rem; diff --git a/website_sale_aplicoop/static/src/css/sections/info-cards.css b/website_sale_aplicoop/static/src/css/sections/info-cards.css index 0a0bf9a..92927d2 100644 --- a/website_sale_aplicoop/static/src/css/sections/info-cards.css +++ b/website_sale_aplicoop/static/src/css/sections/info-cards.css @@ -19,7 +19,7 @@ grid-template-columns: 1fr 1fr; gap: 0.5rem; align-items: start; - margin-top: 0.5rem; + margin-top: 0.5rem; } .card-meta-compact { diff --git a/website_sale_aplicoop/static/src/css/website_sale.css b/website_sale_aplicoop/static/src/css/website_sale.css index 995c23d..dbce461 100644 --- a/website_sale_aplicoop/static/src/css/website_sale.css +++ b/website_sale_aplicoop/static/src/css/website_sale.css @@ -3,7 +3,7 @@ /** * Website Sale Aplicoop - Main CSS Index File * This file imports all component stylesheets in the correct order - * + * * Architecture: * 1. Base & Variables (colors, spacing, typography) * 2. Layout & Pages (page backgrounds, containers) @@ -15,36 +15,36 @@ /* ============================================ 1. BASE & VARIABLES ============================================ */ -@import 'base/variables.css'; -@import 'base/utilities.css'; +@import "base/variables.css"; +@import "base/utilities.css"; /* ============================================ 2. LAYOUT & PAGES ============================================ */ -@import 'layout/pages.css'; -@import 'layout/header.css'; +@import "layout/pages.css"; +@import "layout/header.css"; /* ============================================ 3. COMPONENTS (Reusable UI elements) ============================================ */ -@import 'components/product-card.css'; -@import 'components/order-card.css'; -@import 'components/cart.css'; -@import 'components/buttons.css'; -@import 'components/quantity-control.css'; -@import 'components/forms.css'; -@import 'components/alerts.css'; -@import 'components/tag-filter.css'; +@import "components/product-card.css"; +@import "components/order-card.css"; +@import "components/cart.css"; +@import "components/buttons.css"; +@import "components/quantity-control.css"; +@import "components/forms.css"; +@import "components/alerts.css"; +@import "components/tag-filter.css"; /* ============================================ 4. SECTIONS (Page-specific layouts) ============================================ */ -@import 'sections/products-grid.css'; -@import 'sections/order-list.css'; -@import 'sections/checkout.css'; -@import 'sections/info-cards.css'; +@import "sections/products-grid.css"; +@import "sections/order-list.css"; +@import "sections/checkout.css"; +@import "sections/info-cards.css"; /* ============================================ 5. RESPONSIVE DESIGN (Media queries) ============================================ */ -@import 'layout/responsive.css'; +@import "layout/responsive.css"; diff --git a/website_sale_aplicoop/static/src/js/checkout_labels.js b/website_sale_aplicoop/static/src/js/checkout_labels.js index 494c22b..b2537db 100644 --- a/website_sale_aplicoop/static/src/js/checkout_labels.js +++ b/website_sale_aplicoop/static/src/js/checkout_labels.js @@ -5,140 +5,158 @@ * before rendering the checkout summary. */ -(function() { - 'use strict'; +(function () { + "use strict"; - console.log('[CHECKOUT] Script loaded'); + console.log("[CHECKOUT] Script loaded"); // Get order ID from button - var confirmBtn = document.getElementById('confirm-order-btn'); + var confirmBtn = document.getElementById("confirm-order-btn"); if (!confirmBtn) { - console.log('[CHECKOUT] No confirm button found'); + console.log("[CHECKOUT] No confirm button found"); return; } - var orderId = confirmBtn.getAttribute('data-order-id'); + var orderId = confirmBtn.getAttribute("data-order-id"); if (!orderId) { - console.log('[CHECKOUT] No order ID found'); + console.log("[CHECKOUT] No order ID found"); return; } - console.log('[CHECKOUT] Order ID:', orderId); + console.log("[CHECKOUT] Order ID:", orderId); // Get summary div - var summaryDiv = document.getElementById('checkout-summary'); + var summaryDiv = document.getElementById("checkout-summary"); if (!summaryDiv) { - console.log('[CHECKOUT] No summary div found'); + console.log("[CHECKOUT] No summary div found"); return; } // Function to fetch labels and render checkout - var fetchLabelsAndRender = function() { - console.log('[CHECKOUT] Fetching labels...'); - + var fetchLabelsAndRender = function () { + console.log("[CHECKOUT] Fetching labels..."); + // Wait for window.groupOrderShop.labels to be initialized (contains hardcoded labels) - var waitForLabels = function(callback, maxWait = 3000, checkInterval = 50) { + var waitForLabels = function (callback, maxWait = 3000, checkInterval = 50) { var startTime = Date.now(); - var checkLabels = function() { - if (window.groupOrderShop && window.groupOrderShop.labels && Object.keys(window.groupOrderShop.labels).length > 0) { - console.log('[CHECKOUT] ✅ Hardcoded labels found, proceeding'); + var checkLabels = function () { + if ( + window.groupOrderShop && + window.groupOrderShop.labels && + Object.keys(window.groupOrderShop.labels).length > 0 + ) { + console.log("[CHECKOUT] ✅ Hardcoded labels found, proceeding"); callback(); } else if (Date.now() - startTime < maxWait) { setTimeout(checkLabels, checkInterval); } else { - console.log('[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway'); + console.log("[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway"); callback(); } }; checkLabels(); }; - - waitForLabels(function() { + + waitForLabels(function () { // Now fetch additional labels from server // Detect current language from document or navigator - var currentLang = document.documentElement.lang || - document.documentElement.getAttribute('lang') || - navigator.language || - 'es_ES'; - console.log('[CHECKOUT] Detected language:', currentLang); - - fetch('/eskaera/labels', { - method: 'POST', + var currentLang = + document.documentElement.lang || + document.documentElement.getAttribute("lang") || + navigator.language || + "es_ES"; + console.log("[CHECKOUT] Detected language:", currentLang); + + fetch("/eskaera/labels", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ - lang: currentLang + lang: currentLang, + }), + }) + .then(function (response) { + console.log("[CHECKOUT] Response status:", response.status); + return response.json(); }) - }) - .then(function(response) { - console.log('[CHECKOUT] Response status:', response.status); - return response.json(); - }) - .then(function(data) { - console.log('[CHECKOUT] Response data:', data); - var serverLabels = data.result || data; - console.log('[CHECKOUT] Server labels count:', Object.keys(serverLabels).length); - console.log('[CHECKOUT] Sample server labels:', { - draft_merged_success: serverLabels.draft_merged_success, - home_delivery: serverLabels.home_delivery - }); - - // CRITICAL: Merge server labels with existing hardcoded labels - // Hardcoded labels MUST take precedence over server labels - if (window.groupOrderShop && window.groupOrderShop.labels) { - var existingLabels = window.groupOrderShop.labels; - console.log('[CHECKOUT] Existing hardcoded labels count:', Object.keys(existingLabels).length); - console.log('[CHECKOUT] Sample existing labels:', { - draft_merged_success: existingLabels.draft_merged_success, - home_delivery: existingLabels.home_delivery + .then(function (data) { + console.log("[CHECKOUT] Response data:", data); + var serverLabels = data.result || data; + console.log( + "[CHECKOUT] Server labels count:", + Object.keys(serverLabels).length + ); + console.log("[CHECKOUT] Sample server labels:", { + draft_merged_success: serverLabels.draft_merged_success, + home_delivery: serverLabels.home_delivery, }); - - // Start with server labels, then overwrite with hardcoded ones - var mergedLabels = Object.assign({}, serverLabels); - Object.assign(mergedLabels, existingLabels); - - window.groupOrderShop.labels = mergedLabels; - console.log('[CHECKOUT] ✅ Merged labels - final count:', Object.keys(mergedLabels).length); - console.log('[CHECKOUT] Verification:', { - draft_merged_success: mergedLabels.draft_merged_success, - home_delivery: mergedLabels.home_delivery - }); - } else { - // If no existing labels, use server labels as fallback - if (window.groupOrderShop) { - window.groupOrderShop.labels = serverLabels; + + // CRITICAL: Merge server labels with existing hardcoded labels + // Hardcoded labels MUST take precedence over server labels + if (window.groupOrderShop && window.groupOrderShop.labels) { + var existingLabels = window.groupOrderShop.labels; + console.log( + "[CHECKOUT] Existing hardcoded labels count:", + Object.keys(existingLabels).length + ); + console.log("[CHECKOUT] Sample existing labels:", { + draft_merged_success: existingLabels.draft_merged_success, + home_delivery: existingLabels.home_delivery, + }); + + // Start with server labels, then overwrite with hardcoded ones + var mergedLabels = Object.assign({}, serverLabels); + Object.assign(mergedLabels, existingLabels); + + window.groupOrderShop.labels = mergedLabels; + console.log( + "[CHECKOUT] ✅ Merged labels - final count:", + Object.keys(mergedLabels).length + ); + console.log("[CHECKOUT] Verification:", { + draft_merged_success: mergedLabels.draft_merged_success, + home_delivery: mergedLabels.home_delivery, + }); + } else { + // If no existing labels, use server labels as fallback + if (window.groupOrderShop) { + window.groupOrderShop.labels = serverLabels; + } + console.log("[CHECKOUT] ⚠️ No existing labels, using server labels"); } - console.log('[CHECKOUT] ⚠️ No existing labels, using server labels'); - } - - window.renderCheckoutSummary(window.groupOrderShop.labels); - }) - .catch(function(error) { - console.error('[CHECKOUT] Error:', error); - // Fallback to translated labels - window.renderCheckoutSummary(window.getCheckoutLabels()); - }); + + window.renderCheckoutSummary(window.groupOrderShop.labels); + }) + .catch(function (error) { + console.error("[CHECKOUT] Error:", error); + // Fallback to translated labels + window.renderCheckoutSummary(window.getCheckoutLabels()); + }); }); }; // Listen for cart ready event instead of polling if (window.groupOrderShop && window.groupOrderShop.orderId) { // Cart already initialized, render immediately - console.log('[CHECKOUT] Cart already ready'); + console.log("[CHECKOUT] Cart already ready"); fetchLabelsAndRender(); } else { // Wait for cart initialization event - console.log('[CHECKOUT] Waiting for cart ready event...'); - document.addEventListener('groupOrderCartReady', function() { - console.log('[CHECKOUT] Cart ready event received'); - fetchLabelsAndRender(); - }, { once: true }); - + console.log("[CHECKOUT] Waiting for cart ready event..."); + document.addEventListener( + "groupOrderCartReady", + function () { + console.log("[CHECKOUT] Cart ready event received"); + fetchLabelsAndRender(); + }, + { once: true } + ); + // Fallback timeout in case event never fires - setTimeout(function() { + setTimeout(function () { if (window.groupOrderShop && window.groupOrderShop.orderId) { - console.log('[CHECKOUT] Fallback timeout triggered'); + console.log("[CHECKOUT] Fallback timeout triggered"); fetchLabelsAndRender(); } }, 500); @@ -148,67 +166,88 @@ * Render order summary table or empty message * Exposed globally so other scripts can call it */ - window.renderCheckoutSummary = function(labels) { + window.renderCheckoutSummary = function (labels) { labels = labels || window.getCheckoutLabels(); - var summaryDiv = document.getElementById('checkout-summary'); + var summaryDiv = document.getElementById("checkout-summary"); if (!summaryDiv) return; - var cartKey = 'eskaera_' + (document.getElementById('confirm-order-btn') ? document.getElementById('confirm-order-btn').getAttribute('data-order-id') : '1') + '_cart'; - var cart = JSON.parse(localStorage.getItem(cartKey) || '{}'); + var cartKey = + "eskaera_" + + (document.getElementById("confirm-order-btn") + ? document.getElementById("confirm-order-btn").getAttribute("data-order-id") + : "1") + + "_cart"; + var cart = JSON.parse(localStorage.getItem(cartKey) || "{}"); - var summaryTable = summaryDiv.querySelector('.checkout-summary-table'); - var tbody = summaryDiv.querySelector('#checkout-summary-tbody'); - var totalSection = summaryDiv.querySelector('.checkout-total-section'); + var summaryTable = summaryDiv.querySelector(".checkout-summary-table"); + var tbody = summaryDiv.querySelector("#checkout-summary-tbody"); + var totalSection = summaryDiv.querySelector(".checkout-total-section"); // If no table found, create it with headers (shouldn't happen, but fallback) if (!summaryTable) { - var html = '' + - '' + - '' + - '' + - '' + + var html = + '
' + escapeHtml(labels.product) + '' + escapeHtml(labels.quantity) + '' + escapeHtml(labels.price) + '' + escapeHtml(labels.subtotal) + '
' + + '" + + '" + + '" + + '" + '
' + + escapeHtml(labels.product) + + "' + + escapeHtml(labels.quantity) + + "' + + escapeHtml(labels.price) + + "' + + escapeHtml(labels.subtotal) + + "
' + '
' + - '' + escapeHtml(labels.total) + '' + + '' + + escapeHtml(labels.total) + + "" + '€0.00' + - '
'; + ""; summaryDiv.innerHTML = html; - summaryTable = summaryDiv.querySelector('.checkout-summary-table'); - tbody = summaryDiv.querySelector('#checkout-summary-tbody'); - totalSection = summaryDiv.querySelector('.checkout-total-section'); + summaryTable = summaryDiv.querySelector(".checkout-summary-table"); + tbody = summaryDiv.querySelector("#checkout-summary-tbody"); + totalSection = summaryDiv.querySelector(".checkout-total-section"); } // Clear only tbody, preserve headers - tbody.innerHTML = ''; + tbody.innerHTML = ""; if (Object.keys(cart).length === 0) { // Show empty message if cart is empty - var emptyRow = document.createElement('tr'); - emptyRow.id = 'checkout-empty-row'; - emptyRow.className = 'empty-message'; - emptyRow.innerHTML = '' + + var emptyRow = document.createElement("tr"); + emptyRow.id = "checkout-empty-row"; + emptyRow.className = "empty-message"; + emptyRow.innerHTML = + '' + '' + - '

' + escapeHtml(labels.empty) + '

' + - ''; + "

" + + escapeHtml(labels.empty) + + "

" + + ""; tbody.appendChild(emptyRow); - + // Hide total section - totalSection.style.display = 'none'; + totalSection.style.display = "none"; } else { // Hide empty row if visible - var emptyRow = tbody.querySelector('#checkout-empty-row'); + var emptyRow = tbody.querySelector("#checkout-empty-row"); if (emptyRow) emptyRow.remove(); // Get delivery product ID from page data - var checkoutPage = document.querySelector('.eskaera-checkout-page'); - var deliveryProductId = checkoutPage ? checkoutPage.getAttribute('data-delivery-product-id') : null; + var checkoutPage = document.querySelector(".eskaera-checkout-page"); + var deliveryProductId = checkoutPage + ? checkoutPage.getAttribute("data-delivery-product-id") + : null; // Separate normal products from delivery product var normalProducts = []; var deliveryProduct = null; - - Object.keys(cart).forEach(function(productId) { + + Object.keys(cart).forEach(function (productId) { if (productId === deliveryProductId) { deliveryProduct = { id: productId, item: cart[productId] }; } else { @@ -217,14 +256,14 @@ }); // Sort normal products numerically - normalProducts.sort(function(a, b) { + normalProducts.sort(function (a, b) { return parseInt(a.id) - parseInt(b.id); }); var total = 0; - + // Render normal products first - normalProducts.forEach(function(product) { + normalProducts.forEach(function (product) { var item = product.item; var qty = parseFloat(item.quantity || item.qty || 1); if (isNaN(qty)) qty = 1; @@ -233,11 +272,20 @@ var subtotal = qty * price; total += subtotal; - var row = document.createElement('tr'); - row.innerHTML = '' + escapeHtml(item.name) + '' + - '' + qty.toFixed(2).replace(/\.?0+$/, '') + '' + - '€' + price.toFixed(2) + '' + - '€' + subtotal.toFixed(2) + ''; + var row = document.createElement("tr"); + row.innerHTML = + "" + + escapeHtml(item.name) + + "" + + '' + + qty.toFixed(2).replace(/\.?0+$/, "") + + "" + + '€' + + price.toFixed(2) + + "" + + '€' + + subtotal.toFixed(2) + + ""; tbody.appendChild(row); }); @@ -251,32 +299,41 @@ var subtotal = qty * price; total += subtotal; - var row = document.createElement('tr'); - row.innerHTML = '' + escapeHtml(item.name) + '' + - '' + qty.toFixed(2).replace(/\.?0+$/, '') + '' + - '€' + price.toFixed(2) + '' + - '€' + subtotal.toFixed(2) + ''; + var row = document.createElement("tr"); + row.innerHTML = + "" + + escapeHtml(item.name) + + "" + + '' + + qty.toFixed(2).replace(/\.?0+$/, "") + + "" + + '€' + + price.toFixed(2) + + "" + + '€' + + subtotal.toFixed(2) + + ""; tbody.appendChild(row); } // Update total - var totalAmount = summaryDiv.querySelector('#checkout-total-amount'); + var totalAmount = summaryDiv.querySelector("#checkout-total-amount"); if (totalAmount) { - totalAmount.textContent = '€' + total.toFixed(2); + totalAmount.textContent = "€" + total.toFixed(2); } - + // Show total section - totalSection.style.display = 'block'; + totalSection.style.display = "block"; } - console.log('[CHECKOUT] Summary rendered'); + console.log("[CHECKOUT] Summary rendered"); }; /** * Escape HTML to prevent XSS */ function escapeHtml(text) { - var div = document.createElement('div'); + var div = document.createElement("div"); div.textContent = text; return div.innerHTML; } diff --git a/website_sale_aplicoop/static/src/js/checkout_summary.js b/website_sale_aplicoop/static/src/js/checkout_summary.js index ad6e333..836ec67 100644 --- a/website_sale_aplicoop/static/src/js/checkout_summary.js +++ b/website_sale_aplicoop/static/src/js/checkout_summary.js @@ -3,9 +3,7 @@ * This file is kept for backwards compatibility but is no longer needed. * The main renderSummary() logic is in checkout_labels.js */ -(function() { - 'use strict'; +(function () { + "use strict"; // Checkout rendering is handled by checkout_labels.js })(); - - diff --git a/website_sale_aplicoop/static/src/js/home_delivery.js b/website_sale_aplicoop/static/src/js/home_delivery.js index bb2cd7c..0479b8e 100644 --- a/website_sale_aplicoop/static/src/js/home_delivery.js +++ b/website_sale_aplicoop/static/src/js/home_delivery.js @@ -3,56 +3,65 @@ * Manages home delivery checkbox and product addition/removal */ -(function() { - 'use strict'; +(function () { + "use strict"; var HomeDeliveryManager = { deliveryProductId: null, deliveryProductPrice: 5.74, - deliveryProductName: 'Home Delivery', // Default fallback + deliveryProductName: "Home Delivery", // Default fallback orderId: null, homeDeliveryEnabled: false, - - init: function() { + + init: function () { // Get delivery product info from data attributes - var checkoutPage = document.querySelector('.eskaera-checkout-page'); + var checkoutPage = document.querySelector(".eskaera-checkout-page"); if (checkoutPage) { - this.deliveryProductId = checkoutPage.getAttribute('data-delivery-product-id'); - console.log('[HomeDelivery] deliveryProductId from attribute:', this.deliveryProductId, 'type:', typeof this.deliveryProductId); - - var price = checkoutPage.getAttribute('data-delivery-product-price'); + this.deliveryProductId = checkoutPage.getAttribute("data-delivery-product-id"); + console.log( + "[HomeDelivery] deliveryProductId from attribute:", + this.deliveryProductId, + "type:", + typeof this.deliveryProductId + ); + + var price = checkoutPage.getAttribute("data-delivery-product-price"); if (price) { this.deliveryProductPrice = parseFloat(price); } - + // Get translated product name from data attribute (auto-translated by Odoo server) - var productName = checkoutPage.getAttribute('data-delivery-product-name'); + var productName = checkoutPage.getAttribute("data-delivery-product-name"); if (productName) { this.deliveryProductName = productName; - console.log('[HomeDelivery] Using translated product name from server:', this.deliveryProductName); + console.log( + "[HomeDelivery] Using translated product name from server:", + this.deliveryProductName + ); } - + // Check if home delivery is enabled for this order - var homeDeliveryAttr = checkoutPage.getAttribute('data-home-delivery-enabled'); - this.homeDeliveryEnabled = homeDeliveryAttr === 'true' || homeDeliveryAttr === 'True'; - console.log('[HomeDelivery] Home delivery enabled:', this.homeDeliveryEnabled); - + var homeDeliveryAttr = checkoutPage.getAttribute("data-home-delivery-enabled"); + this.homeDeliveryEnabled = + homeDeliveryAttr === "true" || homeDeliveryAttr === "True"; + console.log("[HomeDelivery] Home delivery enabled:", this.homeDeliveryEnabled); + // Show/hide home delivery section based on configuration this.toggleHomeDeliverySection(); } - + // Get order ID from confirm button - var confirmBtn = document.getElementById('confirm-order-btn'); + var confirmBtn = document.getElementById("confirm-order-btn"); if (confirmBtn) { - this.orderId = confirmBtn.getAttribute('data-order-id'); - console.log('[HomeDelivery] orderId from button:', this.orderId); + this.orderId = confirmBtn.getAttribute("data-order-id"); + console.log("[HomeDelivery] orderId from button:", this.orderId); } - var checkbox = document.getElementById('home-delivery-checkbox'); + var checkbox = document.getElementById("home-delivery-checkbox"); if (!checkbox) return; var self = this; - checkbox.addEventListener('change', function() { + checkbox.addEventListener("change", function () { if (this.checked) { self.addDeliveryProduct(); self.showDeliveryInfo(); @@ -66,42 +75,44 @@ this.checkDeliveryInCart(); }, - toggleHomeDeliverySection: function() { - var homeDeliverySection = document.querySelector('[id*="home-delivery"], [class*="home-delivery"]'); - var checkbox = document.getElementById('home-delivery-checkbox'); - var homeDeliveryContainer = document.getElementById('home-delivery-container'); - + toggleHomeDeliverySection: function () { + var homeDeliverySection = document.querySelector( + '[id*="home-delivery"], [class*="home-delivery"]' + ); + var checkbox = document.getElementById("home-delivery-checkbox"); + var homeDeliveryContainer = document.getElementById("home-delivery-container"); + if (this.homeDeliveryEnabled) { // Show home delivery option if (checkbox) { - checkbox.closest('.form-check').style.display = 'block'; + checkbox.closest(".form-check").style.display = "block"; } if (homeDeliveryContainer) { - homeDeliveryContainer.style.display = 'block'; + homeDeliveryContainer.style.display = "block"; } - console.log('[HomeDelivery] Home delivery option shown'); + console.log("[HomeDelivery] Home delivery option shown"); } else { // Hide home delivery option and delivery info alert if (checkbox) { - checkbox.closest('.form-check').style.display = 'none'; + checkbox.closest(".form-check").style.display = "none"; checkbox.checked = false; } if (homeDeliveryContainer) { - homeDeliveryContainer.style.display = 'none'; + homeDeliveryContainer.style.display = "none"; } // Also hide the delivery info alert when home delivery is disabled this.hideDeliveryInfo(); this.removeDeliveryProduct(); - console.log('[HomeDelivery] Home delivery option and delivery info hidden'); + console.log("[HomeDelivery] Home delivery option and delivery info hidden"); } }, - checkDeliveryInCart: function() { + checkDeliveryInCart: function () { if (!this.deliveryProductId) return; var cart = this.getCart(); if (cart[this.deliveryProductId]) { - var checkbox = document.getElementById('home-delivery-checkbox'); + var checkbox = document.getElementById("home-delivery-checkbox"); if (checkbox) { checkbox.checked = true; this.showDeliveryInfo(); @@ -109,93 +120,103 @@ } }, - getCart: function() { + getCart: function () { if (!this.orderId) return {}; - var cartKey = 'eskaera_' + this.orderId + '_cart'; + var cartKey = "eskaera_" + this.orderId + "_cart"; var cartStr = localStorage.getItem(cartKey); return cartStr ? JSON.parse(cartStr) : {}; }, - saveCart: function(cart) { + saveCart: function (cart) { if (!this.orderId) return; - var cartKey = 'eskaera_' + this.orderId + '_cart'; + var cartKey = "eskaera_" + this.orderId + "_cart"; localStorage.setItem(cartKey, JSON.stringify(cart)); - + // Re-render checkout summary without reloading var self = this; - setTimeout(function() { + setTimeout(function () { // Use the global function from checkout_labels.js - if (typeof window.renderCheckoutSummary === 'function') { + if (typeof window.renderCheckoutSummary === "function") { window.renderCheckoutSummary(); } }, 50); }, - - renderCheckoutSummary: function() { + + renderCheckoutSummary: function () { // Stub - now handled by global window.renderCheckoutSummary }, - addDeliveryProduct: function() { + addDeliveryProduct: function () { if (!this.deliveryProductId) { - console.warn('[HomeDelivery] Delivery product ID not found'); + console.warn("[HomeDelivery] Delivery product ID not found"); return; } - console.log('[HomeDelivery] Adding delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId); + console.log( + "[HomeDelivery] Adding delivery product - deliveryProductId:", + this.deliveryProductId, + "orderId:", + this.orderId + ); var cart = this.getCart(); - console.log('[HomeDelivery] Current cart before adding:', cart); - + console.log("[HomeDelivery] Current cart before adding:", cart); + cart[this.deliveryProductId] = { id: this.deliveryProductId, name: this.deliveryProductName, price: this.deliveryProductPrice, - qty: 1 + qty: 1, }; - console.log('[HomeDelivery] Cart after adding delivery:', cart); + console.log("[HomeDelivery] Cart after adding delivery:", cart); this.saveCart(cart); - console.log('[HomeDelivery] Delivery product added to localStorage'); + console.log("[HomeDelivery] Delivery product added to localStorage"); }, - removeDeliveryProduct: function() { + removeDeliveryProduct: function () { if (!this.deliveryProductId) { - console.warn('[HomeDelivery] Delivery product ID not found'); + console.warn("[HomeDelivery] Delivery product ID not found"); return; } - console.log('[HomeDelivery] Removing delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId); + console.log( + "[HomeDelivery] Removing delivery product - deliveryProductId:", + this.deliveryProductId, + "orderId:", + this.orderId + ); var cart = this.getCart(); - console.log('[HomeDelivery] Current cart before removing:', cart); - + console.log("[HomeDelivery] Current cart before removing:", cart); + if (cart[this.deliveryProductId]) { delete cart[this.deliveryProductId]; - console.log('[HomeDelivery] Cart after removing delivery:', cart); + console.log("[HomeDelivery] Cart after removing delivery:", cart); } this.saveCart(cart); - console.log('[HomeDelivery] Delivery product removed from localStorage'); + console.log("[HomeDelivery] Delivery product removed from localStorage"); }, - showDeliveryInfo: function() { - var alert = document.getElementById('delivery-info-alert'); + showDeliveryInfo: function () { + var alert = document.getElementById("delivery-info-alert"); if (alert) { - console.log('[HomeDelivery] Showing delivery info alert'); - alert.classList.remove('d-none'); - alert.style.display = 'block'; + console.log("[HomeDelivery] Showing delivery info alert"); + alert.classList.remove("d-none"); + alert.style.display = "block"; } }, - hideDeliveryInfo: function() { - var alert = document.getElementById('delivery-info-alert'); + hideDeliveryInfo: function () { + var alert = document.getElementById("delivery-info-alert"); if (alert) { - console.log('[HomeDelivery] Hiding delivery info alert'); - alert.classList.add('d-none'); - alert.style.display = 'none'; + console.log("[HomeDelivery] Hiding delivery info alert"); + alert.classList.add("d-none"); + alert.style.display = "none"; } - } + }, }; // Initialize on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", function () { HomeDeliveryManager.init(); }); } else { diff --git a/website_sale_aplicoop/static/src/js/i18n_helpers.js b/website_sale_aplicoop/static/src/js/i18n_helpers.js index a55ca06..4462bf5 100644 --- a/website_sale_aplicoop/static/src/js/i18n_helpers.js +++ b/website_sale_aplicoop/static/src/js/i18n_helpers.js @@ -1,30 +1,30 @@ /** * DEPRECATED: Use i18n_manager.js instead - * + * * This file is kept for backwards compatibility only. * All translation logic has been moved to i18n_manager.js which * fetches translations from the server endpoint /eskaera/i18n - * + * * Migration guide: * OLD: window.getCheckoutLabels() * NEW: i18nManager.getAll() - * + * * OLD: window.formatCurrency(amount) * NEW: i18nManager.formatCurrency(amount) - * + * * Copyright 2025 Criptomart * License AGPL-3.0 or later */ -(function() { - 'use strict'; +(function () { + "use strict"; // Keep legacy functions as wrappers for backwards compatibility - + /** * DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead */ - window.getCheckoutLabels = function(key) { + window.getCheckoutLabels = function (key) { if (window.i18nManager && window.i18nManager.initialized) { if (key) { return window.i18nManager.get(key); @@ -38,30 +38,29 @@ /** * DEPRECATED - Use i18nManager.getAll() instead */ - window.getSearchLabels = function() { + window.getSearchLabels = function () { if (window.i18nManager && window.i18nManager.initialized) { return { - 'searchPlaceholder': window.i18nManager.get('search_products'), - 'noResults': window.i18nManager.get('no_results') + searchPlaceholder: window.i18nManager.get("search_products"), + noResults: window.i18nManager.get("no_results"), }; } return { - 'searchPlaceholder': 'Search products...', - 'noResults': 'No products found' + searchPlaceholder: "Search products...", + noResults: "No products found", }; }; /** * DEPRECATED - Use i18nManager.formatCurrency(amount) instead */ - window.formatCurrency = function(amount) { + window.formatCurrency = function (amount) { if (window.i18nManager) { return window.i18nManager.formatCurrency(amount); } // Fallback - return '€' + parseFloat(amount).toFixed(2); + return "€" + parseFloat(amount).toFixed(2); }; - console.log('[i18n_helpers] DEPRECATED - Use i18n_manager.js instead'); - + console.log("[i18n_helpers] DEPRECATED - Use i18n_manager.js instead"); })(); diff --git a/website_sale_aplicoop/static/src/js/i18n_manager.js b/website_sale_aplicoop/static/src/js/i18n_manager.js index 08a0f9e..cc13201 100644 --- a/website_sale_aplicoop/static/src/js/i18n_manager.js +++ b/website_sale_aplicoop/static/src/js/i18n_manager.js @@ -1,21 +1,21 @@ /** * I18N Manager - Unified Translation Management - * + * * Single point of truth for all translations. * Fetches from server endpoint /eskaera/i18n once and caches. - * + * * Usage: * i18nManager.init().then(function() { * var translated = i18nManager.get('product'); // Returns translated string * var allLabels = i18nManager.getAll(); // Returns all labels * }); - * + * * Copyright 2025 Criptomart * License AGPL-3.0 or later */ -(function() { - 'use strict'; +(function () { + "use strict"; window.i18nManager = { labels: null, @@ -26,7 +26,7 @@ * Initialize by fetching translations from server * Returns a Promise that resolves when translations are loaded */ - init: function() { + init: function () { if (this.initialized) { return Promise.resolve(); } @@ -38,41 +38,45 @@ var self = this; // Detect user's language from document or fallback to en_US - var detectedLang = document.documentElement.lang || 'es_ES'; - console.log('[i18nManager] Detected language:', detectedLang); + var detectedLang = document.documentElement.lang || "es_ES"; + console.log("[i18nManager] Detected language:", detectedLang); // Fetch translations from server - this.initPromise = fetch('/eskaera/i18n', { - method: 'POST', + this.initPromise = fetch("/eskaera/i18n", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify({ lang: detectedLang }) + body: JSON.stringify({ lang: detectedLang }), }) - .then(function(response) { - if (!response.ok) { - throw new Error('HTTP error, status = ' + response.status); - } - return response.json(); - }) - .then(function(data) { - // Handle JSON-RPC response format - // Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } } - // Extract the actual labels from the result property - var labels = data.result || data; - - console.log('[i18nManager] ✓ Loaded', Object.keys(labels).length, 'translation labels'); - self.labels = labels; - self.initialized = true; - return labels; - }) - .catch(function(error) { - console.error('[i18nManager] Error loading translations:', error); - // Fallback to empty object so app doesn't crash - self.labels = {}; - self.initialized = true; - return {}; - }); + .then(function (response) { + if (!response.ok) { + throw new Error("HTTP error, status = " + response.status); + } + return response.json(); + }) + .then(function (data) { + // Handle JSON-RPC response format + // Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } } + // Extract the actual labels from the result property + var labels = data.result || data; + + console.log( + "[i18nManager] ✓ Loaded", + Object.keys(labels).length, + "translation labels" + ); + self.labels = labels; + self.initialized = true; + return labels; + }) + .catch(function (error) { + console.error("[i18nManager] Error loading translations:", error); + // Fallback to empty object so app doesn't crash + self.labels = {}; + self.initialized = true; + return {}; + }); return this.initPromise; }, @@ -81,9 +85,9 @@ * Get a specific translation label * Returns the translated string or the key if not found */ - get: function(key) { + get: function (key) { if (!this.initialized) { - console.warn('[i18nManager] Not yet initialized. Call init() first.'); + console.warn("[i18nManager] Not yet initialized. Call init() first."); return key; } return this.labels[key] || key; @@ -92,9 +96,9 @@ /** * Get all translation labels as object */ - getAll: function() { + getAll: function () { if (!this.initialized) { - console.warn('[i18nManager] Not yet initialized. Call init() first.'); + console.warn("[i18nManager] Not yet initialized. Call init() first."); return {}; } return this.labels; @@ -103,7 +107,7 @@ /** * Check if a specific label exists */ - has: function(key) { + has: function (key) { if (!this.initialized) return false; return key in this.labels; }, @@ -111,43 +115,42 @@ /** * Format currency to Euro format */ - formatCurrency: function(amount) { + formatCurrency: function (amount) { try { - return new Intl.NumberFormat(document.documentElement.lang || 'es_ES', { - style: 'currency', - currency: 'EUR' + return new Intl.NumberFormat(document.documentElement.lang || "es_ES", { + style: "currency", + currency: "EUR", }).format(amount); } catch (e) { // Fallback to simple Euro format - return '€' + parseFloat(amount).toFixed(2); + return "€" + parseFloat(amount).toFixed(2); } }, /** * Escape HTML to prevent XSS */ - escapeHtml: function(text) { - if (!text) return ''; - var div = document.createElement('div'); + escapeHtml: function (text) { + if (!text) return ""; + var div = document.createElement("div"); div.textContent = text; return div.innerHTML; - } + }, }; // Auto-initialize on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - i18nManager.init().catch(function(err) { - console.error('[i18nManager] Auto-init failed:', err); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", function () { + i18nManager.init().catch(function (err) { + console.error("[i18nManager] Auto-init failed:", err); }); }); } else { // DOM already loaded - setTimeout(function() { - i18nManager.init().catch(function(err) { - console.error('[i18nManager] Auto-init failed:', err); + setTimeout(function () { + i18nManager.init().catch(function (err) { + console.error("[i18nManager] Auto-init failed:", err); }); }, 100); } - })(); diff --git a/website_sale_aplicoop/static/tests/README.md b/website_sale_aplicoop/static/tests/README.md index 82e9c51..eee11fd 100644 --- a/website_sale_aplicoop/static/tests/README.md +++ b/website_sale_aplicoop/static/tests/README.md @@ -134,7 +134,7 @@ docker-compose exec odoo odoo -d odoo --test-enable --log-level=test --stop-afte odoo.define('website_sale_aplicoop.test_my_feature', function (require) { 'use strict'; var QUnit = window.QUnit; - + QUnit.module('website_sale_aplicoop.my_feature', { beforeEach: function() { // Setup code @@ -257,6 +257,6 @@ exit $exit_code --- -**Maintainer**: Criptomart -**License**: AGPL-3.0 +**Maintainer**: Criptomart +**License**: AGPL-3.0 **Last Updated**: February 3, 2026 diff --git a/website_sale_aplicoop/static/tests/test_cart_functions.js b/website_sale_aplicoop/static/tests/test_cart_functions.js index 1160a05..3fabe68 100644 --- a/website_sale_aplicoop/static/tests/test_cart_functions.js +++ b/website_sale_aplicoop/static/tests/test_cart_functions.js @@ -3,222 +3,273 @@ * Tests core cart functionality (add, remove, update, calculate) */ -odoo.define('website_sale_aplicoop.test_cart_functions', function (require) { - 'use strict'; +odoo.define("website_sale_aplicoop.test_cart_functions", function (require) { + "use strict"; var QUnit = window.QUnit; - QUnit.module('website_sale_aplicoop', { - beforeEach: function() { - // Setup: Initialize groupOrderShop object - window.groupOrderShop = { - orderId: '1', - cart: {}, - labels: { - 'save_cart': 'Save Cart', - 'reload_cart': 'Reload Cart', - 'checkout': 'Checkout', - 'confirm_order': 'Confirm Order', - 'back_to_cart': 'Back to Cart' - } - }; - - // Clear localStorage - localStorage.clear(); + QUnit.module( + "website_sale_aplicoop", + { + beforeEach: function () { + // Setup: Initialize groupOrderShop object + window.groupOrderShop = { + orderId: "1", + cart: {}, + labels: { + save_cart: "Save Cart", + reload_cart: "Reload Cart", + checkout: "Checkout", + confirm_order: "Confirm Order", + back_to_cart: "Back to Cart", + }, + }; + + // Clear localStorage + localStorage.clear(); + }, + afterEach: function () { + // Cleanup + localStorage.clear(); + delete window.groupOrderShop; + }, }, - afterEach: function() { - // Cleanup - localStorage.clear(); - delete window.groupOrderShop; + function () { + QUnit.test("groupOrderShop object initializes correctly", function (assert) { + assert.expect(3); + + assert.ok(window.groupOrderShop, "groupOrderShop object exists"); + assert.equal(window.groupOrderShop.orderId, "1", "orderId is set"); + assert.ok(typeof window.groupOrderShop.cart === "object", "cart is an object"); + }); + + QUnit.test("cart starts empty", function (assert) { + assert.expect(1); + + var cartKeys = Object.keys(window.groupOrderShop.cart); + assert.equal(cartKeys.length, 0, "cart has no items initially"); + }); + + QUnit.test("can add item to cart", function (assert) { + assert.expect(4); + + // Add a product to cart + var productId = "123"; + var productData = { + name: "Test Product", + price: 10.5, + quantity: 2, + }; + + window.groupOrderShop.cart[productId] = productData; + + assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, "cart has 1 item"); + assert.ok(window.groupOrderShop.cart[productId], "product exists in cart"); + assert.equal( + window.groupOrderShop.cart[productId].name, + "Test Product", + "product name is correct" + ); + assert.equal( + window.groupOrderShop.cart[productId].quantity, + 2, + "product quantity is correct" + ); + }); + + QUnit.test("can remove item from cart", function (assert) { + assert.expect(2); + + // Add then remove + var productId = "123"; + window.groupOrderShop.cart[productId] = { + name: "Test Product", + price: 10.5, + quantity: 2, + }; + + assert.equal( + Object.keys(window.groupOrderShop.cart).length, + 1, + "cart has 1 item after add" + ); + + delete window.groupOrderShop.cart[productId]; + + assert.equal( + Object.keys(window.groupOrderShop.cart).length, + 0, + "cart is empty after remove" + ); + }); + + QUnit.test("can update item quantity", function (assert) { + assert.expect(3); + + var productId = "123"; + window.groupOrderShop.cart[productId] = { + name: "Test Product", + price: 10.5, + quantity: 2, + }; + + assert.equal( + window.groupOrderShop.cart[productId].quantity, + 2, + "initial quantity is 2" + ); + + // Update quantity + window.groupOrderShop.cart[productId].quantity = 5; + + assert.equal( + window.groupOrderShop.cart[productId].quantity, + 5, + "quantity updated to 5" + ); + assert.equal( + Object.keys(window.groupOrderShop.cart).length, + 1, + "still only 1 item in cart" + ); + }); + + QUnit.test("cart total calculates correctly", function (assert) { + assert.expect(1); + + // Add multiple products + window.groupOrderShop.cart["123"] = { + name: "Product 1", + price: 10.0, + quantity: 2, + }; + + window.groupOrderShop.cart["456"] = { + name: "Product 2", + price: 5.5, + quantity: 3, + }; + + // Calculate total manually + var total = 0; + Object.keys(window.groupOrderShop.cart).forEach(function (productId) { + var item = window.groupOrderShop.cart[productId]; + total += item.price * item.quantity; + }); + + // Expected: (10.00 * 2) + (5.50 * 3) = 20.00 + 16.50 = 36.50 + assert.equal(total.toFixed(2), "36.50", "cart total is correct"); + }); + + QUnit.test("localStorage saves cart correctly", function (assert) { + assert.expect(2); + + var cartKey = "eskaera_1_cart"; + var testCart = { + 123: { + name: "Test Product", + price: 10.5, + quantity: 2, + }, + }; + + // Save to localStorage + localStorage.setItem(cartKey, JSON.stringify(testCart)); + + // Retrieve and verify + var savedCart = JSON.parse(localStorage.getItem(cartKey)); + + assert.ok(savedCart, "cart was saved to localStorage"); + assert.equal(savedCart["123"].name, "Test Product", "cart data is correct"); + }); + + QUnit.test("labels object is initialized", function (assert) { + assert.expect(5); + + assert.ok(window.groupOrderShop.labels, "labels object exists"); + assert.equal( + window.groupOrderShop.labels["save_cart"], + "Save Cart", + "save_cart label exists" + ); + assert.equal( + window.groupOrderShop.labels["reload_cart"], + "Reload Cart", + "reload_cart label exists" + ); + assert.equal( + window.groupOrderShop.labels["checkout"], + "Checkout", + "checkout label exists" + ); + assert.equal( + window.groupOrderShop.labels["confirm_order"], + "Confirm Order", + "confirm_order label exists" + ); + }); + + QUnit.test("cart handles decimal quantities correctly", function (assert) { + assert.expect(2); + + window.groupOrderShop.cart["123"] = { + name: "Weight Product", + price: 8.99, + quantity: 1.5, + }; + + var item = window.groupOrderShop.cart["123"]; + var subtotal = item.price * item.quantity; + + assert.equal(item.quantity, 1.5, "decimal quantity stored correctly"); + assert.equal( + subtotal.toFixed(2), + "13.49", + "subtotal with decimal quantity is correct" + ); + }); + + QUnit.test("cart handles zero quantity", function (assert) { + assert.expect(1); + + window.groupOrderShop.cart["123"] = { + name: "Test Product", + price: 10.0, + quantity: 0, + }; + + var item = window.groupOrderShop.cart["123"]; + var subtotal = item.price * item.quantity; + + assert.equal(subtotal, 0, "zero quantity results in zero subtotal"); + }); + + QUnit.test("cart handles multiple items with same price", function (assert) { + assert.expect(2); + + window.groupOrderShop.cart["123"] = { + name: "Product A", + price: 10.0, + quantity: 2, + }; + + window.groupOrderShop.cart["456"] = { + name: "Product B", + price: 10.0, + quantity: 3, + }; + + var total = 0; + Object.keys(window.groupOrderShop.cart).forEach(function (productId) { + var item = window.groupOrderShop.cart[productId]; + total += item.price * item.quantity; + }); + + assert.equal(Object.keys(window.groupOrderShop.cart).length, 2, "cart has 2 items"); + assert.equal(total.toFixed(2), "50.00", "total is correct with same prices"); + }); } - }, function() { - - QUnit.test('groupOrderShop object initializes correctly', function(assert) { - assert.expect(3); - - assert.ok(window.groupOrderShop, 'groupOrderShop object exists'); - assert.equal(window.groupOrderShop.orderId, '1', 'orderId is set'); - assert.ok(typeof window.groupOrderShop.cart === 'object', 'cart is an object'); - }); - - QUnit.test('cart starts empty', function(assert) { - assert.expect(1); - - var cartKeys = Object.keys(window.groupOrderShop.cart); - assert.equal(cartKeys.length, 0, 'cart has no items initially'); - }); - - QUnit.test('can add item to cart', function(assert) { - assert.expect(4); - - // Add a product to cart - var productId = '123'; - var productData = { - name: 'Test Product', - price: 10.50, - quantity: 2 - }; - - window.groupOrderShop.cart[productId] = productData; - - assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'cart has 1 item'); - assert.ok(window.groupOrderShop.cart[productId], 'product exists in cart'); - assert.equal(window.groupOrderShop.cart[productId].name, 'Test Product', 'product name is correct'); - assert.equal(window.groupOrderShop.cart[productId].quantity, 2, 'product quantity is correct'); - }); - - QUnit.test('can remove item from cart', function(assert) { - assert.expect(2); - - // Add then remove - var productId = '123'; - window.groupOrderShop.cart[productId] = { - name: 'Test Product', - price: 10.50, - quantity: 2 - }; - - assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'cart has 1 item after add'); - - delete window.groupOrderShop.cart[productId]; - - assert.equal(Object.keys(window.groupOrderShop.cart).length, 0, 'cart is empty after remove'); - }); - - QUnit.test('can update item quantity', function(assert) { - assert.expect(3); - - var productId = '123'; - window.groupOrderShop.cart[productId] = { - name: 'Test Product', - price: 10.50, - quantity: 2 - }; - - assert.equal(window.groupOrderShop.cart[productId].quantity, 2, 'initial quantity is 2'); - - // Update quantity - window.groupOrderShop.cart[productId].quantity = 5; - - assert.equal(window.groupOrderShop.cart[productId].quantity, 5, 'quantity updated to 5'); - assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'still only 1 item in cart'); - }); - - QUnit.test('cart total calculates correctly', function(assert) { - assert.expect(1); - - // Add multiple products - window.groupOrderShop.cart['123'] = { - name: 'Product 1', - price: 10.00, - quantity: 2 - }; - - window.groupOrderShop.cart['456'] = { - name: 'Product 2', - price: 5.50, - quantity: 3 - }; - - // Calculate total manually - var total = 0; - Object.keys(window.groupOrderShop.cart).forEach(function(productId) { - var item = window.groupOrderShop.cart[productId]; - total += item.price * item.quantity; - }); - - // Expected: (10.00 * 2) + (5.50 * 3) = 20.00 + 16.50 = 36.50 - assert.equal(total.toFixed(2), '36.50', 'cart total is correct'); - }); - - QUnit.test('localStorage saves cart correctly', function(assert) { - assert.expect(2); - - var cartKey = 'eskaera_1_cart'; - var testCart = { - '123': { - name: 'Test Product', - price: 10.50, - quantity: 2 - } - }; - - // Save to localStorage - localStorage.setItem(cartKey, JSON.stringify(testCart)); - - // Retrieve and verify - var savedCart = JSON.parse(localStorage.getItem(cartKey)); - - assert.ok(savedCart, 'cart was saved to localStorage'); - assert.equal(savedCart['123'].name, 'Test Product', 'cart data is correct'); - }); - - QUnit.test('labels object is initialized', function(assert) { - assert.expect(5); - - assert.ok(window.groupOrderShop.labels, 'labels object exists'); - assert.equal(window.groupOrderShop.labels['save_cart'], 'Save Cart', 'save_cart label exists'); - assert.equal(window.groupOrderShop.labels['reload_cart'], 'Reload Cart', 'reload_cart label exists'); - assert.equal(window.groupOrderShop.labels['checkout'], 'Checkout', 'checkout label exists'); - assert.equal(window.groupOrderShop.labels['confirm_order'], 'Confirm Order', 'confirm_order label exists'); - }); - - QUnit.test('cart handles decimal quantities correctly', function(assert) { - assert.expect(2); - - window.groupOrderShop.cart['123'] = { - name: 'Weight Product', - price: 8.99, - quantity: 1.5 - }; - - var item = window.groupOrderShop.cart['123']; - var subtotal = item.price * item.quantity; - - assert.equal(item.quantity, 1.5, 'decimal quantity stored correctly'); - assert.equal(subtotal.toFixed(2), '13.49', 'subtotal with decimal quantity is correct'); - }); - - QUnit.test('cart handles zero quantity', function(assert) { - assert.expect(1); - - window.groupOrderShop.cart['123'] = { - name: 'Test Product', - price: 10.00, - quantity: 0 - }; - - var item = window.groupOrderShop.cart['123']; - var subtotal = item.price * item.quantity; - - assert.equal(subtotal, 0, 'zero quantity results in zero subtotal'); - }); - - QUnit.test('cart handles multiple items with same price', function(assert) { - assert.expect(2); - - window.groupOrderShop.cart['123'] = { - name: 'Product A', - price: 10.00, - quantity: 2 - }; - - window.groupOrderShop.cart['456'] = { - name: 'Product B', - price: 10.00, - quantity: 3 - }; - - var total = 0; - Object.keys(window.groupOrderShop.cart).forEach(function(productId) { - var item = window.groupOrderShop.cart[productId]; - total += item.price * item.quantity; - }); - - assert.equal(Object.keys(window.groupOrderShop.cart).length, 2, 'cart has 2 items'); - assert.equal(total.toFixed(2), '50.00', 'total is correct with same prices'); - }); - }); + ); return {}; }); diff --git a/website_sale_aplicoop/static/tests/test_realtime_search.js b/website_sale_aplicoop/static/tests/test_realtime_search.js index bbac93c..9cd9468 100644 --- a/website_sale_aplicoop/static/tests/test_realtime_search.js +++ b/website_sale_aplicoop/static/tests/test_realtime_search.js @@ -3,239 +3,247 @@ * Tests product filtering and search behavior */ -odoo.define('website_sale_aplicoop.test_realtime_search', function (require) { - 'use strict'; +odoo.define("website_sale_aplicoop.test_realtime_search", function (require) { + "use strict"; var QUnit = window.QUnit; - QUnit.module('website_sale_aplicoop.realtime_search', { - beforeEach: function() { - // Setup: Create test DOM with product cards - this.$fixture = $('#qunit-fixture'); - - this.$fixture.append( - '' + - '' + - '
' + - '
' + - '
' + - '
' - ); - - // Initialize search object - window.realtimeSearch = { - searchInput: document.getElementById('realtime-search-input'), - categorySelect: document.getElementById('realtime-category-select'), - productCards: document.querySelectorAll('.product-card'), - - filterProducts: function() { - var searchTerm = this.searchInput.value.toLowerCase().trim(); - var selectedCategory = this.categorySelect.value; - - var visibleCount = 0; - var hiddenCount = 0; - - this.productCards.forEach(function(card) { - var productName = card.getAttribute('data-product-name').toLowerCase(); - var categoryId = card.getAttribute('data-category-id'); - - var matchesSearch = !searchTerm || productName.includes(searchTerm); - var matchesCategory = !selectedCategory || categoryId === selectedCategory; - - if (matchesSearch && matchesCategory) { - card.classList.remove('d-none'); - visibleCount++; - } else { - card.classList.add('d-none'); - hiddenCount++; - } - }); - - return { visible: visibleCount, hidden: hiddenCount }; - } - }; + QUnit.module( + "website_sale_aplicoop.realtime_search", + { + beforeEach: function () { + // Setup: Create test DOM with product cards + this.$fixture = $("#qunit-fixture"); + + this.$fixture.append( + '' + + '" + + '
' + + '
' + + '
' + + '
' + ); + + // Initialize search object + window.realtimeSearch = { + searchInput: document.getElementById("realtime-search-input"), + categorySelect: document.getElementById("realtime-category-select"), + productCards: document.querySelectorAll(".product-card"), + + filterProducts: function () { + var searchTerm = this.searchInput.value.toLowerCase().trim(); + var selectedCategory = this.categorySelect.value; + + var visibleCount = 0; + var hiddenCount = 0; + + this.productCards.forEach(function (card) { + var productName = card.getAttribute("data-product-name").toLowerCase(); + var categoryId = card.getAttribute("data-category-id"); + + var matchesSearch = !searchTerm || productName.includes(searchTerm); + var matchesCategory = + !selectedCategory || categoryId === selectedCategory; + + if (matchesSearch && matchesCategory) { + card.classList.remove("d-none"); + visibleCount++; + } else { + card.classList.add("d-none"); + hiddenCount++; + } + }); + + return { visible: visibleCount, hidden: hiddenCount }; + }, + }; + }, + afterEach: function () { + // Cleanup + this.$fixture.empty(); + delete window.realtimeSearch; + }, }, - afterEach: function() { - // Cleanup - this.$fixture.empty(); - delete window.realtimeSearch; + function () { + QUnit.test("search input element exists", function (assert) { + assert.expect(1); + + var searchInput = document.getElementById("realtime-search-input"); + assert.ok(searchInput, "search input element exists"); + }); + + QUnit.test("category select element exists", function (assert) { + assert.expect(1); + + var categorySelect = document.getElementById("realtime-category-select"); + assert.ok(categorySelect, "category select element exists"); + }); + + QUnit.test("product cards are found", function (assert) { + assert.expect(1); + + var productCards = document.querySelectorAll(".product-card"); + assert.equal(productCards.length, 4, "found 4 product cards"); + }); + + QUnit.test("search filters by product name", function (assert) { + assert.expect(2); + + // Search for "cab" + window.realtimeSearch.searchInput.value = "cab"; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 1, "1 product visible (Cabbage)"); + assert.equal(result.hidden, 3, "3 products hidden"); + }); + + QUnit.test("search is case insensitive", function (assert) { + assert.expect(2); + + // Search for "CARROT" in uppercase + window.realtimeSearch.searchInput.value = "CARROT"; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 1, "1 product visible (Carrot)"); + assert.equal(result.hidden, 3, "3 products hidden"); + }); + + QUnit.test("empty search shows all products", function (assert) { + assert.expect(2); + + window.realtimeSearch.searchInput.value = ""; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 4, "all 4 products visible"); + assert.equal(result.hidden, 0, "no products hidden"); + }); + + QUnit.test("category filter works", function (assert) { + assert.expect(2); + + // Select category 1 + window.realtimeSearch.categorySelect.value = "1"; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 2, "2 products visible (Cabbage, Carrot)"); + assert.equal(result.hidden, 2, "2 products hidden (Apple, Banana)"); + }); + + QUnit.test("search and category filter work together", function (assert) { + assert.expect(2); + + // Search for "ca" in category 1 + window.realtimeSearch.searchInput.value = "ca"; + window.realtimeSearch.categorySelect.value = "1"; + var result = window.realtimeSearch.filterProducts(); + + // Should show: Cabbage, Carrot (both in category 1 and match "ca") + assert.equal(result.visible, 2, "2 products visible"); + assert.equal(result.hidden, 2, "2 products hidden"); + }); + + QUnit.test("search for non-existent product shows none", function (assert) { + assert.expect(2); + + window.realtimeSearch.searchInput.value = "xyz123"; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 0, "no products visible"); + assert.equal(result.hidden, 4, "all 4 products hidden"); + }); + + QUnit.test("partial match works", function (assert) { + assert.expect(2); + + // Search for "an" should match "Banana" + window.realtimeSearch.searchInput.value = "an"; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 1, "1 product visible (Banana)"); + assert.equal(result.hidden, 3, "3 products hidden"); + }); + + QUnit.test("search trims whitespace", function (assert) { + assert.expect(2); + + // Search with extra whitespace + window.realtimeSearch.searchInput.value = " apple "; + var result = window.realtimeSearch.filterProducts(); + + assert.equal(result.visible, 1, "1 product visible (Apple)"); + assert.equal(result.hidden, 3, "3 products hidden"); + }); + + QUnit.test("d-none class is added to hidden products", function (assert) { + assert.expect(1); + + window.realtimeSearch.searchInput.value = "cabbage"; + window.realtimeSearch.filterProducts(); + + var productCards = document.querySelectorAll(".product-card"); + var hiddenCards = Array.from(productCards).filter(function (card) { + return card.classList.contains("d-none"); + }); + + assert.equal(hiddenCards.length, 3, "3 cards have d-none class"); + }); + + QUnit.test("d-none class is removed from visible products", function (assert) { + assert.expect(2); + + // First hide all + window.realtimeSearch.searchInput.value = "xyz"; + window.realtimeSearch.filterProducts(); + + var allHidden = Array.from(window.realtimeSearch.productCards).every(function ( + card + ) { + return card.classList.contains("d-none"); + }); + assert.ok(allHidden, "all cards hidden initially"); + + // Then show all + window.realtimeSearch.searchInput.value = ""; + window.realtimeSearch.filterProducts(); + + var allVisible = Array.from(window.realtimeSearch.productCards).every(function ( + card + ) { + return !card.classList.contains("d-none"); + }); + assert.ok(allVisible, "all cards visible after clearing search"); + }); + + QUnit.test("filterProducts returns correct counts", function (assert) { + assert.expect(4); + + // All visible + window.realtimeSearch.searchInput.value = ""; + var result1 = window.realtimeSearch.filterProducts(); + assert.equal(result1.visible + result1.hidden, 4, "total count is 4"); + + // 1 visible + window.realtimeSearch.searchInput.value = "apple"; + var result2 = window.realtimeSearch.filterProducts(); + assert.equal(result2.visible, 1, "visible count is 1"); + + // None visible + window.realtimeSearch.searchInput.value = "xyz"; + var result3 = window.realtimeSearch.filterProducts(); + assert.equal(result3.visible, 0, "visible count is 0"); + + // Category filter + window.realtimeSearch.searchInput.value = ""; + window.realtimeSearch.categorySelect.value = "2"; + var result4 = window.realtimeSearch.filterProducts(); + assert.equal(result4.visible, 2, "category filter shows 2 products"); + }); } - }, function() { - - QUnit.test('search input element exists', function(assert) { - assert.expect(1); - - var searchInput = document.getElementById('realtime-search-input'); - assert.ok(searchInput, 'search input element exists'); - }); - - QUnit.test('category select element exists', function(assert) { - assert.expect(1); - - var categorySelect = document.getElementById('realtime-category-select'); - assert.ok(categorySelect, 'category select element exists'); - }); - - QUnit.test('product cards are found', function(assert) { - assert.expect(1); - - var productCards = document.querySelectorAll('.product-card'); - assert.equal(productCards.length, 4, 'found 4 product cards'); - }); - - QUnit.test('search filters by product name', function(assert) { - assert.expect(2); - - // Search for "cab" - window.realtimeSearch.searchInput.value = 'cab'; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 1, '1 product visible (Cabbage)'); - assert.equal(result.hidden, 3, '3 products hidden'); - }); - - QUnit.test('search is case insensitive', function(assert) { - assert.expect(2); - - // Search for "CARROT" in uppercase - window.realtimeSearch.searchInput.value = 'CARROT'; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 1, '1 product visible (Carrot)'); - assert.equal(result.hidden, 3, '3 products hidden'); - }); - - QUnit.test('empty search shows all products', function(assert) { - assert.expect(2); - - window.realtimeSearch.searchInput.value = ''; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 4, 'all 4 products visible'); - assert.equal(result.hidden, 0, 'no products hidden'); - }); - - QUnit.test('category filter works', function(assert) { - assert.expect(2); - - // Select category 1 - window.realtimeSearch.categorySelect.value = '1'; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 2, '2 products visible (Cabbage, Carrot)'); - assert.equal(result.hidden, 2, '2 products hidden (Apple, Banana)'); - }); - - QUnit.test('search and category filter work together', function(assert) { - assert.expect(2); - - // Search for "ca" in category 1 - window.realtimeSearch.searchInput.value = 'ca'; - window.realtimeSearch.categorySelect.value = '1'; - var result = window.realtimeSearch.filterProducts(); - - // Should show: Cabbage, Carrot (both in category 1 and match "ca") - assert.equal(result.visible, 2, '2 products visible'); - assert.equal(result.hidden, 2, '2 products hidden'); - }); - - QUnit.test('search for non-existent product shows none', function(assert) { - assert.expect(2); - - window.realtimeSearch.searchInput.value = 'xyz123'; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 0, 'no products visible'); - assert.equal(result.hidden, 4, 'all 4 products hidden'); - }); - - QUnit.test('partial match works', function(assert) { - assert.expect(2); - - // Search for "an" should match "Banana" - window.realtimeSearch.searchInput.value = 'an'; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 1, '1 product visible (Banana)'); - assert.equal(result.hidden, 3, '3 products hidden'); - }); - - QUnit.test('search trims whitespace', function(assert) { - assert.expect(2); - - // Search with extra whitespace - window.realtimeSearch.searchInput.value = ' apple '; - var result = window.realtimeSearch.filterProducts(); - - assert.equal(result.visible, 1, '1 product visible (Apple)'); - assert.equal(result.hidden, 3, '3 products hidden'); - }); - - QUnit.test('d-none class is added to hidden products', function(assert) { - assert.expect(1); - - window.realtimeSearch.searchInput.value = 'cabbage'; - window.realtimeSearch.filterProducts(); - - var productCards = document.querySelectorAll('.product-card'); - var hiddenCards = Array.from(productCards).filter(function(card) { - return card.classList.contains('d-none'); - }); - - assert.equal(hiddenCards.length, 3, '3 cards have d-none class'); - }); - - QUnit.test('d-none class is removed from visible products', function(assert) { - assert.expect(2); - - // First hide all - window.realtimeSearch.searchInput.value = 'xyz'; - window.realtimeSearch.filterProducts(); - - var allHidden = Array.from(window.realtimeSearch.productCards).every(function(card) { - return card.classList.contains('d-none'); - }); - assert.ok(allHidden, 'all cards hidden initially'); - - // Then show all - window.realtimeSearch.searchInput.value = ''; - window.realtimeSearch.filterProducts(); - - var allVisible = Array.from(window.realtimeSearch.productCards).every(function(card) { - return !card.classList.contains('d-none'); - }); - assert.ok(allVisible, 'all cards visible after clearing search'); - }); - - QUnit.test('filterProducts returns correct counts', function(assert) { - assert.expect(4); - - // All visible - window.realtimeSearch.searchInput.value = ''; - var result1 = window.realtimeSearch.filterProducts(); - assert.equal(result1.visible + result1.hidden, 4, 'total count is 4'); - - // 1 visible - window.realtimeSearch.searchInput.value = 'apple'; - var result2 = window.realtimeSearch.filterProducts(); - assert.equal(result2.visible, 1, 'visible count is 1'); - - // None visible - window.realtimeSearch.searchInput.value = 'xyz'; - var result3 = window.realtimeSearch.filterProducts(); - assert.equal(result3.visible, 0, 'visible count is 0'); - - // Category filter - window.realtimeSearch.searchInput.value = ''; - window.realtimeSearch.categorySelect.value = '2'; - var result4 = window.realtimeSearch.filterProducts(); - assert.equal(result4.visible, 2, 'category filter shows 2 products'); - }); - }); + ); return {}; }); diff --git a/website_sale_aplicoop/static/tests/test_suite.js b/website_sale_aplicoop/static/tests/test_suite.js index 210f2d4..6e9682f 100644 --- a/website_sale_aplicoop/static/tests/test_suite.js +++ b/website_sale_aplicoop/static/tests/test_suite.js @@ -1,10 +1,10 @@ -odoo.define('website_sale_aplicoop.test_suite', function (require) { - 'use strict'; +odoo.define("website_sale_aplicoop.test_suite", function (require) { + "use strict"; // Import all test modules - require('website_sale_aplicoop.test_cart_functions'); - require('website_sale_aplicoop.test_tooltips_labels'); - require('website_sale_aplicoop.test_realtime_search'); + require("website_sale_aplicoop.test_cart_functions"); + require("website_sale_aplicoop.test_tooltips_labels"); + require("website_sale_aplicoop.test_realtime_search"); // Test suite is automatically registered by importing modules }); diff --git a/website_sale_aplicoop/static/tests/test_tooltips_labels.js b/website_sale_aplicoop/static/tests/test_tooltips_labels.js index f8386c2..e89b4a5 100644 --- a/website_sale_aplicoop/static/tests/test_tooltips_labels.js +++ b/website_sale_aplicoop/static/tests/test_tooltips_labels.js @@ -3,185 +3,214 @@ * Tests tooltip initialization and label loading */ -odoo.define('website_sale_aplicoop.test_tooltips_labels', function (require) { - 'use strict'; +odoo.define("website_sale_aplicoop.test_tooltips_labels", function (require) { + "use strict"; var QUnit = window.QUnit; - QUnit.module('website_sale_aplicoop.tooltips_labels', { - beforeEach: function() { - // Setup: Create test DOM elements - this.$fixture = $('#qunit-fixture'); - - // Add test buttons with tooltip labels - this.$fixture.append( - '' + - '' + - '' - ); - - // Initialize groupOrderShop - window.groupOrderShop = { - orderId: '1', - cart: {}, - labels: { - 'save_cart': 'Guardar Carrito', - 'reload_cart': 'Recargar Carrito', - 'checkout': 'Proceder al Pago', - 'confirm_order': 'Confirmar Pedido', - 'back_to_cart': 'Volver al Carrito' - }, - _initTooltips: function() { - var labels = window.groupOrderShop.labels || this.labels || {}; - var tooltipElements = document.querySelectorAll('[data-tooltip-label]'); - - tooltipElements.forEach(function(el) { - var labelKey = el.getAttribute('data-tooltip-label'); - if (labelKey && labels[labelKey]) { - el.setAttribute('title', labels[labelKey]); - } - }); - } - }; + QUnit.module( + "website_sale_aplicoop.tooltips_labels", + { + beforeEach: function () { + // Setup: Create test DOM elements + this.$fixture = $("#qunit-fixture"); + + // Add test buttons with tooltip labels + this.$fixture.append( + '' + + '' + + '' + ); + + // Initialize groupOrderShop + window.groupOrderShop = { + orderId: "1", + cart: {}, + labels: { + save_cart: "Guardar Carrito", + reload_cart: "Recargar Carrito", + checkout: "Proceder al Pago", + confirm_order: "Confirmar Pedido", + back_to_cart: "Volver al Carrito", + }, + _initTooltips: function () { + var labels = window.groupOrderShop.labels || this.labels || {}; + var tooltipElements = document.querySelectorAll("[data-tooltip-label]"); + + tooltipElements.forEach(function (el) { + var labelKey = el.getAttribute("data-tooltip-label"); + if (labelKey && labels[labelKey]) { + el.setAttribute("title", labels[labelKey]); + } + }); + }, + }; + }, + afterEach: function () { + // Cleanup + this.$fixture.empty(); + delete window.groupOrderShop; + }, }, - afterEach: function() { - // Cleanup - this.$fixture.empty(); - delete window.groupOrderShop; - } - }, function() { + function () { + QUnit.test("tooltips are initialized from labels", function (assert) { + assert.expect(3); - QUnit.test('tooltips are initialized from labels', function(assert) { - assert.expect(3); - - // Initialize tooltips - window.groupOrderShop._initTooltips(); - - var btn1 = document.getElementById('test-btn-1'); - var btn2 = document.getElementById('test-btn-2'); - var btn3 = document.getElementById('test-btn-3'); - - assert.equal(btn1.getAttribute('title'), 'Guardar Carrito', 'save_cart tooltip is correct'); - assert.equal(btn2.getAttribute('title'), 'Proceder al Pago', 'checkout tooltip is correct'); - assert.equal(btn3.getAttribute('title'), 'Recargar Carrito', 'reload_cart tooltip is correct'); - }); - - QUnit.test('tooltips handle missing labels gracefully', function(assert) { - assert.expect(1); - - // Add button with non-existent label - this.$fixture.append(''); - - window.groupOrderShop._initTooltips(); - - var btn4 = document.getElementById('test-btn-4'); - var title = btn4.getAttribute('title'); - - // Should be null or empty since label doesn't exist - assert.ok(!title || title === '', 'missing label does not set tooltip'); - }); - - QUnit.test('labels object contains expected keys', function(assert) { - assert.expect(5); - - var labels = window.groupOrderShop.labels; - - assert.ok('save_cart' in labels, 'has save_cart label'); - assert.ok('reload_cart' in labels, 'has reload_cart label'); - assert.ok('checkout' in labels, 'has checkout label'); - assert.ok('confirm_order' in labels, 'has confirm_order label'); - assert.ok('back_to_cart' in labels, 'has back_to_cart label'); - }); - - QUnit.test('labels are strings', function(assert) { - assert.expect(5); - - var labels = window.groupOrderShop.labels; - - assert.equal(typeof labels.save_cart, 'string', 'save_cart is string'); - assert.equal(typeof labels.reload_cart, 'string', 'reload_cart is string'); - assert.equal(typeof labels.checkout, 'string', 'checkout is string'); - assert.equal(typeof labels.confirm_order, 'string', 'confirm_order is string'); - assert.equal(typeof labels.back_to_cart, 'string', 'back_to_cart is string'); - }); - - QUnit.test('_initTooltips uses window.groupOrderShop.labels', function(assert) { - assert.expect(1); - - // Update global labels - window.groupOrderShop.labels = { - 'save_cart': 'Updated Label', - 'checkout': 'Updated Checkout', - 'reload_cart': 'Updated Reload' - }; - - window.groupOrderShop._initTooltips(); - - var btn1 = document.getElementById('test-btn-1'); - assert.equal(btn1.getAttribute('title'), 'Updated Label', 'uses updated global labels'); - }); - - QUnit.test('tooltips can be reinitialized', function(assert) { - assert.expect(2); - - // First initialization - window.groupOrderShop._initTooltips(); - var btn1 = document.getElementById('test-btn-1'); - assert.equal(btn1.getAttribute('title'), 'Guardar Carrito', 'first init correct'); - - // Update labels and reinitialize - window.groupOrderShop.labels.save_cart = 'New Translation'; - window.groupOrderShop._initTooltips(); - - assert.equal(btn1.getAttribute('title'), 'New Translation', 'reinitialized with new label'); - }); - - QUnit.test('elements without data-tooltip-label are ignored', function(assert) { - assert.expect(1); - - this.$fixture.append(''); - - window.groupOrderShop._initTooltips(); - - var btnNoLabel = document.getElementById('test-btn-no-label'); - var title = btnNoLabel.getAttribute('title'); - - assert.ok(!title || title === '', 'button without data-tooltip-label has no title'); - }); - - QUnit.test('querySelectorAll finds all tooltip elements', function(assert) { - assert.expect(1); - - var tooltipElements = document.querySelectorAll('[data-tooltip-label]'); - - // We have 3 buttons with data-tooltip-label - assert.equal(tooltipElements.length, 3, 'finds all 3 elements with data-tooltip-label'); - }); - - QUnit.test('labels survive JSON serialization', function(assert) { - assert.expect(2); - - var labels = window.groupOrderShop.labels; - var serialized = JSON.stringify(labels); - var deserialized = JSON.parse(serialized); - - assert.ok(serialized, 'labels can be serialized to JSON'); - assert.deepEqual(deserialized, labels, 'deserialized labels match original'); - }); - - QUnit.test('empty labels object does not break initialization', function(assert) { - assert.expect(1); - - window.groupOrderShop.labels = {}; - - try { + // Initialize tooltips window.groupOrderShop._initTooltips(); - assert.ok(true, 'initialization with empty labels does not throw error'); - } catch (e) { - assert.ok(false, 'initialization threw error: ' + e.message); - } - }); - }); + + var btn1 = document.getElementById("test-btn-1"); + var btn2 = document.getElementById("test-btn-2"); + var btn3 = document.getElementById("test-btn-3"); + + assert.equal( + btn1.getAttribute("title"), + "Guardar Carrito", + "save_cart tooltip is correct" + ); + assert.equal( + btn2.getAttribute("title"), + "Proceder al Pago", + "checkout tooltip is correct" + ); + assert.equal( + btn3.getAttribute("title"), + "Recargar Carrito", + "reload_cart tooltip is correct" + ); + }); + + QUnit.test("tooltips handle missing labels gracefully", function (assert) { + assert.expect(1); + + // Add button with non-existent label + this.$fixture.append( + '' + ); + + window.groupOrderShop._initTooltips(); + + var btn4 = document.getElementById("test-btn-4"); + var title = btn4.getAttribute("title"); + + // Should be null or empty since label doesn't exist + assert.ok(!title || title === "", "missing label does not set tooltip"); + }); + + QUnit.test("labels object contains expected keys", function (assert) { + assert.expect(5); + + var labels = window.groupOrderShop.labels; + + assert.ok("save_cart" in labels, "has save_cart label"); + assert.ok("reload_cart" in labels, "has reload_cart label"); + assert.ok("checkout" in labels, "has checkout label"); + assert.ok("confirm_order" in labels, "has confirm_order label"); + assert.ok("back_to_cart" in labels, "has back_to_cart label"); + }); + + QUnit.test("labels are strings", function (assert) { + assert.expect(5); + + var labels = window.groupOrderShop.labels; + + assert.equal(typeof labels.save_cart, "string", "save_cart is string"); + assert.equal(typeof labels.reload_cart, "string", "reload_cart is string"); + assert.equal(typeof labels.checkout, "string", "checkout is string"); + assert.equal(typeof labels.confirm_order, "string", "confirm_order is string"); + assert.equal(typeof labels.back_to_cart, "string", "back_to_cart is string"); + }); + + QUnit.test("_initTooltips uses window.groupOrderShop.labels", function (assert) { + assert.expect(1); + + // Update global labels + window.groupOrderShop.labels = { + save_cart: "Updated Label", + checkout: "Updated Checkout", + reload_cart: "Updated Reload", + }; + + window.groupOrderShop._initTooltips(); + + var btn1 = document.getElementById("test-btn-1"); + assert.equal( + btn1.getAttribute("title"), + "Updated Label", + "uses updated global labels" + ); + }); + + QUnit.test("tooltips can be reinitialized", function (assert) { + assert.expect(2); + + // First initialization + window.groupOrderShop._initTooltips(); + var btn1 = document.getElementById("test-btn-1"); + assert.equal(btn1.getAttribute("title"), "Guardar Carrito", "first init correct"); + + // Update labels and reinitialize + window.groupOrderShop.labels.save_cart = "New Translation"; + window.groupOrderShop._initTooltips(); + + assert.equal( + btn1.getAttribute("title"), + "New Translation", + "reinitialized with new label" + ); + }); + + QUnit.test("elements without data-tooltip-label are ignored", function (assert) { + assert.expect(1); + + this.$fixture.append(''); + + window.groupOrderShop._initTooltips(); + + var btnNoLabel = document.getElementById("test-btn-no-label"); + var title = btnNoLabel.getAttribute("title"); + + assert.ok(!title || title === "", "button without data-tooltip-label has no title"); + }); + + QUnit.test("querySelectorAll finds all tooltip elements", function (assert) { + assert.expect(1); + + var tooltipElements = document.querySelectorAll("[data-tooltip-label]"); + + // We have 3 buttons with data-tooltip-label + assert.equal( + tooltipElements.length, + 3, + "finds all 3 elements with data-tooltip-label" + ); + }); + + QUnit.test("labels survive JSON serialization", function (assert) { + assert.expect(2); + + var labels = window.groupOrderShop.labels; + var serialized = JSON.stringify(labels); + var deserialized = JSON.parse(serialized); + + assert.ok(serialized, "labels can be serialized to JSON"); + assert.deepEqual(deserialized, labels, "deserialized labels match original"); + }); + + QUnit.test("empty labels object does not break initialization", function (assert) { + assert.expect(1); + + window.groupOrderShop.labels = {}; + + try { + window.groupOrderShop._initTooltips(); + assert.ok(true, "initialization with empty labels does not throw error"); + } catch (e) { + assert.ok(false, "initialization threw error: " + e.message); + } + }); + } + ); return {}; }); diff --git a/website_sale_aplicoop/tests/COBERTURA_TESTS_ANALISIS.md b/website_sale_aplicoop/tests/COBERTURA_TESTS_ANALISIS.md index 360d1aa..5f881c9 100644 --- a/website_sale_aplicoop/tests/COBERTURA_TESTS_ANALISIS.md +++ b/website_sale_aplicoop/tests/COBERTURA_TESTS_ANALISIS.md @@ -1,7 +1,7 @@ # Análisis de Cobertura de Tests - website_sale_aplicoop -**Fecha**: 11 de febrero de 2026 -**Estado**: ✅ **ACTUALIZADO** - Tests de pricing agregados +**Fecha**: 11 de febrero de 2026 +**Estado**: ✅ **ACTUALIZADO** - Tests de pricing agregados **Última actualización**: Sistema de precios completamente cubierto (16 nuevos tests) --- @@ -310,7 +310,7 @@ Sistema de precios: 0% coverage (CRÍTICO) Sistema de precios: ~95% coverage (✅ RESUELTO) ``` -**Tiempo invertido**: ~2 horas +**Tiempo invertido**: ~2 horas **ROI**: Alto - Se cubrió funcionalidad crítica de cálculo de precios --- @@ -343,7 +343,7 @@ Si se necesita más cobertura, priorizar en este orden: --- -**Conclusión Final**: +**Conclusión Final**: ✅ **El sistema de precios está completamente testeado y producción-ready.** diff --git a/website_sale_aplicoop/tests/test_draft_persistence.py b/website_sale_aplicoop/tests/test_draft_persistence.py index 44563ba..e862e9c 100644 --- a/website_sale_aplicoop/tests/test_draft_persistence.py +++ b/website_sale_aplicoop/tests/test_draft_persistence.py @@ -13,7 +13,8 @@ Coverage: - Draft timeline (very old draft, recent draft) """ -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta from odoo.tests.common import TransactionCase @@ -23,91 +24,117 @@ class TestSaveDraftOrder(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) - self.product1 = self.env['product.product'].create({ - 'name': 'Product 1', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.category.id, - }) + self.product1 = self.env["product.product"].create( + { + "name": "Product 1", + "type": "consu", + "list_price": 10.0, + "categ_id": self.category.id, + } + ) - self.product2 = self.env['product.product'].create({ - 'name': 'Product 2', - 'type': 'consu', - 'list_price': 20.0, - 'categ_id': self.category.id, - }) + self.product2 = self.env["product.product"].create( + { + "name": "Product 2", + "type": "consu", + "list_price": 20.0, + "categ_id": self.category.id, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'pickup_date': start_date + timedelta(days=3), - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "pickup_date": start_date + timedelta(days=3), + "cutoff_day": "0", + } + ) self.group_order.action_open() self.group_order.product_ids = [(4, self.product1.id), (4, self.product2.id)] def test_save_draft_with_items(self): """Test saving draft order with products.""" - draft_order = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [ - (0, 0, { - 'product_id': self.product1.id, - 'product_qty': 2, - 'price_unit': self.product1.list_price, - }), - (0, 0, { - 'product_id': self.product2.id, - 'product_qty': 1, - 'price_unit': self.product2.list_price, - }), - ], - }) + draft_order = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product1.id, + "product_qty": 2, + "price_unit": self.product1.list_price, + }, + ), + ( + 0, + 0, + { + "product_id": self.product2.id, + "product_qty": 1, + "price_unit": self.product2.list_price, + }, + ), + ], + } + ) self.assertTrue(draft_order.exists()) - self.assertEqual(draft_order.state, 'draft') + self.assertEqual(draft_order.state, "draft") self.assertEqual(len(draft_order.order_line), 2) def test_save_draft_empty_order(self): """Test saving draft order without items.""" # Edge case: empty draft - empty_draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [], - }) + empty_draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [], + } + ) # Should be valid (user hasn't added products yet) self.assertTrue(empty_draft.exists()) @@ -116,15 +143,23 @@ class TestSaveDraftOrder(TransactionCase): def test_save_draft_updates_existing(self): """Test that saving draft updates existing draft, not creates new.""" # Create initial draft - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [(0, 0, { - 'product_id': self.product1.id, - 'product_qty': 1, - })], - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product1.id, + "product_qty": 1, + }, + ) + ], + } + ) draft_id = draft.id @@ -132,29 +167,33 @@ class TestSaveDraftOrder(TransactionCase): draft.order_line[0].product_qty = 5 # Should be same draft, not new one - updated_draft = self.env['sale.order'].browse(draft_id) + updated_draft = self.env["sale.order"].browse(draft_id) self.assertTrue(updated_draft.exists()) self.assertEqual(updated_draft.order_line[0].product_qty, 5) def test_save_draft_preserves_group_order_reference(self): """Test that group_order_id is preserved when saving.""" - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + } + ) # Link must be preserved self.assertEqual(draft.group_order_id, self.group_order) def test_save_draft_preserves_pickup_date(self): """Test that pickup_date is preserved in draft.""" - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_date': self.group_order.pickup_date, - 'state': 'draft', - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_date": self.group_order.pickup_date, + "state": "draft", + } + ) self.assertEqual(draft.pickup_date, self.group_order.pickup_date) @@ -164,63 +203,83 @@ class TestLoadDraftOrder(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.group_order.action_open() def test_load_existing_draft(self): """Test loading an existing draft order.""" # Create draft - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [(0, 0, { - 'product_id': self.product.id, - 'product_qty': 3, - })], - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 3, + }, + ) + ], + } + ) # Load it - loaded = self.env['sale.order'].search([ - ('id', '=', draft.id), - ('partner_id', '=', self.member_partner.id), - ('state', '=', 'draft'), - ]) + loaded = self.env["sale.order"].search( + [ + ("id", "=", draft.id), + ("partner_id", "=", self.member_partner.id), + ("state", "=", "draft"), + ] + ) self.assertEqual(len(loaded), 1) self.assertEqual(loaded[0].order_line[0].product_qty, 3) @@ -228,29 +287,37 @@ class TestLoadDraftOrder(TransactionCase): def test_load_draft_not_visible_to_other_user(self): """Test that draft from one user not accessible to another.""" # Create draft for member_partner - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + } + ) # Create another user/partner - other_partner = self.env['res.partner'].create({ - 'name': 'Other Member', - 'email': 'other@test.com', - }) + other_partner = self.env["res.partner"].create( + { + "name": "Other Member", + "email": "other@test.com", + } + ) - other_user = self.env['res.users'].create({ - 'name': 'Other User', - 'login': 'other@test.com', - 'partner_id': other_partner.id, - }) + other_user = self.env["res.users"].create( + { + "name": "Other User", + "login": "other@test.com", + "partner_id": other_partner.id, + } + ) # Other user should not see original draft - other_drafts = self.env['sale.order'].search([ - ('id', '=', draft.id), - ('partner_id', '=', other_partner.id), - ]) + other_drafts = self.env["sale.order"].search( + [ + ("id", "=", draft.id), + ("partner_id", "=", other_partner.id), + ] + ) self.assertEqual(len(other_drafts), 0) @@ -260,14 +327,16 @@ class TestLoadDraftOrder(TransactionCase): self.group_order.action_close() # Create draft before closure (simulated) - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + } + ) # Draft should still be loadable (but should warn) - loaded = self.env['sale.order'].browse(draft.id) + loaded = self.env["sale.order"].browse(draft.id) self.assertTrue(loaded.exists()) # Controller should check: group_order.state and warn if closed @@ -277,41 +346,51 @@ class TestDraftConsistency(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 100.0, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 100.0, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.group_order.action_open() def test_draft_price_snapshot(self): @@ -319,16 +398,24 @@ class TestDraftConsistency(TransactionCase): original_price = self.product.list_price # Save draft with current price - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [(0, 0, { - 'product_id': self.product.id, - 'product_qty': 1, - 'price_unit': original_price, - })], - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 1, + "price_unit": original_price, + }, + ) + ], + } + ) saved_price = draft.order_line[0].price_unit @@ -342,18 +429,26 @@ class TestDraftConsistency(TransactionCase): def test_draft_quantity_consistency(self): """Test that quantities are preserved across saves.""" # Save draft - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [(0, 0, { - 'product_id': self.product.id, - 'product_qty': 5, - })], - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 5, + }, + ) + ], + } + ) # Re-load draft - reloaded = self.env['sale.order'].browse(draft.id) + reloaded = self.env["sale.order"].browse(draft.id) self.assertEqual(reloaded.order_line[0].product_qty, 5) @@ -362,62 +457,80 @@ class TestProductArchivedInDraft(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - 'active': True, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + "active": True, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.group_order.action_open() def test_load_draft_with_archived_product(self): """Test loading draft when product has been archived.""" # Create draft with active product - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - 'order_line': [(0, 0, { - 'product_id': self.product.id, - 'product_qty': 2, - })], - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 2, + }, + ) + ], + } + ) # Archive the product self.product.active = False # Load draft - should still work (historical data) - loaded = self.env['sale.order'].browse(draft.id) + loaded = self.env["sale.order"].browse(draft.id) self.assertTrue(loaded.exists()) # But product may not be editable/accessible @@ -427,108 +540,128 @@ class TestDraftTimeline(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + } + ) def test_draft_from_current_week(self): """Test draft from current/open group order.""" start_date = datetime.now().date() - current_order = self.env['group.order'].create({ - 'name': 'Current Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + current_order = self.env["group.order"].create( + { + "name": "Current Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) current_order.action_open() - draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': current_order.id, - 'state': 'draft', - }) + draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": current_order.id, + "state": "draft", + } + ) # Should be accessible and valid self.assertTrue(draft.exists()) - self.assertEqual(draft.group_order_id.state, 'open') + self.assertEqual(draft.group_order_id.state, "open") def test_draft_from_old_order_6_months_ago(self): """Test draft from order that was 6 months ago.""" old_start = datetime.now().date() - timedelta(days=180) old_end = old_start + timedelta(days=7) - old_order = self.env['group.order'].create({ - 'name': 'Old Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': old_start, - 'end_date': old_end, - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + old_order = self.env["group.order"].create( + { + "name": "Old Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": old_start, + "end_date": old_end, + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) old_order.action_open() old_order.action_close() - old_draft = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': old_order.id, - 'state': 'draft', - }) + old_draft = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": old_order.id, + "state": "draft", + } + ) # Should still exist but be inaccessible (order closed) self.assertTrue(old_draft.exists()) - self.assertEqual(old_order.state, 'closed') + self.assertEqual(old_order.state, "closed") def test_draft_order_count_for_user(self): """Test counting total drafts for a user.""" # Create multiple orders and drafts orders = [] for i in range(3): - start = datetime.now().date() + timedelta(days=i*7) - order = self.env['group.order'].create({ - 'name': f'Order {i}', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': start + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + start = datetime.now().date() + timedelta(days=i * 7) + order = self.env["group.order"].create( + { + "name": f"Order {i}", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": start + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) order.action_open() orders.append(order) # Create draft for each for order in orders: - self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': order.id, - 'state': 'draft', - }) + self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": order.id, + "state": "draft", + } + ) # Count drafts for user - user_drafts = self.env['sale.order'].search([ - ('partner_id', '=', self.member_partner.id), - ('state', '=', 'draft'), - ]) + user_drafts = self.env["sale.order"].search( + [ + ("partner_id", "=", self.member_partner.id), + ("state", "=", "draft"), + ] + ) self.assertEqual(len(user_drafts), 3) diff --git a/website_sale_aplicoop/tests/test_edge_cases.py b/website_sale_aplicoop/tests/test_edge_cases.py index 53d7d8c..bcacf48 100644 --- a/website_sale_aplicoop/tests/test_edge_cases.py +++ b/website_sale_aplicoop/tests/test_edge_cases.py @@ -13,11 +13,13 @@ Coverage: - Extreme dates (year 1900, year 2099) """ -from datetime import datetime, timedelta, date +from datetime import date +from datetime import timedelta + from dateutil.relativedelta import relativedelta -from odoo.tests.common import TransactionCase from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase class TestLeapYearHandling(TransactionCase): @@ -25,10 +27,12 @@ class TestLeapYearHandling(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_order_spans_leap_day(self): """Test order that includes Feb 29 (leap year).""" @@ -36,16 +40,18 @@ class TestLeapYearHandling(TransactionCase): start = date(2024, 2, 25) end = date(2024, 3, 3) # Spans Feb 29 - order = self.env['group.order'].create({ - 'name': 'Leap Year Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '2', # Wednesday (Feb 28 or 29 depending on week) - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Leap Year Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "2", # Wednesday (Feb 28 or 29 depending on week) + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Should correctly calculate pickup date @@ -57,16 +63,18 @@ class TestLeapYearHandling(TransactionCase): start = date(2024, 2, 26) # Monday end = date(2024, 3, 3) - order = self.env['group.order'].create({ - 'name': 'Feb 29 Pickup', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '3', # Thursday = Feb 29 - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Feb 29 Pickup", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "3", # Thursday = Feb 29 + "cutoff_day": "0", + } + ) self.assertEqual(order.pickup_date, date(2024, 2, 29)) @@ -76,16 +84,18 @@ class TestLeapYearHandling(TransactionCase): start = date(2023, 2, 25) end = date(2023, 3, 3) - order = self.env['group.order'].create({ - 'name': 'Non-Leap Year Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '2', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Non-Leap Year Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "2", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Pickup should be Feb 28 (last day of Feb) @@ -97,26 +107,30 @@ class TestLongDurationOrders(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_order_spans_entire_year(self): """Test order running for 365 days.""" start = date(2024, 1, 1) end = date(2024, 12, 31) - order = self.env['group.order'].create({ - 'name': 'Year-Long Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '3', # Same day each week - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Year-Long Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "3", # Same day each week + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Should handle 52+ weeks correctly @@ -128,16 +142,18 @@ class TestLongDurationOrders(TransactionCase): start = date(2024, 1, 1) end = date(2026, 12, 31) # 3 years - order = self.env['group.order'].create({ - 'name': 'Multi-Year Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'monthly', - 'pickup_day': '15', - 'cutoff_day': '10', - }) + order = self.env["group.order"].create( + { + "name": "Multi-Year Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "monthly", + "pickup_day": "15", + "cutoff_day": "10", + } + ) self.assertTrue(order.exists()) days_diff = (end - start).days @@ -147,16 +163,18 @@ class TestLongDurationOrders(TransactionCase): """Test order with start_date == end_date (single day).""" same_day = date(2024, 2, 15) - order = self.env['group.order'].create({ - 'name': 'One-Day Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'once', - 'start_date': same_day, - 'end_date': same_day, - 'period': 'once', - 'pickup_day': '0', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "One-Day Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "once", + "start_date": same_day, + "end_date": same_day, + "period": "once", + "pickup_day": "0", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) @@ -166,10 +184,12 @@ class TestPickupDayBoundary(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_pickup_day_same_as_start_date(self): """Test when pickup_day equals start date (today).""" @@ -177,16 +197,18 @@ class TestPickupDayBoundary(TransactionCase): start = today end = today + timedelta(days=7) - order = self.env['group.order'].create({ - 'name': 'Today Pickup', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': str(start.weekday()), # Same as start - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Today Pickup", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": str(start.weekday()), # Same as start + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Pickup should be today @@ -198,16 +220,18 @@ class TestPickupDayBoundary(TransactionCase): start = date(2024, 1, 24) end = date(2024, 2, 1) - order = self.env['group.order'].create({ - 'name': 'Month-End Pickup', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'once', - 'pickup_day': '2', # Wednesday = Jan 31 - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Month-End Pickup", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "once", + "pickup_day": "2", # Wednesday = Jan 31 + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) @@ -217,16 +241,18 @@ class TestPickupDayBoundary(TransactionCase): start = date(2024, 1, 28) end = date(2024, 2, 5) - order = self.env['group.order'].create({ - 'name': 'Month Boundary Pickup', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '4', # Friday (Feb 2) - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Month Boundary Pickup", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "4", # Friday (Feb 2) + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Pickup should be in Feb @@ -238,16 +264,18 @@ class TestPickupDayBoundary(TransactionCase): end = date(2024, 1, 8) for day_num in range(7): - order = self.env['group.order'].create({ - 'name': f'Pickup Day {day_num}', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': str(day_num), - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": f"Pickup Day {day_num}", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": str(day_num), + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Each should have valid pickup_date @@ -259,10 +287,12 @@ class TestFutureStartDateOrders(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_order_starts_tomorrow(self): """Test order starting tomorrow.""" @@ -270,16 +300,18 @@ class TestFutureStartDateOrders(TransactionCase): start = today + timedelta(days=1) end = start + timedelta(days=7) - order = self.env['group.order'].create({ - 'name': 'Future Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Future Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) self.assertGreater(order.start_date, today) @@ -290,16 +322,18 @@ class TestFutureStartDateOrders(TransactionCase): start = today + relativedelta(months=6) end = start + timedelta(days=30) - order = self.env['group.order'].create({ - 'name': 'Far Future Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'monthly', - 'pickup_day': '15', - 'cutoff_day': '10', - }) + order = self.env["group.order"].create( + { + "name": "Far Future Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "monthly", + "pickup_day": "15", + "cutoff_day": "10", + } + ) self.assertTrue(order.exists()) @@ -309,26 +343,30 @@ class TestExtremeDate(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_order_year_2000(self): """Test order in year 2000 (Y2K edge case).""" start = date(2000, 1, 1) end = date(2000, 12, 31) - order = self.env['group.order'].create({ - 'name': 'Y2K Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Y2K Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) @@ -337,16 +375,18 @@ class TestExtremeDate(TransactionCase): start = date(2099, 1, 1) end = date(2099, 12, 31) - order = self.env['group.order'].create({ - 'name': 'Far Future Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Far Future Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) @@ -355,16 +395,18 @@ class TestExtremeDate(TransactionCase): start = date(1999, 12, 26) end = date(2000, 1, 2) - order = self.env['group.order'].create({ - 'name': 'Century Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '6', # Saturday - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Century Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "6", # Saturday + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # Should handle date arithmetic correctly across years @@ -377,25 +419,29 @@ class TestOrderWithoutEndDate(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_permanent_order_with_null_end_date(self): """Test order with end_date = NULL (ongoing order).""" start = date.today() - order = self.env['group.order'].create({ - 'name': 'Permanent Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': False, # No end date - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Permanent Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": False, # No end date + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) # If supported, should handle gracefully # Otherwise, may be optional validation @@ -406,10 +452,12 @@ class TestPickupCalculationAccuracy(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) def test_pickup_date_calculation_multiple_weeks(self): """Test pickup_date calculation over multiple weeks.""" @@ -417,16 +465,18 @@ class TestPickupCalculationAccuracy(TransactionCase): start = date(2024, 1, 1) end = date(2024, 1, 22) - order = self.env['group.order'].create({ - 'name': 'Multi-Week Pickup', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'weekly', - 'pickup_day': '3', # Thursday - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Multi-Week Pickup", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "weekly", + "pickup_day": "3", # Thursday + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) # First pickup should be first Thursday on or after start @@ -438,16 +488,18 @@ class TestPickupCalculationAccuracy(TransactionCase): start = date(2024, 2, 1) end = date(2024, 3, 31) - order = self.env['group.order'].create({ - 'name': 'Monthly Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start, - 'end_date': end, - 'period': 'monthly', - 'pickup_day': '15', - 'cutoff_day': '10', - }) + order = self.env["group.order"].create( + { + "name": "Monthly Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start, + "end_date": end, + "period": "monthly", + "pickup_day": "15", + "cutoff_day": "10", + } + ) self.assertTrue(order.exists()) # First pickup should be Feb 15 diff --git a/website_sale_aplicoop/tests/test_endpoints.py b/website_sale_aplicoop/tests/test_endpoints.py index 1c2c1ab..d7a7e50 100644 --- a/website_sale_aplicoop/tests/test_endpoints.py +++ b/website_sale_aplicoop/tests/test_endpoints.py @@ -16,11 +16,13 @@ Coverage: - /eskaera/labels (GET) - Get translated labels """ -from datetime import datetime, timedelta -import json +from datetime import datetime +from datetime import timedelta -from odoo.tests.common import TransactionCase, HttpCase -from odoo.exceptions import ValidationError, AccessError +from odoo.exceptions import AccessError +from odoo.exceptions import ValidationError +from odoo.tests.common import HttpCase +from odoo.tests.common import TransactionCase class TestEskaearaListEndpoint(TransactionCase): @@ -28,63 +30,75 @@ class TestEskaearaListEndpoint(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - 'email': 'group@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + "email": "group@test.com", + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) # Create multiple group orders (some open, some closed) start_date = datetime.now().date() - self.open_order = self.env['group.order'].create({ - 'name': 'Open Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.open_order = self.env["group.order"].create( + { + "name": "Open Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.open_order.action_open() - self.draft_order = self.env['group.order'].create({ - 'name': 'Draft Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date - timedelta(days=14), - 'end_date': start_date - timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.draft_order = self.env["group.order"].create( + { + "name": "Draft Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date - timedelta(days=14), + "end_date": start_date - timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) # Stay in draft - self.closed_order = self.env['group.order'].create({ - 'name': 'Closed Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date - timedelta(days=21), - 'end_date': start_date - timedelta(days=14), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.closed_order = self.env["group.order"].create( + { + "name": "Closed Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date - timedelta(days=21), + "end_date": start_date - timedelta(days=14), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.closed_order.action_open() self.closed_order.action_close() @@ -92,10 +106,12 @@ class TestEskaearaListEndpoint(TransactionCase): """Test that /eskaera shows only open/draft orders, not closed.""" # In controller context, only open and draft should be visible to members # This is business logic: closed orders are historical - visible_orders = self.env['group.order'].search([ - ('state', 'in', ['open', 'draft']), - ('group_ids', 'in', self.group.id), - ]) + visible_orders = self.env["group.order"].search( + [ + ("state", "in", ["open", "draft"]), + ("group_ids", "in", self.group.id), + ] + ) self.assertIn(self.open_order, visible_orders) self.assertIn(self.draft_order, visible_orders) @@ -103,30 +119,36 @@ class TestEskaearaListEndpoint(TransactionCase): def test_eskaera_list_filters_by_user_groups(self): """Test that user only sees orders from their groups.""" - other_group = self.env['res.partner'].create({ - 'name': 'Other Group', - 'is_company': True, - 'email': 'other@test.com', - }) + other_group = self.env["res.partner"].create( + { + "name": "Other Group", + "is_company": True, + "email": "other@test.com", + } + ) - other_order = self.env['group.order'].create({ - 'name': 'Other Group Order', - 'group_ids': [(6, 0, [other_group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + other_order = self.env["group.order"].create( + { + "name": "Other Group Order", + "group_ids": [(6, 0, [other_group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) other_order.action_open() # User should not see orders from groups they're not in user_groups = self.member_partner.group_ids - visible_orders = self.env['group.order'].search([ - ('state', 'in', ['open', 'draft']), - ('group_ids', 'in', user_groups.ids), - ]) + visible_orders = self.env["group.order"].search( + [ + ("state", "in", ["open", "draft"]), + ("group_ids", "in", user_groups.ids), + ] + ) self.assertNotIn(other_order, visible_orders) @@ -136,61 +158,75 @@ class TestAddToCartEndpoint(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - 'email': 'group@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + "email": "group@test.com", + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) # Published product - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.category.id, - 'sale_ok': True, - 'is_published': True, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + "categ_id": self.category.id, + "sale_ok": True, + "is_published": True, + } + ) # Unpublished product (should not be available) - self.unpublished_product = self.env['product.product'].create({ - 'name': 'Unpublished Product', - 'type': 'consu', - 'list_price': 15.0, - 'categ_id': self.category.id, - 'sale_ok': False, - 'is_published': False, - }) + self.unpublished_product = self.env["product.product"].create( + { + "name": "Unpublished Product", + "type": "consu", + "list_price": 15.0, + "categ_id": self.category.id, + "sale_ok": False, + "is_published": False, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) self.group_order.action_open() self.group_order.product_ids = [(4, self.product.id)] @@ -198,13 +234,13 @@ class TestAddToCartEndpoint(TransactionCase): """Test adding published product to cart.""" # Simulate controller logic cart_line = { - 'product_id': self.product.id, - 'quantity': 2, - 'group_order_id': self.group_order.id, - 'partner_id': self.member_partner.id, + "product_id": self.product.id, + "quantity": 2, + "group_order_id": self.group_order.id, + "partner_id": self.member_partner.id, } # Should succeed - self.assertTrue(cart_line['product_id']) + self.assertTrue(cart_line["product_id"]) def test_add_to_cart_zero_quantity(self): """Test that adding zero quantity is rejected.""" @@ -228,11 +264,13 @@ class TestAddToCartEndpoint(TransactionCase): def test_add_to_cart_product_not_in_order(self): """Test that products not in the order cannot be added.""" # Create a product NOT associated with group_order - other_product = self.env['product.product'].create({ - 'name': 'Other Product', - 'type': 'consu', - 'list_price': 25.0, - }) + other_product = self.env["product.product"].create( + { + "name": "Other Product", + "type": "consu", + "list_price": 25.0, + } + ) # Controller should check: product in group_order.product_ids self.assertNotIn(other_product, self.group_order.product_ids) @@ -241,7 +279,7 @@ class TestAddToCartEndpoint(TransactionCase): """Test that adding to closed order is rejected.""" self.group_order.action_close() # Controller should check: order.state == 'open' - self.assertEqual(self.group_order.state, 'closed') + self.assertEqual(self.group_order.state, "closed") class TestCheckoutEndpoint(TransactionCase): @@ -249,38 +287,46 @@ class TestCheckoutEndpoint(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - 'email': 'group@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + "email": "group@test.com", + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'pickup_date': start_date + timedelta(days=3), - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "pickup_date": start_date + timedelta(days=3), + "cutoff_day": "0", + } + ) self.group_order.action_open() def test_checkout_page_loads(self): @@ -301,16 +347,18 @@ class TestCheckoutEndpoint(TransactionCase): def test_checkout_order_without_products(self): """Test checkout when no products available.""" # Order with empty product_ids - empty_order = self.env['group.order'].create({ - 'name': 'Empty Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + empty_order = self.env["group.order"].create( + { + "name": "Empty Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) empty_order.action_open() # Should handle gracefully @@ -322,95 +370,115 @@ class TestConfirmOrderEndpoint(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - 'email': 'group@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + "email": "group@test.com", + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.category.id, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + "categ_id": self.category.id, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'pickup_date': start_date + timedelta(days=3), - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "pickup_date": start_date + timedelta(days=3), + "cutoff_day": "0", + } + ) self.group_order.action_open() self.group_order.product_ids = [(4, self.product.id)] # Create a draft sale order - self.draft_sale = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_date': self.group_order.pickup_date, - 'state': 'draft', - }) + self.draft_sale = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_date": self.group_order.pickup_date, + "state": "draft", + } + ) def test_confirm_order_creates_sale_order(self): """Test that confirming creates a confirmed sale.order.""" # Controller should change state from draft to sale self.draft_sale.action_confirm() - self.assertEqual(self.draft_sale.state, 'sale') + self.assertEqual(self.draft_sale.state, "sale") def test_confirm_empty_order(self): """Test confirming order without items fails.""" # Order with no order_lines should fail - empty_sale = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - }) + empty_sale = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + } + ) # Should validate: must have at least one line self.assertEqual(len(empty_sale.order_line), 0) def test_confirm_order_wrong_group(self): """Test that user cannot confirm order from different group.""" - other_group = self.env['res.partner'].create({ - 'name': 'Other Group', - 'is_company': True, - }) + other_group = self.env["res.partner"].create( + { + "name": "Other Group", + "is_company": True, + } + ) - other_order = self.env['group.order'].create({ - 'name': 'Other Order', - 'group_ids': [(6, 0, [other_group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + other_order = self.env["group.order"].create( + { + "name": "Other Order", + "group_ids": [(6, 0, [other_group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) # User should not be in other_group self.assertNotIn(self.member_partner, other_group.member_ids) @@ -421,76 +489,94 @@ class TestLoadDraftEndpoint(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - 'email': 'group@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + "email": "group@test.com", + } + ) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member", + "email": "member@test.com", + } + ) self.group.member_ids = [(4, self.member_partner.id)] - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) - self.category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.category.id, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + "categ_id": self.category.id, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'pickup_date': start_date + timedelta(days=3), - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "pickup_date": start_date + timedelta(days=3), + "cutoff_day": "0", + } + ) self.group_order.action_open() self.group_order.product_ids = [(4, self.product.id)] def test_load_draft_from_history(self): """Test loading a previous draft order.""" # Create old draft sale - old_sale = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - }) + old_sale = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + } + ) # Should be able to load self.assertTrue(old_sale.exists()) def test_load_draft_not_owned_by_user(self): """Test that user cannot load draft from other user.""" - other_partner = self.env['res.partner'].create({ - 'name': 'Other Member', - 'email': 'other@test.com', - }) + other_partner = self.env["res.partner"].create( + { + "name": "Other Member", + "email": "other@test.com", + } + ) - other_sale = self.env['sale.order'].create({ - 'partner_id': other_partner.id, - 'group_order_id': self.group_order.id, - 'state': 'draft', - }) + other_sale = self.env["sale.order"].create( + { + "partner_id": other_partner.id, + "group_order_id": self.group_order.id, + "state": "draft", + } + ) # User should not be able to load other's draft self.assertNotEqual(other_sale.partner_id, self.member_partner) @@ -500,24 +586,28 @@ class TestLoadDraftEndpoint(TransactionCase): old_start = datetime.now().date() - timedelta(days=30) old_end = datetime.now().date() - timedelta(days=23) - expired_order = self.env['group.order'].create({ - 'name': 'Expired Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': old_start, - 'end_date': old_end, - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + expired_order = self.env["group.order"].create( + { + "name": "Expired Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": old_start, + "end_date": old_end, + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) expired_order.action_open() expired_order.action_close() - old_sale = self.env['sale.order'].create({ - 'partner_id': self.member_partner.id, - 'group_order_id': expired_order.id, - 'state': 'draft', - }) + old_sale = self.env["sale.order"].create( + { + "partner_id": self.member_partner.id, + "group_order_id": expired_order.id, + "state": "draft", + } + ) # Should warn: order expired - self.assertEqual(expired_order.state, 'closed') + self.assertEqual(expired_order.state, "closed") diff --git a/website_sale_aplicoop/tests/test_eskaera_shop.py b/website_sale_aplicoop/tests/test_eskaera_shop.py index 88b0ae9..f46dfbe 100644 --- a/website_sale_aplicoop/tests/test_eskaera_shop.py +++ b/website_sale_aplicoop/tests/test_eskaera_shop.py @@ -1,127 +1,158 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta from odoo.tests.common import TransactionCase class TestEskaerShop(TransactionCase): - '''Test suite para la lógica de eskaera_shop (descubrimiento de productos).''' + """Test suite para la lógica de eskaera_shop (descubrimiento de productos).""" def setUp(self): super().setUp() # Crear un grupo (res.partner) - self.group = self.env['res.partner'].create({ - 'name': 'Grupo Test Eskaera', - 'is_company': True, - 'email': 'grupo@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Grupo Test Eskaera", + "is_company": True, + "email": "grupo@test.com", + } + ) # Crear usuario miembro del grupo - user_partner = self.env['res.partner'].create({ - 'name': 'Usuario Test Partner', - 'email': 'usuario_test@test.com', - }) + user_partner = self.env["res.partner"].create( + { + "name": "Usuario Test Partner", + "email": "usuario_test@test.com", + } + ) - self.user = self.env['res.users'].create({ - 'name': 'Usuario Test', - 'login': 'usuario_test@test.com', - 'email': 'usuario_test@test.com', - 'partner_id': user_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Usuario Test", + "login": "usuario_test@test.com", + "email": "usuario_test@test.com", + "partner_id": user_partner.id, + } + ) # Añadir el partner del usuario como miembro del grupo self.group.member_ids = [(4, user_partner.id)] # Crear categorías de producto - self.category1 = self.env['product.category'].create({ - 'name': 'Categoría Test 1', - }) + self.category1 = self.env["product.category"].create( + { + "name": "Categoría Test 1", + } + ) - self.category2 = self.env['product.category'].create({ - 'name': 'Categoría Test 2', - }) + self.category2 = self.env["product.category"].create( + { + "name": "Categoría Test 2", + } + ) # Crear proveedor - self.supplier = self.env['res.partner'].create({ - 'name': 'Proveedor Test', - 'is_company': True, - 'supplier_rank': 1, - 'email': 'proveedor@test.com', - }) + self.supplier = self.env["res.partner"].create( + { + "name": "Proveedor Test", + "is_company": True, + "supplier_rank": 1, + "email": "proveedor@test.com", + } + ) # Crear productos - self.product_cat1 = self.env['product.product'].create({ - 'name': 'Producto Categoría 1', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.category1.id, - 'active': True, - }) - self.product_cat1.product_tmpl_id.write({ - 'is_published': True, - 'sale_ok': True, - }) + self.product_cat1 = self.env["product.product"].create( + { + "name": "Producto Categoría 1", + "type": "consu", + "list_price": 10.0, + "categ_id": self.category1.id, + "active": True, + } + ) + self.product_cat1.product_tmpl_id.write( + { + "is_published": True, + "sale_ok": True, + } + ) - self.product_cat2 = self.env['product.product'].create({ - 'name': 'Producto Categoría 2', - 'type': 'consu', - 'list_price': 20.0, - 'categ_id': self.category2.id, - 'active': True, - }) - self.product_cat2.product_tmpl_id.write({ - 'is_published': True, - 'sale_ok': True, - }) + self.product_cat2 = self.env["product.product"].create( + { + "name": "Producto Categoría 2", + "type": "consu", + "list_price": 20.0, + "categ_id": self.category2.id, + "active": True, + } + ) + self.product_cat2.product_tmpl_id.write( + { + "is_published": True, + "sale_ok": True, + } + ) # Crear producto con relación a proveedor - self.product_supplier_template = self.env['product.template'].create({ - 'name': 'Producto Proveedor', - 'type': 'consu', - 'list_price': 30.0, - 'categ_id': self.category1.id, - 'is_published': True, - 'sale_ok': True, - }) + self.product_supplier_template = self.env["product.template"].create( + { + "name": "Producto Proveedor", + "type": "consu", + "list_price": 30.0, + "categ_id": self.category1.id, + "is_published": True, + "sale_ok": True, + } + ) self.product_supplier = self.product_supplier_template.product_variant_ids[0] self.product_supplier.active = True # Crear relación con proveedor - self.env['product.supplierinfo'].create({ - 'product_tmpl_id': self.product_supplier_template.id, - 'partner_id': self.supplier.id, - 'min_qty': 1.0, - }) + self.env["product.supplierinfo"].create( + { + "product_tmpl_id": self.product_supplier_template.id, + "partner_id": self.supplier.id, + "min_qty": 1.0, + } + ) - self.product_direct = self.env['product.product'].create({ - 'name': 'Producto Directo', - 'type': 'consu', - 'list_price': 40.0, - 'categ_id': self.category1.id, - 'active': True, - }) - self.product_direct.product_tmpl_id.write({ - 'is_published': True, - 'sale_ok': True, - }) + self.product_direct = self.env["product.product"].create( + { + "name": "Producto Directo", + "type": "consu", + "list_price": 40.0, + "categ_id": self.category1.id, + "active": True, + } + ) + self.product_direct.product_tmpl_id.write( + { + "is_published": True, + "sale_ok": True, + } + ) def test_product_discovery_direct(self): - '''Test que los productos directos se descubren correctamente.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Directo', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'product_ids': [(6, 0, [self.product_direct.id])], - }) + """Test que los productos directos se descubren correctamente.""" + order = self.env["group.order"].create( + { + "name": "Pedido Directo", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "product_ids": [(6, 0, [self.product_direct.id])], + } + ) order.action_open() @@ -131,96 +162,124 @@ class TestEskaerShop(TransactionCase): self.assertEqual(len(products), 1) self.assertIn(self.product_direct, products) - products = self.env['product.product']._get_products_for_group_order(order.id) - self.assertIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id')) + products = self.env["product.product"]._get_products_for_group_order(order.id) + self.assertIn( + self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id") + ) def test_product_discovery_by_category(self): - '''Test que los productos se descubren por categoría cuando no hay directos.''' - order = self.env['group.order'].create({ - 'name': 'Pedido por Categoría', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'category_ids': [(6, 0, [self.category1.id])], - }) + """Test que los productos se descubren por categoría cuando no hay directos.""" + order = self.env["group.order"].create( + { + "name": "Pedido por Categoría", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "category_ids": [(6, 0, [self.category1.id])], + } + ) order.action_open() # Simular lo que hace eskaera_shop (fallback a categorías) products = order.product_ids if not products: - products = self.env['product.product'].search([ - ('categ_id', 'in', order.category_ids.ids), - ]) + products = self.env["product.product"].search( + [ + ("categ_id", "in", order.category_ids.ids), + ] + ) # Debe incluir todos los productos de la categoría 1 self.assertGreaterEqual(len(products), 2) - self.assertIn(self.product_cat1.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id')) + self.assertIn( + self.product_cat1.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertIn( + self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertIn( + self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id") + ) - order.write({'category_ids': [(4, self.category1.id)]}) - products = self.env['product.product']._get_products_for_group_order(order.id) - self.assertIn(self.product_cat1.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertNotIn(self.product_cat2.product_tmpl_id, products.mapped('product_tmpl_id')) + order.write({"category_ids": [(4, self.category1.id)]}) + products = self.env["product.product"]._get_products_for_group_order(order.id) + self.assertIn( + self.product_cat1.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertNotIn( + self.product_cat2.product_tmpl_id, products.mapped("product_tmpl_id") + ) def test_product_discovery_by_supplier(self): - '''Test que los productos se descubren por proveedor cuando no hay directos ni categorías.''' - order = self.env['group.order'].create({ - 'name': 'Pedido por Proveedor', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'supplier_ids': [(6, 0, [self.supplier.id])], - }) + """Test que los productos se descubren por proveedor cuando no hay directos ni categorías.""" + order = self.env["group.order"].create( + { + "name": "Pedido por Proveedor", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "supplier_ids": [(6, 0, [self.supplier.id])], + } + ) order.action_open() # Simular lo que hace eskaera_shop (fallback a proveedores) products = order.product_ids if not products and order.category_ids: - products = self.env['product.product'].search([ - ('categ_id', 'in', order.category_ids.ids), - ]) + products = self.env["product.product"].search( + [ + ("categ_id", "in", order.category_ids.ids), + ] + ) if not products and order.supplier_ids: # Buscar productos que tienen estos proveedores en seller_ids - product_templates = self.env['product.template'].search([ - ('seller_ids.partner_id', 'in', order.supplier_ids.ids), - ]) - products = product_templates.mapped('product_variant_ids') + product_templates = self.env["product.template"].search( + [ + ("seller_ids.partner_id", "in", order.supplier_ids.ids), + ] + ) + products = product_templates.mapped("product_variant_ids") # Debe incluir el producto del proveedor self.assertEqual(len(products), 1) - self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id')) + self.assertIn( + self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id") + ) - order.write({'supplier_ids': [(4, self.supplier.id)]}) - products = self.env['product.product']._get_products_for_group_order(order.id) - self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id')) + order.write({"supplier_ids": [(4, self.supplier.id)]}) + products = self.env["product.product"]._get_products_for_group_order(order.id) + self.assertIn( + self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id") + ) def test_product_discovery_priority(self): - '''Test que la prioridad de descubrimiento es: directos > categorías > proveedores.''' - order = self.env['group.order'].create({ - 'name': 'Pedido con Todos', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'product_ids': [(6, 0, [self.product_direct.id])], - 'category_ids': [(6, 0, [self.category1.id, self.category2.id])], - 'supplier_ids': [(6, 0, [self.supplier.id])], - }) + """Test que la prioridad de descubrimiento es: directos > categorías > proveedores.""" + order = self.env["group.order"].create( + { + "name": "Pedido con Todos", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "product_ids": [(6, 0, [self.product_direct.id])], + "category_ids": [(6, 0, [self.category1.id, self.category2.id])], + "supplier_ids": [(6, 0, [self.supplier.id])], + } + ) order.action_open() @@ -229,94 +288,122 @@ class TestEskaerShop(TransactionCase): # Debe retornar los productos directos, no los de categoría/proveedor self.assertEqual(len(products), 1) - self.assertIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertNotIn(self.product_cat1.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertNotIn(self.product_cat2.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertNotIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id')) + self.assertIn( + self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertNotIn( + self.product_cat1.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertNotIn( + self.product_cat2.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertNotIn( + self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id") + ) # 2. The canonical helper now returns the UNION of all association # sources (direct products, categories, suppliers). Assert all are # present to reflect the new behaviour. - products = self.env['product.product']._get_products_for_group_order(order.id) - tmpl_ids = products.mapped('product_tmpl_id') + products = self.env["product.product"]._get_products_for_group_order(order.id) + tmpl_ids = products.mapped("product_tmpl_id") self.assertIn(self.product_direct.product_tmpl_id, tmpl_ids) self.assertIn(self.product_cat1.product_tmpl_id, tmpl_ids) self.assertIn(self.product_supplier.product_tmpl_id, tmpl_ids) def test_product_discovery_fallback_from_category_to_supplier(self): - '''Test que si no hay directos ni categorías, usa proveedores.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Fallback', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - # Sin product_ids - # Sin category_ids - 'supplier_ids': [(6, 0, [self.supplier.id])], - }) + """Test que si no hay directos ni categorías, usa proveedores.""" + order = self.env["group.order"].create( + { + "name": "Pedido Fallback", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + # Sin product_ids + # Sin category_ids + "supplier_ids": [(6, 0, [self.supplier.id])], + } + ) order.action_open() # Simular lo que hace eskaera_shop products = order.product_ids if not products and order.category_ids: - products = self.env['product.product'].search([ - ('categ_id', 'in', order.category_ids.ids), - ]) + products = self.env["product.product"].search( + [ + ("categ_id", "in", order.category_ids.ids), + ] + ) if not products and order.supplier_ids: # Buscar productos que tienen estos proveedores en seller_ids - product_templates = self.env['product.template'].search([ - ('seller_ids.partner_id', 'in', order.supplier_ids.ids), - ]) - products = product_templates.mapped('product_variant_ids') + product_templates = self.env["product.template"].search( + [ + ("seller_ids.partner_id", "in", order.supplier_ids.ids), + ] + ) + products = product_templates.mapped("product_variant_ids") # Debe retornar productos del proveedor self.assertEqual(len(products), 1) - self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id')) + self.assertIn( + self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id") + ) # Clear categories so supplier-only fallback remains active - order.write({ - 'category_ids': [(5, 0, 0)], - 'supplier_ids': [(4, self.supplier.id)], - }) - products = self.env['product.product']._get_products_for_group_order(order.id) - self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id')) - self.assertNotIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id')) + order.write( + { + "category_ids": [(5, 0, 0)], + "supplier_ids": [(4, self.supplier.id)], + } + ) + products = self.env["product.product"]._get_products_for_group_order(order.id) + self.assertIn( + self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id") + ) + self.assertNotIn( + self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id") + ) def test_no_products_available(self): - '''Test que retorna vacío si no hay productos definidos de ninguna forma.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Sin Productos', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - # Sin product_ids, category_ids, supplier_ids - }) + """Test que retorna vacío si no hay productos definidos de ninguna forma.""" + order = self.env["group.order"].create( + { + "name": "Pedido Sin Productos", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + # Sin product_ids, category_ids, supplier_ids + } + ) order.action_open() # Simular lo que hace eskaera_shop products = order.product_ids if not products and order.category_ids: - products = self.env['product.product'].search([ - ('categ_id', 'in', order.category_ids.ids), - ]) + products = self.env["product.product"].search( + [ + ("categ_id", "in", order.category_ids.ids), + ] + ) if not products and order.supplier_ids: # Buscar productos que tienen estos proveedores en seller_ids - product_templates = self.env['product.template'].search([ - ('seller_ids.partner_id', 'in', order.supplier_ids.ids), - ]) - products = product_templates.mapped('product_variant_ids') + product_templates = self.env["product.template"].search( + [ + ("seller_ids.partner_id", "in", order.supplier_ids.ids), + ] + ) + products = product_templates.mapped("product_variant_ids") # Debe estar vacío self.assertEqual(len(products), 0) diff --git a/website_sale_aplicoop/tests/test_group_order.py b/website_sale_aplicoop/tests/test_group_order.py index 4b71869..ec8b502 100644 --- a/website_sale_aplicoop/tests/test_group_order.py +++ b/website_sale_aplicoop/tests/test_group_order.py @@ -1,310 +1,354 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta -from odoo.tests.common import TransactionCase -from odoo.exceptions import ValidationError from psycopg2 import IntegrityError + from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase class TestGroupOrder(TransactionCase): - '''Test suite para el modelo group.order.''' + """Test suite para el modelo group.order.""" def setUp(self): super().setUp() # Crear un grupo (res.partner) - self.group = self.env['res.partner'].create({ - 'name': 'Grupo Test', - 'is_company': True, - 'email': 'grupo@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Grupo Test", + "is_company": True, + "email": "grupo@test.com", + } + ) # Crear productos - self.product1 = self.env['product.product'].create({ - 'name': 'Producto Test 1', - 'type': 'consu', - 'list_price': 10.0, - }) + self.product1 = self.env["product.product"].create( + { + "name": "Producto Test 1", + "type": "consu", + "list_price": 10.0, + } + ) - self.product2 = self.env['product.product'].create({ - 'name': 'Producto Test 2', - 'type': 'consu', - 'list_price': 20.0, - }) + self.product2 = self.env["product.product"].create( + { + "name": "Producto Test 2", + "type": "consu", + "list_price": 20.0, + } + ) def test_create_group_order(self): - '''Test crear un pedido de grupo.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Semanal Test', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test crear un pedido de grupo.""" + order = self.env["group.order"].create( + { + "name": "Pedido Semanal Test", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) - self.assertEqual(order.state, 'draft') + self.assertEqual(order.state, "draft") self.assertIn(self.group, order.group_ids) def test_group_order_dates_validation(self): - """ Test that start_date must be before end_date """ + """Test that start_date must be before end_date""" with self.assertRaises(ValidationError): - self.env['group.order'].create({ - 'name': 'Pedido Invalid', - 'start_date': fields.Date.today() + timedelta(days=7), - 'end_date': fields.Date.today(), - }) + self.env["group.order"].create( + { + "name": "Pedido Invalid", + "start_date": fields.Date.today() + timedelta(days=7), + "end_date": fields.Date.today(), + } + ) def test_group_order_state_transitions(self): - '''Test transiciones de estado.''' - order = self.env['group.order'].create({ - 'name': 'Pedido State Test', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test transiciones de estado.""" + order = self.env["group.order"].create( + { + "name": "Pedido State Test", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) # Draft -> Open order.action_open() - self.assertEqual(order.state, 'open') + self.assertEqual(order.state, "open") # Open -> Closed order.action_close() - self.assertEqual(order.state, 'closed') + self.assertEqual(order.state, "closed") def test_group_order_action_cancel(self): - '''Test cancelar un pedido.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Cancel Test', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test cancelar un pedido.""" + order = self.env["group.order"].create( + { + "name": "Pedido Cancel Test", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) order.action_cancel() - self.assertEqual(order.state, 'cancelled') + self.assertEqual(order.state, "cancelled") def test_get_active_orders_for_week(self): - '''Test obtener pedidos activos para la semana.''' + """Test obtener pedidos activos para la semana.""" today = datetime.now().date() week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) # Crear pedido activo esta semana - active_order = self.env['group.order'].create({ - 'name': 'Pedido Activo', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': week_start, - 'end_date': week_end, - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'state': 'open', - }) + active_order = self.env["group.order"].create( + { + "name": "Pedido Activo", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": week_start, + "end_date": week_end, + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "state": "open", + } + ) # Crear pedido inactivo (futuro) - future_order = self.env['group.order'].create({ - 'name': 'Pedido Futuro', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': week_end + timedelta(days=1), - 'end_date': week_end + timedelta(days=8), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'state': 'open', - }) + future_order = self.env["group.order"].create( + { + "name": "Pedido Futuro", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": week_end + timedelta(days=1), + "end_date": week_end + timedelta(days=8), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "state": "open", + } + ) - active_orders = self.env['group.order'].search([ - ('state', '=', 'open'), - '|', - ('end_date', '>=', week_start), - ('end_date', '=', False), - ('start_date', '<=', week_end), - ]) + active_orders = self.env["group.order"].search( + [ + ("state", "=", "open"), + "|", + ("end_date", ">=", week_start), + ("end_date", "=", False), + ("start_date", "<=", week_end), + ] + ) self.assertIn(active_order, active_orders) self.assertNotIn(future_order, active_orders) def test_permanent_group_order(self): - '''Test crear un pedido permanente (sin end_date).''' - order = self.env['group.order'].create({ - 'name': 'Pedido Permanente', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': False, - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test crear un pedido permanente (sin end_date).""" + order = self.env["group.order"].create( + { + "name": "Pedido Permanente", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": False, + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) self.assertFalse(order.end_date) def test_get_active_orders_excludes_draft(self): - '''Test que get_active_orders_for_week NO incluye pedidos en draft.''' + """Test que get_active_orders_for_week NO incluye pedidos en draft.""" today = datetime.now().date() # Crear pedido en draft (no abierto) - draft_order = self.env['group.order'].create({ - 'name': 'Pedido Draft', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': today, - 'end_date': today + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'state': 'draft', - }) + draft_order = self.env["group.order"].create( + { + "name": "Pedido Draft", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": today, + "end_date": today + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "state": "draft", + } + ) today = datetime.now().date() week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) - active_orders = self.env['group.order'].search([ - ('state', '=', 'open'), - '|', - ('end_date', '>=', week_start), - ('end_date', '=', False), - ('start_date', '<=', week_end), - ]) + active_orders = self.env["group.order"].search( + [ + ("state", "=", "open"), + "|", + ("end_date", ">=", week_start), + ("end_date", "=", False), + ("start_date", "<=", week_end), + ] + ) self.assertNotIn(draft_order, active_orders) def test_get_active_orders_excludes_closed(self): - '''Test que get_active_orders_for_week NO incluye pedidos cerrados.''' + """Test que get_active_orders_for_week NO incluye pedidos cerrados.""" today = datetime.now().date() - closed_order = self.env['group.order'].create({ - 'name': 'Pedido Cerrado', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': today, - 'end_date': today + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'state': 'closed', - }) + closed_order = self.env["group.order"].create( + { + "name": "Pedido Cerrado", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": today, + "end_date": today + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "state": "closed", + } + ) today = datetime.now().date() week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) - active_orders = self.env['group.order'].search([ - ('state', '=', 'open'), - '|', - ('end_date', '>=', week_start), - ('end_date', '=', False), - ('start_date', '<=', week_end), - ]) + active_orders = self.env["group.order"].search( + [ + ("state", "=", "open"), + "|", + ("end_date", ">=", week_start), + ("end_date", "=", False), + ("start_date", "<=", week_end), + ] + ) self.assertNotIn(closed_order, active_orders) def test_get_active_orders_excludes_cancelled(self): - '''Test que get_active_orders_for_week NO incluye pedidos cancelados.''' + """Test que get_active_orders_for_week NO incluye pedidos cancelados.""" today = datetime.now().date() - cancelled_order = self.env['group.order'].create({ - 'name': 'Pedido Cancelado', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': today, - 'end_date': today + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - 'state': 'cancelled', - }) + cancelled_order = self.env["group.order"].create( + { + "name": "Pedido Cancelado", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": today, + "end_date": today + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + "state": "cancelled", + } + ) today = datetime.now().date() week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) - active_orders = self.env['group.order'].search([ - ('state', '=', 'open'), - '|', - ('end_date', '>=', week_start), - ('end_date', '=', False), - ('start_date', '<=', week_end), - ]) + active_orders = self.env["group.order"].search( + [ + ("state", "=", "open"), + "|", + ("end_date", ">=", week_start), + ("end_date", "=", False), + ("start_date", "<=", week_end), + ] + ) self.assertNotIn(cancelled_order, active_orders) def test_state_transition_draft_to_open(self): - '''Test que un pedido pasa de draft a open.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Estado Test', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test que un pedido pasa de draft a open.""" + order = self.env["group.order"].create( + { + "name": "Pedido Estado Test", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) - self.assertEqual(order.state, 'draft') + self.assertEqual(order.state, "draft") order.action_open() - self.assertEqual(order.state, 'open') + self.assertEqual(order.state, "open") def test_state_transition_open_to_closed(self): - '''Test transición válida open -> closed.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Estado Test', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test transición válida open -> closed.""" + order = self.env["group.order"].create( + { + "name": "Pedido Estado Test", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) order.action_open() - self.assertEqual(order.state, 'open') + self.assertEqual(order.state, "open") order.action_close() - self.assertEqual(order.state, 'closed') + self.assertEqual(order.state, "closed") def test_state_transition_any_to_cancelled(self): - '''Test cancelar desde cualquier estado.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Estado Test', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test cancelar desde cualquier estado.""" + order = self.env["group.order"].create( + { + "name": "Pedido Estado Test", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) # Desde draft order.action_cancel() - self.assertEqual(order.state, 'cancelled') + self.assertEqual(order.state, "cancelled") # Crear otro desde open - order2 = self.env['group.order'].create({ - 'name': 'Pedido Estado Test 2', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + order2 = self.env["group.order"].create( + { + "name": "Pedido Estado Test 2", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) order2.action_open() order2.action_cancel() - self.assertEqual(order2.state, 'cancelled') + self.assertEqual(order2.state, "cancelled") diff --git a/website_sale_aplicoop/tests/test_multi_company.py b/website_sale_aplicoop/tests/test_multi_company.py index de20bb5..c503812 100644 --- a/website_sale_aplicoop/tests/test_multi_company.py +++ b/website_sale_aplicoop/tests/test_multi_company.py @@ -1,147 +1,178 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta -from odoo.tests.common import TransactionCase from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase class TestMultiCompanyGroupOrder(TransactionCase): - '''Test suite para el soporte multicompañía en group.order.''' + """Test suite para el soporte multicompañía en group.order.""" def setUp(self): super().setUp() # Crear dos compañías - self.company1 = self.env['res.company'].create({ - 'name': 'Company 1', - }) - self.company2 = self.env['res.company'].create({ - 'name': 'Company 2', - }) + self.company1 = self.env["res.company"].create( + { + "name": "Company 1", + } + ) + self.company2 = self.env["res.company"].create( + { + "name": "Company 2", + } + ) # Crear grupos en diferentes compañías - self.group1 = self.env['res.partner'].create({ - 'name': 'Grupo Company 1', - 'is_company': True, - 'email': 'grupo1@test.com', - 'company_id': self.company1.id, - }) + self.group1 = self.env["res.partner"].create( + { + "name": "Grupo Company 1", + "is_company": True, + "email": "grupo1@test.com", + "company_id": self.company1.id, + } + ) - self.group2 = self.env['res.partner'].create({ - 'name': 'Grupo Company 2', - 'is_company': True, - 'email': 'grupo2@test.com', - 'company_id': self.company2.id, - }) + self.group2 = self.env["res.partner"].create( + { + "name": "Grupo Company 2", + "is_company": True, + "email": "grupo2@test.com", + "company_id": self.company2.id, + } + ) # Crear productos en cada compañía - self.product1 = self.env['product.product'].create({ - 'name': 'Producto Company 1', - 'type': 'consu', - 'list_price': 10.0, - 'company_id': self.company1.id, - }) + self.product1 = self.env["product.product"].create( + { + "name": "Producto Company 1", + "type": "consu", + "list_price": 10.0, + "company_id": self.company1.id, + } + ) - self.product2 = self.env['product.product'].create({ - 'name': 'Producto Company 2', - 'type': 'consu', - 'list_price': 20.0, - 'company_id': self.company2.id, - }) + self.product2 = self.env["product.product"].create( + { + "name": "Producto Company 2", + "type": "consu", + "list_price": 20.0, + "company_id": self.company2.id, + } + ) def test_group_order_has_company_id(self): - '''Test que group.order tenga el campo company_id.''' - order = self.env['group.order'].create({ - 'name': 'Pedido Company 1', - 'group_ids': [(6, 0, [self.group1.id])], - 'company_id': self.company1.id, - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + """Test que group.order tenga el campo company_id.""" + order = self.env["group.order"].create( + { + "name": "Pedido Company 1", + "group_ids": [(6, 0, [self.group1.id])], + "company_id": self.company1.id, + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) self.assertEqual(order.company_id, self.company1) def test_group_order_default_company(self): - '''Test que company_id por defecto sea la compañía del usuario.''' + """Test que company_id por defecto sea la compañía del usuario.""" # Crear usuario con compañía específica - user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser', - 'password': 'test123', - 'company_id': self.company1.id, - 'company_ids': [(6, 0, [self.company1.id])], - }) + user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser", + "password": "test123", + "company_id": self.company1.id, + "company_ids": [(6, 0, [self.company1.id])], + } + ) - order = self.env['group.order'].with_user(user).create({ - 'name': 'Pedido Default Company', - 'group_ids': [(6, 0, [self.group1.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + order = ( + self.env["group.order"] + .with_user(user) + .create( + { + "name": "Pedido Default Company", + "group_ids": [(6, 0, [self.group1.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) + ) # Verificar que se asignó la compañía del usuario self.assertEqual(order.company_id, self.company1) def test_group_order_company_constraint(self): - '''Test que solo grupos de la misma compañía se puedan asignar.''' + """Test que solo grupos de la misma compañía se puedan asignar.""" # Intentar asignar un grupo de otra compañía with self.assertRaises(ValidationError): - self.env['group.order'].create({ - 'name': 'Pedido Mixed Companies', - 'group_ids': [(6, 0, [self.group1.id, self.group2.id])], - 'company_id': self.company1.id, - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + self.env["group.order"].create( + { + "name": "Pedido Mixed Companies", + "group_ids": [(6, 0, [self.group1.id, self.group2.id])], + "company_id": self.company1.id, + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) def test_group_order_multi_company_filter(self): - '''Test que get_active_orders_for_week() respete company_id.''' + """Test que get_active_orders_for_week() respete company_id.""" # Crear órdenes en diferentes compañías - order1 = self.env['group.order'].create({ - 'name': 'Pedido Company 1', - 'group_ids': [(6, 0, [self.group1.id])], - 'company_id': self.company1.id, - 'type': 'regular', - 'state': 'open', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + order1 = self.env["group.order"].create( + { + "name": "Pedido Company 1", + "group_ids": [(6, 0, [self.group1.id])], + "company_id": self.company1.id, + "type": "regular", + "state": "open", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) - order2 = self.env['group.order'].create({ - 'name': 'Pedido Company 2', - 'group_ids': [(6, 0, [self.group2.id])], - 'company_id': self.company2.id, - 'type': 'regular', - 'state': 'open', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + order2 = self.env["group.order"].create( + { + "name": "Pedido Company 2", + "group_ids": [(6, 0, [self.group2.id])], + "company_id": self.company2.id, + "type": "regular", + "state": "open", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) # Obtener órdenes activas de company1 - active_orders = self.env['group.order'].with_context( - allowed_company_ids=[self.company1.id] - ).get_active_orders_for_week() + active_orders = ( + self.env["group.order"] + .with_context(allowed_company_ids=[self.company1.id]) + .get_active_orders_for_week() + ) # Debería contener solo order1 self.assertIn(order1, active_orders) @@ -149,24 +180,28 @@ class TestMultiCompanyGroupOrder(TransactionCase): # el filtro de compañía correctamente def test_product_company_isolation(self): - '''Test que los productos de diferentes compañías estén aislados.''' + """Test que los productos de diferentes compañías estén aislados.""" # Crear categoría para products - category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) - order = self.env['group.order'].create({ - 'name': 'Pedido con Categoría', - 'group_ids': [(6, 0, [self.group1.id])], - 'category_ids': [(6, 0, [category.id])], - 'company_id': self.company1.id, - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Pedido con Categoría", + "group_ids": [(6, 0, [self.group1.id])], + "category_ids": [(6, 0, [category.id])], + "company_id": self.company1.id, + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) self.assertTrue(order.exists()) self.assertEqual(order.company_id, self.company1) diff --git a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py index be8e8af..e5050a9 100644 --- a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py +++ b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py @@ -13,131 +13,166 @@ Coverage: - Product price info structure in eskaera_shop """ -import json -from odoo.tests.common import TransactionCase from odoo.tests import tagged +from odoo.tests.common import TransactionCase -@tagged('post_install', '-at_install') +@tagged("post_install", "-at_install") class TestPricingWithPricelist(TransactionCase): """Test pricing calculations using OCA product_get_price_helper addon.""" def setUp(self): super().setUp() - + # Create test company - self.company = self.env['res.company'].create({ - 'name': 'Test Company Pricing', - }) - + self.company = self.env["res.company"].create( + { + "name": "Test Company Pricing", + } + ) + # Create test group - self.group = self.env['res.partner'].create({ - 'name': 'Test Group Pricing', - 'is_company': True, - 'company_id': self.company.id, - }) - + self.group = self.env["res.partner"].create( + { + "name": "Test Group Pricing", + "is_company": True, + "company_id": self.company.id, + } + ) + # Create test user - self.user = self.env['res.users'].create({ - 'name': 'Test User Pricing', - 'login': 'testpricing@example.com', - 'company_id': self.company.id, - 'company_ids': [(6, 0, [self.company.id])], - }) - + self.user = self.env["res.users"].create( + { + "name": "Test User Pricing", + "login": "testpricing@example.com", + "company_id": self.company.id, + "company_ids": [(6, 0, [self.company.id])], + } + ) + # Get or create default tax group - tax_group = self.env['account.tax.group'].search([ - ('company_id', '=', self.company.id) - ], limit=1) + tax_group = self.env["account.tax.group"].search( + [("company_id", "=", self.company.id)], limit=1 + ) if not tax_group: - tax_group = self.env['account.tax.group'].create({ - 'name': 'IVA', - 'company_id': self.company.id, - }) - + tax_group = self.env["account.tax.group"].create( + { + "name": "IVA", + "company_id": self.company.id, + } + ) + # Get default country (Spain) - country_es = self.env.ref('base.es') - + country_es = self.env.ref("base.es") + # Create tax (21% IVA) - self.tax_21 = self.env['account.tax'].create({ - 'name': 'IVA 21%', - 'amount': 21.0, - 'amount_type': 'percent', - 'type_tax_use': 'sale', - 'company_id': self.company.id, - 'country_id': country_es.id, - 'tax_group_id': tax_group.id, - }) - + self.tax_21 = self.env["account.tax"].create( + { + "name": "IVA 21%", + "amount": 21.0, + "amount_type": "percent", + "type_tax_use": "sale", + "company_id": self.company.id, + "country_id": country_es.id, + "tax_group_id": tax_group.id, + } + ) + # Create tax (10% IVA reducido) - self.tax_10 = self.env['account.tax'].create({ - 'name': 'IVA 10%', - 'amount': 10.0, - 'amount_type': 'percent', - 'type_tax_use': 'sale', - 'company_id': self.company.id, - 'country_id': country_es.id, - 'tax_group_id': tax_group.id, - }) - + self.tax_10 = self.env["account.tax"].create( + { + "name": "IVA 10%", + "amount": 10.0, + "amount_type": "percent", + "type_tax_use": "sale", + "company_id": self.company.id, + "country_id": country_es.id, + "tax_group_id": tax_group.id, + } + ) + # Create fiscal position (maps 21% to 10%) - self.fiscal_position = self.env['account.fiscal.position'].create({ - 'name': 'Test Fiscal Position', - 'company_id': self.company.id, - }) - self.env['account.fiscal.position.tax'].create({ - 'position_id': self.fiscal_position.id, - 'tax_src_id': self.tax_21.id, - 'tax_dest_id': self.tax_10.id, - }) - + self.fiscal_position = self.env["account.fiscal.position"].create( + { + "name": "Test Fiscal Position", + "company_id": self.company.id, + } + ) + self.env["account.fiscal.position.tax"].create( + { + "position_id": self.fiscal_position.id, + "tax_src_id": self.tax_21.id, + "tax_dest_id": self.tax_10.id, + } + ) + # Create product category - self.category = self.env['product.category'].create({ - 'name': 'Test Category Pricing', - }) - + self.category = self.env["product.category"].create( + { + "name": "Test Category Pricing", + } + ) + # Create test products with different tax configurations - self.product_with_tax = self.env['product.product'].create({ - 'name': 'Product With 21% Tax', - 'list_price': 100.0, - 'categ_id': self.category.id, - 'taxes_id': [(6, 0, [self.tax_21.id])], - 'company_id': self.company.id, - }) - - self.product_without_tax = self.env['product.product'].create({ - 'name': 'Product Without Tax', - 'list_price': 50.0, - 'categ_id': self.category.id, - 'taxes_id': False, - 'company_id': self.company.id, - }) - + self.product_with_tax = self.env["product.product"].create( + { + "name": "Product With 21% Tax", + "list_price": 100.0, + "categ_id": self.category.id, + "taxes_id": [(6, 0, [self.tax_21.id])], + "company_id": self.company.id, + } + ) + + self.product_without_tax = self.env["product.product"].create( + { + "name": "Product Without Tax", + "list_price": 50.0, + "categ_id": self.category.id, + "taxes_id": False, + "company_id": self.company.id, + } + ) + # Create pricelist with discount - self.pricelist_with_discount = self.env['product.pricelist'].create({ - 'name': 'Test Pricelist 10% Discount', - 'company_id': self.company.id, - 'item_ids': [(0, 0, { - 'compute_price': 'percentage', - 'percent_price': 10.0, # 10% discount - 'applied_on': '3_global', - })], - }) - + self.pricelist_with_discount = self.env["product.pricelist"].create( + { + "name": "Test Pricelist 10% Discount", + "company_id": self.company.id, + "item_ids": [ + ( + 0, + 0, + { + "compute_price": "percentage", + "percent_price": 10.0, # 10% discount + "applied_on": "3_global", + }, + ) + ], + } + ) + # Create pricelist without discount - self.pricelist_no_discount = self.env['product.pricelist'].create({ - 'name': 'Test Pricelist No Discount', - 'company_id': self.company.id, - }) - + self.pricelist_no_discount = self.env["product.pricelist"].create( + { + "name": "Test Pricelist No Discount", + "company_id": self.company.id, + } + ) + # Create group order - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order Pricing', - 'state': 'open', - 'group_ids': [(6, 0, [self.group.id])], - 'product_ids': [(6, 0, [self.product_with_tax.id, self.product_without_tax.id])], - 'company_id': self.company.id, - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order Pricing", + "state": "open", + "group_ids": [(6, 0, [self.group.id])], + "product_ids": [ + (6, 0, [self.product_with_tax.id, self.product_without_tax.id]) + ], + "company_id": self.company.id, + } + ) def test_add_to_cart_basic_price_without_tax(self): """Test basic price calculation for product without taxes.""" @@ -147,11 +182,15 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_no_discount, fposition=False, ) - - self.assertEqual(result['value'], 50.0, - "Product without tax should have price = list_price") - self.assertEqual(result.get('discount', 0), 0, - "No discount pricelist should have 0% discount") + + self.assertEqual( + result["value"], 50.0, "Product without tax should have price = list_price" + ) + self.assertEqual( + result.get("discount", 0), + 0, + "No discount pricelist should have 0% discount", + ) def test_add_to_cart_with_pricelist_discount(self): """Test that discounted prices are calculated correctly.""" @@ -161,14 +200,18 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_with_discount, fposition=False, ) - + # OCA addon returns price without taxes by default expected_price = 100.0 * 0.9 # 90.0 - - self.assertIn('value', result, "Result must contain 'value' key") - self.assertIn('tax_included', result, "Result must contain 'tax_included' key") - self.assertAlmostEqual(result['value'], expected_price, places=2, - msg=f"Expected {expected_price}, got {result['value']}") + + self.assertIn("value", result, "Result must contain 'value' key") + self.assertIn("tax_included", result, "Result must contain 'tax_included' key") + self.assertAlmostEqual( + result["value"], + expected_price, + places=2, + msg=f"Expected {expected_price}, got {result['value']}", + ) def test_add_to_cart_with_fiscal_position(self): """Test fiscal position maps taxes correctly (21% -> 10%).""" @@ -178,19 +221,19 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_no_discount, fposition=False, ) - + result_with_fp = self.product_with_tax._get_price( qty=1.0, pricelist=self.pricelist_no_discount, fposition=self.fiscal_position, ) - + # Both should return base price (100.0) without tax by default # Tax mapping only affects tax calculation, not the base price returned - self.assertIn('value', result_without_fp, "Result must contain 'value'") - self.assertIn('value', result_with_fp, "Result must contain 'value'") - self.assertEqual(result_without_fp['value'], 100.0) - self.assertEqual(result_with_fp['value'], 100.0) + self.assertIn("value", result_without_fp, "Result must contain 'value'") + self.assertIn("value", result_with_fp, "Result must contain 'value'") + self.assertEqual(result_without_fp["value"], 100.0) + self.assertEqual(result_with_fp["value"], 100.0) def test_add_to_cart_with_tax_included(self): """Test price calculation returns tax_included flag correctly.""" @@ -200,43 +243,53 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_no_discount, fposition=False, ) - + # By default, tax is not included in price - self.assertIn('tax_included', result) - self.assertEqual(result['value'], 100.0, "Price should be base price without tax") + self.assertIn("tax_included", result) + self.assertEqual( + result["value"], 100.0, "Price should be base price without tax" + ) def test_add_to_cart_with_quantity_discount(self): """Test quantity-based discounts if applicable.""" # Create pricelist with quantity-based rule - pricelist_qty = self.env['product.pricelist'].create({ - 'name': 'Quantity Discount Pricelist', - 'company_id': self.company.id, - 'item_ids': [(0, 0, { - 'compute_price': 'percentage', - 'percent_price': 20.0, # 20% discount - 'min_quantity': 5.0, - 'applied_on': '3_global', - })], - }) - + pricelist_qty = self.env["product.pricelist"].create( + { + "name": "Quantity Discount Pricelist", + "company_id": self.company.id, + "item_ids": [ + ( + 0, + 0, + { + "compute_price": "percentage", + "percent_price": 20.0, # 20% discount + "min_quantity": 5.0, + "applied_on": "3_global", + }, + ) + ], + } + ) + # Quantity 1: No discount result_qty_1 = self.product_with_tax._get_price( qty=1.0, pricelist=pricelist_qty, fposition=False, ) - + # Quantity 5: 20% discount result_qty_5 = self.product_with_tax._get_price( qty=5.0, pricelist=pricelist_qty, fposition=False, ) - + # Qty 1: 100.0 (no discount, no tax in value) # Qty 5: 100 * 0.8 = 80.0 (with 20% discount, no tax in value) - self.assertAlmostEqual(result_qty_1['value'], 100.0, places=2) - self.assertAlmostEqual(result_qty_5['value'], 80.0, places=2) + self.assertAlmostEqual(result_qty_1["value"], 100.0, places=2) + self.assertAlmostEqual(result_qty_5["value"], 80.0, places=2) def test_add_to_cart_price_fallback_no_pricelist(self): """Test fallback to list_price when pricelist is not available.""" @@ -247,35 +300,39 @@ class TestPricingWithPricelist(TransactionCase): pricelist=False, fposition=False, ) - + # Should return list_price with taxes (fallback behavior) # This depends on OCA addon implementation self.assertIsNotNone(result, "Should not fail when pricelist is False") - self.assertIn('value', result, "Result should contain 'value' key") + self.assertIn("value", result, "Result should contain 'value' key") def test_add_to_cart_price_fallback_no_variant(self): """Test handling when product has no variants.""" # Create product template without variants - product_template = self.env['product.template'].create({ - 'name': 'Product Without Variant', - 'list_price': 75.0, - 'categ_id': self.category.id, - 'company_id': self.company.id, - }) - + product_template = self.env["product.template"].create( + { + "name": "Product Without Variant", + "list_price": 75.0, + "categ_id": self.category.id, + "company_id": self.company.id, + } + ) + # Should have auto-created variant - self.assertTrue(product_template.product_variant_ids, - "Product template should have at least one variant") - + self.assertTrue( + product_template.product_variant_ids, + "Product template should have at least one variant", + ) + variant = product_template.product_variant_ids[0] result = variant._get_price( qty=1.0, pricelist=self.pricelist_no_discount, fposition=False, ) - + self.assertIsNotNone(result, "Should handle product with auto-created variant") - self.assertAlmostEqual(result['value'], 75.0, places=2) + self.assertAlmostEqual(result["value"], 75.0, places=2) def test_product_price_info_structure(self): """Test product_price_info dict structure returned by _get_price.""" @@ -284,18 +341,19 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_with_discount, fposition=False, ) - + # Verify structure - self.assertIn('value', result, "Result must contain 'value' key") - self.assertIsInstance(result['value'], (int, float), - "Price value must be numeric") - + self.assertIn("value", result, "Result must contain 'value' key") + self.assertIsInstance( + result["value"], (int, float), "Price value must be numeric" + ) + # Optional keys (depends on OCA addon version) - if 'discount' in result: - self.assertIsInstance(result['discount'], (int, float)) - - if 'original_value' in result: - self.assertIsInstance(result['original_value'], (int, float)) + if "discount" in result: + self.assertIsInstance(result["discount"], (int, float)) + + if "original_value" in result: + self.assertIsInstance(result["original_value"], (int, float)) def test_discounted_price_visual_comparison(self): """Test comparison of original vs discounted price for UI display.""" @@ -304,71 +362,83 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_with_discount, fposition=False, ) - + # When there's a discount, original_value should be higher than value - if result.get('discount', 0) > 0: - original = result.get('original_value', result['value']) - discounted = result['value'] - self.assertGreater(original, discounted, - "Original price should be higher than discounted price") + if result.get("discount", 0) > 0: + original = result.get("original_value", result["value"]) + discounted = result["value"] + self.assertGreater( + original, + discounted, + "Original price should be higher than discounted price", + ) def test_price_calculation_with_multiple_taxes(self): """Test product with multiple taxes applied.""" # Get tax group and country from existing tax tax_group = self.tax_21.tax_group_id country = self.tax_21.country_id - + # Create additional tax - tax_extra = self.env['account.tax'].create({ - 'name': 'Extra Tax 5%', - 'amount': 5.0, - 'amount_type': 'percent', - 'type_tax_use': 'sale', - 'company_id': self.company.id, - 'country_id': country.id, - 'tax_group_id': tax_group.id, - }) - - product_multi_tax = self.env['product.product'].create({ - 'name': 'Product With Multiple Taxes', - 'list_price': 100.0, - 'categ_id': self.category.id, - 'taxes_id': [(6, 0, [self.tax_21.id, tax_extra.id])], - 'company_id': self.company.id, - }) - + tax_extra = self.env["account.tax"].create( + { + "name": "Extra Tax 5%", + "amount": 5.0, + "amount_type": "percent", + "type_tax_use": "sale", + "company_id": self.company.id, + "country_id": country.id, + "tax_group_id": tax_group.id, + } + ) + + product_multi_tax = self.env["product.product"].create( + { + "name": "Product With Multiple Taxes", + "list_price": 100.0, + "categ_id": self.category.id, + "taxes_id": [(6, 0, [self.tax_21.id, tax_extra.id])], + "company_id": self.company.id, + } + ) + result = product_multi_tax._get_price( qty=1.0, pricelist=self.pricelist_no_discount, fposition=False, ) - + # Base price 100.0 (taxes not included in value by default) - self.assertEqual(result['value'], 100.0, - msg="Should return base price (taxes applied separately)") + self.assertEqual( + result["value"], + 100.0, + msg="Should return base price (taxes applied separately)", + ) def test_price_currency_handling(self): """Test price calculation with different currencies.""" # Get or use existing EUR currency - eur = self.env['res.currency'].search([('name', '=', 'EUR')], limit=1) + eur = self.env["res.currency"].search([("name", "=", "EUR")], limit=1) if not eur: self.skipTest("EUR currency not available in test database") - + # Create pricelist with EUR - pricelist_eur = self.env['product.pricelist'].create({ - 'name': 'EUR Pricelist', - 'currency_id': eur.id, - 'company_id': self.company.id, - }) - + pricelist_eur = self.env["product.pricelist"].create( + { + "name": "EUR Pricelist", + "currency_id": eur.id, + "company_id": self.company.id, + } + ) + result = self.product_with_tax._get_price( qty=1.0, pricelist=pricelist_eur, fposition=False, ) - + self.assertIsNotNone(result, "Should handle different currency pricelist") - self.assertIn('value', result) + self.assertIn("value", result) def test_price_consistency_across_calls(self): """Test that multiple calls with same params return same price.""" @@ -377,33 +447,37 @@ class TestPricingWithPricelist(TransactionCase): pricelist=self.pricelist_with_discount, fposition=False, ) - + result2 = self.product_with_tax._get_price( qty=1.0, pricelist=self.pricelist_with_discount, fposition=False, ) - - self.assertEqual(result1['value'], result2['value'], - "Price calculation should be deterministic") + + self.assertEqual( + result1["value"], + result2["value"], + "Price calculation should be deterministic", + ) def test_zero_price_product(self): """Test handling of free products (price = 0).""" - free_product = self.env['product.product'].create({ - 'name': 'Free Product', - 'list_price': 0.0, - 'categ_id': self.category.id, - 'company_id': self.company.id, - }) - + free_product = self.env["product.product"].create( + { + "name": "Free Product", + "list_price": 0.0, + "categ_id": self.category.id, + "company_id": self.company.id, + } + ) + result = free_product._get_price( qty=1.0, pricelist=self.pricelist_no_discount, fposition=False, ) - - self.assertEqual(result['value'], 0.0, - "Free product should have price = 0") + + self.assertEqual(result["value"], 0.0, "Free product should have price = 0") def test_negative_quantity_handling(self): """Test that negative quantities are handled properly.""" diff --git a/website_sale_aplicoop/tests/test_product_discovery.py b/website_sale_aplicoop/tests/test_product_discovery.py index 45a8261..cd27b05 100644 --- a/website_sale_aplicoop/tests/test_product_discovery.py +++ b/website_sale_aplicoop/tests/test_product_discovery.py @@ -17,7 +17,8 @@ Coverage: - Ordering and deduplication """ -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta from odoo.tests.common import TransactionCase @@ -27,81 +28,105 @@ class TestProductDiscoveryUnion(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) # Create a supplier - self.supplier = self.env['res.partner'].create({ - 'name': 'Test Supplier', - 'is_supplier': True, - }) + self.supplier = self.env["res.partner"].create( + { + "name": "Test Supplier", + "is_supplier": True, + } + ) # Create categories - self.category1 = self.env['product.category'].create({ - 'name': 'Category 1', - }) + self.category1 = self.env["product.category"].create( + { + "name": "Category 1", + } + ) - self.category2 = self.env['product.category'].create({ - 'name': 'Category 2', - }) + self.category2 = self.env["product.category"].create( + { + "name": "Category 2", + } + ) # Create products # Direct product - self.direct_product = self.env['product.product'].create({ - 'name': 'Direct Product', - 'type': 'consu', - 'list_price': 10.0, - 'is_published': True, - 'sale_ok': True, - }) + self.direct_product = self.env["product.product"].create( + { + "name": "Direct Product", + "type": "consu", + "list_price": 10.0, + "is_published": True, + "sale_ok": True, + } + ) # Category 1 product - self.cat1_product = self.env['product.product'].create({ - 'name': 'Category 1 Product', - 'type': 'consu', - 'list_price': 20.0, - 'categ_id': self.category1.id, - 'is_published': True, - 'sale_ok': True, - }) + self.cat1_product = self.env["product.product"].create( + { + "name": "Category 1 Product", + "type": "consu", + "list_price": 20.0, + "categ_id": self.category1.id, + "is_published": True, + "sale_ok": True, + } + ) # Category 2 product - self.cat2_product = self.env['product.product'].create({ - 'name': 'Category 2 Product', - 'type': 'consu', - 'list_price': 30.0, - 'categ_id': self.category2.id, - 'is_published': True, - 'sale_ok': True, - }) + self.cat2_product = self.env["product.product"].create( + { + "name": "Category 2 Product", + "type": "consu", + "list_price": 30.0, + "categ_id": self.category2.id, + "is_published": True, + "sale_ok": True, + } + ) # Supplier product - self.supplier_product = self.env['product.product'].create({ - 'name': 'Supplier Product', - 'type': 'consu', - 'list_price': 40.0, - 'categ_id': self.category1.id, # Also in category - 'seller_ids': [(0, 0, { - 'partner_id': self.supplier.id, - 'product_name': 'Supplier Product', - })], - 'is_published': True, - 'sale_ok': True, - }) + self.supplier_product = self.env["product.product"].create( + { + "name": "Supplier Product", + "type": "consu", + "list_price": 40.0, + "categ_id": self.category1.id, # Also in category + "seller_ids": [ + ( + 0, + 0, + { + "partner_id": self.supplier.id, + "product_name": "Supplier Product", + }, + ) + ], + "is_published": True, + "sale_ok": True, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) def test_discovery_from_direct_products(self): """Test discovery returns directly linked products.""" @@ -145,14 +170,16 @@ class TestProductDiscoveryUnion(TransactionCase): def test_discovery_filters_unpublished(self): """Test that unpublished products are excluded from discovery.""" - unpublished = self.env['product.product'].create({ - 'name': 'Unpublished Product', - 'type': 'consu', - 'list_price': 50.0, - 'categ_id': self.category1.id, - 'is_published': False, - 'sale_ok': True, - }) + unpublished = self.env["product.product"].create( + { + "name": "Unpublished Product", + "type": "consu", + "list_price": 50.0, + "categ_id": self.category1.id, + "is_published": False, + "sale_ok": True, + } + ) self.group_order.category_ids = [(4, self.category1.id)] discovered = self.group_order.product_ids @@ -162,14 +189,16 @@ class TestProductDiscoveryUnion(TransactionCase): def test_discovery_filters_not_for_sale(self): """Test that non-sellable products are excluded.""" - not_for_sale = self.env['product.product'].create({ - 'name': 'Not For Sale', - 'type': 'consu', - 'list_price': 60.0, - 'categ_id': self.category1.id, - 'is_published': True, - 'sale_ok': False, - }) + not_for_sale = self.env["product.product"].create( + { + "name": "Not For Sale", + "type": "consu", + "list_price": 60.0, + "categ_id": self.category1.id, + "is_published": True, + "sale_ok": False, + } + ) self.group_order.category_ids = [(4, self.category1.id)] discovered = self.group_order.product_ids @@ -183,76 +212,96 @@ class TestDeepCategoryHierarchies(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) # Create nested category structure: # Root -> L1 -> L2 -> L3 -> L4 - self.cat_l1 = self.env['product.category'].create({ - 'name': 'Level 1', - }) + self.cat_l1 = self.env["product.category"].create( + { + "name": "Level 1", + } + ) - self.cat_l2 = self.env['product.category'].create({ - 'name': 'Level 2', - 'parent_id': self.cat_l1.id, - }) + self.cat_l2 = self.env["product.category"].create( + { + "name": "Level 2", + "parent_id": self.cat_l1.id, + } + ) - self.cat_l3 = self.env['product.category'].create({ - 'name': 'Level 3', - 'parent_id': self.cat_l2.id, - }) + self.cat_l3 = self.env["product.category"].create( + { + "name": "Level 3", + "parent_id": self.cat_l2.id, + } + ) - self.cat_l4 = self.env['product.category'].create({ - 'name': 'Level 4', - 'parent_id': self.cat_l3.id, - }) + self.cat_l4 = self.env["product.category"].create( + { + "name": "Level 4", + "parent_id": self.cat_l3.id, + } + ) - self.cat_l5 = self.env['product.category'].create({ - 'name': 'Level 5', - 'parent_id': self.cat_l4.id, - }) + self.cat_l5 = self.env["product.category"].create( + { + "name": "Level 5", + "parent_id": self.cat_l4.id, + } + ) # Create products at each level - self.product_l2 = self.env['product.product'].create({ - 'name': 'Product L2', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.cat_l2.id, - 'is_published': True, - 'sale_ok': True, - }) + self.product_l2 = self.env["product.product"].create( + { + "name": "Product L2", + "type": "consu", + "list_price": 10.0, + "categ_id": self.cat_l2.id, + "is_published": True, + "sale_ok": True, + } + ) - self.product_l4 = self.env['product.product'].create({ - 'name': 'Product L4', - 'type': 'consu', - 'list_price': 20.0, - 'categ_id': self.cat_l4.id, - 'is_published': True, - 'sale_ok': True, - }) + self.product_l4 = self.env["product.product"].create( + { + "name": "Product L4", + "type": "consu", + "list_price": 20.0, + "categ_id": self.cat_l4.id, + "is_published": True, + "sale_ok": True, + } + ) - self.product_l5 = self.env['product.product'].create({ - 'name': 'Product L5', - 'type': 'consu', - 'list_price': 30.0, - 'categ_id': self.cat_l5.id, - 'is_published': True, - 'sale_ok': True, - }) + self.product_l5 = self.env["product.product"].create( + { + "name": "Product L5", + "type": "consu", + "list_price": 30.0, + "categ_id": self.cat_l5.id, + "is_published": True, + "sale_ok": True, + } + ) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) def test_discovery_root_category_includes_all_descendants(self): """Test that linking root category discovers all nested products.""" @@ -307,33 +356,41 @@ class TestEmptySourcesDiscovery(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.category = self.env['product.category'].create({ - 'name': 'Empty Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Empty Category", + } + ) # No products in this category - self.supplier = self.env['res.partner'].create({ - 'name': 'Supplier No Products', - 'is_supplier': True, - }) + self.supplier = self.env["res.partner"].create( + { + "name": "Supplier No Products", + "is_supplier": True, + } + ) # No products from this supplier start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) def test_discovery_empty_category(self): """Test discovery from empty category.""" @@ -371,39 +428,47 @@ class TestProductDiscoveryOrdering(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) - self.category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) # Create products with specific names self.products = [] for i in range(5): - product = self.env['product.product'].create({ - 'name': f'Product {chr(65 + i)}', # A, B, C, D, E - 'type': 'consu', - 'list_price': (i + 1) * 10.0, - 'categ_id': self.category.id, - 'is_published': True, - 'sale_ok': True, - }) + product = self.env["product.product"].create( + { + "name": f"Product {chr(65 + i)}", # A, B, C, D, E + "type": "consu", + "list_price": (i + 1) * 10.0, + "categ_id": self.category.id, + "is_published": True, + "sale_ok": True, + } + ) self.products.append(product) start_date = datetime.now().date() - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) def test_discovery_consistent_ordering(self): """Test that repeated calls return same order.""" @@ -413,10 +478,7 @@ class TestProductDiscoveryOrdering(TransactionCase): discovered2 = list(self.group_order.product_ids) # Order should be consistent - self.assertEqual( - [p.id for p in discovered1], - [p.id for p in discovered2] - ) + self.assertEqual([p.id for p in discovered1], [p.id for p in discovered2]) def test_discovery_alphabetical_or_price_order(self): """Test that products are ordered predictably.""" @@ -427,6 +489,6 @@ class TestProductDiscoveryOrdering(TransactionCase): # Should be in some consistent order (name, price, ID, etc) # Verify they're the same products, regardless of order self.assertEqual(len(discovered), 5) - discovered_ids = set(p.id for p in discovered) - expected_ids = set(p.id for p in self.products) + discovered_ids = {p.id for p in discovered} + expected_ids = {p.id for p in self.products} self.assertEqual(discovered_ids, expected_ids) diff --git a/website_sale_aplicoop/tests/test_product_extension.py b/website_sale_aplicoop/tests/test_product_extension.py index f8ed4d9..2c3d0c8 100644 --- a/website_sale_aplicoop/tests/test_product_extension.py +++ b/website_sale_aplicoop/tests/test_product_extension.py @@ -1,91 +1,106 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime, timedelta from odoo.tests.common import TransactionCase class TestProductExtension(TransactionCase): - '''Test suite para las extensiones de product.template.''' + """Test suite para las extensiones de product.template.""" def setUp(self): - super(TestProductExtension, self).setUp() - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - }) - self.order = self.env['group.order'].create({ - 'name': 'Test Order', - 'product_ids': [(4, self.product.id)] - }) + super().setUp() + self.product = self.env["product.product"].create( + { + "name": "Test Product", + } + ) + self.order = self.env["group.order"].create( + {"name": "Test Order", "product_ids": [(4, self.product.id)]} + ) def test_product_template_group_order_ids_field_exists(self): - '''Test que el campo group_order_ids existe en product.template.''' + """Test que el campo group_order_ids existe en product.template.""" product_template = self.product.product_tmpl_id # El campo debe existir y ser readonly - self.assertTrue(hasattr(product_template, 'group_order_ids')) + self.assertTrue(hasattr(product_template, "group_order_ids")) def test_product_group_order_ids_readonly(self): - """ Test that group_order_ids is a readonly field """ - field = self.env['product.template']._fields['group_order_ids'] + """Test that group_order_ids is a readonly field""" + field = self.env["product.template"]._fields["group_order_ids"] self.assertTrue(field.readonly) def test_product_group_order_ids_reverse_lookup(self): - """ Test that adding a product to an order reflects in group_order_ids """ + """Test that adding a product to an order reflects in group_order_ids""" related_orders = self.product.product_tmpl_id.group_order_ids self.assertIn(self.order, related_orders) def test_product_group_order_ids_empty_by_default(self): - """ Test that a new product has no group orders """ - new_product = self.env['product.product'].create({'name': 'New Product'}) + """Test that a new product has no group orders""" + new_product = self.env["product.product"].create({"name": "New Product"}) self.assertFalse(new_product.product_tmpl_id.group_order_ids) def test_product_group_order_ids_multiple_orders(self): - """ Test that group_order_ids can contain multiple orders """ - order2 = self.env['group.order'].create({ - 'name': 'Test Order 2', - 'product_ids': [(4, self.product.id)] - }) + """Test that group_order_ids can contain multiple orders""" + order2 = self.env["group.order"].create( + {"name": "Test Order 2", "product_ids": [(4, self.product.id)]} + ) self.assertIn(self.order, self.product.product_tmpl_id.group_order_ids) self.assertIn(order2, self.product.product_tmpl_id.group_order_ids) def test_product_group_order_ids_empty_after_remove_from_order(self): - """ Test that group_order_ids is empty after removing the product from all orders """ - self.order.write({'product_ids': [(3, self.product.id)]}) + """Test that group_order_ids is empty after removing the product from all orders""" + self.order.write({"product_ids": [(3, self.product.id)]}) self.assertFalse(self.product.product_tmpl_id.group_order_ids) def test_product_group_order_ids_with_multiple_products(self): - """ Test group_order_ids with multiple products in one order """ - product2 = self.env['product.product'].create({'name': 'Test Product 2'}) - self.order.write({'product_ids': [ - (4, self.product.id), - (4, product2.id) - ]}) + """Test group_order_ids with multiple products in one order""" + product2 = self.env["product.product"].create({"name": "Test Product 2"}) + self.order.write({"product_ids": [(4, self.product.id), (4, product2.id)]}) self.assertIn(self.order, self.product.product_tmpl_id.group_order_ids) self.assertIn(self.order, product2.product_tmpl_id.group_order_ids) def test_product_with_variants_group_order_ids(self): - """ Test that group_order_ids works correctly with product variants """ + """Test that group_order_ids works correctly with product variants""" # Create a product template with two variants - product_template = self.env['product.template'].create({ - 'name': 'Product with Variants', - 'attribute_line_ids': [(0, 0, { - 'attribute_id': self.env.ref('product.product_attribute_1').id, - 'value_ids': [ - (4, self.env.ref('product.product_attribute_value_1').id), - (4, self.env.ref('product.product_attribute_value_2').id) - ] - })] - }) + product_template = self.env["product.template"].create( + { + "name": "Product with Variants", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.env.ref( + "product.product_attribute_1" + ).id, + "value_ids": [ + ( + 4, + self.env.ref( + "product.product_attribute_value_1" + ).id, + ), + ( + 4, + self.env.ref( + "product.product_attribute_value_2" + ).id, + ), + ], + }, + ) + ], + } + ) variant1 = product_template.product_variant_ids[0] variant2 = product_template.product_variant_ids[1] # Add one variant to an order (store variant id, not template id) - order_with_variant = self.env['group.order'].create({ - 'name': 'Order with Variant', - 'product_ids': [(4, variant1.id)] - }) + order_with_variant = self.env["group.order"].create( + {"name": "Order with Variant", "product_ids": [(4, variant1.id)]} + ) # Check that the order appears in the group_order_ids of the template self.assertIn(order_with_variant, product_template.group_order_ids) diff --git a/website_sale_aplicoop/tests/test_record_rules.py b/website_sale_aplicoop/tests/test_record_rules.py index e3f1e81..e8be191 100644 --- a/website_sale_aplicoop/tests/test_record_rules.py +++ b/website_sale_aplicoop/tests/test_record_rules.py @@ -1,145 +1,170 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta -from odoo.tests.common import TransactionCase from odoo.exceptions import AccessError +from odoo.tests.common import TransactionCase class TestGroupOrderRecordRules(TransactionCase): - '''Test suite para record rules de multicompañía en group.order.''' + """Test suite para record rules de multicompañía en group.order.""" def setUp(self): super().setUp() # Crear dos compañías - self.company1 = self.env['res.company'].create({ - 'name': 'Company 1', - }) - self.company2 = self.env['res.company'].create({ - 'name': 'Company 2', - }) + self.company1 = self.env["res.company"].create( + { + "name": "Company 1", + } + ) + self.company2 = self.env["res.company"].create( + { + "name": "Company 2", + } + ) # Crear usuarios para cada compañía - self.user_company1 = self.env['res.users'].create({ - 'name': 'User Company 1', - 'login': 'user_c1', - 'password': 'pass123', - 'company_id': self.company1.id, - 'company_ids': [(6, 0, [self.company1.id])], - }) + self.user_company1 = self.env["res.users"].create( + { + "name": "User Company 1", + "login": "user_c1", + "password": "pass123", + "company_id": self.company1.id, + "company_ids": [(6, 0, [self.company1.id])], + } + ) - self.user_company2 = self.env['res.users'].create({ - 'name': 'User Company 2', - 'login': 'user_c2', - 'password': 'pass123', - 'company_id': self.company2.id, - 'company_ids': [(6, 0, [self.company2.id])], - }) + self.user_company2 = self.env["res.users"].create( + { + "name": "User Company 2", + "login": "user_c2", + "password": "pass123", + "company_id": self.company2.id, + "company_ids": [(6, 0, [self.company2.id])], + } + ) # Crear admin con acceso a ambas compañías - self.admin_user = self.env['res.users'].create({ - 'name': 'Admin Both', - 'login': 'admin_both', - 'password': 'pass123', - 'company_id': self.company1.id, - 'company_ids': [(6, 0, [self.company1.id, self.company2.id])], - }) + self.admin_user = self.env["res.users"].create( + { + "name": "Admin Both", + "login": "admin_both", + "password": "pass123", + "company_id": self.company1.id, + "company_ids": [(6, 0, [self.company1.id, self.company2.id])], + } + ) # Crear grupos en cada compañía - self.group1 = self.env['res.partner'].create({ - 'name': 'Grupo Company 1', - 'is_company': True, - 'email': 'grupo1@test.com', - 'company_id': self.company1.id, - }) + self.group1 = self.env["res.partner"].create( + { + "name": "Grupo Company 1", + "is_company": True, + "email": "grupo1@test.com", + "company_id": self.company1.id, + } + ) - self.group2 = self.env['res.partner'].create({ - 'name': 'Grupo Company 2', - 'is_company': True, - 'email': 'grupo2@test.com', - 'company_id': self.company2.id, - }) + self.group2 = self.env["res.partner"].create( + { + "name": "Grupo Company 2", + "is_company": True, + "email": "grupo2@test.com", + "company_id": self.company2.id, + } + ) # Crear órdenes en cada compañía - self.order1 = self.env['group.order'].create({ - 'name': 'Pedido Company 1', - 'group_ids': [(6, 0, [self.group1.id])], - 'company_id': self.company1.id, - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + self.order1 = self.env["group.order"].create( + { + "name": "Pedido Company 1", + "group_ids": [(6, 0, [self.group1.id])], + "company_id": self.company1.id, + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) - self.order2 = self.env['group.order'].create({ - 'name': 'Pedido Company 2', - 'group_ids': [(6, 0, [self.group2.id])], - 'company_id': self.company2.id, - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': (datetime.now() + timedelta(days=7)).date(), - 'period': 'weekly', - 'pickup_day': '5', - 'cutoff_day': '0', - }) + self.order2 = self.env["group.order"].create( + { + "name": "Pedido Company 2", + "group_ids": [(6, 0, [self.group2.id])], + "company_id": self.company2.id, + "type": "regular", + "start_date": datetime.now().date(), + "end_date": (datetime.now() + timedelta(days=7)).date(), + "period": "weekly", + "pickup_day": "5", + "cutoff_day": "0", + } + ) def test_user_company1_can_read_own_orders(self): - '''Test que usuario de Company 1 puede leer sus propias órdenes.''' - orders = self.env['group.order'].with_user( - self.user_company1 - ).search([('company_id', '=', self.company1.id)]) + """Test que usuario de Company 1 puede leer sus propias órdenes.""" + orders = ( + self.env["group.order"] + .with_user(self.user_company1) + .search([("company_id", "=", self.company1.id)]) + ) self.assertIn(self.order1, orders) def test_user_company1_cannot_read_company2_orders(self): - '''Test que usuario de Company 1 NO puede leer órdenes de Company 2.''' - orders = self.env['group.order'].with_user( - self.user_company1 - ).search([('company_id', '=', self.company2.id)]) + """Test que usuario de Company 1 NO puede leer órdenes de Company 2.""" + orders = ( + self.env["group.order"] + .with_user(self.user_company1) + .search([("company_id", "=", self.company2.id)]) + ) self.assertNotIn(self.order2, orders) self.assertEqual(len(orders), 0) def test_admin_can_read_all_orders(self): - '''Test que admin con acceso a ambas compañías ve todas las órdenes.''' - orders = self.env['group.order'].with_user( - self.admin_user - ).search([]) + """Test que admin con acceso a ambas compañías ve todas las órdenes.""" + orders = self.env["group.order"].with_user(self.admin_user).search([]) self.assertIn(self.order1, orders) self.assertIn(self.order2, orders) def test_user_cannot_write_other_company_order(self): - '''Test que usuario no puede escribir en orden de otra compañía.''' + """Test que usuario no puede escribir en orden de otra compañía.""" with self.assertRaises(AccessError): - self.order2.with_user(self.user_company1).write({ - 'name': 'Intentando cambiar nombre', - }) + self.order2.with_user(self.user_company1).write( + { + "name": "Intentando cambiar nombre", + } + ) def test_record_rule_filters_search(self): - '''Test que búsqueda automáticamente filtra por record rule.''' + """Test que búsqueda automáticamente filtra por record rule.""" # Usuario de Company 1 busca todas las órdenes - orders_c1 = self.env['group.order'].with_user( - self.user_company1 - ).search([('state', '=', 'draft')]) + orders_c1 = ( + self.env["group.order"] + .with_user(self.user_company1) + .search([("state", "=", "draft")]) + ) # Solo debe ver su orden self.assertEqual(len(orders_c1), 1) self.assertEqual(orders_c1[0], self.order1) def test_cross_company_access_denied(self): - '''Test que acceso entre compañías es denegado.''' + """Test que acceso entre compañías es denegado.""" # Usuario company1 intenta acceder a orden de company2 with self.assertRaises(AccessError): self.order2.with_user(self.user_company1).read() def test_admin_can_bypass_company_restriction(self): - '''Test que admin puede acceder a órdenes de cualquier compañía.''' + """Test que admin puede acceder a órdenes de cualquier compañía.""" # Admin lee orden de company2 sin problema order2_admin = self.order2.with_user(self.admin_user) - self.assertEqual(order2_admin.name, 'Pedido Company 2') + self.assertEqual(order2_admin.name, "Pedido Company 2") self.assertEqual(order2_admin.company_id, self.company2) diff --git a/website_sale_aplicoop/tests/test_res_partner.py b/website_sale_aplicoop/tests/test_res_partner.py index 2070ec7..5d0bf03 100644 --- a/website_sale_aplicoop/tests/test_res_partner.py +++ b/website_sale_aplicoop/tests/test_res_partner.py @@ -5,34 +5,40 @@ from odoo.tests.common import TransactionCase class TestResPartnerExtension(TransactionCase): - '''Test suite para la extensión res.partner (user-group relationship).''' + """Test suite para la extensión res.partner (user-group relationship).""" def setUp(self): super().setUp() # Crear grupos (res.partner with is_company=True) - self.group1 = self.env['res.partner'].create({ - 'name': 'Grupo 1', - 'is_company': True, - 'email': 'grupo1@test.com', - }) + self.group1 = self.env["res.partner"].create( + { + "name": "Grupo 1", + "is_company": True, + "email": "grupo1@test.com", + } + ) - self.group2 = self.env['res.partner'].create({ - 'name': 'Grupo 2', - 'is_company': True, - 'email': 'grupo2@test.com', - }) + self.group2 = self.env["res.partner"].create( + { + "name": "Grupo 2", + "is_company": True, + "email": "grupo2@test.com", + } + ) # Crear usuario - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + } + ) def test_partner_can_belong_to_groups(self): - '''Test que un partner (usuario) puede pertenecer a múltiples grupos.''' + """Test que un partner (usuario) puede pertenecer a múltiples grupos.""" partner = self.user.partner_id - + # Agregar partner a grupos (usar campo member_ids) partner.member_ids = [(6, 0, [self.group1.id, self.group2.id])] @@ -42,12 +48,14 @@ class TestResPartnerExtension(TransactionCase): self.assertEqual(len(partner.member_ids), 2) def test_group_can_have_multiple_users(self): - '''Test que un grupo puede tener múltiples usuarios.''' - user2 = self.env['res.users'].create({ - 'name': 'Test User 2', - 'login': 'testuser2@test.com', - 'email': 'testuser2@test.com', - }) + """Test que un grupo puede tener múltiples usuarios.""" + user2 = self.env["res.users"].create( + { + "name": "Test User 2", + "login": "testuser2@test.com", + "email": "testuser2@test.com", + } + ) # Agregar usuarios al grupo self.group1.user_ids = [(6, 0, [self.user.id, user2.id])] @@ -58,26 +66,28 @@ class TestResPartnerExtension(TransactionCase): self.assertEqual(len(self.group1.user_ids), 2) def test_user_group_relationship_is_bidirectional(self): - '''Test que se puede modificar la relación desde el lado del partner o el grupo.''' + """Test que se puede modificar la relación desde el lado del partner o el grupo.""" partner = self.user.partner_id - + # Opción 1: Agregar grupo al usuario (desde el lado del usuario/partner) partner.member_ids = [(6, 0, [self.group1.id])] self.assertIn(self.group1, partner.member_ids) - - # Opción 2: Agregar usuario al grupo (desde el lado del grupo) + + # Opción 2: Agregar usuario al grupo (desde el lado del grupo) # Nota: Esto es una relación Many2many independiente - user2 = self.env['res.users'].create({ - 'name': 'Test User 2', - 'login': 'testuser2@test.com', - 'email': 'testuser2@test.com', - }) + user2 = self.env["res.users"].create( + { + "name": "Test User 2", + "login": "testuser2@test.com", + "email": "testuser2@test.com", + } + ) self.group2.user_ids = [(6, 0, [user2.id])] self.assertIn(user2, self.group2.user_ids) def test_empty_group_ids(self): - '''Test que un partner sin grupos tiene group_ids vacío.''' + """Test que un partner sin grupos tiene group_ids vacío.""" partner = self.user.partner_id - + # Sin agregar a ningún grupo self.assertEqual(len(partner.member_ids), 0) diff --git a/website_sale_aplicoop/tests/test_save_order_endpoints.py b/website_sale_aplicoop/tests/test_save_order_endpoints.py index fe446a7..4e975fe 100644 --- a/website_sale_aplicoop/tests/test_save_order_endpoints.py +++ b/website_sale_aplicoop/tests/test_save_order_endpoints.py @@ -11,7 +11,8 @@ draft sale orders. See: website_sale_aplicoop/controllers/website_sale.py """ -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta from odoo.tests.common import TransactionCase @@ -23,60 +24,72 @@ class TestSaveOrderEndpoints(TransactionCase): super().setUp() # Create a consumer group - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - 'email': 'group@test.com', - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + "email": "group@test.com", + } + ) # Create a group member (user partner) - self.member_partner = self.env['res.partner'].create({ - 'name': 'Group Member Partner', - 'email': 'member@test.com', - }) + self.member_partner = self.env["res.partner"].create( + { + "name": "Group Member Partner", + "email": "member@test.com", + } + ) # Add member to group self.group.member_ids = [(4, self.member_partner.id)] # Create test user - self.user = self.env['res.users'].create({ - 'name': 'Test User', - 'login': 'testuser@test.com', - 'email': 'testuser@test.com', - 'partner_id': self.member_partner.id, - }) + self.user = self.env["res.users"].create( + { + "name": "Test User", + "login": "testuser@test.com", + "email": "testuser@test.com", + "partner_id": self.member_partner.id, + } + ) # Create a group order start_date = datetime.now().date() end_date = start_date + timedelta(days=7) - self.group_order = self.env['group.order'].create({ - 'name': 'Test Group Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': end_date, - 'period': 'weekly', - 'pickup_day': '3', # Wednesday - 'pickup_date': start_date + timedelta(days=3), - 'home_delivery': False, - 'cutoff_day': '0', - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Group Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": end_date, + "period": "weekly", + "pickup_day": "3", # Wednesday + "pickup_date": start_date + timedelta(days=3), + "home_delivery": False, + "cutoff_day": "0", + } + ) # Open the group order self.group_order.action_open() # Create products for the order - self.category = self.env['product.category'].create({ - 'name': 'Test Category', - }) + self.category = self.env["product.category"].create( + { + "name": "Test Category", + } + ) - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', - 'list_price': 10.0, - 'categ_id': self.category.id, - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + "list_price": 10.0, + "categ_id": self.category.id, + } + ) # Associate product with group order self.group_order.product_ids = [(4, self.product.id)] @@ -84,22 +97,22 @@ class TestSaveOrderEndpoints(TransactionCase): def test_save_eskaera_draft_creates_order_with_group_order_id(self): """ Test that save_eskaera_draft() creates a sale.order with group_order_id. - + This is the main fix: ensure that the /eskaera/save-order endpoint correctly links the created sale.order to the group.order. """ # Simulate what the controller does: create order with group_order_id order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'pickup_date': self.group_order.pickup_date, - 'home_delivery': self.group_order.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "pickup_date": self.group_order.pickup_date, + "home_delivery": self.group_order.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify the order was created with group_order_id self.assertIsNotNone(sale_order.id) @@ -109,34 +122,34 @@ class TestSaveOrderEndpoints(TransactionCase): def test_save_eskaera_draft_propagates_pickup_day(self): """Test that save_eskaera_draft() propagates pickup_day correctly.""" order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'pickup_date': self.group_order.pickup_date, - 'home_delivery': self.group_order.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "pickup_date": self.group_order.pickup_date, + "home_delivery": self.group_order.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify pickup_day was propagated - self.assertEqual(sale_order.pickup_day, '3') + self.assertEqual(sale_order.pickup_day, "3") self.assertEqual(sale_order.pickup_day, self.group_order.pickup_day) def test_save_eskaera_draft_propagates_pickup_date(self): """Test that save_eskaera_draft() propagates pickup_date correctly.""" order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'pickup_date': self.group_order.pickup_date, - 'home_delivery': self.group_order.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "pickup_date": self.group_order.pickup_date, + "home_delivery": self.group_order.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify pickup_date was propagated self.assertEqual(sale_order.pickup_date, self.group_order.pickup_date) @@ -144,33 +157,35 @@ class TestSaveOrderEndpoints(TransactionCase): def test_save_eskaera_draft_propagates_home_delivery(self): """Test that save_eskaera_draft() propagates home_delivery correctly.""" # Create a group order with home_delivery=True - group_order_home = self.env['group.order'].create({ - 'name': 'Test Group Order with Home Delivery', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date(), - 'end_date': datetime.now().date() + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'pickup_date': datetime.now().date() + timedelta(days=3), - 'home_delivery': True, # Enable home delivery - 'cutoff_day': '0', - }) + group_order_home = self.env["group.order"].create( + { + "name": "Test Group Order with Home Delivery", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date(), + "end_date": datetime.now().date() + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "pickup_date": datetime.now().date() + timedelta(days=3), + "home_delivery": True, # Enable home delivery + "cutoff_day": "0", + } + ) group_order_home.action_open() # Test with home_delivery=True order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': group_order_home.id, - 'pickup_day': group_order_home.pickup_day, - 'pickup_date': group_order_home.pickup_date, - 'home_delivery': group_order_home.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": group_order_home.id, + "pickup_day": group_order_home.pickup_day, + "pickup_date": group_order_home.pickup_date, + "home_delivery": group_order_home.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify home_delivery was propagated self.assertTrue(sale_order.home_delivery) @@ -179,65 +194,65 @@ class TestSaveOrderEndpoints(TransactionCase): def test_save_eskaera_draft_order_is_draft_state(self): """Test that save_eskaera_draft() creates order in draft state.""" order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'pickup_date': self.group_order.pickup_date, - 'home_delivery': self.group_order.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "pickup_date": self.group_order.pickup_date, + "home_delivery": self.group_order.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify order is in draft state - self.assertEqual(sale_order.state, 'draft') + self.assertEqual(sale_order.state, "draft") def test_save_eskaera_draft_multiple_fields_together(self): """ Test that all fields are saved together correctly. - + This test ensures that the fix didn't break any field and that all group_order-related fields are propagated together. """ order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'pickup_date': self.group_order.pickup_date, - 'home_delivery': self.group_order.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "pickup_date": self.group_order.pickup_date, + "home_delivery": self.group_order.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify all fields together self.assertEqual(sale_order.group_order_id.id, self.group_order.id) self.assertEqual(sale_order.pickup_day, self.group_order.pickup_day) self.assertEqual(sale_order.pickup_date, self.group_order.pickup_date) self.assertEqual(sale_order.home_delivery, self.group_order.home_delivery) - self.assertEqual(sale_order.state, 'draft') + self.assertEqual(sale_order.state, "draft") def test_save_cart_draft_also_saves_group_order_id(self): """ Test that save_cart_draft() (the working endpoint) also saves group_order_id. - + This is a regression test to ensure that save_cart_draft() continues to work correctly after the fix to save_eskaera_draft(). """ # save_cart_draft should also include group_order_id order_vals = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'pickup_date': self.group_order.pickup_date, - 'home_delivery': self.group_order.home_delivery, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "pickup_date": self.group_order.pickup_date, + "home_delivery": self.group_order.home_delivery, + "order_line": [], + "state": "draft", } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify all fields self.assertEqual(sale_order.group_order_id.id, self.group_order.id) @@ -247,18 +262,18 @@ class TestSaveOrderEndpoints(TransactionCase): def test_save_draft_order_without_group_order_id_still_works(self): """ Test that creating a normal sale.order (without group_order_id) still works. - + This ensures backward compatibility - you should still be able to create sale orders without associating them to a group order. """ order_vals = { - 'partner_id': self.member_partner.id, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "order_line": [], + "state": "draft", # No group_order_id } - sale_order = self.env['sale.order'].create(order_vals) + sale_order = self.env["sale.order"].create(order_vals) # Verify order was created without group_order_id self.assertIsNotNone(sale_order.id) @@ -267,62 +282,64 @@ class TestSaveOrderEndpoints(TransactionCase): def test_group_order_id_field_exists_and_is_stored(self): """ Test that group_order_id field exists on sale.order and is stored correctly. - + This is a sanity check to ensure the field is properly defined in the model. """ # Verify the field exists in the model - sale_order_model = self.env['sale.order'] - self.assertIn('group_order_id', sale_order_model._fields) + sale_order_model = self.env["sale.order"] + self.assertIn("group_order_id", sale_order_model._fields) # Verify it's a Many2one field - field = sale_order_model._fields['group_order_id'] - self.assertEqual(field.type, 'many2one') - self.assertEqual(field.comodel_name, 'group.order') + field = sale_order_model._fields["group_order_id"] + self.assertEqual(field.type, "many2one") + self.assertEqual(field.comodel_name, "group.order") def test_different_group_orders_map_to_different_sale_orders(self): """ Test that different group orders create separate sale orders. - + This ensures that two users buying from different group orders don't accidentally share the same sale.order. """ # Create a second group order - group_order_2 = self.env['group.order'].create({ - 'name': 'Test Group Order 2', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': datetime.now().date() + timedelta(days=10), - 'end_date': datetime.now().date() + timedelta(days=17), - 'period': 'weekly', - 'pickup_day': '5', - 'pickup_date': datetime.now().date() + timedelta(days=12), - 'home_delivery': True, - 'cutoff_day': '0', - }) + group_order_2 = self.env["group.order"].create( + { + "name": "Test Group Order 2", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": datetime.now().date() + timedelta(days=10), + "end_date": datetime.now().date() + timedelta(days=17), + "period": "weekly", + "pickup_day": "5", + "pickup_date": datetime.now().date() + timedelta(days=12), + "home_delivery": True, + "cutoff_day": "0", + } + ) group_order_2.action_open() # Create order for first group order order_vals_1 = { - 'partner_id': self.member_partner.id, - 'group_order_id': self.group_order.id, - 'pickup_day': self.group_order.pickup_day, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": self.group_order.id, + "pickup_day": self.group_order.pickup_day, + "order_line": [], + "state": "draft", } - sale_order_1 = self.env['sale.order'].create(order_vals_1) + sale_order_1 = self.env["sale.order"].create(order_vals_1) # Create order for second group order order_vals_2 = { - 'partner_id': self.member_partner.id, - 'group_order_id': group_order_2.id, - 'pickup_day': group_order_2.pickup_day, - 'order_line': [], - 'state': 'draft', + "partner_id": self.member_partner.id, + "group_order_id": group_order_2.id, + "pickup_day": group_order_2.pickup_day, + "order_line": [], + "state": "draft", } - sale_order_2 = self.env['sale.order'].create(order_vals_2) + sale_order_2 = self.env["sale.order"].create(order_vals_2) # Verify they're different orders with different group_order_ids self.assertNotEqual(sale_order_1.id, sale_order_2.id) diff --git a/website_sale_aplicoop/tests/test_templates_rendering.py b/website_sale_aplicoop/tests/test_templates_rendering.py index 1998c9f..5389f70 100644 --- a/website_sale_aplicoop/tests/test_templates_rendering.py +++ b/website_sale_aplicoop/tests/test_templates_rendering.py @@ -1,77 +1,90 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import date, timedelta -from odoo.tests.common import TransactionCase, tagged +from datetime import date +from datetime import timedelta + from odoo import _ +from odoo.tests.common import TransactionCase +from odoo.tests.common import tagged -@tagged('post_install', '-at_install') +@tagged("post_install", "-at_install") class TestTemplatesRendering(TransactionCase): - '''Test suite to verify QWeb templates work with day_names context. + """Test suite to verify QWeb templates work with day_names context. This test covers the fix for the issue where _() function calls in QWeb t-value attributes caused TypeError: 'NoneType' object is not callable. The fix moves day_names definition to Python controller and passes it as context. - ''' + """ def setUp(self): - '''Set up test data: create a test group order.''' + """Set up test data: create a test group order.""" super().setUp() # Create a test supplier - self.supplier = self.env['res.partner'].create({ - 'name': 'Test Supplier', - 'is_company': True, - }) + self.supplier = self.env["res.partner"].create( + { + "name": "Test Supplier", + "is_company": True, + } + ) # Create test products - self.product = self.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'consu', # consumable (consu), service, or storable - }) + self.product = self.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", # consumable (consu), service, or storable + } + ) # Create a test group - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) # Create a group order - self.group_order = self.env['group.order'].create({ - 'name': 'Test Order', - 'state': 'open', - 'supplier_ids': [(6, 0, [self.supplier.id])], - 'product_ids': [(6, 0, [self.product.id])], - 'group_ids': [(6, 0, [self.group.id])], - 'start_date': date.today(), - 'end_date': date.today() + timedelta(days=7), - 'pickup_day': '5', # Saturday - 'cutoff_day': '3', # Thursday - }) + self.group_order = self.env["group.order"].create( + { + "name": "Test Order", + "state": "open", + "supplier_ids": [(6, 0, [self.supplier.id])], + "product_ids": [(6, 0, [self.product.id])], + "group_ids": [(6, 0, [self.group.id])], + "start_date": date.today(), + "end_date": date.today() + timedelta(days=7), + "pickup_day": "5", # Saturday + "cutoff_day": "3", # Thursday + } + ) def test_eskaera_page_template_exists(self): - '''Test that eskaera_page template compiles without errors.''' - template = self.env.ref('website_sale_aplicoop.eskaera_page') + """Test that eskaera_page template compiles without errors.""" + template = self.env.ref("website_sale_aplicoop.eskaera_page") self.assertIsNotNone(template) - self.assertEqual(template.type, 'qweb') + self.assertEqual(template.type, "qweb") def test_eskaera_shop_template_exists(self): - '''Test that eskaera_shop template compiles without errors.''' - template = self.env.ref('website_sale_aplicoop.eskaera_shop') + """Test that eskaera_shop template compiles without errors.""" + template = self.env.ref("website_sale_aplicoop.eskaera_shop") self.assertIsNotNone(template) - self.assertEqual(template.type, 'qweb') + self.assertEqual(template.type, "qweb") def test_eskaera_checkout_template_exists(self): - '''Test that eskaera_checkout template compiles without errors.''' - template = self.env.ref('website_sale_aplicoop.eskaera_checkout') + """Test that eskaera_checkout template compiles without errors.""" + template = self.env.ref("website_sale_aplicoop.eskaera_checkout") self.assertIsNotNone(template) - self.assertEqual(template.type, 'qweb') + self.assertEqual(template.type, "qweb") def test_day_names_context_is_provided(self): - '''Test that day_names context is provided by the controller method.''' + """Test that day_names context is provided by the controller method.""" # Simulate what the controller does, passing env for test context - from odoo.addons.website_sale_aplicoop.controllers.website_sale import AplicoopWebsiteSale + from odoo.addons.website_sale_aplicoop.controllers.website_sale import ( + AplicoopWebsiteSale, + ) controller = AplicoopWebsiteSale() day_names = controller._get_day_names(env=self.env) @@ -86,45 +99,61 @@ class TestTemplatesRendering(TransactionCase): self.assertGreater(len(day_name), 0, f"Day at index {i} is empty string") def test_day_names_not_using_inline_underscore(self): - '''Test that day_names are defined in Python, not in t-value attributes. + """Test that day_names are defined in Python, not in t-value attributes. This test ensures the fix has been applied: - day_names MUST be passed from controller context - day_names MUST NOT be defined with _() inside t-value attributes - Templates use day_names[index] from context, not t-set with _() - ''' - template = self.env.ref('website_sale_aplicoop.eskaera_page') + """ + template = self.env.ref("website_sale_aplicoop.eskaera_page") # Read the template source to verify it doesn't have inline _() in t-value - self.assertIn('day_names', template.arch_db, - "Template must reference day_names from context") + self.assertIn( + "day_names", + template.arch_db, + "Template must reference day_names from context", + ) # The fix ensures no exists # which was causing the NoneType error def test_eskaera_checkout_summary_template_exists(self): - '''Test that eskaera_checkout_summary sub-template exists.''' - template = self.env.ref('website_sale_aplicoop.eskaera_checkout_summary') + """Test that eskaera_checkout_summary sub-template exists.""" + template = self.env.ref("website_sale_aplicoop.eskaera_checkout_summary") self.assertIsNotNone(template) - self.assertEqual(template.type, 'qweb') + self.assertEqual(template.type, "qweb") # Verify it has the expected structure - self.assertIn('checkout-summary-table', template.arch_db, - "Template must have checkout-summary-table id") - self.assertIn('Product', template.arch_db, - "Template must have Product label for translation") - self.assertIn('Quantity', template.arch_db, - "Template must have Quantity label for translation") - self.assertIn('Price', template.arch_db, - "Template must have Price label for translation") - self.assertIn('Subtotal', template.arch_db, - "Template must have Subtotal label for translation") + self.assertIn( + "checkout-summary-table", + template.arch_db, + "Template must have checkout-summary-table id", + ) + self.assertIn( + "Product", + template.arch_db, + "Template must have Product label for translation", + ) + self.assertIn( + "Quantity", + template.arch_db, + "Template must have Quantity label for translation", + ) + self.assertIn( + "Price", template.arch_db, "Template must have Price label for translation" + ) + self.assertIn( + "Subtotal", + template.arch_db, + "Template must have Subtotal label for translation", + ) def test_eskaera_checkout_summary_renders(self): - '''Test that eskaera_checkout_summary renders without errors.''' - template = self.env.ref('website_sale_aplicoop.eskaera_checkout_summary') + """Test that eskaera_checkout_summary renders without errors.""" + template = self.env.ref("website_sale_aplicoop.eskaera_checkout_summary") # Render the template with empty context html = template._render_template(template.xml_id, {}) # Should contain the basic table structure - self.assertIn(' Closed transition is not allowed.""" # Should not allow skipping Open state - self.assertEqual(self.order.state, 'draft') - + self.assertEqual(self.order.state, "draft") + # Calling action_close() without action_open() should fail with self.assertRaises((ValidationError, UserError)): self.order.action_close() @@ -261,36 +293,36 @@ class TestStateTransitions(TransactionCase): def test_illegal_transition_cancelled_to_open(self): """Test that Cancelled -> Open transition is not allowed.""" self.order.action_cancel() - self.assertEqual(self.order.state, 'cancelled') - + self.assertEqual(self.order.state, "cancelled") + # Should not allow re-opening cancelled order with self.assertRaises((ValidationError, UserError)): self.order.action_open() def test_legal_transition_draft_open_closed(self): """Test that Draft -> Open -> Closed is allowed.""" - self.assertEqual(self.order.state, 'draft') - + self.assertEqual(self.order.state, "draft") + self.order.action_open() - self.assertEqual(self.order.state, 'open') - + self.assertEqual(self.order.state, "open") + self.order.action_close() - self.assertEqual(self.order.state, 'closed') + self.assertEqual(self.order.state, "closed") def test_transition_draft_to_cancelled(self): """Test that Draft -> Cancelled is allowed.""" - self.assertEqual(self.order.state, 'draft') - + self.assertEqual(self.order.state, "draft") + self.order.action_cancel() - self.assertEqual(self.order.state, 'cancelled') + self.assertEqual(self.order.state, "cancelled") def test_transition_open_to_cancelled(self): """Test that Open -> Cancelled is allowed (emergency stop).""" self.order.action_open() - self.assertEqual(self.order.state, 'open') - + self.assertEqual(self.order.state, "open") + self.order.action_cancel() - self.assertEqual(self.order.state, 'cancelled') + self.assertEqual(self.order.state, "cancelled") class TestUserPartnerValidation(TransactionCase): @@ -298,31 +330,37 @@ class TestUserPartnerValidation(TransactionCase): def setUp(self): super().setUp() - self.group = self.env['res.partner'].create({ - 'name': 'Test Group', - 'is_company': True, - }) + self.group = self.env["res.partner"].create( + { + "name": "Test Group", + "is_company": True, + } + ) # Create user without partner (edge case) - self.user_no_partner = self.env['res.users'].create({ - 'name': 'User No Partner', - 'login': 'noparnter@test.com', - 'partner_id': False, # Explicitly no partner - }) + self.user_no_partner = self.env["res.users"].create( + { + "name": "User No Partner", + "login": "noparnter@test.com", + "partner_id": False, # Explicitly no partner + } + ) def test_user_without_partner_cannot_access_order(self): """Test that user without partner_id has no access to orders.""" start_date = datetime.now().date() - order = self.env['group.order'].create({ - 'name': 'Test Order', - 'group_ids': [(6, 0, [self.group.id])], - 'type': 'regular', - 'start_date': start_date, - 'end_date': start_date + timedelta(days=7), - 'period': 'weekly', - 'pickup_day': '3', - 'cutoff_day': '0', - }) + order = self.env["group.order"].create( + { + "name": "Test Order", + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + ) # User without partner should not have access # This should be validated in controller diff --git a/website_sale_aplicoop/views/load_from_history_templates.xml b/website_sale_aplicoop/views/load_from_history_templates.xml index f664934..1f254df 100644 --- a/website_sale_aplicoop/views/load_from_history_templates.xml +++ b/website_sale_aplicoop/views/load_from_history_templates.xml @@ -18,7 +18,7 @@ var pickupDate = ''; var homeDelivery = ; var sameGroupOrder = ; - + console.log('load_from_history template: groupOrderId=', groupOrderId); console.log('load_from_history template: saleOrderName=', saleOrderName); console.log('load_from_history template: pickupDay=', pickupDay); @@ -27,16 +27,16 @@ console.log('load_from_history template: sameGroupOrder=', sameGroupOrder); console.log('load_from_history template: itemsJson type=', typeof itemsJson); console.log('load_from_history template: itemsJson value=', itemsJson); - + // If itemsJson is already a string, use it directly; if it's an array, stringify it var itemsJsonString = (typeof itemsJson === 'string') ? itemsJson : JSON.stringify(itemsJson); - + // Store items to sessionStorage sessionStorage['load_from_history_' + groupOrderId] = itemsJsonString; - + // Store sale order name separately sessionStorage['load_from_history_order_name_' + groupOrderId] = saleOrderName; - + // Store pickup fields ONLY if from same group order if (sameGroupOrder === 'true') { sessionStorage['load_from_history_pickup_day_' + groupOrderId] = pickupDay; @@ -46,10 +46,10 @@ } else { console.log('Skipped saving pickup fields (different group order - will use current group order days)'); } - + console.log('Saved to sessionStorage[load_from_history_' + groupOrderId + ']:', itemsJsonString); console.log('Saved order name to sessionStorage[load_from_history_order_name_' + groupOrderId + ']:', saleOrderName); - + // Redirect to group order page // The JavaScript on that page will detect this and load the items window.location.href = '/eskaera/' + groupOrderId; diff --git a/website_sale_aplicoop/views/portal_templates.xml b/website_sale_aplicoop/views/portal_templates.xml index 9532ff5..10d1f77 100644 --- a/website_sale_aplicoop/views/portal_templates.xml +++ b/website_sale_aplicoop/views/portal_templates.xml @@ -9,7 +9,7 @@ Pickup Day Actions
- + @@ -35,15 +35,15 @@ - @@ -57,7 +57,7 @@