diff --git a/.gitignore b/.gitignore index a3957ae..e03074a 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ dmypy.json # Pyre type checker .pyre/ + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e97523..beb2928 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 a84ff41..4b86fa5 100644 --- a/account_invoice_triple_discount_readonly/tests/test_account_move.py +++ b/account_invoice_triple_discount_readonly/tests/test_account_move.py @@ -10,88 +10,72 @@ 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) @@ -99,14 +83,12 @@ 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 @@ -117,19 +99,17 @@ 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) @@ -140,13 +120,11 @@ 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) @@ -156,13 +134,11 @@ 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 @@ -170,20 +146,18 @@ 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) @@ -191,23 +165,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 cd12fe5..fc22dd0 100644 --- a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py +++ b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py @@ -10,47 +10,37 @@ 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, - } - ) - - # Create a product template first, then get the variant - cls.product_template = cls.env["product.template"].create( - { - "name": "Test Product PO", - "type": "consu", - "list_price": 150.0, - "standard_price": 80.0, - } - ) - # Get the auto-created product variant - cls.product = cls.product_template.product_variant_ids[0] - + 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, + }) + # 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""" @@ -59,27 +49,23 @@ 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) @@ -87,35 +73,33 @@ 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) @@ -126,13 +110,11 @@ 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) @@ -142,13 +124,11 @@ 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 @@ -156,20 +136,18 @@ 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) @@ -177,40 +155,38 @@ 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 33b931f..f4600a2 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,45 +10,35 @@ class TestTripleDiscountMixin(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - + # Create a partner - cls.partner = cls.env["res.partner"].create( - { - "name": "Test Partner", - } - ) - - # Create a product template first, then get the variant - cls.product_template = cls.env["product.template"].create( - { - "name": "Test Product", - "type": "consu", - "list_price": 100.0, - "standard_price": 50.0, - } - ) - # Get the auto-created product variant - cls.product = cls.product_template.product_variant_ids[0] - + 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, + }) + # 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""" @@ -58,20 +48,18 @@ 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) @@ -79,13 +67,11 @@ 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 @@ -94,13 +80,11 @@ 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 @@ -109,13 +93,11 @@ 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 @@ -125,21 +107,17 @@ 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) @@ -148,24 +126,20 @@ 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) @@ -173,14 +147,12 @@ 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) @@ -189,22 +161,18 @@ 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) @@ -212,14 +180,12 @@ 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) @@ -227,15 +193,13 @@ 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 cc4a1e5..5f3372c 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 f53306f..2e9c0de 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 ae72534..efb6cc5 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 c53f792..731436c 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 9670cb6..5971b2f 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 a6eafa2..29e2bda 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 cdac99a..d3c5fd4 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 0eec36f..e1e9e08 100644 --- a/product_price_category_supplier/models/res_partner.py +++ b/product_price_category_supplier/models/res_partner.py @@ -1,21 +1,18 @@ # Copyright 2026 Your Company # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _ -from odoo import api -from odoo import fields -from odoo import models +from odoo import _, api, fields, 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=[], ) @@ -24,26 +21,24 @@ 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 d7ca3f0..d49c421 100644 --- a/product_price_category_supplier/models/wizard_update_product_category.py +++ b/product_price_category_supplier/models/wizard_update_product_category.py @@ -1,40 +1,37 @@ # Copyright 2026 Your Company # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _ -from odoo import api -from odoo import fields -from odoo import models +from odoo import _, api, fields, 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, ) @@ -44,33 +41,36 @@ 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 5d6d660..c65df85 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 @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError class TestProductPriceCategorySupplier(TransactionCase): @@ -13,127 +14,68 @@ 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 (with seller_ids) - cls.product_1 = cls.env["product.template"].create( - { - "name": "Product 1", - "seller_ids": [ - ( - 0, - 0, - { - "partner_id": cls.supplier_a.id, - "sequence": 10, - "min_qty": 0, - }, - ) - ], - } - ) - cls.product_2 = cls.env["product.template"].create( - { - "name": "Product 2", - "seller_ids": [ - ( - 0, - 0, - { - "partner_id": cls.supplier_a.id, - "sequence": 10, - "min_qty": 0, - }, - ) - ], - } - ) - cls.product_3 = cls.env["product.template"].create( - { - "name": "Product 3", - "seller_ids": [ - ( - 0, - 0, - { - "partner_id": cls.supplier_a.id, - "sequence": 10, - "min_qty": 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, + }) # Create product with supplier B - cls.product_4 = cls.env["product.template"].create( - { - "name": "Product 4", - "seller_ids": [ - ( - 0, - 0, - { - "partner_id": cls.supplier_b.id, - "sequence": 10, - "min_qty": 0, - }, - ) - ], - } - ) + 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", - } - ) + 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): @@ -141,38 +83,41 @@ 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): @@ -180,82 +125,75 @@ 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): @@ -263,10 +201,11 @@ 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): @@ -276,99 +215,87 @@ 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( - [("main_seller_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", + wizard.product_count, actual_count, + f'Wizard should count {actual_count} products' + ) + self.assertEqual( + wizard.product_count, 3, + 'Supplier A should have 3 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 596c8c4..699d53c 100644 --- a/product_sale_price_from_pricelist/models/res_config.py +++ b/product_sale_price_from_pricelist/models/res_config.py @@ -1,6 +1,4 @@ -from odoo import api -from odoo import fields -from odoo import models +from odoo import models, fields, api class ResConfigSettings(models.TransientModel): diff --git a/product_sale_price_from_pricelist/tests/test_pricelist.py b/product_sale_price_from_pricelist/tests/test_pricelist.py index 88698cb..b879ae3 100644 --- a/product_sale_price_from_pricelist/tests/test_pricelist.py +++ b/product_sale_price_from_pricelist/tests/test_pricelist.py @@ -93,7 +93,7 @@ class TestPricelist(TransactionCase): # _compute_price should return the base price (last_purchase_price_received) result = pricelist_item._compute_price( - self.product, quantity=1, uom=self.product.uom_id, date=False, currency=None + self.product, qty=1, uom=self.product.uom_id, date=False, currency=None ) # Should return the last purchase price as base @@ -112,7 +112,7 @@ class TestPricelist(TransactionCase): ) result = pricelist_item._compute_price( - self.product, quantity=1, uom=self.product.uom_id, date=False, currency=None + self.product, qty=1, uom=self.product.uom_id, date=False, currency=None ) # Should return last_purchase_price_received diff --git a/product_sale_price_from_pricelist/tests/test_product_template.py b/product_sale_price_from_pricelist/tests/test_product_template.py index 59bf89c..6f3419c 100644 --- a/product_sale_price_from_pricelist/tests/test_product_template.py +++ b/product_sale_price_from_pricelist/tests/test_product_template.py @@ -203,9 +203,16 @@ class TestProductTemplate(TransactionCase): def test_company_dependent_fields(self): """Test that price fields are company dependent""" - # NOTE: company_dependent=True would require adding schema migration - # to convert existing columns in production databases. These fields - # use standard float/selection storage instead. + # Verify field properties + field_last_purchase = self.product._fields["last_purchase_price_received"] + field_theoritical = self.product._fields["list_price_theoritical"] + field_updated = self.product._fields["last_purchase_price_updated"] + field_compute_type = self.product._fields["last_purchase_price_compute_type"] + + self.assertTrue(field_last_purchase.company_dependent) + self.assertTrue(field_theoritical.company_dependent) + self.assertTrue(field_updated.company_dependent) + self.assertTrue(field_compute_type.company_dependent) def test_compute_theoritical_price_with_actual_purchase_price(self): """Test that theoretical price is calculated correctly from last purchase price 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 25669b0..7c48926 100644 --- a/product_sale_price_from_pricelist/tests/test_res_config.py +++ b/product_sale_price_from_pricelist/tests/test_res_config.py @@ -10,31 +10,25 @@ 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 = 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") + 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): @@ -42,51 +36,43 @@ 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 = 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, - } - ) + 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") + 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 813df8a..4a4a372 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,3 +19,4 @@ + diff --git a/purchase_triple_discount/models/purchase_order.py b/purchase_triple_discount/models/purchase_order.py index 5ddc2cb..de23b23 100644 --- a/purchase_triple_discount/models/purchase_order.py +++ b/purchase_triple_discount/models/purchase_order.py @@ -10,6 +10,9 @@ 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 5a48af5..0dd635e 100644 --- a/purchase_triple_discount/models/purchase_order_line.py +++ b/purchase_triple_discount/models/purchase_order_line.py @@ -43,7 +43,10 @@ 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 31b6e57..83d75d5 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 029d3ea..5d2060c 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/README.md b/website_sale_aplicoop/README.md index 8f9e2fd..63b947c 100644 --- a/website_sale_aplicoop/README.md +++ b/website_sale_aplicoop/README.md @@ -240,29 +240,6 @@ python -m pytest website_sale_aplicoop/tests/ -v ## Changelog -### 18.0.1.3.1 (2026-02-18) -- **Date Calculation Fixes (Critical)**: - - Fixed `_compute_cutoff_date` logic: Changed `days_ahead <= 0` to `days_ahead < 0` to allow cutoff_date to be the same day as today - - Enabled `store=True` for `delivery_date` field to persist calculated values and enable database filtering - - Added constraint `_check_cutoff_before_pickup` to validate that pickup_day >= cutoff_day in weekly orders - - Added `@api.onchange` methods for immediate UI feedback when changing cutoff_day or pickup_day -- **Automatic Date Updates**: - - Created daily cron job `_cron_update_dates` to automatically recalculate dates for active orders - - Ensures computed dates stay current as time passes -- **UI Improvements**: - - Added "Calculated Dates" section in form view showing readonly cutoff_date, pickup_date, and delivery_date - - Improved visibility of automatically calculated dates for administrators -- **Testing**: - - Added 6 regression tests with `@tagged('post_install', 'date_calculations')`: - - `test_cutoff_same_day_as_today_bug_fix`: Validates cutoff can be today - - `test_delivery_date_stored_correctly`: Ensures delivery_date persistence - - `test_constraint_cutoff_before_pickup_invalid`: Tests invalid configurations are rejected - - `test_constraint_cutoff_before_pickup_valid`: Tests valid configurations work - - `test_all_weekday_combinations_consistency`: Tests all 49 date combinations - - `test_cron_update_dates_executes`: Validates cron job execution -- **Documentation**: - - Documented that this is a more robust fix than v18.0.1.2.0, addressing edge cases in date calculations - ### 18.0.1.3.0 (2026-02-16) - **Performance**: Lazy loading of products for faster page loads - Configurable product pagination (default: 20 per page) @@ -329,7 +306,7 @@ For issues, feature requests, or contributions: --- -**Version:** 18.0.1.3.1 +**Version:** 18.0.1.2.0 **Odoo:** 18.0+ **License:** AGPL-3 **Maintainer:** Criptomart SL diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 8f1a7fe..740e206 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -3,7 +3,7 @@ { # noqa: B018 "name": "Website Sale - Aplicoop", - "version": "18.0.1.3.1", + "version": "18.0.1.1.1", "category": "Website/Sale", "summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders", "author": "Odoo Community Association (OCA), Criptomart", @@ -24,8 +24,6 @@ "data/groups.xml", # Datos: Menús del website "data/website_menus.xml", - # Datos: Cron jobs - "data/cron.xml", # Vistas de seguridad "security/ir.model.access.csv", "security/record_rules.xml", @@ -50,17 +48,7 @@ "assets": { "web.assets_frontend": [ "website_sale_aplicoop/static/src/css/website_sale.css", - # i18n and helpers must load first - "website_sale_aplicoop/static/src/js/i18n_manager.js", - "website_sale_aplicoop/static/src/js/i18n_helpers.js", - # Core shop functionality - "website_sale_aplicoop/static/src/js/website_sale.js", - "website_sale_aplicoop/static/src/js/checkout_labels.js", - "website_sale_aplicoop/static/src/js/home_delivery.js", - "website_sale_aplicoop/static/src/js/checkout_summary.js", - # Search and pagination "website_sale_aplicoop/static/src/js/infinite_scroll.js", - "website_sale_aplicoop/static/src/js/realtime_search.js", ], "web.assets_tests": [ "website_sale_aplicoop/static/tests/test_suite.js", diff --git a/website_sale_aplicoop/controllers/portal.py b/website_sale_aplicoop/controllers/portal.py index d9457bd..40ae0cd 100644 --- a/website_sale_aplicoop/controllers/portal.py +++ b/website_sale_aplicoop/controllers/portal.py @@ -1,72 +1,61 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -import logging - from odoo import _ -from odoo.http import request -from odoo.http import route - +from odoo.http import request, route from odoo.addons.sale.controllers import portal as sale_portal +import logging _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 93c0cf6..2ca0776 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -1441,17 +1441,12 @@ class AplicoopWebsiteSale(WebsiteSale): all_products = group_order._get_products_for_group_order(group_order.id) filtered_products = all_products - # Apply search filter (only if search_query is not empty) + # Apply search if search_query: - _logger.info("load_products_ajax: Applying search filter: %s", search_query) filtered_products = filtered_products.filtered( lambda p: search_query.lower() in p.name.lower() or search_query.lower() in (p.description or "").lower() ) - _logger.info( - "load_products_ajax: After search filter: %d products", - len(filtered_products), - ) # Apply category filter if category_filter != "0": @@ -1460,11 +1455,6 @@ class AplicoopWebsiteSale(WebsiteSale): selected_category = request.env["product.category"].browse(category_id) if selected_category.exists(): - _logger.info( - "load_products_ajax: Applying category filter: %d (%s)", - category_id, - selected_category.name, - ) all_category_ids = [category_id] def get_all_children(category): @@ -1499,10 +1489,6 @@ class AplicoopWebsiteSale(WebsiteSale): # Preserve search filter by using intersection filtered_products = filtered_products & cat_filtered - _logger.info( - "load_products_ajax: After category filter: %d products", - len(filtered_products), - ) except (ValueError, TypeError) as e: _logger.warning( "load_products_ajax: Invalid category filter: %s", str(e) @@ -1514,16 +1500,6 @@ class AplicoopWebsiteSale(WebsiteSale): products_page = filtered_products[offset : offset + per_page] has_next = offset + per_page < total_products - _logger.info( - "load_products_ajax: Pagination - page=%d, offset=%d, per_page=%d, " - "total=%d, has_next=%s", - page, - offset, - per_page, - total_products, - has_next, - ) - # Get prices pricelist = self._resolve_pricelist() product_price_info = {} @@ -1809,8 +1785,10 @@ class AplicoopWebsiteSale(WebsiteSale): ) _logger.warning("========================================") - # Get delivery product from group_order (configured per group order) - delivery_product = group_order.delivery_product_id + # 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 + ) delivery_product_id = delivery_product.id if delivery_product else None # Get translated product name based on current language if delivery_product: @@ -2802,8 +2780,9 @@ class AplicoopWebsiteSale(WebsiteSale): return request.redirect("/eskaera/%d" % sale_order.group_order_id.id) # Extract items from the order (skip delivery product) - # Use the delivery_product_id from the group_order - delivery_product = sale_order.group_order_id.delivery_product_id + delivery_product = request.env.ref( + "website_sale_aplicoop.product_home_delivery", raise_if_not_found=False + ) delivery_product_id = delivery_product.id if delivery_product else None items = [] diff --git a/website_sale_aplicoop/data/cron.xml b/website_sale_aplicoop/data/cron.xml deleted file mode 100644 index 7194ae1..0000000 --- a/website_sale_aplicoop/data/cron.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Group Order: Update Dates Daily - - code - model._cron_update_dates() - 1 - days - - - - diff --git a/website_sale_aplicoop/i18n/README.md b/website_sale_aplicoop/i18n/README.md index 0dc25b6..e203387 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 93245bd..23a9ef7 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,40 +1,36 @@ """Fill pickup_day and pickup_date for existing group orders.""" -from datetime import datetime -from datetime import timedelta +from datetime import datetime, 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 SUPERUSER_ID - from odoo import api - + from odoo import api, SUPERUSER_ID + 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 29f7523..6095a69 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,8 +1,7 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import SUPERUSER_ID -from odoo import api +from odoo import api, SUPERUSER_ID def migrate(cr, version): @@ -14,7 +13,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 @@ -24,7 +23,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 2e90b0d..643fb33 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -4,6 +4,7 @@ import logging from datetime import timedelta +from odoo import _ from odoo import api from odoo import fields from odoo import models @@ -18,47 +19,52 @@ class GroupOrder(models.Model): _inherit = ["mail.thread", "mail.activity.mixin"] _order = "start_date desc" - def _get_order_type_selection(self): + @staticmethod + def _get_order_type_selection(records): """Return order type selection options with translations.""" return [ - ("regular", self.env._("Regular Order")), - ("special", self.env._("Special Order")), - ("promotional", self.env._("Promotional Order")), + ("regular", _("Regular Order")), + ("special", _("Special Order")), + ("promotional", _("Promotional Order")), ] - def _get_period_selection(self): + @staticmethod + def _get_period_selection(records): """Return period selection options with translations.""" return [ - ("once", self.env._("One-time")), - ("weekly", self.env._("Weekly")), - ("biweekly", self.env._("Biweekly")), - ("monthly", self.env._("Monthly")), + ("once", _("One-time")), + ("weekly", _("Weekly")), + ("biweekly", _("Biweekly")), + ("monthly", _("Monthly")), ] - def _get_day_selection(self): + @staticmethod + def _get_day_selection(records): """Return day of week selection options with translations.""" return [ - ("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")), + ("0", _("Monday")), + ("1", _("Tuesday")), + ("2", _("Wednesday")), + ("3", _("Thursday")), + ("4", _("Friday")), + ("5", _("Saturday")), + ("6", _("Sunday")), ] - def _get_state_selection(self): + @staticmethod + def _get_state_selection(records): """Return state selection options with translations.""" return [ - ("draft", self.env._("Draft")), - ("open", self.env._("Open")), - ("closed", self.env._("Closed")), - ("cancelled", self.env._("Cancelled")), + ("draft", _("Draft")), + ("open", _("Open")), + ("closed", _("Closed")), + ("cancelled", _("Cancelled")), ] # === Multicompañía === company_id = fields.Many2one( "res.company", + string="Company", required=True, default=lambda self: self.env.company, tracking=True, @@ -67,6 +73,7 @@ class GroupOrder(models.Model): # === Campos básicos === name = fields.Char( + string="Name", required=True, tracking=True, translate=True, @@ -77,6 +84,7 @@ class GroupOrder(models.Model): "group_order_group_rel", "order_id", "group_id", + string="Consumer Groups", required=True, domain=[("is_group", "=", True)], tracking=True, @@ -84,6 +92,7 @@ class GroupOrder(models.Model): ) type = fields.Selection( selection=_get_order_type_selection, + string="Order Type", required=True, default="regular", tracking=True, @@ -92,11 +101,13 @@ 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", @@ -105,6 +116,7 @@ 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, @@ -112,12 +124,14 @@ 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.", @@ -125,31 +139,36 @@ 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, + store=False, readonly=True, help="Calculated delivery date (pickup date + 1 day)", ) # === 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, @@ -162,6 +181,7 @@ 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.", @@ -171,6 +191,7 @@ class GroupOrder(models.Model): "group_order_product_rel", "order_id", "product_id", + string="Products", tracking=True, help="Directly assigned products.", ) @@ -179,6 +200,7 @@ class GroupOrder(models.Model): "group_order_category_rel", "order_id", "category_id", + string="Categories", tracking=True, help="Products in these categories will be available", ) @@ -186,24 +208,29 @@ 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", @@ -222,6 +249,7 @@ 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", @@ -241,15 +269,8 @@ class GroupOrder(models.Model): for group in record.group_ids: if group.company_id and group.company_id != record.company_id: raise ValidationError( - self.env._( - "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, - } + f"Group {group.name} belongs to company " + f"{group.company_id.name}, not to {record.company_id.name}." ) @api.constrains("start_date", "end_date") @@ -257,9 +278,7 @@ class GroupOrder(models.Model): for record in self: if record.start_date and record.end_date: if record.start_date > record.end_date: - raise ValidationError( - self.env._("Start date cannot be greater than end date") - ) + raise ValidationError("Start date cannot be greater than end date") def action_open(self): """Open order for purchases.""" @@ -484,11 +503,10 @@ class GroupOrder(models.Model): # Calculate days to NEXT occurrence of cutoff_day days_ahead = target_weekday - current_weekday - if days_ahead < 0: - # Target day already passed this week + if days_ahead <= 0: + # Target day already passed this week or is today # Jump to next week's occurrence days_ahead += 7 - # If days_ahead == 0, cutoff is today (allowed) record.cutoff_date = reference_date + timedelta(days=days_ahead) _logger.info( @@ -516,65 +534,3 @@ class GroupOrder(models.Model): ) else: record.delivery_date = None - - # === Constraints === - - @api.constrains("cutoff_day", "pickup_day", "period") - def _check_cutoff_before_pickup(self): - """Validate that pickup_day comes after or equals cutoff_day in weekly orders. - - For weekly orders, if pickup_day < cutoff_day numerically, it means pickup - would be scheduled BEFORE cutoff in the same week cycle, which is illogical. - - Example: - - cutoff_day=3 (Thursday), pickup_day=1 (Tuesday): INVALID - (pickup Tuesday would be before cutoff Thursday) - - cutoff_day=1 (Tuesday), pickup_day=5 (Saturday): VALID - (pickup Saturday is after cutoff Tuesday) - - cutoff_day=5 (Saturday), pickup_day=5 (Saturday): VALID - (same day allowed) - """ - for record in self: - if record.cutoff_day and record.pickup_day and record.period == "weekly": - cutoff = int(record.cutoff_day) - pickup = int(record.pickup_day) - if pickup < cutoff: - pickup_name = dict(self._get_day_selection())[str(pickup)] - cutoff_name = dict(self._get_day_selection())[str(cutoff)] - raise ValidationError( - 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." - ) - % {"pickup": pickup_name, "cutoff": cutoff_name} - ) - - # === Onchange Methods === - - @api.onchange("cutoff_day", "start_date") - def _onchange_cutoff_day(self): - """Force recompute cutoff_date on UI change for immediate feedback.""" - self._compute_cutoff_date() - - @api.onchange("pickup_day", "cutoff_day", "start_date") - def _onchange_pickup_day(self): - """Force recompute pickup_date on UI change for immediate feedback.""" - self._compute_pickup_date() - - # === Cron Methods === - - @api.model - def _cron_update_dates(self): - """Cron job to recalculate dates for active orders daily. - - This ensures that computed dates stay up-to-date as time passes. - Only updates orders in 'draft' or 'open' states. - """ - orders = self.search([("state", "in", ["draft", "open"])]) - _logger.info("Cron: Updating dates for %d active group orders", len(orders)) - for order in orders: - order._compute_cutoff_date() - order._compute_pickup_date() - order._compute_delivery_date() - _logger.info("Cron: Date update completed") diff --git a/website_sale_aplicoop/models/js_translations.py b/website_sale_aplicoop/models/js_translations.py index 01e24ad..686c0fa 100644 --- a/website_sale_aplicoop/models/js_translations.py +++ b/website_sale_aplicoop/models/js_translations.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. """ @@ -19,157 +20,151 @@ 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 d02fdbd..90d896a 100644 --- a/website_sale_aplicoop/models/product_extension.py +++ b/website_sale_aplicoop/models/product_extension.py @@ -1,23 +1,20 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _ -from odoo import api -from odoo import fields -from odoo import models +from odoo import _, api, fields, 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 @@ -28,25 +25,26 @@ 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 0168c9e..4095037 100644 --- a/website_sale_aplicoop/models/res_partner_extension.py +++ b/website_sale_aplicoop/models/res_partner_extension.py @@ -1,39 +1,37 @@ # Copyright 2025-Today Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _ -from odoo import fields -from odoo import models +from odoo import _, fields, 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 873b9a9..72a16be 100644 --- a/website_sale_aplicoop/models/sale_order_extension.py +++ b/website_sale_aplicoop/models/sale_order_extension.py @@ -1,52 +1,56 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import fields -from odoo import models +from odoo import _, fields, models class SaleOrder(models.Model): - _inherit = "sale.order" + _inherit = 'sale.order' - def _get_pickup_day_selection(self): + @staticmethod + def _get_pickup_day_selection(records): """Return pickup day selection options with translations.""" return [ - ("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")), + ('0', _('Monday')), + ('1', _('Tuesday')), + ('2', _('Wednesday')), + ('3', _('Thursday')), + ('4', _('Friday')), + ('5', _('Saturday')), + ('6', _('Sunday')), ] pickup_day = fields.Selection( selection=_get_pickup_day_selection, - help="Day of week when this order will be picked up (inherited from group order)", + string='Pickup Day', + help='Day of week when this order will be picked up (inherited from group order)', ) group_order_id = fields.Many2one( - "group.order", - help="Reference to the consumer group order that originated this sale order", + 'group.order', + string='Consumer Group Order', + help='Reference to the consumer group order that originated this sale order', ) pickup_date = fields.Date( - help="Calculated pickup/delivery date (inherited from consumer group order)", + string='Pickup Date', + 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 70ad028..d1e1972 100644 --- a/website_sale_aplicoop/readme/CREDITS.md +++ b/website_sale_aplicoop/readme/CREDITS.md @@ -6,3 +6,4 @@ 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 db09130..4dcd7a5 100644 --- a/website_sale_aplicoop/readme/USAGE.md +++ b/website_sale_aplicoop/readme/USAGE.md @@ -48,3 +48,4 @@ - `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 10027e4..484ee9b 100644 --- a/website_sale_aplicoop/security/ir.model.access.csv +++ b/website_sale_aplicoop/security/ir.model.access.csv @@ -4,3 +4,4 @@ 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 ee38ad1..43c6b0c 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 find_packages -from setuptools import setup +from setuptools import setup, find_packages -with open("README.rst", encoding="utf-8") as fh: +with open("README.rst", "r", 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 938b4a0..fc15249 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 9152e76..7289115 100644 --- a/website_sale_aplicoop/static/src/css/base/variables.css +++ b/website_sale_aplicoop/static/src/css/base/variables.css @@ -16,27 +16,26 @@ --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; @@ -44,23 +43,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(0.2, 0.9, 0.2, 1); + --transition-normal: 320ms cubic-bezier(.2, .9, .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 781e166..699798f 100644 --- a/website_sale_aplicoop/static/src/css/components/order-card.css +++ b/website_sale_aplicoop/static/src/css/components/order-card.css @@ -17,8 +17,7 @@ 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(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms, - background 320ms; + transition: transform 320ms cubic-bezier(.2, .9, .2, 1), box-shadow 320ms, border-color 320ms, background 320ms; overflow: hidden; display: flex; flex-direction: column; @@ -140,7 +139,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 c6eef5c..5d5b73e 100644 --- a/website_sale_aplicoop/static/src/css/components/product-card.css +++ b/website_sale_aplicoop/static/src/css/components/product-card.css @@ -51,11 +51,7 @@ } .product-card:hover .card-body { - background: linear-gradient( - 135deg, - rgba(108, 117, 125, 0.1) 0%, - rgba(108, 117, 125, 0.08) 100% - ); + background: linear-gradient(135deg, rgba(108, 117, 125, 0.10) 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 64a0087..78e8ed1 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 45022d5..0ad42be 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 267e560..34ce83a 100644 --- a/website_sale_aplicoop/static/src/css/layout/pages.css +++ b/website_sale_aplicoop/static/src/css/layout/pages.css @@ -4,15 +4,13 @@ * 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; @@ -34,24 +32,21 @@ 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; @@ -59,54 +54,29 @@ 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 1e62b6e..a0d0fd0 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 92927d2..0a0bf9a 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 dbce461..995c23d 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 b2537db..494c22b 100644 --- a/website_sale_aplicoop/static/src/js/checkout_labels.js +++ b/website_sale_aplicoop/static/src/js/checkout_labels.js @@ -5,158 +5,140 @@ * 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 (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, - }); - - // 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"); - } - - window.renderCheckoutSummary(window.groupOrderShop.labels); - }) - .catch(function (error) { - console.error("[CHECKOUT] Error:", error); - // Fallback to translated labels - window.renderCheckoutSummary(window.getCheckoutLabels()); + .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 + }); + + // 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'); + } + + 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); @@ -166,88 +148,67 @@ * 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 { @@ -256,14 +217,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; @@ -272,20 +233,11 @@ 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); }); @@ -299,41 +251,32 @@ 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 836ec67..ad6e333 100644 --- a/website_sale_aplicoop/static/src/js/checkout_summary.js +++ b/website_sale_aplicoop/static/src/js/checkout_summary.js @@ -3,7 +3,9 @@ * 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 0479b8e..bb2cd7c 100644 --- a/website_sale_aplicoop/static/src/js/home_delivery.js +++ b/website_sale_aplicoop/static/src/js/home_delivery.js @@ -3,65 +3,56 @@ * 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(); @@ -75,44 +66,42 @@ 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(); @@ -120,103 +109,93 @@ } }, - 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 4462bf5..a55ca06 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,29 +38,30 @@ /** * 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 cc13201..08a0f9e 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,45 +38,41 @@ 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; }, @@ -85,9 +81,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; @@ -96,9 +92,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; @@ -107,7 +103,7 @@ /** * Check if a specific label exists */ - has: function (key) { + has: function(key) { if (!this.initialized) return false; return key in this.labels; }, @@ -115,42 +111,43 @@ /** * 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/src/js/infinite_scroll.js b/website_sale_aplicoop/static/src/js/infinite_scroll.js index 1ccf661..65b6f35 100644 --- a/website_sale_aplicoop/static/src/js/infinite_scroll.js +++ b/website_sale_aplicoop/static/src/js/infinite_scroll.js @@ -7,105 +7,25 @@ console.log("[INFINITE_SCROLL] Script loaded!"); -// DEBUG: Add MutationObserver to detect WHO is clearing the products grid -(function () { - var setupGridObserver = function () { - var grid = document.getElementById("products-grid"); - if (!grid) { - console.log("[MUTATION_DEBUG] products-grid not found yet, will retry..."); - setTimeout(setupGridObserver, 100); - return; - } - - console.log("[MUTATION_DEBUG] 🔍 Setting up MutationObserver on products-grid"); - console.log("[MUTATION_DEBUG] Initial child count:", grid.children.length); - console.log("[MUTATION_DEBUG] Grid innerHTML length:", grid.innerHTML.length); - - // Watch the grid itself for child changes - var gridObserver = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - if (mutation.type === "childList") { - if (mutation.removedNodes.length > 0) { - console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS REMOVED FROM GRID!"); - console.log( - "[MUTATION_DEBUG] Removed nodes count:", - mutation.removedNodes.length - ); - console.log("[MUTATION_DEBUG] Stack trace:"); - console.trace(); - } - if (mutation.addedNodes.length > 0) { - console.log("[MUTATION_DEBUG] Products added:", mutation.addedNodes.length); - } - } - }); - }); - gridObserver.observe(grid, { childList: true, subtree: false }); - - // ALSO watch the parent for the grid element itself being replaced/removed - var parent = grid.parentElement; - if (parent) { - console.log( - "[MUTATION_DEBUG] 🔍 Also watching parent element:", - parent.tagName, - parent.className - ); - var parentObserver = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - if (mutation.type === "childList") { - mutation.removedNodes.forEach(function (node) { - if ( - node.id === "products-grid" || - (node.querySelector && node.querySelector("#products-grid")) - ) { - console.log( - "[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS-GRID ELEMENT ITSELF WAS REMOVED!" - ); - console.log("[MUTATION_DEBUG] Stack trace:"); - console.trace(); - } - }); - } - }); - }); - parentObserver.observe(parent, { childList: true, subtree: true }); - } - - // Poll to detect innerHTML being cleared (as backup) - var lastChildCount = grid.children.length; - setInterval(function () { - var currentGrid = document.getElementById("products-grid"); - if (!currentGrid) { - console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID ELEMENT NO LONGER EXISTS!"); - console.trace(); - return; - } - var currentChildCount = currentGrid.children.length; - if (currentChildCount !== lastChildCount) { - console.log( - "[MUTATION_DEBUG] 📊 Child count changed: " + - lastChildCount + - " → " + - currentChildCount - ); - if (currentChildCount === 0 && lastChildCount > 0) { - console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID WAS EMPTIED!"); - console.trace(); - } - lastChildCount = currentChildCount; - } - }, 100); - - console.log("[MUTATION_DEBUG] ✅ Observers attached (grid + parent + polling)"); - }; - - // Start observing as soon as possible - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupGridObserver); - } else { - setupGridObserver(); +// Visual indicator for debugging +if (typeof document !== "undefined") { + try { + var debugDiv = document.createElement("div"); + debugDiv.innerHTML = "[INFINITE_SCROLL LOADED]"; + debugDiv.style.position = "fixed"; + debugDiv.style.top = "0"; + debugDiv.style.right = "0"; + debugDiv.style.backgroundColor = "#00ff00"; + debugDiv.style.color = "#000"; + debugDiv.style.padding = "5px 10px"; + debugDiv.style.fontSize = "12px"; + debugDiv.style.zIndex = "99999"; + debugDiv.id = "infinite-scroll-debug"; + document.body.appendChild(debugDiv); + } catch (e) { + console.error("[INFINITE_SCROLL] Error adding debug div:", e); } -})(); +} (function () { "use strict"; @@ -125,16 +45,10 @@ console.log("[INFINITE_SCROLL] Script loaded!"); config: {}, init: function () { - console.log("[INFINITE_SCROLL] 🔧 init() called"); - // Get configuration from page data var configEl = document.getElementById("eskaera-config"); - console.log("[INFINITE_SCROLL] eskaera-config element:", configEl); - if (!configEl) { - console.error( - "[INFINITE_SCROLL] ❌ No eskaera-config found, lazy loading disabled" - ); + console.log("[INFINITE_SCROLL] No eskaera-config found, lazy loading disabled"); return; } @@ -144,33 +58,15 @@ console.log("[INFINITE_SCROLL] Script loaded!"); this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20; this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1; - console.log("[INFINITE_SCROLL] Config loaded:", { - orderId: this.orderId, - searchQuery: this.searchQuery, - category: this.category, - perPage: this.perPage, - currentPage: this.currentPage, - }); - // Check if there are more products to load from data attribute var hasNextAttr = configEl.getAttribute("data-has-next"); this.hasMore = hasNextAttr === "true" || hasNextAttr === "True"; - console.log( - "[INFINITE_SCROLL] hasMore=" + - this.hasMore + - " (data-has-next=" + - hasNextAttr + - ")" - ); - if (!this.hasMore) { console.log( - "[INFINITE_SCROLL] ⚠️ No more pages available, but keeping initialized for filter handling (has_next=" + - hasNextAttr + - ")" + "[INFINITE_SCROLL] No more products to load (has_next=" + hasNextAttr + ")" ); - // Don't return - we need to stay initialized so realtime_search can call resetWithFilters() + return; } console.log("[INFINITE_SCROLL] Initialized with:", { @@ -181,50 +77,36 @@ console.log("[INFINITE_SCROLL] Script loaded!"); currentPage: this.currentPage, }); - // Only attach scroll listener if there are more pages to load - if (this.hasMore) { - this.attachScrollListener(); - this.attachFallbackButtonListener(); - } else { - console.log("[INFINITE_SCROLL] Skipping scroll listener (no more pages)"); - } + this.attachScrollListener(); + // Also keep the button listener as fallback + this.attachFallbackButtonListener(); }, attachScrollListener: function () { var self = this; - var scrollThreshold = 300; // Load when within 300px of the bottom of the grid + var scrollThreshold = 0.8; // Load when 80% scrolled window.addEventListener("scroll", function () { if (self.isLoading || !self.hasMore) { return; } - var grid = document.getElementById("products-grid"); - if (!grid) { - return; - } + var scrollHeight = document.documentElement.scrollHeight; + var scrollTop = window.scrollY; + var clientHeight = window.innerHeight; + var scrollPercent = (scrollTop + clientHeight) / scrollHeight; - // Calculate distance from bottom of grid to bottom of viewport - var gridRect = grid.getBoundingClientRect(); - var gridBottom = gridRect.bottom; - var viewportBottom = window.innerHeight; - var distanceFromBottom = gridBottom - viewportBottom; - - // Load more if we're within threshold pixels of the grid bottom - if (distanceFromBottom <= scrollThreshold && distanceFromBottom > 0) { + if (scrollPercent >= scrollThreshold) { console.log( - "[INFINITE_SCROLL] Near grid bottom (distance: " + - Math.round(distanceFromBottom) + - "px), loading next page" + "[INFINITE_SCROLL] Scroll threshold reached, loading next page" ); self.loadNextPage(); } }); console.log( - "[INFINITE_SCROLL] Scroll listener attached (threshold: " + - scrollThreshold + - "px from grid bottom)" + "[INFINITE_SCROLL] Scroll listener attached (threshold:", + scrollThreshold * 100 + "%)" ); }, @@ -252,53 +134,20 @@ console.log("[INFINITE_SCROLL] Script loaded!"); /** * Reset infinite scroll to page 1 with new filters and reload products. * Called by realtime_search when filters change. - * - * WARNING: This clears the grid! Only call when filters actually change. */ console.log( - "[INFINITE_SCROLL] ⚠️⚠️⚠️ resetWithFilters CALLED - search=" + + "[INFINITE_SCROLL] Resetting with filters: search=" + searchQuery + " category=" + categoryId ); - console.trace("[INFINITE_SCROLL] ⚠️⚠️⚠️ WHO CALLED resetWithFilters? Call stack:"); - // Normalize values: empty string to "", null to "0" for category - var newSearchQuery = (searchQuery || "").trim(); - var newCategory = (categoryId || "").trim() || "0"; - - // CHECK IF VALUES ACTUALLY CHANGED before clearing grid! - if (newSearchQuery === this.searchQuery && newCategory === this.category) { - console.log( - "[INFINITE_SCROLL] ✅ NO CHANGE - Skipping reset (values are identical)" - ); - return; // Don't clear grid if nothing changed! - } - - console.log( - "[INFINITE_SCROLL] 🔥 VALUES CHANGED - Old: search=" + - this.searchQuery + - " category=" + - this.category + - " → New: search=" + - newSearchQuery + - " category=" + - newCategory - ); - - this.searchQuery = newSearchQuery; - this.category = newCategory; + this.searchQuery = searchQuery || ""; + this.category = categoryId || "0"; this.currentPage = 0; // Set to 0 so loadNextPage() increments to 1 this.isLoading = false; this.hasMore = true; - console.log( - "[INFINITE_SCROLL] After normalization: search=" + - this.searchQuery + - " category=" + - this.category - ); - // Update the config element data attributes for consistency var configEl = document.getElementById("eskaera-config"); if (configEl) { @@ -306,58 +155,26 @@ console.log("[INFINITE_SCROLL] Script loaded!"); configEl.setAttribute("data-category", this.category); configEl.setAttribute("data-current-page", "1"); configEl.setAttribute("data-has-next", "true"); - console.log("[INFINITE_SCROLL] Updated eskaera-config attributes"); } // Clear the grid and reload from page 1 var grid = document.getElementById("products-grid"); if (grid) { - console.log("[INFINITE_SCROLL] 🗑️ CLEARING GRID NOW!"); grid.innerHTML = ""; console.log("[INFINITE_SCROLL] Grid cleared"); } // Load first page with new filters - console.log("[INFINITE_SCROLL] Calling loadNextPage()..."); this.loadNextPage(); }, loadNextPage: function () { - console.log( - "[INFINITE_SCROLL] 🚀 loadNextPage() CALLED - currentPage=" + - this.currentPage + - " isLoading=" + - this.isLoading + - " hasMore=" + - this.hasMore - ); - - if (this.isLoading || !this.hasMore) { - console.log("[INFINITE_SCROLL] ❌ ABORTING - already loading or no more pages"); - return; - } - var self = this; this.isLoading = true; - - // Only increment if we're not loading first page (currentPage will be 0 after reset) - if (this.currentPage === 0) { - console.log( - "[INFINITE_SCROLL] ✅ Incrementing from 0 to 1 (first page after reset)" - ); - this.currentPage = 1; - } else { - console.log( - "[INFINITE_SCROLL] ✅ Incrementing page " + - this.currentPage + - " → " + - (this.currentPage + 1) - ); - this.currentPage += 1; - } + this.currentPage += 1; console.log( - "[INFINITE_SCROLL] 📡 About to fetch page", + "[INFINITE_SCROLL] Loading page", this.currentPage, "for order", this.orderId diff --git a/website_sale_aplicoop/static/src/js/realtime_search.js b/website_sale_aplicoop/static/src/js/realtime_search.js index c096bac..71fd4cb 100644 --- a/website_sale_aplicoop/static/src/js/realtime_search.js +++ b/website_sale_aplicoop/static/src/js/realtime_search.js @@ -135,9 +135,6 @@ _attachEventListeners: function () { var self = this; - // Flag to prevent filtering during initialization - self.isInitializing = true; - // Initialize available tags from DOM self._initializeAvailableTags(); @@ -145,68 +142,8 @@ self.originalTagColors = {}; // Maps tag ID to original color // Store last values at instance level so polling can access them - // Initialize to current values to avoid triggering reset on first poll - self.lastSearchValue = self.searchInput.value.trim(); - self.lastCategoryValue = self.categorySelect.value; - - console.log( - "[realtimeSearch] Initial values stored - search:", - JSON.stringify(self.lastSearchValue), - "category:", - JSON.stringify(self.lastCategoryValue) - ); - - // Clear search button - self.clearSearchBtn = document.getElementById("clear-search-btn"); - if (self.clearSearchBtn) { - console.log("[realtimeSearch] Clear search button found, attaching listeners"); - - // Show/hide button based on input content (passive, no filtering) - // This listener is separate from the filtering listener - self.searchInput.addEventListener("input", function () { - if (self.searchInput.value.trim().length > 0) { - self.clearSearchBtn.style.display = "block"; - } else { - self.clearSearchBtn.style.display = "none"; - } - }); - - // Clear search when button clicked - self.clearSearchBtn.addEventListener("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - console.log("[realtimeSearch] Clear search button clicked"); - - // Clear the input - self.searchInput.value = ""; - self.clearSearchBtn.style.display = "none"; - - // Update last stored value to prevent polling from detecting "change" - self.lastSearchValue = ""; - - // Reset infinite scroll to reload all products from server - if ( - window.infiniteScroll && - typeof window.infiniteScroll.resetWithFilters === "function" - ) { - console.log( - "[realtimeSearch] Resetting infinite scroll to show all products" - ); - window.infiniteScroll.resetWithFilters("", self.lastCategoryValue); - } else if (!self.isInitializing) { - // Fallback: filter locally - self._filterProducts(); - } - - // Focus back to search input - self.searchInput.focus(); - }); - - // Initial check - don't show if empty - if (self.searchInput.value.trim().length > 0) { - self.clearSearchBtn.style.display = "block"; - } - } + self.lastSearchValue = ""; + self.lastCategoryValue = ""; // Prevent form submission completely var form = self.searchInput.closest("form"); @@ -232,11 +169,6 @@ // Search input: listen to 'input' for real-time filtering self.searchInput.addEventListener("input", function (e) { try { - // Skip filtering during initialization - if (self.isInitializing) { - console.log("[realtimeSearch] INPUT event during init - skipping filter"); - return; - } console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"'); self._filterProducts(); } catch (error) { @@ -247,11 +179,6 @@ // Also keep 'keyup' for extra compatibility self.searchInput.addEventListener("keyup", function (e) { try { - // Skip filtering during initialization - if (self.isInitializing) { - console.log("[realtimeSearch] KEYUP event during init - skipping filter"); - return; - } console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"'); self._filterProducts(); } catch (error) { @@ -262,11 +189,6 @@ // Category select self.categorySelect.addEventListener("change", function (e) { try { - // Skip filtering during initialization - if (self.isInitializing) { - console.log("[realtimeSearch] CHANGE event during init - skipping filter"); - return; - } console.log( '[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"' ); @@ -393,10 +315,7 @@ }); // Filter products (independent of search/category state) - // Skip during initialization - if (!self.isInitializing) { - self._filterProducts(); - } + self._filterProducts(); }); }); @@ -409,11 +328,6 @@ var pollingCounter = 0; var pollInterval = setInterval(function () { try { - // Skip polling during initialization to avoid clearing products - if (self.isInitializing) { - return; - } - pollingCounter++; // Try multiple ways to get the search value @@ -504,13 +418,10 @@ ); } else { // Fallback: filter locally (but this only filters loaded products) - // Skip during initialization - if (!self.isInitializing) { - console.log( - "[realtimeSearch] infiniteScroll not available, filtering locally only" - ); - self._filterProducts(); - } + console.log( + "[realtimeSearch] infiniteScroll not available, filtering locally only" + ); + self._filterProducts(); } } } catch (error) { @@ -521,10 +432,6 @@ console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval); console.log("[realtimeSearch] Event listeners attached with polling fallback"); - - // Initialization complete - allow filtering now - self.isInitializing = false; - console.log("[realtimeSearch] ✅ Initialization complete - filtering enabled"); }, _initializeAvailableTags: function () { diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index a02f2b7..1a17339 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -45,10 +45,6 @@ console.log("Initializing cart for order:", this.orderId); - // Attach event listeners FIRST (doesn't depend on translations) - this._attachEventListeners(); - console.log("[groupOrderShop] Event listeners attached"); - // Wait for i18nManager to load translations from server i18nManager .init() @@ -56,6 +52,8 @@ console.log("[groupOrderShop] Translations loaded from server"); self.labels = i18nManager.getAll(); + // Initialize event listeners and state after translations are ready + self._attachEventListeners(); self._loadCart(); self._checkConfirmationMessage(); self._initializeTooltips(); @@ -605,82 +603,12 @@ _attachEventListeners: function () { var self = this; - // Helper function to round decimals correctly - function roundDecimal(value, decimals) { - var factor = Math.pow(10, decimals); - return Math.round(value * factor) / factor; - } - - // ============ ATTACH CHECKOUT BUTTONS (ALWAYS, on any page) ============ - // These buttons exist on checkout page, not on cart pages - if (!this._cartCheckoutListenersAttached) { - console.log("[_attachEventListeners] Attaching checkout button listeners..."); - - // Button to save as draft (in checkout page) - var saveBtn = document.getElementById("save-order-btn"); - console.log("[_attachEventListeners] save-order-btn found:", !!saveBtn); - - if (saveBtn) { - saveBtn.addEventListener("click", function (e) { - console.log("[CLICK] save-order-btn clicked"); - e.preventDefault(); - self._saveOrderDraft(); - }); - } - - // Confirm order button (in checkout page) - var confirmBtn = document.getElementById("confirm-order-btn"); - console.log("[_attachEventListeners] confirm-order-btn found:", !!confirmBtn); - - if (confirmBtn) { - confirmBtn.addEventListener("click", function (e) { - console.log("[CLICK] confirm-order-btn clicked"); - e.preventDefault(); - self._confirmOrder(); - }); - } - - // Button to reload from draft (in My Cart header - cart pages) - var reloadCartBtn = document.getElementById("reload-cart-btn"); - console.log("[_attachEventListeners] reload-cart-btn found:", !!reloadCartBtn); - - if (reloadCartBtn) { - reloadCartBtn.addEventListener("click", function (e) { - console.log("[CLICK] reload-cart-btn clicked"); - e.preventDefault(); - self._loadDraftCart(); - }); - } - - // Button to save cart as draft (in My Cart header - shop pages) - var saveCartBtn = document.getElementById("save-cart-btn"); - console.log("[_attachEventListeners] save-cart-btn found:", !!saveCartBtn); - - if (saveCartBtn) { - saveCartBtn.addEventListener("click", function (e) { - console.log("[CLICK] save-cart-btn clicked"); - e.preventDefault(); - self._saveCartAsDraft(); - }); - } - - this._cartCheckoutListenersAttached = true; - console.log("[_attachEventListeners] Checkout listeners attached (one-time)"); - } - // ============ LAZY LOADING: Load More Button ============ this._attachLoadMoreListener(); - // ============ USE EVENT DELEGATION FOR QUANTITY & CART BUTTONS ============ - // This way, new products loaded via AJAX will automatically have listeners - var productsGrid = document.getElementById("products-grid"); - - if (!productsGrid) { - console.log("[_attachEventListeners] No products-grid found (checkout page?)"); - return; - } - - // First, adjust quantity steps for all existing inputs + // Adjust quantity step based on UoM category + // Categories without decimals (per unit): "Unit", "Units", etc. + // Categories with decimals: "Weight", "Volume", "Length", etc. var unitInputs = document.querySelectorAll(".product-qty"); console.log("=== ADJUSTING QUANTITY STEPS ==="); console.log("Found " + unitInputs.length + " quantity inputs"); @@ -710,92 +638,142 @@ } console.log("=== END ADJUSTING QUANTITY STEPS ==="); - // IMPORTANT: Do NOT clone the grid node - this destroys all products! - // Instead, use a flag to prevent adding duplicate event listeners - if (productsGrid._delegationListenersAttached) { - console.log( - "[_attachEventListeners] Grid delegation listeners already attached, skipping" - ); - return; + // Botones + y - para aumentar/disminuir cantidad + // Helper function to round decimals correctly + function roundDecimal(value, decimals) { + var factor = Math.pow(10, decimals); + return Math.round(value * factor) / factor; } - productsGrid._delegationListenersAttached = true; - console.log("[_attachEventListeners] Attaching grid delegation listeners (one-time)"); - // Quantity decrease button (via event delegation) - productsGrid.addEventListener("click", function (e) { - var decreaseBtn = e.target.closest(".qty-decrease"); - if (!decreaseBtn) return; + // Remove old listeners by cloning elements (to avoid duplication) + var decreaseButtons = document.querySelectorAll(".qty-decrease"); + for (var k = 0; k < decreaseButtons.length; k++) { + var newBtn = decreaseButtons[k].cloneNode(true); + decreaseButtons[k].parentNode.replaceChild(newBtn, decreaseButtons[k]); + } - e.preventDefault(); - var productId = decreaseBtn.getAttribute("data-product-id"); - var input = document.getElementById("qty_" + productId); - if (!input) return; + var increaseButtons = document.querySelectorAll(".qty-increase"); + for (var k = 0; k < increaseButtons.length; k++) { + var newBtn = increaseButtons[k].cloneNode(true); + increaseButtons[k].parentNode.replaceChild(newBtn, increaseButtons[k]); + } - var step = parseFloat(input.step) || 1; - var currentValue = parseFloat(input.value) || 0; - var min = parseFloat(input.min) || 0; - var newValue = Math.max(min, roundDecimal(currentValue - step, 1)); + // Ahora asignar nuevos listeners + decreaseButtons = document.querySelectorAll(".qty-decrease"); + for (var k = 0; k < decreaseButtons.length; k++) { + decreaseButtons[k].addEventListener("click", function (e) { + e.preventDefault(); + var productId = this.getAttribute("data-product-id"); + var input = document.getElementById("qty_" + productId); + if (!input) return; - // Si es unidad, mostrar como entero - if (input.dataset.isUnit === "true") { - input.value = Math.floor(newValue); - } else { - input.value = newValue; - } - }); + var step = parseFloat(input.step) || 1; + var currentValue = parseFloat(input.value) || 0; + var min = parseFloat(input.min) || 0; + var newValue = Math.max(min, roundDecimal(currentValue - step, 1)); - // Quantity increase button (via event delegation) - productsGrid.addEventListener("click", function (e) { - var increaseBtn = e.target.closest(".qty-increase"); - if (!increaseBtn) return; - - e.preventDefault(); - var productId = increaseBtn.getAttribute("data-product-id"); - var input = document.getElementById("qty_" + productId); - if (!input) return; - - var step = parseFloat(input.step) || 1; - var currentValue = parseFloat(input.value) || 0; - var newValue = roundDecimal(currentValue + step, 1); - - // Si es unidad, mostrar como entero - if (input.dataset.isUnit === "true") { - input.value = Math.floor(newValue); - } else { - input.value = newValue; - } - }); - - // Add to cart button (via event delegation) - productsGrid.addEventListener("click", function (e) { - var cartBtn = e.target.closest(".add-to-cart-btn"); - if (!cartBtn) return; - - e.preventDefault(); - var form = cartBtn.closest(".add-to-cart-form"); - var productId = form.getAttribute("data-product-id"); - var productName = form.getAttribute("data-product-name") || "Product"; - var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0; - var quantityInput = form.querySelector(".product-qty"); - var quantity = quantityInput ? parseFloat(quantityInput.value) : 1; - - console.log("Adding:", { - productId: productId, - productName: productName, - productPrice: productPrice, - quantity: quantity, + // Si es unidad, mostrar como entero + if (input.dataset.isUnit === "true") { + input.value = Math.floor(newValue); + } else { + input.value = newValue; + } }); + } - if (quantity > 0) { - self._addToCart(productId, productName, productPrice, quantity); - } else { - var labels = self._getLabels(); - self._showNotification( - labels.invalid_quantity || "Please enter a valid quantity", - "warning" - ); - } - }); + increaseButtons = document.querySelectorAll(".qty-increase"); + for (var k = 0; k < increaseButtons.length; k++) { + increaseButtons[k].addEventListener("click", function (e) { + e.preventDefault(); + var productId = this.getAttribute("data-product-id"); + var input = document.getElementById("qty_" + productId); + if (!input) return; + + var step = parseFloat(input.step) || 1; + var currentValue = parseFloat(input.value) || 0; + var newValue = roundDecimal(currentValue + step, 1); + + // Si es unidad, mostrar como entero + if (input.dataset.isUnit === "true") { + input.value = Math.floor(newValue); + } else { + input.value = newValue; + } + }); + } + + // Botones de agregar al carrito + var buttons = document.querySelectorAll(".add-to-cart-btn"); + for (var i = 0; i < buttons.length; i++) { + buttons[i].addEventListener("click", function (e) { + e.preventDefault(); + var form = this.closest(".add-to-cart-form"); + var productId = form.getAttribute("data-product-id"); + var productName = form.getAttribute("data-product-name") || "Product"; + var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0; + var quantityInput = form.querySelector(".product-qty"); + var quantity = quantityInput ? parseFloat(quantityInput.value) : 1; + + console.log("Adding:", { + productId: productId, + productName: productName, + productPrice: productPrice, + quantity: quantity, + }); + + if (quantity > 0) { + self._addToCart(productId, productName, productPrice, quantity); + } else { + var labels = self._getLabels(); + self._showNotification( + labels.invalid_quantity || "Please enter a valid quantity", + "warning" + ); + } + }); + } + + // Button to save cart as draft (in My Cart header) + var savCartBtn = document.getElementById("save-cart-btn"); + if (savCartBtn) { + // Remove old listeners by cloning + var savCartBtnNew = savCartBtn.cloneNode(true); + savCartBtn.parentNode.replaceChild(savCartBtnNew, savCartBtn); + savCartBtnNew.addEventListener("click", function (e) { + e.preventDefault(); + self._saveCartAsDraft(); + }); + } + + // Button to reload from draft (in My Cart header) + var reloadCartBtn = document.getElementById("reload-cart-btn"); + if (reloadCartBtn) { + // Remove old listeners by cloning + var reloadCartBtnNew = reloadCartBtn.cloneNode(true); + reloadCartBtn.parentNode.replaceChild(reloadCartBtnNew, reloadCartBtn); + reloadCartBtnNew.addEventListener("click", function (e) { + e.preventDefault(); + self._loadDraftCart(); + }); + } + + // Button to save as draft + var saveBtn = document.getElementById("save-order-btn"); + if (saveBtn) { + saveBtn.addEventListener("click", function (e) { + e.preventDefault(); + self._saveOrderDraft(); + }); + } + + // Confirm order button + var confirmBtn = document.getElementById("confirm-order-btn"); + if (confirmBtn) { + confirmBtn.addEventListener("click", function (e) { + e.preventDefault(); + self._confirmOrder(); + }); + } }, _addToCart: function (productId, productName, productPrice, quantity) { @@ -1497,7 +1475,7 @@ }, _saveOrderDraft: function () { - console.log("[_saveOrderDraft] Starting - this.orderId:", this.orderId); + console.log("Saving order as draft:", this.orderId); var self = this; var items = []; @@ -2001,10 +1979,13 @@ console.log("cart-items-container found:", !!cartContainer); console.log("confirm-order-btn found:", !!confirmBtn); - // Always initialize - it handles both cart pages and checkout pages - console.log("Calling init()"); - var result = window.groupOrderShop.init(); - console.log("init() result:", result); + if (cartContainer || confirmBtn) { + console.log("Calling init()"); + var result = window.groupOrderShop.init(); + console.log("init() result:", result); + } else { + console.warn("No elements found to initialize cart"); + } }); // Handle confirm order buttons in portal (My Orders page) diff --git a/website_sale_aplicoop/static/tests/README.md b/website_sale_aplicoop/static/tests/README.md index eee11fd..82e9c51 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 3fabe68..1160a05 100644 --- a/website_sale_aplicoop/static/tests/test_cart_functions.js +++ b/website_sale_aplicoop/static/tests/test_cart_functions.js @@ -3,273 +3,222 @@ * 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(); - }, - afterEach: function () { - // Cleanup - localStorage.clear(); - delete window.groupOrderShop; - }, + 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(); }, - 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"); - }); + 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.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 9cd9468..bbac93c 100644 --- a/website_sale_aplicoop/static/tests/test_realtime_search.js +++ b/website_sale_aplicoop/static/tests/test_realtime_search.js @@ -3,247 +3,239 @@ * 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 }; - }, - }; - }, - afterEach: function () { - // Cleanup - this.$fixture.empty(); - delete window.realtimeSearch; - }, + 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 }; + } + }; }, - 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"); - }); + 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'); + }); + }); return {}; }); diff --git a/website_sale_aplicoop/static/tests/test_suite.js b/website_sale_aplicoop/static/tests/test_suite.js index 6e9682f..210f2d4 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 e89b4a5..f8386c2 100644 --- a/website_sale_aplicoop/static/tests/test_tooltips_labels.js +++ b/website_sale_aplicoop/static/tests/test_tooltips_labels.js @@ -3,214 +3,185 @@ * 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]); - } - }); - }, - }; - }, - afterEach: function () { - // Cleanup - this.$fixture.empty(); - delete window.groupOrderShop; - }, - }, - function () { - 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 { - window.groupOrderShop._initTooltips(); - assert.ok(true, "initialization with empty labels does not throw error"); - } catch (e) { - assert.ok(false, "initialization threw error: " + e.message); + 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; } - ); + }, function() { + + 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 { + 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 5f881c9..360d1aa 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_date_calculations.py b/website_sale_aplicoop/tests/test_date_calculations.py index 98190c6..ec6aa1b 100644 --- a/website_sale_aplicoop/tests/test_date_calculations.py +++ b/website_sale_aplicoop/tests/test_date_calculations.py @@ -1,31 +1,26 @@ # Copyright 2026 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import timedelta +from datetime import datetime, timedelta -from odoo import fields -from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase -from odoo.tests.common import tagged +from odoo import fields -@tagged("post_install", "date_calculations") class TestDateCalculations(TransactionCase): - """Test suite for date calculation methods in group.order model.""" + '''Test suite for date calculation methods in group.order model.''' def setUp(self): super().setUp() # Create a test 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', + }) def test_compute_pickup_date_basic(self): - """Test pickup_date calculation returns next occurrence of pickup day.""" + '''Test pickup_date calculation returns next occurrence of pickup day.''' # Use today as reference and calculate next Tuesday today = fields.Date.today() # Find next Sunday (weekday 6) from today @@ -34,18 +29,16 @@ class TestDateCalculations(TransactionCase): start_date = today else: start_date = today + timedelta(days=days_until_sunday) - + # Create order with pickup_day = Tuesday (1), starting on Sunday # NO cutoff_day to avoid dependency on cutoff_date - order = self.env["group.order"].create( - { - "name": "Test Order", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, # Sunday - "pickup_day": "1", # Tuesday - "cutoff_day": False, # Disable to avoid cutoff_date interference - } - ) + order = self.env['group.order'].create({ + 'name': 'Test Order', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': start_date, # Sunday + 'pickup_day': '1', # Tuesday + 'cutoff_day': False, # Disable to avoid cutoff_date interference + }) # Force computation order._compute_pickup_date() @@ -55,11 +48,11 @@ class TestDateCalculations(TransactionCase): self.assertEqual( order.pickup_date, expected_date, - f"Expected {expected_date}, got {order.pickup_date}", + f"Expected {expected_date}, got {order.pickup_date}" ) def test_compute_pickup_date_same_day(self): - """Test pickup_date when start_date is same weekday as pickup_day.""" + '''Test pickup_date when start_date is same weekday as pickup_day.''' # Find next Tuesday from today today = fields.Date.today() days_until_tuesday = (1 - today.weekday()) % 7 @@ -67,16 +60,14 @@ class TestDateCalculations(TransactionCase): start_date = today else: start_date = today + timedelta(days=days_until_tuesday) - + # Start on Tuesday, pickup also Tuesday - should return next week's Tuesday - order = self.env["group.order"].create( - { - "name": "Test Order Same Day", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, # Tuesday - "pickup_day": "1", # Tuesday - } - ) + order = self.env['group.order'].create({ + 'name': 'Test Order Same Day', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': start_date, # Tuesday + 'pickup_day': '1', # Tuesday + }) order._compute_pickup_date() @@ -85,15 +76,13 @@ class TestDateCalculations(TransactionCase): self.assertEqual(order.pickup_date, expected_date) def test_compute_pickup_date_no_start_date(self): - """Test pickup_date calculation when no start_date is set.""" - order = self.env["group.order"].create( - { - "name": "Test Order No Start", - "group_ids": [(6, 0, [self.group.id])], - "start_date": False, - "pickup_day": "1", # Tuesday - } - ) + '''Test pickup_date calculation when no start_date is set.''' + order = self.env['group.order'].create({ + 'name': 'Test Order No Start', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': False, + 'pickup_day': '1', # Tuesday + }) order._compute_pickup_date() @@ -104,43 +93,32 @@ class TestDateCalculations(TransactionCase): self.assertEqual(order.pickup_date.weekday(), 1) # 1 = Tuesday def test_compute_pickup_date_without_pickup_day(self): - """Test pickup_date is None when pickup_day is not set.""" - order = self.env["group.order"].create( - { - "name": "Test Order No Pickup Day", - "group_ids": [(6, 0, [self.group.id])], - "start_date": fields.Date.today(), - "pickup_day": False, - } - ) + '''Test pickup_date is None when pickup_day is not set.''' + order = self.env['group.order'].create({ + 'name': 'Test Order No Pickup Day', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': fields.Date.today(), + 'pickup_day': False, + }) order._compute_pickup_date() # In Odoo, computed Date fields return False (not None) when no value self.assertFalse(order.pickup_date) def test_compute_pickup_date_all_weekdays(self): - """Test pickup_date calculation for each day of the week.""" - base_date = fields.Date.from_string("2026-02-02") # Monday + '''Test pickup_date calculation for each day of the week.''' + base_date = fields.Date.from_string('2026-02-02') # Monday for day_num in range(7): - day_name = [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ][day_num] + day_name = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'][day_num] - order = self.env["group.order"].create( - { - "name": f"Test Order {day_name}", - "group_ids": [(6, 0, [self.group.id])], - "start_date": base_date, - "pickup_day": str(day_num), - } - ) + order = self.env['group.order'].create({ + 'name': f'Test Order {day_name}', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': base_date, + 'pickup_day': str(day_num), + }) order._compute_pickup_date() @@ -148,14 +126,14 @@ class TestDateCalculations(TransactionCase): self.assertEqual( order.pickup_date.weekday(), day_num, - f"Pickup date weekday should be {day_num} ({day_name})", + f"Pickup date weekday should be {day_num} ({day_name})" ) # Verify it's after start_date self.assertGreater(order.pickup_date, base_date) def test_compute_delivery_date_basic(self): - """Test delivery_date is pickup_date + 1 day.""" + '''Test delivery_date is pickup_date + 1 day.''' # Find next Sunday from today today = fields.Date.today() days_until_sunday = (6 - today.weekday()) % 7 @@ -163,15 +141,13 @@ class TestDateCalculations(TransactionCase): start_date = today else: start_date = today + timedelta(days=days_until_sunday) - - order = self.env["group.order"].create( - { - "name": "Test Delivery Date", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, # Sunday - "pickup_day": "1", # Tuesday = start_date + 2 days - } - ) + + order = self.env['group.order'].create({ + 'name': 'Test Delivery Date', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': start_date, # Sunday + 'pickup_day': '1', # Tuesday = start_date + 2 days + }) order._compute_pickup_date() order._compute_delivery_date() @@ -183,15 +159,13 @@ class TestDateCalculations(TransactionCase): self.assertEqual(order.delivery_date, expected_delivery) def test_compute_delivery_date_without_pickup(self): - """Test delivery_date is None when pickup_date is not set.""" - order = self.env["group.order"].create( - { - "name": "Test No Delivery", - "group_ids": [(6, 0, [self.group.id])], - "start_date": fields.Date.today(), - "pickup_day": False, # No pickup day = no pickup_date - } - ) + '''Test delivery_date is None when pickup_date is not set.''' + order = self.env['group.order'].create({ + 'name': 'Test No Delivery', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': fields.Date.today(), + 'pickup_day': False, # No pickup day = no pickup_date + }) order._compute_pickup_date() order._compute_delivery_date() @@ -200,17 +174,15 @@ class TestDateCalculations(TransactionCase): self.assertFalse(order.delivery_date) def test_compute_cutoff_date_basic(self): - """Test cutoff_date calculation returns next occurrence of cutoff day.""" + '''Test cutoff_date calculation returns next occurrence of cutoff day.''' # Create order with cutoff_day = Sunday (6) # If today is Sunday, cutoff should be today (days_ahead = 0 is allowed) - order = self.env["group.order"].create( - { - "name": "Test Cutoff Date", - "group_ids": [(6, 0, [self.group.id])], - "start_date": fields.Date.from_string("2026-02-01"), # Sunday - "cutoff_day": "6", # Sunday - } - ) + order = self.env['group.order'].create({ + 'name': 'Test Cutoff Date', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': fields.Date.from_string('2026-02-01'), # Sunday + 'cutoff_day': '6', # Sunday + }) order._compute_cutoff_date() @@ -221,22 +193,20 @@ class TestDateCalculations(TransactionCase): self.assertEqual(order.cutoff_date.weekday(), 6) # Sunday def test_compute_cutoff_date_without_cutoff_day(self): - """Test cutoff_date is None when cutoff_day is not set.""" - order = self.env["group.order"].create( - { - "name": "Test No Cutoff", - "group_ids": [(6, 0, [self.group.id])], - "start_date": fields.Date.today(), - "cutoff_day": False, - } - ) + '''Test cutoff_date is None when cutoff_day is not set.''' + order = self.env['group.order'].create({ + 'name': 'Test No Cutoff', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': fields.Date.today(), + 'cutoff_day': False, + }) order._compute_cutoff_date() # In Odoo, computed Date fields return False (not None) when no value self.assertFalse(order.cutoff_date) def test_date_dependency_chain(self): - """Test that changing start_date triggers recomputation of date fields.""" + '''Test that changing start_date triggers recomputation of date fields.''' # Find next Sunday from today today = fields.Date.today() days_until_sunday = (6 - today.weekday()) % 7 @@ -244,16 +214,14 @@ class TestDateCalculations(TransactionCase): start_date = today else: start_date = today + timedelta(days=days_until_sunday) - - order = self.env["group.order"].create( - { - "name": "Test Date Chain", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, # Dynamic Sunday - "pickup_day": "6", # Sunday (must be >= cutoff_day) - "cutoff_day": "5", # Saturday - } - ) + + order = self.env['group.order'].create({ + 'name': 'Test Date Chain', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': start_date, # Dynamic Sunday + 'pickup_day': '1', # Tuesday + 'cutoff_day': '6', # Sunday + }) # Get initial dates initial_pickup = order.pickup_date @@ -262,7 +230,7 @@ class TestDateCalculations(TransactionCase): # Change start_date to a week later new_start_date = start_date + timedelta(days=7) - order.write({"start_date": new_start_date}) + order.write({'start_date': new_start_date}) # Verify pickup and delivery dates changed self.assertNotEqual(order.pickup_date, initial_pickup) @@ -274,17 +242,17 @@ class TestDateCalculations(TransactionCase): self.assertEqual(delta.days, 1) def test_pickup_date_no_extra_week_bug(self): - """Regression test: ensure pickup_date doesn't add extra week incorrectly. + '''Regression test: ensure pickup_date doesn't add extra week incorrectly. Bug context: Previously when cutoff_day >= pickup_day numerically, logic incorrectly added 7 extra days even when pickup was already ahead in the calendar. - """ + ''' # Scenario: Pickup Tuesday (1) # Start: Sunday (dynamic) # Expected pickup: Tuesday (2 days later, NOT +9 days) # NOTE: NO cutoff_day to avoid cutoff_date dependency - + # Find next Sunday from today today = fields.Date.today() days_until_sunday = (6 - today.weekday()) % 7 @@ -292,16 +260,14 @@ class TestDateCalculations(TransactionCase): start_date = today else: start_date = today + timedelta(days=days_until_sunday) - - order = self.env["group.order"].create( - { - "name": "Regression Test Extra Week", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, # Sunday (dynamic) - "pickup_day": "1", # Tuesday (numerically < 6) - "cutoff_day": False, # Disable to test pure start_date logic - } - ) + + order = self.env['group.order'].create({ + 'name': 'Regression Test Extra Week', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': start_date, # Sunday (dynamic) + 'pickup_day': '1', # Tuesday (numerically < 6) + 'cutoff_day': False, # Disable to test pure start_date logic + }) order._compute_pickup_date() @@ -310,30 +276,30 @@ class TestDateCalculations(TransactionCase): self.assertEqual( order.pickup_date, expected, - f"Bug detected: pickup_date should be {expected} not {order.pickup_date}", + f"Bug detected: pickup_date should be {expected} not {order.pickup_date}" ) # Verify it's exactly 2 days after start_date delta = order.pickup_date - order.start_date self.assertEqual( - delta.days, 2, "Pickup should be 2 days after Sunday start_date" + delta.days, + 2, + "Pickup should be 2 days after Sunday start_date" ) def test_multiple_orders_same_pickup_day(self): - """Test multiple orders with same pickup day get consistent dates.""" - start = fields.Date.from_string("2026-02-01") - pickup_day = "1" # Tuesday + '''Test multiple orders with same pickup day get consistent dates.''' + start = fields.Date.from_string('2026-02-01') + pickup_day = '1' # Tuesday orders = [] for i in range(3): - order = self.env["group.order"].create( - { - "name": f"Test Order {i}", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start, - "pickup_day": pickup_day, - } - ) + order = self.env['group.order'].create({ + 'name': f'Test Order {i}', + 'group_ids': [(6, 0, [self.group.id])], + 'start_date': start, + 'pickup_day': pickup_day, + }) orders.append(order) # All should have same pickup_date @@ -341,229 +307,5 @@ class TestDateCalculations(TransactionCase): self.assertEqual( len(set(pickup_dates)), 1, - "All orders with same start_date and pickup_day should have same pickup_date", + "All orders with same start_date and pickup_day should have same pickup_date" ) - - # === NEW REGRESSION TESTS (v18.0.1.3.1) === - - def test_cutoff_same_day_as_today_bug_fix(self): - """Regression test: cutoff_date should allow same day as today. - - Bug fixed in v18.0.1.3.1: Previously, if cutoff_day == today.weekday(), - the system would incorrectly add 7 days, scheduling cutoff for next week. - Now cutoff_date can be today if cutoff_day matches today's weekday. - """ - today = fields.Date.today() - cutoff_day = str(today.weekday()) # Same as today - - order = self.env["group.order"].create( - { - "name": "Test Cutoff Today", - "group_ids": [(6, 0, [self.group.id])], - "start_date": today, - "cutoff_day": cutoff_day, - "period": "weekly", - } - ) - - # cutoff_date should be TODAY, not next week - self.assertEqual( - order.cutoff_date, - today, - f"Expected cutoff_date={today} (today), got {order.cutoff_date}. " - "Cutoff should be allowed on the same day.", - ) - - def test_delivery_date_stored_correctly(self): - """Regression test: delivery_date must be stored in database. - - Bug fixed in v18.0.1.3.1: delivery_date had store=False, causing - inconsistent values and inability to search/filter by this field. - Now delivery_date is stored (store=True). - """ - today = fields.Date.today() - # Set pickup for next Monday - days_until_monday = (0 - today.weekday()) % 7 - if days_until_monday == 0: - days_until_monday = 7 - start_date = today + timedelta(days=days_until_monday - 1) - - order = self.env["group.order"].create( - { - "name": "Test Delivery Stored", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, - "pickup_day": "0", # Monday - "period": "weekly", - } - ) - - # Force computation - order._compute_pickup_date() - order._compute_delivery_date() - - expected_delivery = order.pickup_date + timedelta(days=1) - self.assertEqual( - order.delivery_date, - expected_delivery, - f"Expected delivery_date={expected_delivery}, got {order.delivery_date}", - ) - - # Verify it's stored: read from database - order_from_db = self.env["group.order"].browse(order.id) - self.assertEqual( - order_from_db.delivery_date, - expected_delivery, - "delivery_date should be persisted in database (store=True)", - ) - - def test_constraint_cutoff_before_pickup_invalid(self): - """Test constraint: pickup_day must be >= cutoff_day for weekly orders. - - New constraint in v18.0.1.3.1: For weekly orders, if pickup_day < cutoff_day - numerically, it creates an illogical scenario where pickup would be - scheduled before cutoff in the same week cycle. - """ - today = fields.Date.today() - - # Invalid configuration: pickup (Tuesday=1) < cutoff (Thursday=3) - with self.assertRaises( - ValidationError, - msg="Should raise ValidationError for pickup_day < cutoff_day", - ): - self.env["group.order"].create( - { - "name": "Invalid Order", - "group_ids": [(6, 0, [self.group.id])], - "start_date": today, - "cutoff_day": "3", # Thursday - "pickup_day": "1", # Tuesday (BEFORE Thursday) - "period": "weekly", - } - ) - - def test_constraint_cutoff_before_pickup_valid(self): - """Test constraint allows valid configurations. - - Valid scenarios: - - pickup_day > cutoff_day: pickup is after cutoff ✓ - - pickup_day == cutoff_day: same day allowed ✓ - """ - today = fields.Date.today() - - # Valid: pickup (Saturday=5) > cutoff (Tuesday=1) - order1 = self.env["group.order"].create( - { - "name": "Valid Order 1", - "group_ids": [(6, 0, [self.group.id])], - "start_date": today, - "cutoff_day": "1", # Tuesday - "pickup_day": "5", # Saturday (AFTER Tuesday) - "period": "weekly", - } - ) - self.assertTrue(order1.id, "Valid configuration should create order") - - # Valid: pickup == cutoff (same day) - order2 = self.env["group.order"].create( - { - "name": "Valid Order 2", - "group_ids": [(6, 0, [self.group.id])], - "start_date": today, - "cutoff_day": "5", # Saturday - "pickup_day": "5", # Saturday (SAME DAY) - "period": "weekly", - } - ) - self.assertTrue(order2.id, "Same day configuration should be allowed") - - def test_all_weekday_combinations_consistency(self): - """Test that all valid weekday combinations produce consistent results. - - This regression test ensures the date calculation logic works correctly - for all 49 combinations of start_date × pickup_day (7 × 7). - """ - today = fields.Date.today() - errors = [] - - for start_offset in range(7): # 7 possible start days - start_date = today + timedelta(days=start_offset) - - for pickup_weekday in range(7): # 7 possible pickup days - order = self.env["group.order"].create( - { - "name": f"Test S{start_offset}P{pickup_weekday}", - "group_ids": [(6, 0, [self.group.id])], - "start_date": start_date, - "pickup_day": str(pickup_weekday), - "period": "weekly", - } - ) - - # Validate pickup_date is set - if not order.pickup_date: - errors.append( - f"start_offset={start_offset}, pickup_day={pickup_weekday}: " - f"pickup_date is None" - ) - continue - - # Validate pickup_date is in the future or today - if order.pickup_date < start_date: - errors.append( - f"start_offset={start_offset}, pickup_day={pickup_weekday}: " - f"pickup_date {order.pickup_date} < start_date {start_date}" - ) - - # Validate pickup_date weekday matches pickup_day - if order.pickup_date.weekday() != pickup_weekday: - errors.append( - f"start_offset={start_offset}, pickup_day={pickup_weekday}: " - f"pickup_date weekday is {order.pickup_date.weekday()}, " - f"expected {pickup_weekday}" - ) - - self.assertEqual( - len(errors), - 0, - f"Found {len(errors)} errors in weekday combinations:\n" - + "\n".join(errors), - ) - - def test_cron_update_dates_executes(self): - """Test that cron job method executes without errors. - - New feature in v18.0.1.3.1: Daily cron job to recalculate dates - for active orders to keep them up-to-date as time passes. - """ - today = fields.Date.today() - - # Create multiple orders in different states - orders = [] - for i, state in enumerate(["draft", "open", "closed"]): - order = self.env["group.order"].create( - { - "name": f"Test Cron Order {state}", - "group_ids": [(6, 0, [self.group.id])], - "start_date": today, - "pickup_day": str((i + 1) % 7), - "cutoff_day": str(i % 7), - "period": "weekly", - "state": state, - } - ) - orders.append(order) - - # Execute cron method (should not raise errors) - try: - self.env["group.order"]._cron_update_dates() - except Exception as e: - self.fail(f"Cron method raised exception: {e}") - - # Verify dates are still valid for active orders - active_orders = [o for o in orders if o.state in ["draft", "open"]] - for order in active_orders: - self.assertIsNotNone( - order.pickup_date, - f"Pickup date should be set for active order {order.name}", - ) diff --git a/website_sale_aplicoop/tests/test_draft_persistence.py b/website_sale_aplicoop/tests/test_draft_persistence.py index e862e9c..44563ba 100644 --- a/website_sale_aplicoop/tests/test_draft_persistence.py +++ b/website_sale_aplicoop/tests/test_draft_persistence.py @@ -13,8 +13,7 @@ Coverage: - Draft timeline (very old draft, recent draft) """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from odoo.tests.common import TransactionCase @@ -24,117 +23,91 @@ 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()) @@ -143,23 +116,15 @@ 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 @@ -167,33 +132,29 @@ 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) @@ -203,83 +164,63 @@ 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) @@ -287,37 +228,29 @@ 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) @@ -327,16 +260,14 @@ 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 @@ -346,51 +277,41 @@ 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): @@ -398,24 +319,16 @@ 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 @@ -429,26 +342,18 @@ 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) @@ -457,80 +362,62 @@ 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 @@ -540,128 +427,108 @@ 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 bcacf48..53d7d8c 100644 --- a/website_sale_aplicoop/tests/test_edge_cases.py +++ b/website_sale_aplicoop/tests/test_edge_cases.py @@ -13,13 +13,11 @@ Coverage: - Extreme dates (year 1900, year 2099) """ -from datetime import date -from datetime import timedelta - +from datetime import datetime, timedelta, date from dateutil.relativedelta import relativedelta -from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError class TestLeapYearHandling(TransactionCase): @@ -27,12 +25,10 @@ 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).""" @@ -40,18 +36,16 @@ 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 @@ -63,18 +57,16 @@ 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)) @@ -84,18 +76,16 @@ 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) @@ -107,30 +97,26 @@ 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 @@ -142,18 +128,16 @@ 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 @@ -163,18 +147,16 @@ 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()) @@ -184,12 +166,10 @@ 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).""" @@ -197,18 +177,16 @@ 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 @@ -220,18 +198,16 @@ 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()) @@ -241,18 +217,16 @@ 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 @@ -264,18 +238,16 @@ 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 @@ -287,12 +259,10 @@ 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.""" @@ -300,18 +270,16 @@ 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) @@ -322,18 +290,16 @@ 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()) @@ -343,30 +309,26 @@ 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()) @@ -375,18 +337,16 @@ 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()) @@ -395,18 +355,16 @@ 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 @@ -419,29 +377,25 @@ 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 @@ -452,12 +406,10 @@ 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.""" @@ -465,18 +417,16 @@ 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 @@ -488,18 +438,16 @@ 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 d7a7e50..1c2c1ab 100644 --- a/website_sale_aplicoop/tests/test_endpoints.py +++ b/website_sale_aplicoop/tests/test_endpoints.py @@ -16,13 +16,11 @@ Coverage: - /eskaera/labels (GET) - Get translated labels """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta +import json -from odoo.exceptions import AccessError -from odoo.exceptions import ValidationError -from odoo.tests.common import HttpCase -from odoo.tests.common import TransactionCase +from odoo.tests.common import TransactionCase, HttpCase +from odoo.exceptions import ValidationError, AccessError class TestEskaearaListEndpoint(TransactionCase): @@ -30,75 +28,63 @@ 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() @@ -106,12 +92,10 @@ 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) @@ -119,36 +103,30 @@ 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) @@ -158,75 +136,61 @@ 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)] @@ -234,13 +198,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.""" @@ -264,13 +228,11 @@ 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) @@ -279,7 +241,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): @@ -287,46 +249,38 @@ 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): @@ -347,18 +301,16 @@ 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 @@ -370,115 +322,95 @@ 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) @@ -489,94 +421,76 @@ 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) @@ -586,28 +500,24 @@ 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 f46dfbe..88b0ae9 100644 --- a/website_sale_aplicoop/tests/test_eskaera_shop.py +++ b/website_sale_aplicoop/tests/test_eskaera_shop.py @@ -1,158 +1,127 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime -from datetime import timedelta +from datetime import datetime, 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() @@ -162,124 +131,96 @@ 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() @@ -288,122 +229,94 @@ 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 ec8b502..4b71869 100644 --- a/website_sale_aplicoop/tests/test_group_order.py +++ b/website_sale_aplicoop/tests/test_group_order.py @@ -1,354 +1,310 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta -from psycopg2 import IntegrityError - -from odoo import fields -from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from psycopg2 import IntegrityError +from odoo import fields 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 c503812..de20bb5 100644 --- a/website_sale_aplicoop/tests/test_multi_company.py +++ b/website_sale_aplicoop/tests/test_multi_company.py @@ -1,178 +1,147 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta -from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError 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) @@ -180,28 +149,24 @@ 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 e5050a9..be8e8af 100644 --- a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py +++ b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py @@ -13,166 +13,131 @@ Coverage: - Product price info structure in eskaera_shop """ -from odoo.tests import tagged +import json from odoo.tests.common import TransactionCase +from odoo.tests import tagged -@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.""" @@ -182,15 +147,11 @@ 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.""" @@ -200,18 +161,14 @@ 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%).""" @@ -221,19 +178,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.""" @@ -243,53 +200,43 @@ 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.""" @@ -300,39 +247,35 @@ 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.""" @@ -341,19 +284,18 @@ 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.""" @@ -362,83 +304,71 @@ 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.""" @@ -447,37 +377,33 @@ 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 cd27b05..45a8261 100644 --- a/website_sale_aplicoop/tests/test_product_discovery.py +++ b/website_sale_aplicoop/tests/test_product_discovery.py @@ -17,8 +17,7 @@ Coverage: - Ordering and deduplication """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from odoo.tests.common import TransactionCase @@ -28,105 +27,81 @@ 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.""" @@ -170,16 +145,14 @@ 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 @@ -189,16 +162,14 @@ 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 @@ -212,96 +183,76 @@ 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.""" @@ -356,41 +307,33 @@ 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.""" @@ -428,47 +371,39 @@ 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.""" @@ -478,7 +413,10 @@ 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.""" @@ -489,6 +427,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 = {p.id for p in discovered} - expected_ids = {p.id for p in self.products} + discovered_ids = set(p.id for p in discovered) + expected_ids = set(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 2c3d0c8..f8ed4d9 100644 --- a/website_sale_aplicoop/tests/test_product_extension.py +++ b/website_sale_aplicoop/tests/test_product_extension.py @@ -1,106 +1,91 @@ # 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().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(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)] + }) 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 e8be191..e3f1e81 100644 --- a/website_sale_aplicoop/tests/test_record_rules.py +++ b/website_sale_aplicoop/tests/test_record_rules.py @@ -1,170 +1,145 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta -from odoo.exceptions import AccessError from odoo.tests.common import TransactionCase +from odoo.exceptions import AccessError 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 5d0bf03..2070ec7 100644 --- a/website_sale_aplicoop/tests/test_res_partner.py +++ b/website_sale_aplicoop/tests/test_res_partner.py @@ -5,40 +5,34 @@ 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])] @@ -48,14 +42,12 @@ 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])] @@ -66,28 +58,26 @@ 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 4e975fe..fe446a7 100644 --- a/website_sale_aplicoop/tests/test_save_order_endpoints.py +++ b/website_sale_aplicoop/tests/test_save_order_endpoints.py @@ -11,8 +11,7 @@ draft sale orders. See: website_sale_aplicoop/controllers/website_sale.py """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from odoo.tests.common import TransactionCase @@ -24,72 +23,60 @@ 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)] @@ -97,22 +84,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) @@ -122,34 +109,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) @@ -157,35 +144,33 @@ 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) @@ -194,65 +179,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) @@ -262,18 +247,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) @@ -282,64 +267,62 @@ 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 5389f70..1998c9f 100644 --- a/website_sale_aplicoop/tests/test_templates_rendering.py +++ b/website_sale_aplicoop/tests/test_templates_rendering.py @@ -1,90 +1,77 @@ # Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import date -from datetime import timedelta - +from datetime import date, timedelta +from odoo.tests.common import TransactionCase, tagged 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) @@ -99,61 +86,45 @@ 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() @@ -293,36 +261,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): @@ -330,37 +298,31 @@ 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/group_order_views.xml b/website_sale_aplicoop/views/group_order_views.xml index ed87af4..c7d9946 100644 --- a/website_sale_aplicoop/views/group_order_views.xml +++ b/website_sale_aplicoop/views/group_order_views.xml @@ -63,15 +63,6 @@ - - - - - - - - - diff --git a/website_sale_aplicoop/views/load_from_history_templates.xml b/website_sale_aplicoop/views/load_from_history_templates.xml index 1f254df..f664934 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 10d1f77..9532ff5 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 @@