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,
|
# este repo (ecocentral depende de él): se prueba el checkout del PR,
|
||||||
# no ese clon.
|
# no ese clon.
|
||||||
ODOO_SRC_SKIP: addons-cm
|
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.
|
# 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.
|
# Está en la caché que el runner monta en todos los jobs.
|
||||||
BOT_POT_DIR: /root/.cache/eco-pr-bot/pot
|
BOT_POT_DIR: /root/.cache/eco-pr-bot/pot
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,6 @@ from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
@tagged("post_install", "-at_install")
|
@tagged("post_install", "-at_install")
|
||||||
class TestPurchaseOrder(TransactionCase):
|
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
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
@ -30,15 +20,13 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a product template first, then get the variant. Billed on
|
# Create a product template first, then get the variant
|
||||||
# ordered quantities, so the bill can be created without a receipt.
|
|
||||||
cls.product_template = cls.env["product.template"].create(
|
cls.product_template = cls.env["product.template"].create(
|
||||||
{
|
{
|
||||||
"name": "Test Product PO",
|
"name": "Test Product PO",
|
||||||
"type": "consu",
|
"type": "consu",
|
||||||
"list_price": 150.0,
|
"list_price": 150.0,
|
||||||
"standard_price": 80.0,
|
"standard_price": 80.0,
|
||||||
"purchase_method": "purchase",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Get the auto-created product variant
|
# Get the auto-created product variant
|
||||||
|
|
@ -64,24 +52,16 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_bill_line(self):
|
def test_po_line_discount_readonly(self):
|
||||||
self.purchase_order.button_confirm()
|
"""Test that discount field is readonly in PO lines"""
|
||||||
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"]
|
field = self.po_line._fields["discount"]
|
||||||
self.assertTrue(field.compute)
|
self.assertTrue(field.readonly, "Discount field should be readonly in PO lines")
|
||||||
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):
|
def test_po_line_write_with_explicit_discounts(self):
|
||||||
"""Test writing PO line with explicit discounts"""
|
"""Test writing PO line with explicit discounts"""
|
||||||
self.po_line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
|
"discount": 25.0, # Should be ignored
|
||||||
"discount1": 12.0,
|
"discount1": 12.0,
|
||||||
"discount2": 8.0,
|
"discount2": 8.0,
|
||||||
"discount3": 4.0,
|
"discount3": 4.0,
|
||||||
|
|
@ -91,11 +71,22 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
self.assertEqual(self.po_line.discount1, 12.0)
|
self.assertEqual(self.po_line.discount1, 12.0)
|
||||||
self.assertEqual(self.po_line.discount2, 8.0)
|
self.assertEqual(self.po_line.discount2, 8.0)
|
||||||
self.assertEqual(self.po_line.discount3, 4.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):
|
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(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount1": 15.0,
|
"discount1": 15.0,
|
||||||
|
|
@ -104,9 +95,11 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# The PO line keeps the total discount with the "Discount" precision
|
# Base: 10 * 150 = 1500
|
||||||
# (27.33 %, not 27.3325 %), so the subtotal is computed from it.
|
# After 15% discount: 1275
|
||||||
expected_subtotal = 10 * 150 * (1 - self.po_line.discount / 100)
|
# 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):
|
def test_multiple_po_lines(self):
|
||||||
|
|
@ -124,7 +117,7 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify both lines have correct discounts
|
# 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.discount1, 20.0)
|
||||||
self.assertEqual(line2.discount2, 15.0)
|
self.assertEqual(line2.discount2, 15.0)
|
||||||
self.assertEqual(line2.discount3, 10.0)
|
self.assertEqual(line2.discount3, 10.0)
|
||||||
|
|
@ -181,8 +174,40 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
expected = 10 * 150
|
expected = 10 * 150
|
||||||
self.assertEqual(self.po_line.price_subtotal, expected)
|
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):
|
def test_po_confirm_with_discounts(self):
|
||||||
"""Test confirming PO doesn't alter discounts"""
|
"""Test confirming PO doesn't alter discounts"""
|
||||||
|
self.po_line.write(
|
||||||
|
{
|
||||||
|
"discount1": 10.0,
|
||||||
|
"discount2": 5.0,
|
||||||
|
"discount3": 2.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Confirm the purchase order
|
# Confirm the purchase order
|
||||||
self.purchase_order.button_confirm()
|
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.discount1, 10.0)
|
||||||
self.assertEqual(self.po_line.discount2, 5.0)
|
self.assertEqual(self.po_line.discount2, 5.0)
|
||||||
self.assertEqual(self.po_line.discount3, 2.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")
|
@tagged("post_install", "-at_install")
|
||||||
class TestTripleDiscountMixin(TransactionCase):
|
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
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
@ -35,21 +30,19 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
# Get the auto-created product variant
|
# Get the auto-created product variant
|
||||||
cls.product = cls.product_template.product_variant_ids[0]
|
cls.product = cls.product_template.product_variant_ids[0]
|
||||||
|
|
||||||
# Create an invoice
|
# Create a purchase order
|
||||||
cls.invoice = cls.env["account.move"].create(
|
cls.purchase_order = cls.env["purchase.order"].create(
|
||||||
{
|
{
|
||||||
"move_type": "out_invoice",
|
|
||||||
"partner_id": cls.partner.id,
|
"partner_id": cls.partner.id,
|
||||||
"invoice_date": "2026-01-01",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an invoice line
|
# Create a purchase order line
|
||||||
cls.line = cls.env["account.move.line"].create(
|
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,
|
"product_id": cls.product.id,
|
||||||
"quantity": 10,
|
"product_qty": 10,
|
||||||
"price_unit": 100.0,
|
"price_unit": 100.0,
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
|
|
@ -59,13 +52,13 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
def test_discount_field_is_readonly(self):
|
def test_discount_field_is_readonly(self):
|
||||||
"""Test that the discount field is readonly"""
|
"""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")
|
self.assertTrue(field.readonly, "Discount field should be readonly")
|
||||||
|
|
||||||
def test_write_with_explicit_discounts(self):
|
def test_write_with_explicit_discounts(self):
|
||||||
"""Test writing with explicit discount1, discount2, discount3"""
|
"""Test writing with explicit discount1, discount2, discount3"""
|
||||||
# Write with explicit discounts
|
# Write with explicit discounts
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 20.0, # This should be ignored
|
"discount": 20.0, # This should be ignored
|
||||||
"discount1": 15.0,
|
"discount1": 15.0,
|
||||||
|
|
@ -75,18 +68,18 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify explicit discounts were applied
|
# Verify explicit discounts were applied
|
||||||
self.assertEqual(self.line.discount1, 15.0)
|
self.assertEqual(self.po_line.discount1, 15.0)
|
||||||
self.assertEqual(self.line.discount2, 10.0)
|
self.assertEqual(self.po_line.discount2, 10.0)
|
||||||
self.assertEqual(self.line.discount3, 5.0)
|
self.assertEqual(self.po_line.discount3, 5.0)
|
||||||
|
|
||||||
# The computed discount field should reflect the combined discounts
|
# The computed discount field should reflect the combined discounts
|
||||||
# Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05))
|
# Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05))
|
||||||
expected_discount = 100 - (100 * 0.85 * 0.90 * 0.95)
|
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):
|
def test_write_only_discount1(self):
|
||||||
"""Test writing only discount1 explicitly"""
|
"""Test writing only discount1 explicitly"""
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 25.0, # This should be ignored
|
"discount": 25.0, # This should be ignored
|
||||||
"discount1": 20.0,
|
"discount1": 20.0,
|
||||||
|
|
@ -94,43 +87,45 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only discount1 should change
|
# Only discount1 should change
|
||||||
self.assertEqual(self.line.discount1, 20.0)
|
self.assertEqual(self.po_line.discount1, 20.0)
|
||||||
# Others should remain unchanged
|
# Others should remain unchanged
|
||||||
self.assertEqual(self.line.discount2, 5.0)
|
self.assertEqual(self.po_line.discount2, 5.0)
|
||||||
self.assertEqual(self.line.discount3, 2.0)
|
self.assertEqual(self.po_line.discount3, 2.0)
|
||||||
|
|
||||||
def test_write_only_discount2(self):
|
def test_write_only_discount2(self):
|
||||||
"""Test writing only discount2 explicitly"""
|
"""Test writing only discount2 explicitly"""
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 30.0, # This should be ignored
|
"discount": 30.0, # This should be ignored
|
||||||
"discount2": 12.0,
|
"discount2": 12.0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only discount2 should change; each test starts from setUpClass
|
# Only discount2 should change
|
||||||
self.assertEqual(self.line.discount2, 12.0)
|
self.assertEqual(self.po_line.discount2, 12.0)
|
||||||
self.assertEqual(self.line.discount1, 10.0)
|
# Others should remain unchanged from previous test
|
||||||
self.assertEqual(self.line.discount3, 2.0)
|
self.assertEqual(self.po_line.discount1, 20.0)
|
||||||
|
self.assertEqual(self.po_line.discount3, 2.0)
|
||||||
|
|
||||||
def test_write_only_discount3(self):
|
def test_write_only_discount3(self):
|
||||||
"""Test writing only discount3 explicitly"""
|
"""Test writing only discount3 explicitly"""
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 35.0, # This should be ignored
|
"discount": 35.0, # This should be ignored
|
||||||
"discount3": 8.0,
|
"discount3": 8.0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only discount3 should change; each test starts from setUpClass
|
# Only discount3 should change
|
||||||
self.assertEqual(self.line.discount3, 8.0)
|
self.assertEqual(self.po_line.discount3, 8.0)
|
||||||
self.assertEqual(self.line.discount1, 10.0)
|
# Others should remain unchanged from previous tests
|
||||||
self.assertEqual(self.line.discount2, 5.0)
|
self.assertEqual(self.po_line.discount1, 20.0)
|
||||||
|
self.assertEqual(self.po_line.discount2, 12.0)
|
||||||
|
|
||||||
def test_write_legacy_discount_only(self):
|
def test_write_legacy_discount_only(self):
|
||||||
"""Test legacy behavior: writing only discount field"""
|
"""Test legacy behavior: writing only discount field"""
|
||||||
# Reset to known state first
|
# Reset to known state first
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
|
|
@ -139,32 +134,32 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Write only discount (legacy behavior)
|
# Write only discount (legacy behavior)
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 25.0,
|
"discount": 25.0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should map to discount1 and reset others
|
# Should map to discount1 and reset others
|
||||||
self.assertEqual(self.line.discount1, 25.0)
|
self.assertEqual(self.po_line.discount1, 25.0)
|
||||||
self.assertEqual(self.line.discount2, 0.0)
|
self.assertEqual(self.po_line.discount2, 0.0)
|
||||||
self.assertEqual(self.line.discount3, 0.0)
|
self.assertEqual(self.po_line.discount3, 0.0)
|
||||||
|
|
||||||
def test_write_multiple_times(self):
|
def test_write_multiple_times(self):
|
||||||
"""Test writing multiple times to ensure consistency"""
|
"""Test writing multiple times to ensure consistency"""
|
||||||
# First write
|
# First write
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(self.line.discount1, 10.0)
|
self.assertEqual(self.po_line.discount1, 10.0)
|
||||||
self.assertEqual(self.line.discount2, 10.0)
|
self.assertEqual(self.po_line.discount2, 10.0)
|
||||||
|
|
||||||
# Second write
|
# Second write
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 5.0,
|
"discount": 5.0,
|
||||||
"discount3": 5.0,
|
"discount3": 5.0,
|
||||||
|
|
@ -172,13 +167,13 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# discount3 should change, others remain
|
# discount3 should change, others remain
|
||||||
self.assertEqual(self.line.discount1, 10.0)
|
self.assertEqual(self.po_line.discount1, 10.0)
|
||||||
self.assertEqual(self.line.discount2, 10.0)
|
self.assertEqual(self.po_line.discount2, 10.0)
|
||||||
self.assertEqual(self.line.discount3, 5.0)
|
self.assertEqual(self.po_line.discount3, 5.0)
|
||||||
|
|
||||||
def test_write_zero_discounts(self):
|
def test_write_zero_discounts(self):
|
||||||
"""Test writing zero discounts"""
|
"""Test writing zero discounts"""
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount1": 0.0,
|
"discount1": 0.0,
|
||||||
"discount2": 0.0,
|
"discount2": 0.0,
|
||||||
|
|
@ -186,15 +181,15 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(self.line.discount1, 0.0)
|
self.assertEqual(self.po_line.discount1, 0.0)
|
||||||
self.assertEqual(self.line.discount2, 0.0)
|
self.assertEqual(self.po_line.discount2, 0.0)
|
||||||
self.assertEqual(self.line.discount3, 0.0)
|
self.assertEqual(self.po_line.discount3, 0.0)
|
||||||
self.assertEqual(self.line.discount, 0.0)
|
self.assertEqual(self.po_line.discount, 0.0)
|
||||||
|
|
||||||
def test_write_combined_scenario(self):
|
def test_write_combined_scenario(self):
|
||||||
"""Test a realistic combined scenario"""
|
"""Test a realistic combined scenario"""
|
||||||
# Initial state
|
# Initial state
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount1": 15.0,
|
"discount1": 15.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
|
|
@ -203,7 +198,7 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# User tries to update discount field (should be ignored if explicit discounts present)
|
# User tries to update discount field (should be ignored if explicit discounts present)
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount": 50.0,
|
"discount": 50.0,
|
||||||
"discount1": 20.0,
|
"discount1": 20.0,
|
||||||
|
|
@ -211,13 +206,13 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# discount1 should be updated, others unchanged
|
# discount1 should be updated, others unchanged
|
||||||
self.assertEqual(self.line.discount1, 20.0)
|
self.assertEqual(self.po_line.discount1, 20.0)
|
||||||
self.assertEqual(self.line.discount2, 5.0)
|
self.assertEqual(self.po_line.discount2, 5.0)
|
||||||
self.assertEqual(self.line.discount3, 0.0)
|
self.assertEqual(self.po_line.discount3, 0.0)
|
||||||
|
|
||||||
def test_discount_calculation_accuracy(self):
|
def test_discount_calculation_accuracy(self):
|
||||||
"""Test that discount calculation is accurate"""
|
"""Test that discount calculation is accurate"""
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
|
|
@ -227,22 +222,22 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
# Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1
|
# Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1
|
||||||
expected = 100 - (100 * 0.9 * 0.9 * 0.9)
|
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):
|
def test_write_without_discount_field(self):
|
||||||
"""Test writing other fields without touching discount fields"""
|
"""Test writing other fields without touching discount fields"""
|
||||||
initial_discount1 = self.line.discount1
|
initial_discount1 = self.po_line.discount1
|
||||||
|
|
||||||
# Write other fields
|
# Write other fields
|
||||||
self.line.write(
|
self.po_line.write(
|
||||||
{
|
{
|
||||||
"quantity": 20,
|
"product_qty": 20,
|
||||||
"price_unit": 150.0,
|
"price_unit": 150.0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Discounts should remain unchanged
|
# 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
|
# But other fields should be updated
|
||||||
self.assertEqual(self.line.quantity, 20)
|
self.assertEqual(self.po_line.product_qty, 20)
|
||||||
self.assertEqual(self.line.price_unit, 150.0)
|
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 ...]
|
tools/ci/run_tests.py [addon ...]
|
||||||
|
|
||||||
Sin argumentos prueba todos los addons instalables del repo menos los de
|
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
|
EXCLUDE. Odoo y los repos de los que dependen los addons se toman de ODOO_SRC
|
||||||
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
|
(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
|
tools/issue_bot/README.md), menos las carpetas de ODOO_SRC_SKIP, y la conexión
|
||||||
a PostgreSQL, de las variables PG*.
|
a PostgreSQL, de las variables PG*.
|
||||||
|
|
@ -38,6 +33,7 @@ from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parents[2]
|
REPO = Path(__file__).resolve().parents[2]
|
||||||
ODOO_SRC = Path(os.environ.get("ODOO_SRC", "/odoo-src/18.0"))
|
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")
|
DB = os.environ.get("PGDATABASE", "test")
|
||||||
POT_DIR = os.environ.get("BOT_POT_DIR")
|
POT_DIR = os.environ.get("BOT_POT_DIR")
|
||||||
POT_KEEP_DAYS = 14
|
POT_KEEP_DAYS = 14
|
||||||
|
|
@ -58,9 +54,8 @@ _logger = logging.getLogger("ci")
|
||||||
def find_sources():
|
def find_sources():
|
||||||
"""Devuelve odoo-bin y el addons_path: el repo y lo que haya en ODOO_SRC."""
|
"""Devuelve odoo-bin y el addons_path: el repo y lo que haya en ODOO_SRC."""
|
||||||
odoo_bin, paths = None, [REPO]
|
odoo_bin, paths = None, [REPO]
|
||||||
skip = env_list("ODOO_SRC_SKIP")
|
|
||||||
for src in sorted(ODOO_SRC.iterdir()):
|
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)
|
_logger.info("Se omite %s (ODOO_SRC_SKIP)", src)
|
||||||
elif (src / "odoo-bin").is_file():
|
elif (src / "odoo-bin").is_file():
|
||||||
odoo_bin = src / "odoo-bin"
|
odoo_bin = src / "odoo-bin"
|
||||||
|
|
@ -72,10 +67,6 @@ def find_sources():
|
||||||
return odoo_bin, paths
|
return odoo_bin, paths
|
||||||
|
|
||||||
|
|
||||||
def env_list(name):
|
|
||||||
return set(os.environ.get(name, "").replace(",", " ").split())
|
|
||||||
|
|
||||||
|
|
||||||
def read_manifests(paths):
|
def read_manifests(paths):
|
||||||
manifests = {}
|
manifests = {}
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
|
@ -110,12 +101,12 @@ def wait_for_postgres(timeout=60):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
def odoo(odoo_bin, paths, db, *args):
|
def odoo(odoo_bin, paths, *args):
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(odoo_bin),
|
str(odoo_bin),
|
||||||
f"--addons-path={','.join(map(str, paths))}",
|
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
|
# 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
|
# 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
|
# 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
|
return subprocess.run(cmd, check=False).returncode
|
||||||
|
|
||||||
|
|
||||||
def export_pots(odoo_bin, paths, runs):
|
def export_pots(odoo_bin, paths, addons):
|
||||||
"""Deja en BOT_POT_DIR/<commit> el .pot de cada addon ya instalado.
|
"""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)
|
pot_dir = Path(POT_DIR)
|
||||||
head = subprocess.run(
|
head = subprocess.run(
|
||||||
["git", "rev-parse", "HEAD"],
|
["git", "rev-parse", "HEAD"],
|
||||||
|
|
@ -148,17 +136,16 @@ def export_pots(odoo_bin, paths, runs):
|
||||||
for stale in (out, tmp):
|
for stale in (out, tmp):
|
||||||
shutil.rmtree(stale, ignore_errors=True)
|
shutil.rmtree(stale, ignore_errors=True)
|
||||||
tmp.mkdir(parents=True)
|
tmp.mkdir(parents=True)
|
||||||
_logger.info("Exportando los .pot a %s", out)
|
|
||||||
for db, addons in runs:
|
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(odoo_bin),
|
str(odoo_bin),
|
||||||
"shell",
|
"shell",
|
||||||
f"--addons-path={','.join(map(str, paths))}",
|
f"--addons-path={','.join(map(str, paths))}",
|
||||||
f"--database={db}",
|
f"--database={DB}",
|
||||||
"--no-http",
|
"--no-http",
|
||||||
"--log-level=warn",
|
"--log-level=warn",
|
||||||
]
|
]
|
||||||
|
_logger.info("Exportando los .pot a %s", out)
|
||||||
script = EXPORT_POTS.format(addons=addons, out=str(tmp))
|
script = EXPORT_POTS.format(addons=addons, out=str(tmp))
|
||||||
if subprocess.run(cmd, input=script, text=True, check=False).returncode:
|
if subprocess.run(cmd, input=script, text=True, check=False).returncode:
|
||||||
_logger.warning("No se han podido exportar los .pot")
|
_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)
|
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():
|
def main():
|
||||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
odoo_bin, paths = find_sources()
|
odoo_bin, paths = find_sources()
|
||||||
manifests = read_manifests(paths)
|
manifests = read_manifests(paths)
|
||||||
repo_addons = sorted(m.parent.name for m in REPO.glob("*/__manifest__.py"))
|
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 [
|
addons = sys.argv[1:] or [
|
||||||
addon
|
addon
|
||||||
for addon in repo_addons
|
for addon in repo_addons
|
||||||
|
|
@ -217,25 +189,27 @@ def main():
|
||||||
"(tools/issue_bot/runner/ en el repo de ecocentral) o el addon a "
|
"(tools/issue_bot/runner/ en el repo de ecocentral) o el addon a "
|
||||||
"EXCLUDE."
|
"EXCLUDE."
|
||||||
)
|
)
|
||||||
isolate = env_list("ISOLATE")
|
dependencies = sorted(
|
||||||
together = [addon for addon in addons if addon not in isolate]
|
{dep for addon in addons for dep in manifests[addon].get("depends", [])}
|
||||||
runs = [(DB, together)] if together else []
|
- set(addons)
|
||||||
runs += [(f"{DB}_{addon}", [addon]) for addon in addons if addon in isolate]
|
)
|
||||||
|
|
||||||
wait_for_postgres()
|
wait_for_postgres()
|
||||||
failed = []
|
subprocess.run(["dropdb", "--if-exists", DB], check=True)
|
||||||
for db, run_addons in runs:
|
_logger.info("Instalando las dependencias: %s", ", ".join(dependencies))
|
||||||
if run_tests(odoo_bin, paths, manifests, db, run_addons):
|
init = ["--init", ",".join(dependencies), "--log-level=warn"]
|
||||||
failed.append(db)
|
if odoo(odoo_bin, paths, *init):
|
||||||
if failed:
|
sys.exit("No se ha podido crear la base de datos con las dependencias")
|
||||||
sys.exit(f"Fallan los tests en: {', '.join(failed)}")
|
_logger.info("Probando: %s", ", ".join(addons))
|
||||||
if POT_DIR:
|
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
|
# Que falte la caché no es un fallo de los tests: el bot de revisión
|
||||||
# se queda sin las traducciones y lo dice.
|
# se queda sin las traducciones y lo dice.
|
||||||
try:
|
try:
|
||||||
export_pots(odoo_bin, paths, runs)
|
export_pots(odoo_bin, paths, addons)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
_logger.warning("No se han podido exportar los .pot: %s", e)
|
_logger.warning("No se han podido exportar los .pot: %s", e)
|
||||||
|
sys.exit(result)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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.
|
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`.
|
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:
|
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
|
- 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 types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
from odoo.addons.website_sale_aplicoop.controllers.website_sale import (
|
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"
|
"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):
|
def _env_with_lang(env, lang):
|
||||||
"""Return a cloned environment with language context set."""
|
"""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:
|
if website is None:
|
||||||
# `pricelist_id` is what the pricing helpers read now that Eskaera
|
# `pricelist_id` is what the pricing helpers read now that Eskaera
|
||||||
# prices with the standard website pricelist. Ordered by id so the
|
# prices with the standard website pricelist. Ordered by id on
|
||||||
# pick does not depend on the translated `name` of each language.
|
# purpose: the multilang cases build an env whose language is not
|
||||||
|
# installed, and the default order is by the translated `name`.
|
||||||
website = SimpleNamespace(
|
website = SimpleNamespace(
|
||||||
pricelist_id=env["product.pricelist"]
|
pricelist_id=env["product.pricelist"]
|
||||||
.sudo()
|
.sudo()
|
||||||
|
|
@ -372,7 +364,6 @@ class TestProcessCartItems(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
_activate_test_langs(self.env)
|
|
||||||
self.controller = AplicoopWebsiteSale()
|
self.controller = AplicoopWebsiteSale()
|
||||||
|
|
||||||
# Create test products
|
# Create test products
|
||||||
|
|
@ -534,7 +525,6 @@ class TestBuildConfirmationMessage(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
_activate_test_langs(self.env)
|
|
||||||
self.controller = AplicoopWebsiteSale()
|
self.controller = AplicoopWebsiteSale()
|
||||||
self.user = self.env.ref("base.user_admin")
|
self.user = self.env.ref("base.user_admin")
|
||||||
self.partner = self.env.ref("base.partner_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))
|
request_mock = _build_request_mock(_env_with_lang(self.env, lang))
|
||||||
with patch(REQUEST_PATCH_TARGET, request_mock):
|
with patch(REQUEST_PATCH_TARGET, request_mock):
|
||||||
|
try:
|
||||||
return self.controller._build_confirmation_message(
|
return self.controller._build_confirmation_message(
|
||||||
sale_order or self.sale_order,
|
sale_order or self.sale_order,
|
||||||
group_order or self.group_order,
|
group_order or self.group_order,
|
||||||
is_delivery=is_delivery,
|
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):
|
def test_build_confirmation_message_pickup(self):
|
||||||
"""Test confirmation message for pickup (not delivery)."""
|
"""Test confirmation message for pickup (not delivery)."""
|
||||||
|
|
@ -674,7 +669,6 @@ class TestConfirmEskaera_Integration(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
_activate_test_langs(self.env)
|
|
||||||
self.controller = AplicoopWebsiteSale()
|
self.controller = AplicoopWebsiteSale()
|
||||||
self.consumer_group = self.env["res.partner"].create(
|
self.consumer_group = self.env["res.partner"].create(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,11 @@ class TestLazyLoading(HttpCase):
|
||||||
cls.other_category = cls.env["product.public.category"].create(
|
cls.other_category = cls.env["product.public.category"].create(
|
||||||
{"name": "Other Cat"}
|
{"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(
|
cls.products = cls.env["product.template"].create(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Lazy Product %02d" % index,
|
"name": "Lazy Product %02d" % index,
|
||||||
"list_price": 10.0 + index,
|
"list_price": 10.0 + index,
|
||||||
"website_sequence": index,
|
|
||||||
"is_published": True,
|
"is_published": True,
|
||||||
"public_categ_ids": [(6, 0, cls.category.ids)],
|
"public_categ_ids": [(6, 0, cls.category.ids)],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue