Compare commits
No commits in common. "87c2d1d1d366f6aa9b44c4b3b51d16f754a0d0ef" and "fbd43bb45954ae22daa65e4be5076a6ed2789c02" have entirely different histories.
87c2d1d1d3
...
fbd43bb459
7 changed files with 165 additions and 215 deletions
|
|
@ -50,11 +50,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -7,16 +7,6 @@ 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()
|
||||
|
|
@ -30,15 +20,13 @@ class TestPurchaseOrder(TransactionCase):
|
|||
}
|
||||
)
|
||||
|
||||
# Create a product template first, then get the variant. Billed on
|
||||
# ordered quantities, so the bill can be created without a receipt.
|
||||
# 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,
|
||||
"purchase_method": "purchase",
|
||||
}
|
||||
)
|
||||
# Get the auto-created product variant
|
||||
|
|
@ -64,24 +52,16 @@ class TestPurchaseOrder(TransactionCase):
|
|||
}
|
||||
)
|
||||
|
||||
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"""
|
||||
def test_po_line_discount_readonly(self):
|
||||
"""Test that discount field is readonly in PO lines"""
|
||||
field = self.po_line._fields["discount"]
|
||||
self.assertTrue(field.compute)
|
||||
expected = 100 - (100 * 0.90 * 0.95 * 0.98)
|
||||
self.assertAlmostEqual(self.po_line.discount, expected, places=2)
|
||||
self.assertTrue(field.readonly, "Discount field should be readonly in PO lines")
|
||||
|
||||
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,
|
||||
|
|
@ -91,11 +71,22 @@ 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)
|
||||
expected = 100 - (100 * 0.88 * 0.92 * 0.96)
|
||||
self.assertAlmostEqual(self.po_line.discount, expected, places=2)
|
||||
|
||||
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)
|
||||
|
||||
def test_po_line_price_calculation(self):
|
||||
"""The PO subtotal applies the total discount"""
|
||||
"""Test that price subtotal is calculated correctly with triple discount"""
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 15.0,
|
||||
|
|
@ -104,9 +95,11 @@ class TestPurchaseOrder(TransactionCase):
|
|||
}
|
||||
)
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
def test_multiple_po_lines(self):
|
||||
|
|
@ -124,7 +117,7 @@ class TestPurchaseOrder(TransactionCase):
|
|||
)
|
||||
|
||||
# Verify both lines have correct discounts
|
||||
self.assertEqual(self.po_line.discount1, 10.0)
|
||||
self.assertEqual(self.po_line.discount1, 15.0)
|
||||
self.assertEqual(line2.discount1, 20.0)
|
||||
self.assertEqual(line2.discount2, 15.0)
|
||||
self.assertEqual(line2.discount3, 10.0)
|
||||
|
|
@ -181,8 +174,40 @@ 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()
|
||||
|
||||
|
|
@ -190,25 +215,3 @@ 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)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,6 @@ 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()
|
||||
|
|
@ -35,21 +30,19 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
# Get the auto-created product variant
|
||||
cls.product = cls.product_template.product_variant_ids[0]
|
||||
|
||||
# Create an invoice
|
||||
cls.invoice = cls.env["account.move"].create(
|
||||
# Create a purchase order
|
||||
cls.purchase_order = cls.env["purchase.order"].create(
|
||||
{
|
||||
"move_type": "out_invoice",
|
||||
"partner_id": cls.partner.id,
|
||||
"invoice_date": "2026-01-01",
|
||||
}
|
||||
)
|
||||
|
||||
# Create an invoice line
|
||||
cls.line = cls.env["account.move.line"].create(
|
||||
# Create a purchase order line
|
||||
cls.po_line = cls.env["purchase.order.line"].create(
|
||||
{
|
||||
"move_id": cls.invoice.id,
|
||||
"order_id": cls.purchase_order.id,
|
||||
"product_id": cls.product.id,
|
||||
"quantity": 10,
|
||||
"product_qty": 10,
|
||||
"price_unit": 100.0,
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
|
|
@ -59,13 +52,13 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
|
||||
def test_discount_field_is_readonly(self):
|
||||
"""Test that the discount field is readonly"""
|
||||
field = self.line._fields["discount"]
|
||||
field = self.po_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.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 20.0, # This should be ignored
|
||||
"discount1": 15.0,
|
||||
|
|
@ -75,18 +68,18 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
)
|
||||
|
||||
# Verify explicit discounts were applied
|
||||
self.assertEqual(self.line.discount1, 15.0)
|
||||
self.assertEqual(self.line.discount2, 10.0)
|
||||
self.assertEqual(self.line.discount3, 5.0)
|
||||
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)
|
||||
self.assertAlmostEqual(self.line.discount, expected_discount, places=2)
|
||||
self.assertAlmostEqual(self.po_line.discount, expected_discount, places=2)
|
||||
|
||||
def test_write_only_discount1(self):
|
||||
"""Test writing only discount1 explicitly"""
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 25.0, # This should be ignored
|
||||
"discount1": 20.0,
|
||||
|
|
@ -94,43 +87,45 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
)
|
||||
|
||||
# Only discount1 should change
|
||||
self.assertEqual(self.line.discount1, 20.0)
|
||||
self.assertEqual(self.po_line.discount1, 20.0)
|
||||
# Others should remain unchanged
|
||||
self.assertEqual(self.line.discount2, 5.0)
|
||||
self.assertEqual(self.line.discount3, 2.0)
|
||||
self.assertEqual(self.po_line.discount2, 5.0)
|
||||
self.assertEqual(self.po_line.discount3, 2.0)
|
||||
|
||||
def test_write_only_discount2(self):
|
||||
"""Test writing only discount2 explicitly"""
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 30.0, # This should be ignored
|
||||
"discount2": 12.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)
|
||||
# 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)
|
||||
|
||||
def test_write_only_discount3(self):
|
||||
"""Test writing only discount3 explicitly"""
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 35.0, # This should be ignored
|
||||
"discount3": 8.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)
|
||||
# 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)
|
||||
|
||||
def test_write_legacy_discount_only(self):
|
||||
"""Test legacy behavior: writing only discount field"""
|
||||
# Reset to known state first
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
|
|
@ -139,32 +134,32 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
)
|
||||
|
||||
# Write only discount (legacy behavior)
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 25.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Should map to discount1 and reset others
|
||||
self.assertEqual(self.line.discount1, 25.0)
|
||||
self.assertEqual(self.line.discount2, 0.0)
|
||||
self.assertEqual(self.line.discount3, 0.0)
|
||||
self.assertEqual(self.po_line.discount1, 25.0)
|
||||
self.assertEqual(self.po_line.discount2, 0.0)
|
||||
self.assertEqual(self.po_line.discount3, 0.0)
|
||||
|
||||
def test_write_multiple_times(self):
|
||||
"""Test writing multiple times to ensure consistency"""
|
||||
# First write
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 10.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(self.line.discount1, 10.0)
|
||||
self.assertEqual(self.line.discount2, 10.0)
|
||||
self.assertEqual(self.po_line.discount1, 10.0)
|
||||
self.assertEqual(self.po_line.discount2, 10.0)
|
||||
|
||||
# Second write
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 5.0,
|
||||
"discount3": 5.0,
|
||||
|
|
@ -172,13 +167,13 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
)
|
||||
|
||||
# discount3 should change, others remain
|
||||
self.assertEqual(self.line.discount1, 10.0)
|
||||
self.assertEqual(self.line.discount2, 10.0)
|
||||
self.assertEqual(self.line.discount3, 5.0)
|
||||
self.assertEqual(self.po_line.discount1, 10.0)
|
||||
self.assertEqual(self.po_line.discount2, 10.0)
|
||||
self.assertEqual(self.po_line.discount3, 5.0)
|
||||
|
||||
def test_write_zero_discounts(self):
|
||||
"""Test writing zero discounts"""
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 0.0,
|
||||
"discount2": 0.0,
|
||||
|
|
@ -186,15 +181,15 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
}
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
def test_write_combined_scenario(self):
|
||||
"""Test a realistic combined scenario"""
|
||||
# Initial state
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 15.0,
|
||||
"discount2": 5.0,
|
||||
|
|
@ -203,7 +198,7 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
)
|
||||
|
||||
# User tries to update discount field (should be ignored if explicit discounts present)
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount": 50.0,
|
||||
"discount1": 20.0,
|
||||
|
|
@ -211,13 +206,13 @@ class TestTripleDiscountMixin(TransactionCase):
|
|||
)
|
||||
|
||||
# discount1 should be updated, others unchanged
|
||||
self.assertEqual(self.line.discount1, 20.0)
|
||||
self.assertEqual(self.line.discount2, 5.0)
|
||||
self.assertEqual(self.line.discount3, 0.0)
|
||||
self.assertEqual(self.po_line.discount1, 20.0)
|
||||
self.assertEqual(self.po_line.discount2, 5.0)
|
||||
self.assertEqual(self.po_line.discount3, 0.0)
|
||||
|
||||
def test_discount_calculation_accuracy(self):
|
||||
"""Test that discount calculation is accurate"""
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 10.0,
|
||||
|
|
@ -227,22 +222,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.line.discount, expected, places=2)
|
||||
self.assertAlmostEqual(self.po_line.discount, expected, places=2)
|
||||
|
||||
def test_write_without_discount_field(self):
|
||||
"""Test writing other fields without touching discount fields"""
|
||||
initial_discount1 = self.line.discount1
|
||||
initial_discount1 = self.po_line.discount1
|
||||
|
||||
# Write other fields
|
||||
self.line.write(
|
||||
self.po_line.write(
|
||||
{
|
||||
"quantity": 20,
|
||||
"product_qty": 20,
|
||||
"price_unit": 150.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Discounts should remain unchanged
|
||||
self.assertEqual(self.line.discount1, initial_discount1)
|
||||
self.assertEqual(self.po_line.discount1, initial_discount1)
|
||||
# But other fields should be updated
|
||||
self.assertEqual(self.line.quantity, 20)
|
||||
self.assertEqual(self.line.price_unit, 150.0)
|
||||
self.assertEqual(self.po_line.product_qty, 20)
|
||||
self.assertEqual(self.po_line.price_unit, 150.0)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,7 @@ 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. 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
|
||||
EXCLUDE. 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*.
|
||||
|
|
@ -38,6 +33,7 @@ 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
|
||||
|
|
@ -58,9 +54,8 @@ _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"
|
||||
|
|
@ -72,10 +67,6 @@ 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:
|
||||
|
|
@ -110,12 +101,12 @@ def wait_for_postgres(timeout=60):
|
|||
time.sleep(1)
|
||||
|
||||
|
||||
def odoo(odoo_bin, paths, db, *args):
|
||||
def odoo(odoo_bin, paths, *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
|
||||
|
|
@ -128,11 +119,8 @@ def odoo(odoo_bin, paths, db, *args):
|
|||
return subprocess.run(cmd, check=False).returncode
|
||||
|
||||
|
||||
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).
|
||||
"""
|
||||
def export_pots(odoo_bin, paths, addons):
|
||||
"""Deja en BOT_POT_DIR/<commit> el .pot de cada addon ya instalado."""
|
||||
pot_dir = Path(POT_DIR)
|
||||
head = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
|
|
@ -148,17 +136,16 @@ def export_pots(odoo_bin, paths, runs):
|
|||
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")
|
||||
|
|
@ -171,27 +158,12 @@ def export_pots(odoo_bin, paths, runs):
|
|||
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 = env_list("EXCLUDE")
|
||||
exclude = set(os.environ.get("EXCLUDE", "").replace(",", " ").split())
|
||||
addons = sys.argv[1:] or [
|
||||
addon
|
||||
for addon in repo_addons
|
||||
|
|
@ -217,25 +189,27 @@ def main():
|
|||
"(tools/issue_bot/runner/ en el repo de ecocentral) o el addon a "
|
||||
"EXCLUDE."
|
||||
)
|
||||
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]
|
||||
dependencies = sorted(
|
||||
{dep for addon in addons for dep in manifests[addon].get("depends", [])}
|
||||
- set(addons)
|
||||
)
|
||||
|
||||
wait_for_postgres()
|
||||
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:
|
||||
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:
|
||||
# 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, runs)
|
||||
export_pots(odoo_bin, paths, addons)
|
||||
except OSError as e:
|
||||
_logger.warning("No se han podido exportar los .pot: %s", e)
|
||||
sys.exit(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -101,12 +101,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ 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 (
|
||||
|
|
@ -33,16 +34,6 @@ 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."""
|
||||
|
|
@ -68,8 +59,9 @@ 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 so the
|
||||
# pick does not depend on the translated `name` of each language.
|
||||
# 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`.
|
||||
website = SimpleNamespace(
|
||||
pricelist_id=env["product.pricelist"]
|
||||
.sudo()
|
||||
|
|
@ -372,7 +364,6 @@ class TestProcessCartItems(TransactionCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
_activate_test_langs(self.env)
|
||||
self.controller = AplicoopWebsiteSale()
|
||||
|
||||
# Create test products
|
||||
|
|
@ -534,7 +525,6 @@ 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")
|
||||
|
|
@ -572,11 +562,16 @@ 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)."""
|
||||
|
|
@ -674,7 +669,6 @@ 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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,16 +25,11 @@ 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)],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue