Compare commits

..

4 commits

Author SHA1 Message Date
GitHub Copilot
87c2d1d1d3
[IMP] ci: test the ISOLATE addons in their own database
Some checks failed
CI / lint (pull_request) Successful in 31s
CI / test (pull_request) Failing after 11m29s
CI / review (pull_request) Has been skipped
website_sale_disable_cart removes the cart from every website once
installed, which breaks website_membership_signup_required when CI installs
everything together. The repo is shared by clients that never install them
together: run_tests.py now tests each ISOLATE addon in its own database,
after the rest, and ci.yml isolates website_sale_disable_cart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:04:56 +02:00
GitHub Copilot
ca526d55cc
[FIX] account_invoice_triple_discount_readonly: tests for 18.0 purchases
In 18.0 purchase.order.line uses purchase.triple.discount.mixin, not the
triple.discount.mixin this addon patches, so the purchase tests checked 16.0
behaviour. test_purchase_order now checks the 18.0 one plus what matters
here: the bill created from the PO keeps discount1..3, and editing one of
them on the bill keeps the others. test_triple_discount_mixin runs on an
invoice line, and no longer expects the values of the previous test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:04:54 +02:00
GitHub Copilot
9e05a279d7
[FIX] website_sale_lazy_loading: give the test products their shop order
Created in one batch, every product got the same website_sequence (MAX + 5,
read before the insert) and the shop breaks the tie by id desc, so the
listing came out backwards and the page assertions failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:04:53 +02:00
GitHub Copilot
1960f40301
[FIX] website_sale_aplicoop: activate the languages the tests use
The request env of the tests is in es_ES (and eu, ca, gl, pt, fr, it in the
multilang ones), but a fresh database such as the CI's only has en_US active
and env.lang raises "Invalid language code". The tests now activate them,
so the multilang cases run instead of being skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:04:50 +02:00
7 changed files with 215 additions and 165 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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/<commit> el .pot de cada addon ya instalado."""
def export_pots(odoo_bin, paths, runs):
"""Deja en BOT_POT_DIR/<commit> 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,16 +148,17 @@ def export_pots(odoo_bin, paths, addons):
for stale in (out, tmp):
shutil.rmtree(stale, ignore_errors=True)
tmp.mkdir(parents=True)
_logger.info("Exportando los .pot a %s", out)
for db, addons in runs:
cmd = [
sys.executable,
str(odoo_bin),
"shell",
f"--addons-path={','.join(map(str, paths))}",
f"--database={DB}",
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")
@ -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__":

View file

@ -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

View file

@ -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
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(
{

View file

@ -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)],
}