diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c992aaa..a2b7ab5 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -50,6 +50,11 @@ jobs: # este repo (ecocentral depende de él): se prueba el checkout del PR, # no ese clon. ODOO_SRC_SKIP: addons-cm + # Se prueban cada uno en su propia base de datos: instalados junto + # al resto les cambian el comportamiento. website_sale_disable_cart + # quita el carrito a todos los sitios web y rompe, por ejemplo, el + # alta con cuota de website_membership_signup_required. + ISOLATE: website_sale_disable_cart # Si los tests pasan, deja aquí el .pot de cada addon para el job review. # Está en la caché que el runner monta en todos los jobs. BOT_POT_DIR: /root/.cache/eco-pr-bot/pot 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..d4a5e1e 100644 --- a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py +++ b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py @@ -7,6 +7,16 @@ from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestPurchaseOrder(TransactionCase): + """Purchase lines and the vendor bills created from them. + + In 18.0 ``purchase.order.line`` does not use ``triple.discount.mixin`` but + ``purchase.triple.discount.mixin`` (purchase_triple_discount), where the + total discount is a stored compute of discount1..3: the accumulation bug + this addon fixes does not happen there. What matters on the purchase side + is that the three discounts reach the bill untouched, and the bill lines do + use the mixin this addon patches. + """ + @classmethod def setUpClass(cls): super().setUpClass() @@ -20,13 +30,15 @@ class TestPurchaseOrder(TransactionCase): } ) - # Create a product template first, then get the variant + # Create a product template first, then get the variant. Billed on + # ordered quantities, so the bill can be created without a receipt. cls.product_template = cls.env["product.template"].create( { "name": "Test Product PO", "type": "consu", "list_price": 150.0, "standard_price": 80.0, + "purchase_method": "purchase", } ) # Get the auto-created product variant @@ -52,16 +64,24 @@ class TestPurchaseOrder(TransactionCase): } ) - def test_po_line_discount_readonly(self): - """Test that discount field is readonly in PO lines""" + def _create_bill_line(self): + self.purchase_order.button_confirm() + self.purchase_order.action_create_invoice() + return self.purchase_order.invoice_ids.invoice_line_ids.filtered( + lambda line: line.product_id == self.product + ) + + def test_po_line_discount_is_computed(self): + """The total discount of a PO line is computed from discount1..3""" field = self.po_line._fields["discount"] - self.assertTrue(field.readonly, "Discount field should be readonly in PO lines") + self.assertTrue(field.compute) + expected = 100 - (100 * 0.90 * 0.95 * 0.98) + self.assertAlmostEqual(self.po_line.discount, expected, places=2) 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, @@ -71,22 +91,11 @@ class TestPurchaseOrder(TransactionCase): 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, - } - ) - - # Should map to discount1 and reset others - self.assertEqual(self.po_line.discount1, 18.0) - self.assertEqual(self.po_line.discount2, 0.0) - self.assertEqual(self.po_line.discount3, 0.0) + expected = 100 - (100 * 0.88 * 0.92 * 0.96) + self.assertAlmostEqual(self.po_line.discount, expected, places=2) def test_po_line_price_calculation(self): - """Test that price subtotal is calculated correctly with triple discount""" + """The PO subtotal applies the total discount""" self.po_line.write( { "discount1": 15.0, @@ -95,11 +104,9 @@ class TestPurchaseOrder(TransactionCase): } ) - # 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 + # The PO line keeps the total discount with the "Discount" precision + # (27.33 %, not 27.3325 %), so the subtotal is computed from it. + expected_subtotal = 10 * 150 * (1 - self.po_line.discount / 100) self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2) def test_multiple_po_lines(self): @@ -117,7 +124,7 @@ class TestPurchaseOrder(TransactionCase): ) # Verify both lines have correct discounts - self.assertEqual(self.po_line.discount1, 15.0) + self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(line2.discount1, 20.0) self.assertEqual(line2.discount2, 15.0) self.assertEqual(line2.discount3, 10.0) @@ -174,40 +181,8 @@ class TestPurchaseOrder(TransactionCase): expected = 10 * 150 self.assertEqual(self.po_line.price_subtotal, expected) - 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, - } - ) - - # 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) - 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, - } - ) - # Confirm the purchase order self.purchase_order.button_confirm() @@ -215,3 +190,25 @@ class TestPurchaseOrder(TransactionCase): self.assertEqual(self.po_line.discount1, 10.0) self.assertEqual(self.po_line.discount2, 5.0) self.assertEqual(self.po_line.discount3, 2.0) + + def test_bill_keeps_the_three_discounts(self): + """The bill line gets discount1..3 of the PO line, not their sum""" + bill_line = self._create_bill_line() + + self.assertEqual(len(bill_line), 1) + self.assertEqual(bill_line.discount1, 10.0) + self.assertEqual(bill_line.discount2, 5.0) + self.assertEqual(bill_line.discount3, 2.0) + # The bill line computes the total discount without rounding it. + expected = 10 * 150 * 0.90 * 0.95 * 0.98 + self.assertAlmostEqual(bill_line.price_subtotal, expected, places=2) + + def test_bill_line_edit_keeps_the_other_discounts(self): + """Editing one discount on the bill keeps the other two""" + bill_line = self._create_bill_line() + + bill_line.write({"discount": 50.0, "discount2": 12.0}) + + self.assertEqual(bill_line.discount1, 10.0) + self.assertEqual(bill_line.discount2, 12.0) + self.assertEqual(bill_line.discount3, 2.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..4e3a48f 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 @@ -7,6 +7,11 @@ from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestTripleDiscountMixin(TransactionCase): + """The patched ``triple.discount.mixin``, through an invoice line. + + Purchase lines do not use this mixin in 18.0 (see test_purchase_order). + """ + @classmethod def setUpClass(cls): super().setUpClass() @@ -30,19 +35,21 @@ class TestTripleDiscountMixin(TransactionCase): # Get the auto-created product variant cls.product = cls.product_template.product_variant_ids[0] - # Create a purchase order - cls.purchase_order = cls.env["purchase.order"].create( + # Create an invoice + cls.invoice = cls.env["account.move"].create( { + "move_type": "out_invoice", "partner_id": cls.partner.id, + "invoice_date": "2026-01-01", } ) - # Create a purchase order line - cls.po_line = cls.env["purchase.order.line"].create( + # Create an invoice line + cls.line = cls.env["account.move.line"].create( { - "order_id": cls.purchase_order.id, + "move_id": cls.invoice.id, "product_id": cls.product.id, - "product_qty": 10, + "quantity": 10, "price_unit": 100.0, "discount1": 10.0, "discount2": 5.0, @@ -52,13 +59,13 @@ class TestTripleDiscountMixin(TransactionCase): def test_discount_field_is_readonly(self): """Test that the discount field is readonly""" - field = self.po_line._fields["discount"] + field = self.line._fields["discount"] self.assertTrue(field.readonly, "Discount field should be readonly") def test_write_with_explicit_discounts(self): """Test writing with explicit discount1, discount2, discount3""" # Write with explicit discounts - self.po_line.write( + self.line.write( { "discount": 20.0, # This should be ignored "discount1": 15.0, @@ -68,18 +75,18 @@ class TestTripleDiscountMixin(TransactionCase): ) # 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) + self.assertEqual(self.line.discount1, 15.0) + self.assertEqual(self.line.discount2, 10.0) + self.assertEqual(self.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) - self.assertAlmostEqual(self.po_line.discount, expected_discount, places=2) + self.assertAlmostEqual(self.line.discount, expected_discount, places=2) def test_write_only_discount1(self): """Test writing only discount1 explicitly""" - self.po_line.write( + self.line.write( { "discount": 25.0, # This should be ignored "discount1": 20.0, @@ -87,45 +94,43 @@ class TestTripleDiscountMixin(TransactionCase): ) # Only discount1 should change - self.assertEqual(self.po_line.discount1, 20.0) + self.assertEqual(self.line.discount1, 20.0) # Others should remain unchanged - self.assertEqual(self.po_line.discount2, 5.0) - self.assertEqual(self.po_line.discount3, 2.0) + self.assertEqual(self.line.discount2, 5.0) + self.assertEqual(self.line.discount3, 2.0) def test_write_only_discount2(self): """Test writing only discount2 explicitly""" - self.po_line.write( + self.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 - self.assertEqual(self.po_line.discount1, 20.0) - self.assertEqual(self.po_line.discount3, 2.0) + # Only discount2 should change; each test starts from setUpClass + self.assertEqual(self.line.discount2, 12.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount3, 2.0) def test_write_only_discount3(self): """Test writing only discount3 explicitly""" - self.po_line.write( + self.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 - self.assertEqual(self.po_line.discount1, 20.0) - self.assertEqual(self.po_line.discount2, 12.0) + # Only discount3 should change; each test starts from setUpClass + self.assertEqual(self.line.discount3, 8.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount2, 5.0) def test_write_legacy_discount_only(self): """Test legacy behavior: writing only discount field""" # Reset to known state first - self.po_line.write( + self.line.write( { "discount1": 10.0, "discount2": 5.0, @@ -134,32 +139,32 @@ class TestTripleDiscountMixin(TransactionCase): ) # Write only discount (legacy behavior) - self.po_line.write( + self.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) - self.assertEqual(self.po_line.discount3, 0.0) + self.assertEqual(self.line.discount1, 25.0) + self.assertEqual(self.line.discount2, 0.0) + self.assertEqual(self.line.discount3, 0.0) def test_write_multiple_times(self): """Test writing multiple times to ensure consistency""" # First write - self.po_line.write( + self.line.write( { "discount1": 10.0, "discount2": 10.0, } ) - self.assertEqual(self.po_line.discount1, 10.0) - self.assertEqual(self.po_line.discount2, 10.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount2, 10.0) # Second write - self.po_line.write( + self.line.write( { "discount": 5.0, "discount3": 5.0, @@ -167,13 +172,13 @@ class TestTripleDiscountMixin(TransactionCase): ) # discount3 should change, others remain - self.assertEqual(self.po_line.discount1, 10.0) - self.assertEqual(self.po_line.discount2, 10.0) - self.assertEqual(self.po_line.discount3, 5.0) + self.assertEqual(self.line.discount1, 10.0) + self.assertEqual(self.line.discount2, 10.0) + self.assertEqual(self.line.discount3, 5.0) def test_write_zero_discounts(self): """Test writing zero discounts""" - self.po_line.write( + self.line.write( { "discount1": 0.0, "discount2": 0.0, @@ -181,15 +186,15 @@ class TestTripleDiscountMixin(TransactionCase): } ) - self.assertEqual(self.po_line.discount1, 0.0) - self.assertEqual(self.po_line.discount2, 0.0) - self.assertEqual(self.po_line.discount3, 0.0) - self.assertEqual(self.po_line.discount, 0.0) + self.assertEqual(self.line.discount1, 0.0) + self.assertEqual(self.line.discount2, 0.0) + self.assertEqual(self.line.discount3, 0.0) + self.assertEqual(self.line.discount, 0.0) def test_write_combined_scenario(self): """Test a realistic combined scenario""" # Initial state - self.po_line.write( + self.line.write( { "discount1": 15.0, "discount2": 5.0, @@ -198,7 +203,7 @@ class TestTripleDiscountMixin(TransactionCase): ) # User tries to update discount field (should be ignored if explicit discounts present) - self.po_line.write( + self.line.write( { "discount": 50.0, "discount1": 20.0, @@ -206,13 +211,13 @@ class TestTripleDiscountMixin(TransactionCase): ) # discount1 should be updated, others unchanged - self.assertEqual(self.po_line.discount1, 20.0) - self.assertEqual(self.po_line.discount2, 5.0) - self.assertEqual(self.po_line.discount3, 0.0) + self.assertEqual(self.line.discount1, 20.0) + self.assertEqual(self.line.discount2, 5.0) + self.assertEqual(self.line.discount3, 0.0) def test_discount_calculation_accuracy(self): """Test that discount calculation is accurate""" - self.po_line.write( + self.line.write( { "discount1": 10.0, "discount2": 10.0, @@ -222,22 +227,22 @@ class TestTripleDiscountMixin(TransactionCase): # 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) + self.assertAlmostEqual(self.line.discount, expected, places=2) def test_write_without_discount_field(self): """Test writing other fields without touching discount fields""" - initial_discount1 = self.po_line.discount1 + initial_discount1 = self.line.discount1 # Write other fields - self.po_line.write( + self.line.write( { - "product_qty": 20, + "quantity": 20, "price_unit": 150.0, } ) # Discounts should remain unchanged - self.assertEqual(self.po_line.discount1, initial_discount1) + self.assertEqual(self.line.discount1, initial_discount1) # But other fields should be updated - self.assertEqual(self.po_line.product_qty, 20) - self.assertEqual(self.po_line.price_unit, 150.0) + self.assertEqual(self.line.quantity, 20) + self.assertEqual(self.line.price_unit, 150.0) diff --git a/tools/ci/run_tests.py b/tools/ci/run_tests.py index 638f78c..a52a647 100755 --- a/tools/ci/run_tests.py +++ b/tools/ci/run_tests.py @@ -8,7 +8,12 @@ los del core ni los de la OCA. tools/ci/run_tests.py [addon ...] Sin argumentos prueba todos los addons instalables del repo menos los de -EXCLUDE. Odoo y los repos de los que dependen los addons se toman de ODOO_SRC +EXCLUDE. Los de ISOLATE se prueban cada uno en su propia base de datos, después +del resto: son los que cambian el comportamiento de otros con solo estar +instalados (website_sale_disable_cart quita el carrito a todos los sitios web), +y el repo lo comparten varios clientes que no los instalan juntos. + +Odoo y los repos de los que dependen los addons se toman de ODOO_SRC (un checkout por carpeta; en el runner es un montaje, ver tools/issue_bot/README.md), menos las carpetas de ODOO_SRC_SKIP, y la conexión a PostgreSQL, de las variables PG*. @@ -33,7 +38,6 @@ from pathlib import Path REPO = Path(__file__).resolve().parents[2] ODOO_SRC = Path(os.environ.get("ODOO_SRC", "/odoo-src/18.0")) -SKIP = set(os.environ.get("ODOO_SRC_SKIP", "").replace(",", " ").split()) DB = os.environ.get("PGDATABASE", "test") POT_DIR = os.environ.get("BOT_POT_DIR") POT_KEEP_DAYS = 14 @@ -54,8 +58,9 @@ _logger = logging.getLogger("ci") def find_sources(): """Devuelve odoo-bin y el addons_path: el repo y lo que haya en ODOO_SRC.""" odoo_bin, paths = None, [REPO] + skip = env_list("ODOO_SRC_SKIP") for src in sorted(ODOO_SRC.iterdir()): - if src.name in SKIP: + if src.name in skip: _logger.info("Se omite %s (ODOO_SRC_SKIP)", src) elif (src / "odoo-bin").is_file(): odoo_bin = src / "odoo-bin" @@ -67,6 +72,10 @@ def find_sources(): return odoo_bin, paths +def env_list(name): + return set(os.environ.get(name, "").replace(",", " ").split()) + + def read_manifests(paths): manifests = {} for path in paths: @@ -101,12 +110,12 @@ def wait_for_postgres(timeout=60): time.sleep(1) -def odoo(odoo_bin, paths, *args): +def odoo(odoo_bin, paths, db, *args): cmd = [ sys.executable, str(odoo_bin), f"--addons-path={','.join(map(str, paths))}", - f"--database={DB}", + f"--database={db}", # Con --test-enable Odoo levanta su servidor HTTP para los tours, y # el runner lanza el job en la red del host: sin esto escucharía en # todas las interfaces de la máquina. Los tests lo buscan en @@ -119,8 +128,11 @@ def odoo(odoo_bin, paths, *args): return subprocess.run(cmd, check=False).returncode -def export_pots(odoo_bin, paths, addons): - """Deja en BOT_POT_DIR/ el .pot de cada addon ya instalado.""" +def export_pots(odoo_bin, paths, runs): + """Deja en BOT_POT_DIR/ el .pot de cada addon ya instalado. + + `runs` es una lista de (base de datos, addons instalados en ella). + """ pot_dir = Path(POT_DIR) head = subprocess.run( ["git", "rev-parse", "HEAD"], @@ -136,21 +148,22 @@ def export_pots(odoo_bin, paths, addons): for stale in (out, tmp): shutil.rmtree(stale, ignore_errors=True) tmp.mkdir(parents=True) - cmd = [ - sys.executable, - str(odoo_bin), - "shell", - f"--addons-path={','.join(map(str, paths))}", - f"--database={DB}", - "--no-http", - "--log-level=warn", - ] _logger.info("Exportando los .pot a %s", out) - script = EXPORT_POTS.format(addons=addons, out=str(tmp)) - if subprocess.run(cmd, input=script, text=True, check=False).returncode: - _logger.warning("No se han podido exportar los .pot") - shutil.rmtree(tmp, ignore_errors=True) - return + for db, addons in runs: + cmd = [ + sys.executable, + str(odoo_bin), + "shell", + f"--addons-path={','.join(map(str, paths))}", + f"--database={db}", + "--no-http", + "--log-level=warn", + ] + script = EXPORT_POTS.format(addons=addons, out=str(tmp)) + if subprocess.run(cmd, input=script, text=True, check=False).returncode: + _logger.warning("No se han podido exportar los .pot") + shutil.rmtree(tmp, ignore_errors=True) + return tmp.rename(out) limit = time.time() - POT_KEEP_DAYS * 86400 for old in pot_dir.iterdir(): @@ -158,12 +171,27 @@ def export_pots(odoo_bin, paths, addons): shutil.rmtree(old, ignore_errors=True) +def run_tests(odoo_bin, paths, manifests, db, addons): + """Crea `db` con las dependencias de `addons` y los instala con sus tests.""" + dependencies = sorted( + {dep for addon in addons for dep in manifests[addon].get("depends", [])} + - set(addons) + ) + subprocess.run(["dropdb", "--if-exists", db], check=True) + _logger.info("Instalando las dependencias: %s", ", ".join(dependencies)) + init = ["--init", ",".join(dependencies), "--log-level=warn"] + if odoo(odoo_bin, paths, db, *init): + sys.exit("No se ha podido crear la base de datos con las dependencias") + _logger.info("Probando: %s", ", ".join(addons)) + return odoo(odoo_bin, paths, db, "--init", ",".join(addons), "--test-enable") + + def main(): logging.basicConfig(level=logging.INFO, format="%(message)s") odoo_bin, paths = find_sources() manifests = read_manifests(paths) repo_addons = sorted(m.parent.name for m in REPO.glob("*/__manifest__.py")) - exclude = set(os.environ.get("EXCLUDE", "").replace(",", " ").split()) + exclude = env_list("EXCLUDE") addons = sys.argv[1:] or [ addon for addon in repo_addons @@ -189,27 +217,25 @@ def main(): "(tools/issue_bot/runner/ en el repo de ecocentral) o el addon a " "EXCLUDE." ) - dependencies = sorted( - {dep for addon in addons for dep in manifests[addon].get("depends", [])} - - set(addons) - ) + isolate = env_list("ISOLATE") + together = [addon for addon in addons if addon not in isolate] + runs = [(DB, together)] if together else [] + runs += [(f"{DB}_{addon}", [addon]) for addon in addons if addon in isolate] wait_for_postgres() - subprocess.run(["dropdb", "--if-exists", DB], check=True) - _logger.info("Instalando las dependencias: %s", ", ".join(dependencies)) - init = ["--init", ",".join(dependencies), "--log-level=warn"] - if odoo(odoo_bin, paths, *init): - sys.exit("No se ha podido crear la base de datos con las dependencias") - _logger.info("Probando: %s", ", ".join(addons)) - result = odoo(odoo_bin, paths, "--init", ",".join(addons), "--test-enable") - if not result and POT_DIR: + failed = [] + for db, run_addons in runs: + if run_tests(odoo_bin, paths, manifests, db, run_addons): + failed.append(db) + if failed: + sys.exit(f"Fallan los tests en: {', '.join(failed)}") + if POT_DIR: # Que falte la caché no es un fallo de los tests: el bot de revisión # se queda sin las traducciones y lo dice. try: - export_pots(odoo_bin, paths, addons) + export_pots(odoo_bin, paths, runs) except OSError as e: _logger.warning("No se han podido exportar los .pot: %s", e) - sys.exit(result) if __name__ == "__main__": diff --git a/tools/issue_bot/README.md b/tools/issue_bot/README.md index 03104f9..5667a73 100644 --- a/tools/issue_bot/README.md +++ b/tools/issue_bot/README.md @@ -101,6 +101,12 @@ una base de datos con las dependencias de los addons y después instala los addons con `--test-enable`, de modo que solo corren los tests del repo. PostgreSQL es un servicio del job, en el puerto 54330 de `127.0.0.1`. +Los addons de `ISOLATE` (en `ci.yml`) se prueban cada uno en su propia base de +datos, después del resto: son los que con solo estar instalados cambian el +comportamiento de otros addons del repo, que es compartido por clientes que no +los instalan juntos. Por ejemplo, `website_sale_disable_cart` quita el carrito +a todos los sitios web. + Odoo arranca desde las fuentes montadas en `/odoo-src/18.0`, así que: - Todo repo del que dependa un addon de aquí tiene que estar en la parte diff --git a/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py b/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py index d578417..e70d4f6 100644 --- a/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py +++ b/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py @@ -23,7 +23,6 @@ from datetime import timedelta from types import SimpleNamespace from unittest.mock import patch -from odoo.exceptions import UserError from odoo.tests.common import TransactionCase from odoo.addons.website_sale_aplicoop.controllers.website_sale import ( @@ -34,6 +33,16 @@ REQUEST_PATCH_TARGET = ( "odoo.addons.website_sale_aplicoop.controllers.website_sale.request" ) +# Languages the tests put in the request env. A fresh database (the CI's) only +# has en_US active, and `env.lang` raises "Invalid language code" for the rest. +TEST_LANGS = ("es_ES", "eu_ES", "ca_ES", "gl_ES", "pt_PT", "fr_FR", "it_IT") + + +def _activate_test_langs(env): + """Activate TEST_LANGS: no translations are loaded, only the codes.""" + for lang in TEST_LANGS: + env["res.lang"]._activate_lang(lang) + def _env_with_lang(env, lang): """Return a cloned environment with language context set.""" @@ -59,9 +68,8 @@ def _build_request_mock(env, payload=None, website=None): """ if website is None: # `pricelist_id` is what the pricing helpers read now that Eskaera - # prices with the standard website pricelist. Ordered by id on - # purpose: the multilang cases build an env whose language is not - # installed, and the default order is by the translated `name`. + # prices with the standard website pricelist. Ordered by id so the + # pick does not depend on the translated `name` of each language. website = SimpleNamespace( pricelist_id=env["product.pricelist"] .sudo() @@ -364,6 +372,7 @@ class TestProcessCartItems(TransactionCase): def setUp(self): super().setUp() + _activate_test_langs(self.env) self.controller = AplicoopWebsiteSale() # Create test products @@ -525,6 +534,7 @@ class TestBuildConfirmationMessage(TransactionCase): def setUp(self): super().setUp() + _activate_test_langs(self.env) self.controller = AplicoopWebsiteSale() self.user = self.env.ref("base.user_admin") self.partner = self.env.ref("base.partner_admin") @@ -562,16 +572,11 @@ class TestBuildConfirmationMessage(TransactionCase): ): request_mock = _build_request_mock(_env_with_lang(self.env, lang)) with patch(REQUEST_PATCH_TARGET, request_mock): - try: - return self.controller._build_confirmation_message( - sale_order or self.sale_order, - group_order or self.group_order, - is_delivery=is_delivery, - ) - except UserError as err: - if "Invalid language code" in str(err): - self.skipTest(str(err)) - raise + return self.controller._build_confirmation_message( + sale_order or self.sale_order, + group_order or self.group_order, + is_delivery=is_delivery, + ) def test_build_confirmation_message_pickup(self): """Test confirmation message for pickup (not delivery).""" @@ -669,6 +674,7 @@ class TestConfirmEskaera_Integration(TransactionCase): def setUp(self): super().setUp() + _activate_test_langs(self.env) self.controller = AplicoopWebsiteSale() self.consumer_group = self.env["res.partner"].create( { diff --git a/website_sale_lazy_loading/tests/test_lazy_loading.py b/website_sale_lazy_loading/tests/test_lazy_loading.py index 47bea99..f56d571 100644 --- a/website_sale_lazy_loading/tests/test_lazy_loading.py +++ b/website_sale_lazy_loading/tests/test_lazy_loading.py @@ -25,11 +25,16 @@ class TestLazyLoading(HttpCase): cls.other_category = cls.env["product.public.category"].create( {"name": "Other Cat"} ) + # An explicit website_sequence keeps the shop order equal to the + # creation order: in a batch create every product gets the same default + # (MAX + 5, read before the insert) and the shop breaks the tie by + # `id desc`, which would list them backwards. cls.products = cls.env["product.template"].create( [ { "name": "Lazy Product %02d" % index, "list_price": 10.0 + index, + "website_sequence": index, "is_published": True, "public_categ_ids": [(6, 0, cls.category.ids)], }