Compare commits
No commits in common. "ed048c85eb01161afa00d45823616cd27ea2f6c0" and "b15e9bc977794e187aaad03b3f436279bb04ce82" have entirely different histories.
ed048c85eb
...
b15e9bc977
84 changed files with 4702 additions and 6461 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -130,3 +130,4 @@ dmypy.json
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,45 +12,36 @@ class TestAccountMove(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create a partner
|
# Create a partner
|
||||||
cls.partner = cls.env["res.partner"].create(
|
cls.partner = cls.env["res.partner"].create({
|
||||||
{
|
|
||||||
"name": "Test Customer",
|
"name": "Test Customer",
|
||||||
"email": "customer@test.com",
|
"email": "customer@test.com",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create a product
|
# Create a product
|
||||||
cls.product = cls.env["product.product"].create(
|
cls.product = cls.env["product.product"].create({
|
||||||
{
|
|
||||||
"name": "Test Product Invoice",
|
"name": "Test Product Invoice",
|
||||||
"type": "consu",
|
"type": "consu",
|
||||||
"list_price": 200.0,
|
"list_price": 200.0,
|
||||||
"standard_price": 100.0,
|
"standard_price": 100.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create tax
|
# Create tax
|
||||||
cls.tax = cls.env["account.tax"].create(
|
cls.tax = cls.env["account.tax"].create({
|
||||||
{
|
|
||||||
"name": "Test Tax 10%",
|
"name": "Test Tax 10%",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
"amount_type": "percent",
|
"amount_type": "percent",
|
||||||
"type_tax_use": "sale",
|
"type_tax_use": "sale",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create an invoice
|
# Create an invoice
|
||||||
cls.invoice = cls.env["account.move"].create(
|
cls.invoice = cls.env["account.move"].create({
|
||||||
{
|
|
||||||
"move_type": "out_invoice",
|
"move_type": "out_invoice",
|
||||||
"partner_id": cls.partner.id,
|
"partner_id": cls.partner.id,
|
||||||
"invoice_date": "2026-01-01",
|
"invoice_date": "2026-01-01",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create invoice line
|
# Create invoice line
|
||||||
cls.invoice_line = cls.env["account.move.line"].create(
|
cls.invoice_line = cls.env["account.move.line"].create({
|
||||||
{
|
|
||||||
"move_id": cls.invoice.id,
|
"move_id": cls.invoice.id,
|
||||||
"product_id": cls.product.id,
|
"product_id": cls.product.id,
|
||||||
"quantity": 5,
|
"quantity": 5,
|
||||||
|
|
@ -59,26 +50,21 @@ class TestAccountMove(TransactionCase):
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 2.0,
|
"discount3": 2.0,
|
||||||
"tax_ids": [(6, 0, [cls.tax.id])],
|
"tax_ids": [(6, 0, [cls.tax.id])],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
def test_invoice_line_discount_readonly(self):
|
def test_invoice_line_discount_readonly(self):
|
||||||
"""Test that discount field is readonly in invoice lines"""
|
"""Test that discount field is readonly in invoice lines"""
|
||||||
field = self.invoice_line._fields["discount"]
|
field = self.invoice_line._fields["discount"]
|
||||||
self.assertTrue(
|
self.assertTrue(field.readonly, "Discount field should be readonly in invoice lines")
|
||||||
field.readonly, "Discount field should be readonly in invoice lines"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_invoice_line_write_with_explicit_discounts(self):
|
def test_invoice_line_write_with_explicit_discounts(self):
|
||||||
"""Test writing invoice line with explicit discounts"""
|
"""Test writing invoice line with explicit discounts"""
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"discount": 30.0, # Should be ignored
|
"discount": 30.0, # Should be ignored
|
||||||
"discount1": 15.0,
|
"discount1": 15.0,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
"discount3": 5.0,
|
"discount3": 5.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(self.invoice_line.discount1, 15.0)
|
self.assertEqual(self.invoice_line.discount1, 15.0)
|
||||||
self.assertEqual(self.invoice_line.discount2, 10.0)
|
self.assertEqual(self.invoice_line.discount2, 10.0)
|
||||||
|
|
@ -86,11 +72,9 @@ class TestAccountMove(TransactionCase):
|
||||||
|
|
||||||
def test_invoice_line_legacy_discount(self):
|
def test_invoice_line_legacy_discount(self):
|
||||||
"""Test legacy discount behavior in invoice lines"""
|
"""Test legacy discount behavior in invoice lines"""
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"discount": 20.0,
|
"discount": 20.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Should map to discount1 and reset others
|
# Should map to discount1 and reset others
|
||||||
self.assertEqual(self.invoice_line.discount1, 20.0)
|
self.assertEqual(self.invoice_line.discount1, 20.0)
|
||||||
|
|
@ -99,13 +83,11 @@ class TestAccountMove(TransactionCase):
|
||||||
|
|
||||||
def test_invoice_line_price_calculation(self):
|
def test_invoice_line_price_calculation(self):
|
||||||
"""Test that price subtotal is calculated correctly with triple discount"""
|
"""Test that price subtotal is calculated correctly with triple discount"""
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 0.0,
|
"discount3": 0.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Base: 5 * 200 = 1000
|
# Base: 5 * 200 = 1000
|
||||||
# After 10% discount: 900
|
# After 10% discount: 900
|
||||||
|
|
@ -117,8 +99,7 @@ class TestAccountMove(TransactionCase):
|
||||||
|
|
||||||
def test_multiple_invoice_lines(self):
|
def test_multiple_invoice_lines(self):
|
||||||
"""Test multiple invoice lines with different discounts"""
|
"""Test multiple invoice lines with different discounts"""
|
||||||
line2 = self.env["account.move.line"].create(
|
line2 = self.env["account.move.line"].create({
|
||||||
{
|
|
||||||
"move_id": self.invoice.id,
|
"move_id": self.invoice.id,
|
||||||
"product_id": self.product.id,
|
"product_id": self.product.id,
|
||||||
"quantity": 3,
|
"quantity": 3,
|
||||||
|
|
@ -127,8 +108,7 @@ class TestAccountMove(TransactionCase):
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
"discount3": 5.0,
|
"discount3": 5.0,
|
||||||
"tax_ids": [(6, 0, [self.tax.id])],
|
"tax_ids": [(6, 0, [self.tax.id])],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Verify both lines have correct discounts
|
# Verify both lines have correct discounts
|
||||||
self.assertEqual(self.invoice_line.discount1, 10.0)
|
self.assertEqual(self.invoice_line.discount1, 10.0)
|
||||||
|
|
@ -141,11 +121,9 @@ class TestAccountMove(TransactionCase):
|
||||||
initial_discount1 = self.invoice_line.discount1
|
initial_discount1 = self.invoice_line.discount1
|
||||||
initial_discount2 = self.invoice_line.discount2
|
initial_discount2 = self.invoice_line.discount2
|
||||||
|
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"quantity": 10,
|
"quantity": 10,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Discounts should remain unchanged
|
# Discounts should remain unchanged
|
||||||
self.assertEqual(self.invoice_line.discount1, initial_discount1)
|
self.assertEqual(self.invoice_line.discount1, initial_discount1)
|
||||||
|
|
@ -157,11 +135,9 @@ class TestAccountMove(TransactionCase):
|
||||||
"""Test updating price doesn't affect discounts"""
|
"""Test updating price doesn't affect discounts"""
|
||||||
initial_discount1 = self.invoice_line.discount1
|
initial_discount1 = self.invoice_line.discount1
|
||||||
|
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"price_unit": 250.0,
|
"price_unit": 250.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Discount should remain unchanged
|
# Discount should remain unchanged
|
||||||
self.assertEqual(self.invoice_line.discount1, initial_discount1)
|
self.assertEqual(self.invoice_line.discount1, initial_discount1)
|
||||||
|
|
@ -170,13 +146,11 @@ class TestAccountMove(TransactionCase):
|
||||||
|
|
||||||
def test_invoice_with_zero_discounts(self):
|
def test_invoice_with_zero_discounts(self):
|
||||||
"""Test invoice line with all zero discounts"""
|
"""Test invoice line with all zero discounts"""
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"discount1": 0.0,
|
"discount1": 0.0,
|
||||||
"discount2": 0.0,
|
"discount2": 0.0,
|
||||||
"discount3": 0.0,
|
"discount3": 0.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# All discounts should be zero
|
# All discounts should be zero
|
||||||
self.assertEqual(self.invoice_line.discount, 0.0)
|
self.assertEqual(self.invoice_line.discount, 0.0)
|
||||||
|
|
@ -191,15 +165,13 @@ class TestAccountMove(TransactionCase):
|
||||||
def test_invoice_line_combined_operations(self):
|
def test_invoice_line_combined_operations(self):
|
||||||
"""Test combined operations on invoice line"""
|
"""Test combined operations on invoice line"""
|
||||||
# Update multiple fields at once
|
# Update multiple fields at once
|
||||||
self.invoice_line.write(
|
self.invoice_line.write({
|
||||||
{
|
|
||||||
"quantity": 8,
|
"quantity": 8,
|
||||||
"price_unit": 180.0,
|
"price_unit": 180.0,
|
||||||
"discount1": 12.0,
|
"discount1": 12.0,
|
||||||
"discount2": 6.0,
|
"discount2": 6.0,
|
||||||
"discount3": 0.0, # Reset discount3 explicitly
|
"discount3": 0.0, # Reset discount3 explicitly
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# All fields should be updated correctly
|
# All fields should be updated correctly
|
||||||
self.assertEqual(self.invoice_line.quantity, 8)
|
self.assertEqual(self.invoice_line.quantity, 8)
|
||||||
|
|
@ -210,4 +182,6 @@ class TestAccountMove(TransactionCase):
|
||||||
|
|
||||||
# Calculate expected subtotal: 8 * 180 * (1-0.12) * (1-0.06)
|
# Calculate expected subtotal: 8 * 180 * (1-0.12) * (1-0.06)
|
||||||
expected = 8 * 180 * 0.88 * 0.94
|
expected = 8 * 180 * 0.88 * 0.94
|
||||||
self.assertAlmostEqual(self.invoice_line.price_subtotal, expected, places=2)
|
self.assertAlmostEqual(
|
||||||
|
self.invoice_line.price_subtotal, expected, places=2
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,36 +12,27 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create a supplier
|
# Create a supplier
|
||||||
cls.supplier = cls.env["res.partner"].create(
|
cls.supplier = cls.env["res.partner"].create({
|
||||||
{
|
|
||||||
"name": "Test Supplier",
|
"name": "Test Supplier",
|
||||||
"email": "supplier@test.com",
|
"email": "supplier@test.com",
|
||||||
"supplier_rank": 1,
|
"supplier_rank": 1,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create a product template first, then get the variant
|
# Create a product
|
||||||
cls.product_template = cls.env["product.template"].create(
|
cls.product = cls.env["product.product"].create({
|
||||||
{
|
|
||||||
"name": "Test Product PO",
|
"name": "Test Product PO",
|
||||||
"type": "consu",
|
"type": "product",
|
||||||
"list_price": 150.0,
|
"list_price": 150.0,
|
||||||
"standard_price": 80.0,
|
"standard_price": 80.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
# Get the auto-created product variant
|
|
||||||
cls.product = cls.product_template.product_variant_ids[0]
|
|
||||||
|
|
||||||
# Create a purchase order
|
# Create a purchase order
|
||||||
cls.purchase_order = cls.env["purchase.order"].create(
|
cls.purchase_order = cls.env["purchase.order"].create({
|
||||||
{
|
|
||||||
"partner_id": cls.supplier.id,
|
"partner_id": cls.supplier.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create purchase order line
|
# Create purchase order line
|
||||||
cls.po_line = cls.env["purchase.order.line"].create(
|
cls.po_line = cls.env["purchase.order.line"].create({
|
||||||
{
|
|
||||||
"order_id": cls.purchase_order.id,
|
"order_id": cls.purchase_order.id,
|
||||||
"product_id": cls.product.id,
|
"product_id": cls.product.id,
|
||||||
"product_qty": 10,
|
"product_qty": 10,
|
||||||
|
|
@ -49,8 +40,7 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 2.0,
|
"discount3": 2.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
def test_po_line_discount_readonly(self):
|
def test_po_line_discount_readonly(self):
|
||||||
"""Test that discount field is readonly in PO lines"""
|
"""Test that discount field is readonly in PO lines"""
|
||||||
|
|
@ -59,14 +49,12 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
|
|
||||||
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
|
"discount": 25.0, # Should be ignored
|
||||||
"discount1": 12.0,
|
"discount1": 12.0,
|
||||||
"discount2": 8.0,
|
"discount2": 8.0,
|
||||||
"discount3": 4.0,
|
"discount3": 4.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -74,11 +62,9 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
|
|
||||||
def test_po_line_legacy_discount(self):
|
def test_po_line_legacy_discount(self):
|
||||||
"""Test legacy discount behavior in PO lines"""
|
"""Test legacy discount behavior in PO lines"""
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount": 18.0,
|
"discount": 18.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Should map to discount1 and reset others
|
# Should map to discount1 and reset others
|
||||||
self.assertEqual(self.po_line.discount1, 18.0)
|
self.assertEqual(self.po_line.discount1, 18.0)
|
||||||
|
|
@ -87,25 +73,24 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
|
|
||||||
def test_po_line_price_calculation(self):
|
def test_po_line_price_calculation(self):
|
||||||
"""Test that price subtotal is calculated correctly with triple 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,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
"discount3": 5.0,
|
"discount3": 5.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Base: 10 * 150 = 1500
|
# Base: 10 * 150 = 1500
|
||||||
# After 15% discount: 1275
|
# After 15% discount: 1275
|
||||||
# After 10% discount: 1147.5
|
# After 10% discount: 1147.5
|
||||||
# After 5% discount: 1090.125
|
# After 5% discount: 1090.125
|
||||||
expected_subtotal = 10 * 150 * 0.85 * 0.90 * 0.95
|
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):
|
||||||
"""Test multiple PO lines with different discounts"""
|
"""Test multiple PO lines with different discounts"""
|
||||||
line2 = self.env["purchase.order.line"].create(
|
line2 = self.env["purchase.order.line"].create({
|
||||||
{
|
|
||||||
"order_id": self.purchase_order.id,
|
"order_id": self.purchase_order.id,
|
||||||
"product_id": self.product.id,
|
"product_id": self.product.id,
|
||||||
"product_qty": 5,
|
"product_qty": 5,
|
||||||
|
|
@ -113,8 +98,7 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
"discount1": 20.0,
|
"discount1": 20.0,
|
||||||
"discount2": 15.0,
|
"discount2": 15.0,
|
||||||
"discount3": 10.0,
|
"discount3": 10.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Verify both lines have correct discounts
|
# Verify both lines have correct discounts
|
||||||
self.assertEqual(self.po_line.discount1, 15.0)
|
self.assertEqual(self.po_line.discount1, 15.0)
|
||||||
|
|
@ -127,11 +111,9 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
initial_discount1 = self.po_line.discount1
|
initial_discount1 = self.po_line.discount1
|
||||||
initial_discount2 = self.po_line.discount2
|
initial_discount2 = self.po_line.discount2
|
||||||
|
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"product_qty": 20,
|
"product_qty": 20,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Discounts should remain unchanged
|
# Discounts should remain unchanged
|
||||||
self.assertEqual(self.po_line.discount1, initial_discount1)
|
self.assertEqual(self.po_line.discount1, initial_discount1)
|
||||||
|
|
@ -143,11 +125,9 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
"""Test updating price doesn't affect discounts"""
|
"""Test updating price doesn't affect discounts"""
|
||||||
initial_discount1 = self.po_line.discount1
|
initial_discount1 = self.po_line.discount1
|
||||||
|
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"price_unit": 200.0,
|
"price_unit": 200.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Discount should remain unchanged
|
# Discount should remain unchanged
|
||||||
self.assertEqual(self.po_line.discount1, initial_discount1)
|
self.assertEqual(self.po_line.discount1, initial_discount1)
|
||||||
|
|
@ -156,13 +136,11 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
|
|
||||||
def test_po_with_zero_discounts(self):
|
def test_po_with_zero_discounts(self):
|
||||||
"""Test PO line with all zero discounts"""
|
"""Test PO line with all zero discounts"""
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 0.0,
|
"discount1": 0.0,
|
||||||
"discount2": 0.0,
|
"discount2": 0.0,
|
||||||
"discount3": 0.0,
|
"discount3": 0.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# All discounts should be zero
|
# All discounts should be zero
|
||||||
self.assertEqual(self.po_line.discount, 0.0)
|
self.assertEqual(self.po_line.discount, 0.0)
|
||||||
|
|
@ -177,15 +155,13 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
def test_po_line_combined_operations(self):
|
def test_po_line_combined_operations(self):
|
||||||
"""Test combined operations on PO line"""
|
"""Test combined operations on PO line"""
|
||||||
# Update multiple fields at once
|
# Update multiple fields at once
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"product_qty": 15,
|
"product_qty": 15,
|
||||||
"price_unit": 175.0,
|
"price_unit": 175.0,
|
||||||
"discount1": 18.0,
|
"discount1": 18.0,
|
||||||
"discount2": 12.0,
|
"discount2": 12.0,
|
||||||
"discount3": 6.0,
|
"discount3": 6.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# All fields should be updated correctly
|
# All fields should be updated correctly
|
||||||
self.assertEqual(self.po_line.product_qty, 15)
|
self.assertEqual(self.po_line.product_qty, 15)
|
||||||
|
|
@ -196,17 +172,17 @@ class TestPurchaseOrder(TransactionCase):
|
||||||
|
|
||||||
# Calculate expected subtotal
|
# Calculate expected subtotal
|
||||||
expected = 15 * 175 * 0.82 * 0.88 * 0.94
|
expected = 15 * 175 * 0.82 * 0.88 * 0.94
|
||||||
self.assertAlmostEqual(self.po_line.price_subtotal, expected, places=2)
|
self.assertAlmostEqual(
|
||||||
|
self.po_line.price_subtotal, expected, places=2
|
||||||
|
)
|
||||||
|
|
||||||
def test_po_confirm_with_discounts(self):
|
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(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 2.0,
|
"discount3": 2.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Confirm the purchase order
|
# Confirm the purchase order
|
||||||
self.purchase_order.button_confirm()
|
self.purchase_order.button_confirm()
|
||||||
|
|
|
||||||
|
|
@ -12,34 +12,25 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create a partner
|
# Create a partner
|
||||||
cls.partner = cls.env["res.partner"].create(
|
cls.partner = cls.env["res.partner"].create({
|
||||||
{
|
|
||||||
"name": "Test Partner",
|
"name": "Test Partner",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create a product template first, then get the variant
|
# Create a product
|
||||||
cls.product_template = cls.env["product.template"].create(
|
cls.product = cls.env["product.product"].create({
|
||||||
{
|
|
||||||
"name": "Test Product",
|
"name": "Test Product",
|
||||||
"type": "consu",
|
"type": "product",
|
||||||
"list_price": 100.0,
|
"list_price": 100.0,
|
||||||
"standard_price": 50.0,
|
"standard_price": 50.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
# Get the auto-created product variant
|
|
||||||
cls.product = cls.product_template.product_variant_ids[0]
|
|
||||||
|
|
||||||
# Create a purchase order
|
# Create a purchase order
|
||||||
cls.purchase_order = cls.env["purchase.order"].create(
|
cls.purchase_order = cls.env["purchase.order"].create({
|
||||||
{
|
|
||||||
"partner_id": cls.partner.id,
|
"partner_id": cls.partner.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Create a purchase order line
|
# Create a purchase order line
|
||||||
cls.po_line = cls.env["purchase.order.line"].create(
|
cls.po_line = cls.env["purchase.order.line"].create({
|
||||||
{
|
|
||||||
"order_id": cls.purchase_order.id,
|
"order_id": cls.purchase_order.id,
|
||||||
"product_id": cls.product.id,
|
"product_id": cls.product.id,
|
||||||
"product_qty": 10,
|
"product_qty": 10,
|
||||||
|
|
@ -47,8 +38,7 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 2.0,
|
"discount3": 2.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
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"""
|
||||||
|
|
@ -58,14 +48,12 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
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.po_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,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
"discount3": 5.0,
|
"discount3": 5.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Verify explicit discounts were applied
|
# Verify explicit discounts were applied
|
||||||
self.assertEqual(self.po_line.discount1, 15.0)
|
self.assertEqual(self.po_line.discount1, 15.0)
|
||||||
|
|
@ -79,12 +67,10 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
def test_write_only_discount1(self):
|
def test_write_only_discount1(self):
|
||||||
"""Test writing only discount1 explicitly"""
|
"""Test writing only discount1 explicitly"""
|
||||||
self.po_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,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Only discount1 should change
|
# Only discount1 should change
|
||||||
self.assertEqual(self.po_line.discount1, 20.0)
|
self.assertEqual(self.po_line.discount1, 20.0)
|
||||||
|
|
@ -94,12 +80,10 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
def test_write_only_discount2(self):
|
def test_write_only_discount2(self):
|
||||||
"""Test writing only discount2 explicitly"""
|
"""Test writing only discount2 explicitly"""
|
||||||
self.po_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
|
# Only discount2 should change
|
||||||
self.assertEqual(self.po_line.discount2, 12.0)
|
self.assertEqual(self.po_line.discount2, 12.0)
|
||||||
|
|
@ -109,12 +93,10 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
def test_write_only_discount3(self):
|
def test_write_only_discount3(self):
|
||||||
"""Test writing only discount3 explicitly"""
|
"""Test writing only discount3 explicitly"""
|
||||||
self.po_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
|
# Only discount3 should change
|
||||||
self.assertEqual(self.po_line.discount3, 8.0)
|
self.assertEqual(self.po_line.discount3, 8.0)
|
||||||
|
|
@ -125,20 +107,16 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
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.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 2.0,
|
"discount3": 2.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Write only discount (legacy behavior)
|
# Write only discount (legacy behavior)
|
||||||
self.po_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.po_line.discount1, 25.0)
|
self.assertEqual(self.po_line.discount1, 25.0)
|
||||||
|
|
@ -148,23 +126,19 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
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.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(self.po_line.discount1, 10.0)
|
self.assertEqual(self.po_line.discount1, 10.0)
|
||||||
self.assertEqual(self.po_line.discount2, 10.0)
|
self.assertEqual(self.po_line.discount2, 10.0)
|
||||||
|
|
||||||
# Second write
|
# Second write
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount": 5.0,
|
"discount": 5.0,
|
||||||
"discount3": 5.0,
|
"discount3": 5.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# discount3 should change, others remain
|
# discount3 should change, others remain
|
||||||
self.assertEqual(self.po_line.discount1, 10.0)
|
self.assertEqual(self.po_line.discount1, 10.0)
|
||||||
|
|
@ -173,13 +147,11 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
def test_write_zero_discounts(self):
|
def test_write_zero_discounts(self):
|
||||||
"""Test writing zero discounts"""
|
"""Test writing zero discounts"""
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 0.0,
|
"discount1": 0.0,
|
||||||
"discount2": 0.0,
|
"discount2": 0.0,
|
||||||
"discount3": 0.0,
|
"discount3": 0.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(self.po_line.discount1, 0.0)
|
self.assertEqual(self.po_line.discount1, 0.0)
|
||||||
self.assertEqual(self.po_line.discount2, 0.0)
|
self.assertEqual(self.po_line.discount2, 0.0)
|
||||||
|
|
@ -189,21 +161,17 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
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.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 15.0,
|
"discount1": 15.0,
|
||||||
"discount2": 5.0,
|
"discount2": 5.0,
|
||||||
"discount3": 0.0,
|
"discount3": 0.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# 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.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount": 50.0,
|
"discount": 50.0,
|
||||||
"discount1": 20.0,
|
"discount1": 20.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# discount1 should be updated, others unchanged
|
# discount1 should be updated, others unchanged
|
||||||
self.assertEqual(self.po_line.discount1, 20.0)
|
self.assertEqual(self.po_line.discount1, 20.0)
|
||||||
|
|
@ -212,13 +180,11 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
|
|
||||||
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.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"discount1": 10.0,
|
"discount1": 10.0,
|
||||||
"discount2": 10.0,
|
"discount2": 10.0,
|
||||||
"discount3": 10.0,
|
"discount3": 10.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
@ -229,12 +195,10 @@ class TestTripleDiscountMixin(TransactionCase):
|
||||||
initial_discount1 = self.po_line.discount1
|
initial_discount1 = self.po_line.discount1
|
||||||
|
|
||||||
# Write other fields
|
# Write other fields
|
||||||
self.po_line.write(
|
self.po_line.write({
|
||||||
{
|
|
||||||
"product_qty": 20,
|
"product_qty": 20,
|
||||||
"price_unit": 150.0,
|
"price_unit": 150.0,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Discounts should remain unchanged
|
# Discounts should remain unchanged
|
||||||
self.assertEqual(self.po_line.discount1, initial_discount1)
|
self.assertEqual(self.po_line.discount1, initial_discount1)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
# Copyright 2026 Your Company
|
# Copyright 2026 Your Company
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _
|
from odoo import _, api, fields, models
|
||||||
from odoo import api
|
|
||||||
from odoo import fields
|
|
||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class ResPartner(models.Model):
|
class ResPartner(models.Model):
|
||||||
"""Extend res.partner with default price category for suppliers."""
|
"""Extend res.partner with default price category for suppliers."""
|
||||||
|
|
||||||
_inherit = "res.partner"
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
default_price_category_id = fields.Many2one(
|
default_price_category_id = fields.Many2one(
|
||||||
comodel_name="product.price.category",
|
comodel_name='product.price.category',
|
||||||
string="Default Price Category",
|
string='Default Price Category',
|
||||||
help="Default price category for products from this supplier",
|
help='Default price category for products from this supplier',
|
||||||
domain=[],
|
domain=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -24,26 +21,24 @@ class ResPartner(models.Model):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
# Count products where this partner is the default supplier
|
# Count products where this partner is the default supplier
|
||||||
product_count = self.env["product.template"].search_count(
|
product_count = self.env['product.template'].search_count([
|
||||||
[("main_seller_id", "=", self.id)]
|
('main_seller_id', '=', self.id)
|
||||||
)
|
])
|
||||||
|
|
||||||
# Create wizard record with context data
|
# Create wizard record with context data
|
||||||
wizard = self.env["wizard.update.product.category"].create(
|
wizard = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': self.id,
|
||||||
"partner_id": self.id,
|
'partner_name': self.name,
|
||||||
"partner_name": self.name,
|
'price_category_id': self.default_price_category_id.id,
|
||||||
"price_category_id": self.default_price_category_id.id,
|
'product_count': product_count,
|
||||||
"product_count": product_count,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return action to open wizard modal
|
# Return action to open wizard modal
|
||||||
return {
|
return {
|
||||||
"type": "ir.actions.act_window",
|
'type': 'ir.actions.act_window',
|
||||||
"name": _("Update Product Price Category"),
|
'name': _('Update Product Price Category'),
|
||||||
"res_model": "wizard.update.product.category",
|
'res_model': 'wizard.update.product.category',
|
||||||
"res_id": wizard.id,
|
'res_id': wizard.id,
|
||||||
"view_mode": "form",
|
'view_mode': 'form',
|
||||||
"target": "new",
|
'target': 'new',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,37 @@
|
||||||
# Copyright 2026 Your Company
|
# Copyright 2026 Your Company
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _
|
from odoo import _, api, fields, models
|
||||||
from odoo import api
|
|
||||||
from odoo import fields
|
|
||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class WizardUpdateProductCategory(models.TransientModel):
|
class WizardUpdateProductCategory(models.TransientModel):
|
||||||
"""Wizard to confirm and bulk update product price categories."""
|
"""Wizard to confirm and bulk update product price categories."""
|
||||||
|
|
||||||
_name = "wizard.update.product.category"
|
_name = 'wizard.update.product.category'
|
||||||
_description = "Update Product Price Category"
|
_description = 'Update Product Price Category'
|
||||||
|
|
||||||
partner_id = fields.Many2one(
|
partner_id = fields.Many2one(
|
||||||
comodel_name="res.partner",
|
comodel_name='res.partner',
|
||||||
string="Supplier",
|
string='Supplier',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
partner_name = fields.Char(
|
partner_name = fields.Char(
|
||||||
string="Supplier Name",
|
string='Supplier Name',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
related="partner_id.name",
|
related='partner_id.name',
|
||||||
)
|
)
|
||||||
|
|
||||||
price_category_id = fields.Many2one(
|
price_category_id = fields.Many2one(
|
||||||
comodel_name="product.price.category",
|
comodel_name='product.price.category',
|
||||||
string="Price Category",
|
string='Price Category',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
product_count = fields.Integer(
|
product_count = fields.Integer(
|
||||||
string="Number of Products",
|
string='Number of Products',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
@ -44,33 +41,36 @@ class WizardUpdateProductCategory(models.TransientModel):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
# Search all products where this partner is the default supplier
|
# Search all products where this partner is the default supplier
|
||||||
products = self.env["product.template"].search(
|
products = self.env['product.template'].search([
|
||||||
[("main_seller_id", "=", self.partner_id.id)]
|
('main_seller_id', '=', self.partner_id.id)
|
||||||
)
|
])
|
||||||
|
|
||||||
if not products:
|
if not products:
|
||||||
return {
|
return {
|
||||||
"type": "ir.actions.client",
|
'type': 'ir.actions.client',
|
||||||
"tag": "display_notification",
|
'tag': 'display_notification',
|
||||||
"params": {
|
'params': {
|
||||||
"title": _("No Products"),
|
'title': _('No Products'),
|
||||||
"message": _("No products found with this supplier."),
|
'message': _('No products found with this supplier.'),
|
||||||
"type": "warning",
|
'type': 'warning',
|
||||||
"sticky": False,
|
'sticky': False,
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Bulk update all products
|
# Bulk update all products
|
||||||
products.write({"price_category_id": self.price_category_id.id})
|
products.write({
|
||||||
|
'price_category_id': self.price_category_id.id
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": "ir.actions.client",
|
'type': 'ir.actions.client',
|
||||||
"tag": "display_notification",
|
'tag': 'display_notification',
|
||||||
"params": {
|
'params': {
|
||||||
"title": _("Success"),
|
'title': _('Success'),
|
||||||
"message": _('%d products updated with category "%s".')
|
'message': _(
|
||||||
% (len(products), self.price_category_id.display_name),
|
'%d products updated with category "%s".'
|
||||||
"type": "success",
|
) % (len(products), self.price_category_id.display_name),
|
||||||
"sticky": False,
|
'type': 'success',
|
||||||
},
|
'sticky': False,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
class TestProductPriceCategorySupplier(TransactionCase):
|
class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
|
|
@ -13,127 +14,68 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
# Create price categories
|
# Create price categories
|
||||||
cls.category_premium = cls.env["product.price.category"].create(
|
cls.category_premium = cls.env['product.price.category'].create({
|
||||||
{
|
'name': 'Premium',
|
||||||
"name": "Premium",
|
})
|
||||||
}
|
cls.category_standard = cls.env['product.price.category'].create({
|
||||||
)
|
'name': 'Standard',
|
||||||
cls.category_standard = cls.env["product.price.category"].create(
|
})
|
||||||
{
|
|
||||||
"name": "Standard",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create suppliers
|
# Create suppliers
|
||||||
cls.supplier_a = cls.env["res.partner"].create(
|
cls.supplier_a = cls.env['res.partner'].create({
|
||||||
{
|
'name': 'Supplier A',
|
||||||
"name": "Supplier A",
|
'supplier_rank': 1,
|
||||||
"supplier_rank": 1,
|
'default_price_category_id': cls.category_premium.id,
|
||||||
"default_price_category_id": cls.category_premium.id,
|
})
|
||||||
}
|
cls.supplier_b = cls.env['res.partner'].create({
|
||||||
)
|
'name': 'Supplier B',
|
||||||
cls.supplier_b = cls.env["res.partner"].create(
|
'supplier_rank': 1,
|
||||||
{
|
'default_price_category_id': cls.category_standard.id,
|
||||||
"name": "Supplier B",
|
})
|
||||||
"supplier_rank": 1,
|
|
||||||
"default_price_category_id": cls.category_standard.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a non-supplier partner
|
# Create a non-supplier partner
|
||||||
cls.customer = cls.env["res.partner"].create(
|
cls.customer = cls.env['res.partner'].create({
|
||||||
{
|
'name': 'Customer A',
|
||||||
"name": "Customer A",
|
'customer_rank': 1,
|
||||||
"customer_rank": 1,
|
'supplier_rank': 0,
|
||||||
"supplier_rank": 0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create products with supplier A as default (with seller_ids)
|
# Create products with supplier A as default
|
||||||
cls.product_1 = cls.env["product.template"].create(
|
cls.product_1 = cls.env['product.template'].create({
|
||||||
{
|
'name': 'Product 1',
|
||||||
"name": "Product 1",
|
'default_supplier_id': cls.supplier_a.id,
|
||||||
"seller_ids": [
|
})
|
||||||
(
|
cls.product_2 = cls.env['product.template'].create({
|
||||||
0,
|
'name': 'Product 2',
|
||||||
0,
|
'default_supplier_id': cls.supplier_a.id,
|
||||||
{
|
})
|
||||||
"partner_id": cls.supplier_a.id,
|
cls.product_3 = cls.env['product.template'].create({
|
||||||
"sequence": 10,
|
'name': 'Product 3',
|
||||||
"min_qty": 0,
|
'default_supplier_id': cls.supplier_a.id,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cls.product_2 = cls.env["product.template"].create(
|
|
||||||
{
|
|
||||||
"name": "Product 2",
|
|
||||||
"seller_ids": [
|
|
||||||
(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"partner_id": cls.supplier_a.id,
|
|
||||||
"sequence": 10,
|
|
||||||
"min_qty": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cls.product_3 = cls.env["product.template"].create(
|
|
||||||
{
|
|
||||||
"name": "Product 3",
|
|
||||||
"seller_ids": [
|
|
||||||
(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"partner_id": cls.supplier_a.id,
|
|
||||||
"sequence": 10,
|
|
||||||
"min_qty": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create product with supplier B
|
# Create product with supplier B
|
||||||
cls.product_4 = cls.env["product.template"].create(
|
cls.product_4 = cls.env['product.template'].create({
|
||||||
{
|
'name': 'Product 4',
|
||||||
"name": "Product 4",
|
'default_supplier_id': cls.supplier_b.id,
|
||||||
"seller_ids": [
|
})
|
||||||
(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"partner_id": cls.supplier_b.id,
|
|
||||||
"sequence": 10,
|
|
||||||
"min_qty": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create product without supplier
|
# Create product without supplier
|
||||||
cls.product_5 = cls.env["product.template"].create(
|
cls.product_5 = cls.env['product.template'].create({
|
||||||
{
|
'name': 'Product 5',
|
||||||
"name": "Product 5",
|
'default_supplier_id': False,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
def test_01_supplier_has_default_price_category_field(self):
|
def test_01_supplier_has_default_price_category_field(self):
|
||||||
"""Test that supplier has default_price_category_id field."""
|
"""Test that supplier has default_price_category_id field."""
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
hasattr(self.supplier_a, "default_price_category_id"),
|
hasattr(self.supplier_a, 'default_price_category_id'),
|
||||||
"Supplier should have default_price_category_id field",
|
'Supplier should have default_price_category_id field'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.supplier_a.default_price_category_id.id,
|
self.supplier_a.default_price_category_id.id,
|
||||||
self.category_premium.id,
|
self.category_premium.id,
|
||||||
"Supplier should have Premium category assigned",
|
'Supplier should have Premium category assigned'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_02_action_update_products_opens_wizard(self):
|
def test_02_action_update_products_opens_wizard(self):
|
||||||
|
|
@ -141,19 +83,21 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
action = self.supplier_a.action_update_products_price_category()
|
action = self.supplier_a.action_update_products_price_category()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
action["type"], "ir.actions.act_window", "Action should be a window action"
|
action['type'], 'ir.actions.act_window',
|
||||||
|
'Action should be a window action'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
action["res_model"],
|
action['res_model'], 'wizard.update.product.category',
|
||||||
"wizard.update.product.category",
|
'Action should open wizard model'
|
||||||
"Action should open wizard model",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
action["target"], "new", "Action should open in modal (target=new)"
|
action['target'], 'new',
|
||||||
|
'Action should open in modal (target=new)'
|
||||||
)
|
)
|
||||||
self.assertIn("res_id", action, "Action should have res_id")
|
self.assertIn('res_id', action, 'Action should have res_id')
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
action["res_id"] > 0, "res_id should be a valid wizard record ID"
|
action['res_id'] > 0,
|
||||||
|
'res_id should be a valid wizard record ID'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_03_wizard_counts_products_correctly(self):
|
def test_03_wizard_counts_products_correctly(self):
|
||||||
|
|
@ -161,18 +105,19 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
action = self.supplier_a.action_update_products_price_category()
|
action = self.supplier_a.action_update_products_price_category()
|
||||||
|
|
||||||
# Get the wizard record that was created
|
# Get the wizard record that was created
|
||||||
wizard = self.env["wizard.update.product.category"].browse(action["res_id"])
|
wizard = self.env['wizard.update.product.category'].browse(action['res_id'])
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
wizard.product_count, 3, "Wizard should count 3 products from Supplier A"
|
wizard.product_count, 3,
|
||||||
|
'Wizard should count 3 products from Supplier A'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
wizard.partner_name, "Supplier A", "Wizard should display supplier name"
|
wizard.partner_name, 'Supplier A',
|
||||||
|
'Wizard should display supplier name'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
wizard.price_category_id.id,
|
wizard.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Wizard should have Premium category from supplier'
|
||||||
"Wizard should have Premium category from supplier",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_04_wizard_updates_all_products_from_supplier(self):
|
def test_04_wizard_updates_all_products_from_supplier(self):
|
||||||
|
|
@ -180,82 +125,75 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
# Verify initial state - no categories assigned
|
# Verify initial state - no categories assigned
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
self.product_1.price_category_id,
|
self.product_1.price_category_id,
|
||||||
"Product 1 should not have category initially",
|
'Product 1 should not have category initially'
|
||||||
)
|
)
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
self.product_2.price_category_id,
|
self.product_2.price_category_id,
|
||||||
"Product 2 should not have category initially",
|
'Product 2 should not have category initially'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create and execute wizard
|
# Create and execute wizard
|
||||||
wizard = self.env["wizard.update.product.category"].create(
|
wizard = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': self.supplier_a.id,
|
||||||
"partner_id": self.supplier_a.id,
|
'price_category_id': self.category_premium.id,
|
||||||
"price_category_id": self.category_premium.id,
|
'product_count': 3,
|
||||||
"product_count": 3,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
result = wizard.action_confirm()
|
result = wizard.action_confirm()
|
||||||
|
|
||||||
# Verify products were updated
|
# Verify products were updated
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_1.price_category_id.id,
|
self.product_1.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Product 1 should have Premium category'
|
||||||
"Product 1 should have Premium category",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_2.price_category_id.id,
|
self.product_2.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Product 2 should have Premium category'
|
||||||
"Product 2 should have Premium category",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_3.price_category_id.id,
|
self.product_3.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Product 3 should have Premium category'
|
||||||
"Product 3 should have Premium category",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify product from other supplier was NOT updated
|
# Verify product from other supplier was NOT updated
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
self.product_4.price_category_id,
|
self.product_4.price_category_id,
|
||||||
"Product 4 (from Supplier B) should not be updated",
|
'Product 4 (from Supplier B) should not be updated'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify success notification
|
# Verify success notification
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["type"], "ir.actions.client", "Result should be a client action"
|
result['type'], 'ir.actions.client',
|
||||||
|
'Result should be a client action'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["tag"],
|
result['tag'], 'display_notification',
|
||||||
"display_notification",
|
'Result should display a notification'
|
||||||
"Result should display a notification",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_05_wizard_handles_supplier_with_no_products(self):
|
def test_05_wizard_handles_supplier_with_no_products(self):
|
||||||
"""Test wizard behavior when supplier has no products."""
|
"""Test wizard behavior when supplier has no products."""
|
||||||
# Create supplier without products
|
# Create supplier without products
|
||||||
supplier_no_products = self.env["res.partner"].create(
|
supplier_no_products = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Supplier No Products',
|
||||||
"name": "Supplier No Products",
|
'supplier_rank': 1,
|
||||||
"supplier_rank": 1,
|
'default_price_category_id': self.category_standard.id,
|
||||||
"default_price_category_id": self.category_standard.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
wizard = self.env["wizard.update.product.category"].create(
|
wizard = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': supplier_no_products.id,
|
||||||
"partner_id": supplier_no_products.id,
|
'price_category_id': self.category_standard.id,
|
||||||
"price_category_id": self.category_standard.id,
|
'product_count': 0,
|
||||||
"product_count": 0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
result = wizard.action_confirm()
|
result = wizard.action_confirm()
|
||||||
|
|
||||||
# Verify warning notification
|
# Verify warning notification
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["type"], "ir.actions.client", "Result should be a client action"
|
result['type'], 'ir.actions.client',
|
||||||
|
'Result should be a client action'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["params"]["type"], "warning", "Should display warning notification"
|
result['params']['type'], 'warning',
|
||||||
|
'Should display warning notification'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_06_customer_does_not_show_price_category_field(self):
|
def test_06_customer_does_not_show_price_category_field(self):
|
||||||
|
|
@ -263,10 +201,11 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
# This is a view-level test - we verify the field exists but logic is correct
|
# This is a view-level test - we verify the field exists but logic is correct
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
self.customer.default_price_category_id,
|
self.customer.default_price_category_id,
|
||||||
"Customer should not have price category set",
|
'Customer should not have price category set'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.customer.supplier_rank, 0, "Customer should have supplier_rank = 0"
|
self.customer.supplier_rank, 0,
|
||||||
|
'Customer should have supplier_rank = 0'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_07_wizard_overwrites_existing_categories(self):
|
def test_07_wizard_overwrites_existing_categories(self):
|
||||||
|
|
@ -276,82 +215,68 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
self.product_2.price_category_id = self.category_standard.id
|
self.product_2.price_category_id = self.category_standard.id
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_1.price_category_id.id,
|
self.product_1.price_category_id.id, self.category_standard.id,
|
||||||
self.category_standard.id,
|
'Product 1 should have Standard category initially'
|
||||||
"Product 1 should have Standard category initially",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute wizard to change to Premium
|
# Execute wizard to change to Premium
|
||||||
wizard = self.env["wizard.update.product.category"].create(
|
wizard = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': self.supplier_a.id,
|
||||||
"partner_id": self.supplier_a.id,
|
'price_category_id': self.category_premium.id,
|
||||||
"price_category_id": self.category_premium.id,
|
'product_count': 3,
|
||||||
"product_count": 3,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
wizard.action_confirm()
|
wizard.action_confirm()
|
||||||
|
|
||||||
# Verify categories were overwritten
|
# Verify categories were overwritten
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_1.price_category_id.id,
|
self.product_1.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Product 1 category should be overwritten to Premium'
|
||||||
"Product 1 category should be overwritten to Premium",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_2.price_category_id.id,
|
self.product_2.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Product 2 category should be overwritten to Premium'
|
||||||
"Product 2 category should be overwritten to Premium",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_08_multiple_suppliers_independent_updates(self):
|
def test_08_multiple_suppliers_independent_updates(self):
|
||||||
"""Test that updating one supplier doesn't affect other suppliers' products."""
|
"""Test that updating one supplier doesn't affect other suppliers' products."""
|
||||||
# Update Supplier A products
|
# Update Supplier A products
|
||||||
wizard_a = self.env["wizard.update.product.category"].create(
|
wizard_a = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': self.supplier_a.id,
|
||||||
"partner_id": self.supplier_a.id,
|
'price_category_id': self.category_premium.id,
|
||||||
"price_category_id": self.category_premium.id,
|
'product_count': 3,
|
||||||
"product_count": 3,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
wizard_a.action_confirm()
|
wizard_a.action_confirm()
|
||||||
|
|
||||||
# Update Supplier B products
|
# Update Supplier B products
|
||||||
wizard_b = self.env["wizard.update.product.category"].create(
|
wizard_b = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': self.supplier_b.id,
|
||||||
"partner_id": self.supplier_b.id,
|
'price_category_id': self.category_standard.id,
|
||||||
"price_category_id": self.category_standard.id,
|
'product_count': 1,
|
||||||
"product_count": 1,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
wizard_b.action_confirm()
|
wizard_b.action_confirm()
|
||||||
|
|
||||||
# Verify each supplier's products have correct category
|
# Verify each supplier's products have correct category
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_1.price_category_id.id,
|
self.product_1.price_category_id.id, self.category_premium.id,
|
||||||
self.category_premium.id,
|
'Supplier A products should have Premium'
|
||||||
"Supplier A products should have Premium",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.product_4.price_category_id.id,
|
self.product_4.price_category_id.id, self.category_standard.id,
|
||||||
self.category_standard.id,
|
'Supplier B products should have Standard'
|
||||||
"Supplier B products should have Standard",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_09_wizard_readonly_fields(self):
|
def test_09_wizard_readonly_fields(self):
|
||||||
"""Test that wizard display fields are readonly."""
|
"""Test that wizard display fields are readonly."""
|
||||||
wizard = self.env["wizard.update.product.category"].create(
|
wizard = self.env['wizard.update.product.category'].create({
|
||||||
{
|
'partner_id': self.supplier_a.id,
|
||||||
"partner_id": self.supplier_a.id,
|
'price_category_id': self.category_premium.id,
|
||||||
"price_category_id": self.category_premium.id,
|
'product_count': 3,
|
||||||
"product_count": 3,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify partner_name is computed from partner_id
|
# Verify partner_name is computed from partner_id
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
wizard.partner_name,
|
wizard.partner_name, 'Supplier A',
|
||||||
"Supplier A",
|
'partner_name should be related to partner_id.name'
|
||||||
"partner_name should be related to partner_id.name",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_10_action_counts_products_correctly(self):
|
def test_10_action_counts_products_correctly(self):
|
||||||
|
|
@ -359,16 +284,18 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
||||||
action = self.supplier_a.action_update_products_price_category()
|
action = self.supplier_a.action_update_products_price_category()
|
||||||
|
|
||||||
# Get the wizard that was created
|
# Get the wizard that was created
|
||||||
wizard = self.env["wizard.update.product.category"].browse(action["res_id"])
|
wizard = self.env['wizard.update.product.category'].browse(action['res_id'])
|
||||||
|
|
||||||
# Count products manually
|
# Count products manually
|
||||||
actual_count = self.env["product.template"].search_count(
|
actual_count = self.env['product.template'].search_count([
|
||||||
[("main_seller_id", "=", self.supplier_a.id)]
|
('default_supplier_id', '=', self.supplier_a.id)
|
||||||
)
|
])
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
wizard.product_count,
|
wizard.product_count, actual_count,
|
||||||
actual_count,
|
f'Wizard should count {actual_count} products'
|
||||||
f"Wizard should count {actual_count} products",
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
wizard.product_count, 3,
|
||||||
|
'Supplier A should have 3 products'
|
||||||
)
|
)
|
||||||
self.assertEqual(wizard.product_count, 3, "Supplier A should have 3 products")
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
from odoo import api
|
from odoo import models, fields, api
|
||||||
from odoo import fields
|
|
||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class ResConfigSettings(models.TransientModel):
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class TestPricelist(TransactionCase):
|
||||||
|
|
||||||
# _compute_price should return the base price (last_purchase_price_received)
|
# _compute_price should return the base price (last_purchase_price_received)
|
||||||
result = pricelist_item._compute_price(
|
result = pricelist_item._compute_price(
|
||||||
self.product, quantity=1, uom=self.product.uom_id, date=False, currency=None
|
self.product, qty=1, uom=self.product.uom_id, date=False, currency=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should return the last purchase price as base
|
# Should return the last purchase price as base
|
||||||
|
|
@ -112,7 +112,7 @@ class TestPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
result = pricelist_item._compute_price(
|
result = pricelist_item._compute_price(
|
||||||
self.product, quantity=1, uom=self.product.uom_id, date=False, currency=None
|
self.product, qty=1, uom=self.product.uom_id, date=False, currency=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should return last_purchase_price_received
|
# Should return last_purchase_price_received
|
||||||
|
|
|
||||||
|
|
@ -203,9 +203,16 @@ class TestProductTemplate(TransactionCase):
|
||||||
|
|
||||||
def test_company_dependent_fields(self):
|
def test_company_dependent_fields(self):
|
||||||
"""Test that price fields are company dependent"""
|
"""Test that price fields are company dependent"""
|
||||||
# NOTE: company_dependent=True would require adding schema migration
|
# Verify field properties
|
||||||
# to convert existing columns in production databases. These fields
|
field_last_purchase = self.product._fields["last_purchase_price_received"]
|
||||||
# use standard float/selection storage instead.
|
field_theoritical = self.product._fields["list_price_theoritical"]
|
||||||
|
field_updated = self.product._fields["last_purchase_price_updated"]
|
||||||
|
field_compute_type = self.product._fields["last_purchase_price_compute_type"]
|
||||||
|
|
||||||
|
self.assertTrue(field_last_purchase.company_dependent)
|
||||||
|
self.assertTrue(field_theoritical.company_dependent)
|
||||||
|
self.assertTrue(field_updated.company_dependent)
|
||||||
|
self.assertTrue(field_compute_type.company_dependent)
|
||||||
|
|
||||||
def test_compute_theoritical_price_with_actual_purchase_price(self):
|
def test_compute_theoritical_price_with_actual_purchase_price(self):
|
||||||
"""Test that theoretical price is calculated correctly from last purchase price
|
"""Test that theoretical price is calculated correctly from last purchase price
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,22 @@ class TestResConfigSettings(TransactionCase):
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
cls.pricelist = cls.env["product.pricelist"].create(
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
{
|
|
||||||
"name": "Test Config Pricelist",
|
"name": "Test Config Pricelist",
|
||||||
"currency_id": cls.env.company.currency_id.id,
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
def test_config_parameter_set_and_get(self):
|
def test_config_parameter_set_and_get(self):
|
||||||
"""Test setting and getting pricelist configuration"""
|
"""Test setting and getting pricelist configuration"""
|
||||||
config = self.env["res.config.settings"].create(
|
config = self.env["res.config.settings"].create({
|
||||||
{
|
|
||||||
"product_pricelist_automatic": self.pricelist.id,
|
"product_pricelist_automatic": self.pricelist.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
config.execute()
|
config.execute()
|
||||||
|
|
||||||
# Verify parameter was saved
|
# Verify parameter was saved
|
||||||
saved_id = (
|
saved_id = self.env["ir.config_parameter"].sudo().get_param(
|
||||||
self.env["ir.config_parameter"]
|
"product_sale_price_from_pricelist.product_pricelist_automatic"
|
||||||
.sudo()
|
|
||||||
.get_param("product_sale_price_from_pricelist.product_pricelist_automatic")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(int(saved_id), self.pricelist.id)
|
self.assertEqual(int(saved_id), self.pricelist.id)
|
||||||
|
|
@ -42,7 +36,7 @@ class TestResConfigSettings(TransactionCase):
|
||||||
# Set parameter directly
|
# Set parameter directly
|
||||||
self.env["ir.config_parameter"].sudo().set_param(
|
self.env["ir.config_parameter"].sudo().set_param(
|
||||||
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
str(self.pricelist.id),
|
str(self.pricelist.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create config and check if value is loaded
|
# Create config and check if value is loaded
|
||||||
|
|
@ -53,33 +47,25 @@ class TestResConfigSettings(TransactionCase):
|
||||||
def test_config_update_pricelist(self):
|
def test_config_update_pricelist(self):
|
||||||
"""Test updating pricelist configuration"""
|
"""Test updating pricelist configuration"""
|
||||||
# Set initial pricelist
|
# Set initial pricelist
|
||||||
config = self.env["res.config.settings"].create(
|
config = self.env["res.config.settings"].create({
|
||||||
{
|
|
||||||
"product_pricelist_automatic": self.pricelist.id,
|
"product_pricelist_automatic": self.pricelist.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
config.execute()
|
config.execute()
|
||||||
|
|
||||||
# Create new pricelist and update
|
# Create new pricelist and update
|
||||||
new_pricelist = self.env["product.pricelist"].create(
|
new_pricelist = self.env["product.pricelist"].create({
|
||||||
{
|
|
||||||
"name": "New Config Pricelist",
|
"name": "New Config Pricelist",
|
||||||
"currency_id": self.env.company.currency_id.id,
|
"currency_id": self.env.company.currency_id.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
config2 = self.env["res.config.settings"].create(
|
config2 = self.env["res.config.settings"].create({
|
||||||
{
|
|
||||||
"product_pricelist_automatic": new_pricelist.id,
|
"product_pricelist_automatic": new_pricelist.id,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
config2.execute()
|
config2.execute()
|
||||||
|
|
||||||
# Verify new value
|
# Verify new value
|
||||||
saved_id = (
|
saved_id = self.env["ir.config_parameter"].sudo().get_param(
|
||||||
self.env["ir.config_parameter"]
|
"product_sale_price_from_pricelist.product_pricelist_automatic"
|
||||||
.sudo()
|
|
||||||
.get_param("product_sale_price_from_pricelist.product_pricelist_automatic")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(int(saved_id), new_pricelist.id)
|
self.assertEqual(int(saved_id), new_pricelist.id)
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,4 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ class PurchaseOrder(models.Model):
|
||||||
def _prepare_supplier_info(self, partner, line, price, currency):
|
def _prepare_supplier_info(self, partner, line, price, currency):
|
||||||
res = super()._prepare_supplier_info(partner, line, price, currency)
|
res = super()._prepare_supplier_info(partner, line, price, currency)
|
||||||
res.update(
|
res.update(
|
||||||
{fname: line[fname] for fname in line._get_multiple_discount_field_names()}
|
{
|
||||||
|
fname: line[fname]
|
||||||
|
for fname in line._get_multiple_discount_field_names()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ class PurchaseOrderLine(models.Model):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
res = super()._prepare_account_move_line(move)
|
res = super()._prepare_account_move_line(move)
|
||||||
res.update(
|
res.update(
|
||||||
{fname: self[fname] for fname in self._get_multiple_discount_field_names()}
|
{
|
||||||
|
fname: self[fname]
|
||||||
|
for fname in self._get_multiple_discount_field_names()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "2"
|
version: '2'
|
||||||
|
|
||||||
checks:
|
checks:
|
||||||
similar-code:
|
similar-code:
|
||||||
|
|
|
||||||
|
|
@ -240,29 +240,6 @@ python -m pytest website_sale_aplicoop/tests/ -v
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### 18.0.1.3.1 (2026-02-18)
|
|
||||||
- **Date Calculation Fixes (Critical)**:
|
|
||||||
- Fixed `_compute_cutoff_date` logic: Changed `days_ahead <= 0` to `days_ahead < 0` to allow cutoff_date to be the same day as today
|
|
||||||
- Enabled `store=True` for `delivery_date` field to persist calculated values and enable database filtering
|
|
||||||
- Added constraint `_check_cutoff_before_pickup` to validate that pickup_day >= cutoff_day in weekly orders
|
|
||||||
- Added `@api.onchange` methods for immediate UI feedback when changing cutoff_day or pickup_day
|
|
||||||
- **Automatic Date Updates**:
|
|
||||||
- Created daily cron job `_cron_update_dates` to automatically recalculate dates for active orders
|
|
||||||
- Ensures computed dates stay current as time passes
|
|
||||||
- **UI Improvements**:
|
|
||||||
- Added "Calculated Dates" section in form view showing readonly cutoff_date, pickup_date, and delivery_date
|
|
||||||
- Improved visibility of automatically calculated dates for administrators
|
|
||||||
- **Testing**:
|
|
||||||
- Added 6 regression tests with `@tagged('post_install', 'date_calculations')`:
|
|
||||||
- `test_cutoff_same_day_as_today_bug_fix`: Validates cutoff can be today
|
|
||||||
- `test_delivery_date_stored_correctly`: Ensures delivery_date persistence
|
|
||||||
- `test_constraint_cutoff_before_pickup_invalid`: Tests invalid configurations are rejected
|
|
||||||
- `test_constraint_cutoff_before_pickup_valid`: Tests valid configurations work
|
|
||||||
- `test_all_weekday_combinations_consistency`: Tests all 49 date combinations
|
|
||||||
- `test_cron_update_dates_executes`: Validates cron job execution
|
|
||||||
- **Documentation**:
|
|
||||||
- Documented that this is a more robust fix than v18.0.1.2.0, addressing edge cases in date calculations
|
|
||||||
|
|
||||||
### 18.0.1.3.0 (2026-02-16)
|
### 18.0.1.3.0 (2026-02-16)
|
||||||
- **Performance**: Lazy loading of products for faster page loads
|
- **Performance**: Lazy loading of products for faster page loads
|
||||||
- Configurable product pagination (default: 20 per page)
|
- Configurable product pagination (default: 20 per page)
|
||||||
|
|
@ -329,7 +306,7 @@ For issues, feature requests, or contributions:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 18.0.1.3.1
|
**Version:** 18.0.1.2.0
|
||||||
**Odoo:** 18.0+
|
**Odoo:** 18.0+
|
||||||
**License:** AGPL-3
|
**License:** AGPL-3
|
||||||
**Maintainer:** Criptomart SL
|
**Maintainer:** Criptomart SL
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
{ # noqa: B018
|
{ # noqa: B018
|
||||||
"name": "Website Sale - Aplicoop",
|
"name": "Website Sale - Aplicoop",
|
||||||
"version": "18.0.1.3.1",
|
"version": "18.0.1.1.1",
|
||||||
"category": "Website/Sale",
|
"category": "Website/Sale",
|
||||||
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
|
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"author": "Odoo Community Association (OCA), Criptomart",
|
||||||
|
|
@ -24,8 +24,6 @@
|
||||||
"data/groups.xml",
|
"data/groups.xml",
|
||||||
# Datos: Menús del website
|
# Datos: Menús del website
|
||||||
"data/website_menus.xml",
|
"data/website_menus.xml",
|
||||||
# Datos: Cron jobs
|
|
||||||
"data/cron.xml",
|
|
||||||
# Vistas de seguridad
|
# Vistas de seguridad
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
"security/record_rules.xml",
|
"security/record_rules.xml",
|
||||||
|
|
@ -50,17 +48,7 @@
|
||||||
"assets": {
|
"assets": {
|
||||||
"web.assets_frontend": [
|
"web.assets_frontend": [
|
||||||
"website_sale_aplicoop/static/src/css/website_sale.css",
|
"website_sale_aplicoop/static/src/css/website_sale.css",
|
||||||
# i18n and helpers must load first
|
|
||||||
"website_sale_aplicoop/static/src/js/i18n_manager.js",
|
|
||||||
"website_sale_aplicoop/static/src/js/i18n_helpers.js",
|
|
||||||
# Core shop functionality
|
|
||||||
"website_sale_aplicoop/static/src/js/website_sale.js",
|
|
||||||
"website_sale_aplicoop/static/src/js/checkout_labels.js",
|
|
||||||
"website_sale_aplicoop/static/src/js/home_delivery.js",
|
|
||||||
"website_sale_aplicoop/static/src/js/checkout_summary.js",
|
|
||||||
# Search and pagination
|
|
||||||
"website_sale_aplicoop/static/src/js/infinite_scroll.js",
|
"website_sale_aplicoop/static/src/js/infinite_scroll.js",
|
||||||
"website_sale_aplicoop/static/src/js/realtime_search.js",
|
|
||||||
],
|
],
|
||||||
"web.assets_tests": [
|
"web.assets_tests": [
|
||||||
"website_sale_aplicoop/static/tests/test_suite.js",
|
"website_sale_aplicoop/static/tests/test_suite.js",
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,61 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from odoo import _
|
from odoo import _
|
||||||
from odoo.http import request
|
from odoo.http import request, route
|
||||||
from odoo.http import route
|
|
||||||
|
|
||||||
from odoo.addons.sale.controllers import portal as sale_portal
|
from odoo.addons.sale.controllers import portal as sale_portal
|
||||||
|
import logging
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CustomerPortal(sale_portal.CustomerPortal):
|
class CustomerPortal(sale_portal.CustomerPortal):
|
||||||
"""Extend sale portal to include draft orders."""
|
'''Extend sale portal to include draft orders.'''
|
||||||
|
|
||||||
def _prepare_orders_domain(self, partner):
|
def _prepare_orders_domain(self, partner):
|
||||||
"""Override to include draft and done orders."""
|
'''Override to include draft and done orders.'''
|
||||||
return [
|
return [
|
||||||
("message_partner_ids", "child_of", [partner.commercial_partner_id.id]),
|
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||||
("state", "in", ["draft", "sale", "done"]), # Include draft orders
|
('state', 'in', ['draft', 'sale', 'done']), # Include draft orders
|
||||||
]
|
]
|
||||||
|
|
||||||
@route(
|
@route(['/my/orders', '/my/orders/page/<int:page>'],
|
||||||
["/my/orders", "/my/orders/page/<int:page>"],
|
type='http', auth='user', website=True)
|
||||||
type="http",
|
|
||||||
auth="user",
|
|
||||||
website=True,
|
|
||||||
)
|
|
||||||
def portal_my_orders(self, **kwargs):
|
def portal_my_orders(self, **kwargs):
|
||||||
"""Override to add translated day names to context."""
|
'''Override to add translated day names to context.'''
|
||||||
# Get values from parent
|
# Get values from parent
|
||||||
values = self._prepare_sale_portal_rendering_values(
|
values = self._prepare_sale_portal_rendering_values(quotation_page=False, **kwargs)
|
||||||
quotation_page=False, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add translated day names for pickup_day display
|
# Add translated day names for pickup_day display
|
||||||
values["day_names"] = [
|
values['day_names'] = [
|
||||||
_("Monday"),
|
_('Monday'),
|
||||||
_("Tuesday"),
|
_('Tuesday'),
|
||||||
_("Wednesday"),
|
_('Wednesday'),
|
||||||
_("Thursday"),
|
_('Thursday'),
|
||||||
_("Friday"),
|
_('Friday'),
|
||||||
_("Saturday"),
|
_('Saturday'),
|
||||||
_("Sunday"),
|
_('Sunday'),
|
||||||
]
|
]
|
||||||
|
|
||||||
request.session["my_orders_history"] = values["orders"].ids[:100]
|
request.session['my_orders_history'] = values['orders'].ids[:100]
|
||||||
return request.render("sale.portal_my_orders", values)
|
return request.render("sale.portal_my_orders", values)
|
||||||
|
|
||||||
@route(["/my/orders/<int:order_id>"], type="http", auth="public", website=True)
|
@route(['/my/orders/<int:order_id>'], type='http', auth='public', website=True)
|
||||||
def portal_order_page(self, order_id, access_token=None, **kwargs):
|
def portal_order_page(self, order_id, access_token=None, **kwargs):
|
||||||
"""Override to add translated day names for order detail page."""
|
'''Override to add translated day names for order detail page.'''
|
||||||
# Call parent to get response
|
# Call parent to get response
|
||||||
response = super().portal_order_page(
|
response = super().portal_order_page(order_id, access_token=access_token, **kwargs)
|
||||||
order_id, access_token=access_token, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# If it's a template render (not a redirect), add day_names to the context
|
# If it's a template render (not a redirect), add day_names to the context
|
||||||
if hasattr(response, "qcontext"):
|
if hasattr(response, 'qcontext'):
|
||||||
response.qcontext["day_names"] = [
|
response.qcontext['day_names'] = [
|
||||||
_("Monday"),
|
_('Monday'),
|
||||||
_("Tuesday"),
|
_('Tuesday'),
|
||||||
_("Wednesday"),
|
_('Wednesday'),
|
||||||
_("Thursday"),
|
_('Thursday'),
|
||||||
_("Friday"),
|
_('Friday'),
|
||||||
_("Saturday"),
|
_('Saturday'),
|
||||||
_("Sunday"),
|
_('Sunday'),
|
||||||
]
|
]
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -1441,17 +1441,12 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
all_products = group_order._get_products_for_group_order(group_order.id)
|
all_products = group_order._get_products_for_group_order(group_order.id)
|
||||||
filtered_products = all_products
|
filtered_products = all_products
|
||||||
|
|
||||||
# Apply search filter (only if search_query is not empty)
|
# Apply search
|
||||||
if search_query:
|
if search_query:
|
||||||
_logger.info("load_products_ajax: Applying search filter: %s", search_query)
|
|
||||||
filtered_products = filtered_products.filtered(
|
filtered_products = filtered_products.filtered(
|
||||||
lambda p: search_query.lower() in p.name.lower()
|
lambda p: search_query.lower() in p.name.lower()
|
||||||
or search_query.lower() in (p.description or "").lower()
|
or search_query.lower() in (p.description or "").lower()
|
||||||
)
|
)
|
||||||
_logger.info(
|
|
||||||
"load_products_ajax: After search filter: %d products",
|
|
||||||
len(filtered_products),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply category filter
|
# Apply category filter
|
||||||
if category_filter != "0":
|
if category_filter != "0":
|
||||||
|
|
@ -1460,11 +1455,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
selected_category = request.env["product.category"].browse(category_id)
|
selected_category = request.env["product.category"].browse(category_id)
|
||||||
|
|
||||||
if selected_category.exists():
|
if selected_category.exists():
|
||||||
_logger.info(
|
|
||||||
"load_products_ajax: Applying category filter: %d (%s)",
|
|
||||||
category_id,
|
|
||||||
selected_category.name,
|
|
||||||
)
|
|
||||||
all_category_ids = [category_id]
|
all_category_ids = [category_id]
|
||||||
|
|
||||||
def get_all_children(category):
|
def get_all_children(category):
|
||||||
|
|
@ -1499,10 +1489,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
|
|
||||||
# Preserve search filter by using intersection
|
# Preserve search filter by using intersection
|
||||||
filtered_products = filtered_products & cat_filtered
|
filtered_products = filtered_products & cat_filtered
|
||||||
_logger.info(
|
|
||||||
"load_products_ajax: After category filter: %d products",
|
|
||||||
len(filtered_products),
|
|
||||||
)
|
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
"load_products_ajax: Invalid category filter: %s", str(e)
|
"load_products_ajax: Invalid category filter: %s", str(e)
|
||||||
|
|
@ -1514,16 +1500,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
products_page = filtered_products[offset : offset + per_page]
|
products_page = filtered_products[offset : offset + per_page]
|
||||||
has_next = offset + per_page < total_products
|
has_next = offset + per_page < total_products
|
||||||
|
|
||||||
_logger.info(
|
|
||||||
"load_products_ajax: Pagination - page=%d, offset=%d, per_page=%d, "
|
|
||||||
"total=%d, has_next=%s",
|
|
||||||
page,
|
|
||||||
offset,
|
|
||||||
per_page,
|
|
||||||
total_products,
|
|
||||||
has_next,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get prices
|
# Get prices
|
||||||
pricelist = self._resolve_pricelist()
|
pricelist = self._resolve_pricelist()
|
||||||
product_price_info = {}
|
product_price_info = {}
|
||||||
|
|
@ -1809,8 +1785,10 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
)
|
)
|
||||||
_logger.warning("========================================")
|
_logger.warning("========================================")
|
||||||
|
|
||||||
# Get delivery product from group_order (configured per group order)
|
# Get delivery product ID and name (translated to user's language)
|
||||||
delivery_product = group_order.delivery_product_id
|
delivery_product = request.env.ref(
|
||||||
|
"website_sale_aplicoop.product_home_delivery", raise_if_not_found=False
|
||||||
|
)
|
||||||
delivery_product_id = delivery_product.id if delivery_product else None
|
delivery_product_id = delivery_product.id if delivery_product else None
|
||||||
# Get translated product name based on current language
|
# Get translated product name based on current language
|
||||||
if delivery_product:
|
if delivery_product:
|
||||||
|
|
@ -2802,8 +2780,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
return request.redirect("/eskaera/%d" % sale_order.group_order_id.id)
|
return request.redirect("/eskaera/%d" % sale_order.group_order_id.id)
|
||||||
|
|
||||||
# Extract items from the order (skip delivery product)
|
# Extract items from the order (skip delivery product)
|
||||||
# Use the delivery_product_id from the group_order
|
delivery_product = request.env.ref(
|
||||||
delivery_product = sale_order.group_order_id.delivery_product_id
|
"website_sale_aplicoop.product_home_delivery", raise_if_not_found=False
|
||||||
|
)
|
||||||
delivery_product_id = delivery_product.id if delivery_product else None
|
delivery_product_id = delivery_product.id if delivery_product else None
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<data noupdate="1">
|
|
||||||
<!-- Cron job to update dates for active group orders daily -->
|
|
||||||
<record id="ir_cron_update_group_order_dates" model="ir.cron">
|
|
||||||
<field name="name">Group Order: Update Dates Daily</field>
|
|
||||||
<field name="model_id" ref="model_group_order"/>
|
|
||||||
<field name="state">code</field>
|
|
||||||
<field name="code">model._cron_update_dates()</field>
|
|
||||||
<field name="interval_number">1</field>
|
|
||||||
<field name="interval_type">days</field>
|
|
||||||
<field name="active" eval="True"/>
|
|
||||||
</record>
|
|
||||||
</data>
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Fill pickup_day and pickup_date for existing group orders."""
|
"""Fill pickup_day and pickup_date for existing group orders."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
|
|
@ -10,13 +9,12 @@ def migrate(cr, version):
|
||||||
|
|
||||||
This ensures that existing group orders show delivery information.
|
This ensures that existing group orders show delivery information.
|
||||||
"""
|
"""
|
||||||
from odoo import SUPERUSER_ID
|
from odoo import api, SUPERUSER_ID
|
||||||
from odoo import api
|
|
||||||
|
|
||||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
|
||||||
# Get all group orders that don't have pickup_day set
|
# Get all group orders that don't have pickup_day set
|
||||||
group_orders = env["group.order"].search([("pickup_day", "=", False)])
|
group_orders = env['group.order'].search([('pickup_day', '=', False)])
|
||||||
|
|
||||||
if not group_orders:
|
if not group_orders:
|
||||||
return
|
return
|
||||||
|
|
@ -31,10 +29,8 @@ def migrate(cr, version):
|
||||||
friday = today + timedelta(days=days_until_friday)
|
friday = today + timedelta(days=days_until_friday)
|
||||||
|
|
||||||
for order in group_orders:
|
for order in group_orders:
|
||||||
order.write(
|
order.write({
|
||||||
{
|
'pickup_day': 4, # Friday
|
||||||
"pickup_day": 4, # Friday
|
'pickup_date': friday,
|
||||||
"pickup_date": friday,
|
'delivery_notice': 'Home delivery available.',
|
||||||
"delivery_notice": "Home delivery available.",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from odoo import SUPERUSER_ID
|
from odoo import api, SUPERUSER_ID
|
||||||
from odoo import api
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
|
|
@ -14,7 +13,7 @@ def migrate(cr, version):
|
||||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
|
||||||
# Obtener la compañía por defecto
|
# Obtener la compañía por defecto
|
||||||
default_company = env["res.company"].search([], limit=1)
|
default_company = env['res.company'].search([], limit=1)
|
||||||
|
|
||||||
if default_company:
|
if default_company:
|
||||||
# Actualizar todos los registros de group.order que no tengan company_id
|
# Actualizar todos los registros de group.order que no tengan company_id
|
||||||
|
|
@ -24,7 +23,7 @@ def migrate(cr, version):
|
||||||
SET company_id = %s
|
SET company_id = %s
|
||||||
WHERE company_id IS NULL
|
WHERE company_id IS NULL
|
||||||
""",
|
""",
|
||||||
(default_company.id,),
|
(default_company.id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
cr.commit()
|
cr.commit()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from odoo import _
|
||||||
from odoo import api
|
from odoo import api
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
from odoo import models
|
from odoo import models
|
||||||
|
|
@ -18,47 +19,52 @@ class GroupOrder(models.Model):
|
||||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||||
_order = "start_date desc"
|
_order = "start_date desc"
|
||||||
|
|
||||||
def _get_order_type_selection(self):
|
@staticmethod
|
||||||
|
def _get_order_type_selection(records):
|
||||||
"""Return order type selection options with translations."""
|
"""Return order type selection options with translations."""
|
||||||
return [
|
return [
|
||||||
("regular", self.env._("Regular Order")),
|
("regular", _("Regular Order")),
|
||||||
("special", self.env._("Special Order")),
|
("special", _("Special Order")),
|
||||||
("promotional", self.env._("Promotional Order")),
|
("promotional", _("Promotional Order")),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_period_selection(self):
|
@staticmethod
|
||||||
|
def _get_period_selection(records):
|
||||||
"""Return period selection options with translations."""
|
"""Return period selection options with translations."""
|
||||||
return [
|
return [
|
||||||
("once", self.env._("One-time")),
|
("once", _("One-time")),
|
||||||
("weekly", self.env._("Weekly")),
|
("weekly", _("Weekly")),
|
||||||
("biweekly", self.env._("Biweekly")),
|
("biweekly", _("Biweekly")),
|
||||||
("monthly", self.env._("Monthly")),
|
("monthly", _("Monthly")),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_day_selection(self):
|
@staticmethod
|
||||||
|
def _get_day_selection(records):
|
||||||
"""Return day of week selection options with translations."""
|
"""Return day of week selection options with translations."""
|
||||||
return [
|
return [
|
||||||
("0", self.env._("Monday")),
|
("0", _("Monday")),
|
||||||
("1", self.env._("Tuesday")),
|
("1", _("Tuesday")),
|
||||||
("2", self.env._("Wednesday")),
|
("2", _("Wednesday")),
|
||||||
("3", self.env._("Thursday")),
|
("3", _("Thursday")),
|
||||||
("4", self.env._("Friday")),
|
("4", _("Friday")),
|
||||||
("5", self.env._("Saturday")),
|
("5", _("Saturday")),
|
||||||
("6", self.env._("Sunday")),
|
("6", _("Sunday")),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_state_selection(self):
|
@staticmethod
|
||||||
|
def _get_state_selection(records):
|
||||||
"""Return state selection options with translations."""
|
"""Return state selection options with translations."""
|
||||||
return [
|
return [
|
||||||
("draft", self.env._("Draft")),
|
("draft", _("Draft")),
|
||||||
("open", self.env._("Open")),
|
("open", _("Open")),
|
||||||
("closed", self.env._("Closed")),
|
("closed", _("Closed")),
|
||||||
("cancelled", self.env._("Cancelled")),
|
("cancelled", _("Cancelled")),
|
||||||
]
|
]
|
||||||
|
|
||||||
# === Multicompañía ===
|
# === Multicompañía ===
|
||||||
company_id = fields.Many2one(
|
company_id = fields.Many2one(
|
||||||
"res.company",
|
"res.company",
|
||||||
|
string="Company",
|
||||||
required=True,
|
required=True,
|
||||||
default=lambda self: self.env.company,
|
default=lambda self: self.env.company,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
|
|
@ -67,6 +73,7 @@ class GroupOrder(models.Model):
|
||||||
|
|
||||||
# === Campos básicos ===
|
# === Campos básicos ===
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
required=True,
|
required=True,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
translate=True,
|
translate=True,
|
||||||
|
|
@ -77,6 +84,7 @@ class GroupOrder(models.Model):
|
||||||
"group_order_group_rel",
|
"group_order_group_rel",
|
||||||
"order_id",
|
"order_id",
|
||||||
"group_id",
|
"group_id",
|
||||||
|
string="Consumer Groups",
|
||||||
required=True,
|
required=True,
|
||||||
domain=[("is_group", "=", True)],
|
domain=[("is_group", "=", True)],
|
||||||
tracking=True,
|
tracking=True,
|
||||||
|
|
@ -84,6 +92,7 @@ class GroupOrder(models.Model):
|
||||||
)
|
)
|
||||||
type = fields.Selection(
|
type = fields.Selection(
|
||||||
selection=_get_order_type_selection,
|
selection=_get_order_type_selection,
|
||||||
|
string="Order Type",
|
||||||
required=True,
|
required=True,
|
||||||
default="regular",
|
default="regular",
|
||||||
tracking=True,
|
tracking=True,
|
||||||
|
|
@ -92,11 +101,13 @@ class GroupOrder(models.Model):
|
||||||
|
|
||||||
# === Fechas ===
|
# === Fechas ===
|
||||||
start_date = fields.Date(
|
start_date = fields.Date(
|
||||||
|
string="Start Date",
|
||||||
required=False,
|
required=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Day when the consumer group order opens for purchases",
|
help="Day when the consumer group order opens for purchases",
|
||||||
)
|
)
|
||||||
end_date = fields.Date(
|
end_date = fields.Date(
|
||||||
|
string="End Date",
|
||||||
required=False,
|
required=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="If empty, the consumer group order is permanent",
|
help="If empty, the consumer group order is permanent",
|
||||||
|
|
@ -105,6 +116,7 @@ class GroupOrder(models.Model):
|
||||||
# === Período y días ===
|
# === Período y días ===
|
||||||
period = fields.Selection(
|
period = fields.Selection(
|
||||||
selection=_get_period_selection,
|
selection=_get_period_selection,
|
||||||
|
string="Recurrence Period",
|
||||||
required=True,
|
required=True,
|
||||||
default="weekly",
|
default="weekly",
|
||||||
tracking=True,
|
tracking=True,
|
||||||
|
|
@ -112,12 +124,14 @@ class GroupOrder(models.Model):
|
||||||
)
|
)
|
||||||
pickup_day = fields.Selection(
|
pickup_day = fields.Selection(
|
||||||
selection=_get_day_selection,
|
selection=_get_day_selection,
|
||||||
|
string="Pickup Day",
|
||||||
required=False,
|
required=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Day of the week when members pick up their orders",
|
help="Day of the week when members pick up their orders",
|
||||||
)
|
)
|
||||||
cutoff_day = fields.Selection(
|
cutoff_day = fields.Selection(
|
||||||
selection=_get_day_selection,
|
selection=_get_day_selection,
|
||||||
|
string="Cutoff Day",
|
||||||
required=False,
|
required=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Day when purchases stop and the consumer group order is locked for this week.",
|
help="Day when purchases stop and the consumer group order is locked for this week.",
|
||||||
|
|
@ -125,31 +139,36 @@ class GroupOrder(models.Model):
|
||||||
|
|
||||||
# === Home delivery ===
|
# === Home delivery ===
|
||||||
home_delivery = fields.Boolean(
|
home_delivery = fields.Boolean(
|
||||||
|
string="Home Delivery",
|
||||||
default=False,
|
default=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Whether this consumer group order includes home delivery service",
|
help="Whether this consumer group order includes home delivery service",
|
||||||
)
|
)
|
||||||
delivery_product_id = fields.Many2one(
|
delivery_product_id = fields.Many2one(
|
||||||
"product.product",
|
"product.product",
|
||||||
|
string="Delivery Product",
|
||||||
domain=[("type", "=", "service")],
|
domain=[("type", "=", "service")],
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Product to use for home delivery (service type)",
|
help="Product to use for home delivery (service type)",
|
||||||
)
|
)
|
||||||
delivery_date = fields.Date(
|
delivery_date = fields.Date(
|
||||||
|
string="Delivery Date",
|
||||||
compute="_compute_delivery_date",
|
compute="_compute_delivery_date",
|
||||||
store=True,
|
store=False,
|
||||||
readonly=True,
|
readonly=True,
|
||||||
help="Calculated delivery date (pickup date + 1 day)",
|
help="Calculated delivery date (pickup date + 1 day)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Computed date fields ===
|
# === Computed date fields ===
|
||||||
pickup_date = fields.Date(
|
pickup_date = fields.Date(
|
||||||
|
string="Pickup Date",
|
||||||
compute="_compute_pickup_date",
|
compute="_compute_pickup_date",
|
||||||
store=True,
|
store=True,
|
||||||
readonly=True,
|
readonly=True,
|
||||||
help="Calculated next occurrence of pickup day",
|
help="Calculated next occurrence of pickup day",
|
||||||
)
|
)
|
||||||
cutoff_date = fields.Date(
|
cutoff_date = fields.Date(
|
||||||
|
string="Cutoff Date",
|
||||||
compute="_compute_cutoff_date",
|
compute="_compute_cutoff_date",
|
||||||
store=True,
|
store=True,
|
||||||
readonly=True,
|
readonly=True,
|
||||||
|
|
@ -162,6 +181,7 @@ class GroupOrder(models.Model):
|
||||||
"group_order_supplier_rel",
|
"group_order_supplier_rel",
|
||||||
"order_id",
|
"order_id",
|
||||||
"supplier_id",
|
"supplier_id",
|
||||||
|
string="Suppliers",
|
||||||
domain=[("supplier_rank", ">", 0)],
|
domain=[("supplier_rank", ">", 0)],
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Products from these suppliers will be available.",
|
help="Products from these suppliers will be available.",
|
||||||
|
|
@ -171,6 +191,7 @@ class GroupOrder(models.Model):
|
||||||
"group_order_product_rel",
|
"group_order_product_rel",
|
||||||
"order_id",
|
"order_id",
|
||||||
"product_id",
|
"product_id",
|
||||||
|
string="Products",
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Directly assigned products.",
|
help="Directly assigned products.",
|
||||||
)
|
)
|
||||||
|
|
@ -179,6 +200,7 @@ class GroupOrder(models.Model):
|
||||||
"group_order_category_rel",
|
"group_order_category_rel",
|
||||||
"order_id",
|
"order_id",
|
||||||
"category_id",
|
"category_id",
|
||||||
|
string="Categories",
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Products in these categories will be available",
|
help="Products in these categories will be available",
|
||||||
)
|
)
|
||||||
|
|
@ -186,24 +208,29 @@ class GroupOrder(models.Model):
|
||||||
# === Estado ===
|
# === Estado ===
|
||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
selection=_get_state_selection,
|
selection=_get_state_selection,
|
||||||
|
string="State",
|
||||||
default="draft",
|
default="draft",
|
||||||
tracking=True,
|
tracking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Descripción e imagen ===
|
# === Descripción e imagen ===
|
||||||
description = fields.Text(
|
description = fields.Text(
|
||||||
|
string="Description",
|
||||||
translate=True,
|
translate=True,
|
||||||
help="Free text description for this consumer group order",
|
help="Free text description for this consumer group order",
|
||||||
)
|
)
|
||||||
delivery_notice = fields.Text(
|
delivery_notice = fields.Text(
|
||||||
|
string="Delivery Notice",
|
||||||
translate=True,
|
translate=True,
|
||||||
help="Notice about home delivery displayed to users (shown when home delivery is enabled)",
|
help="Notice about home delivery displayed to users (shown when home delivery is enabled)",
|
||||||
)
|
)
|
||||||
image = fields.Binary(
|
image = fields.Binary(
|
||||||
|
string="Image",
|
||||||
help="Image displayed alongside the consumer group order name",
|
help="Image displayed alongside the consumer group order name",
|
||||||
attachment=True,
|
attachment=True,
|
||||||
)
|
)
|
||||||
display_image = fields.Binary(
|
display_image = fields.Binary(
|
||||||
|
string="Display Image",
|
||||||
compute="_compute_display_image",
|
compute="_compute_display_image",
|
||||||
store=True,
|
store=True,
|
||||||
help="Image to display: uses consumer group order image if set, otherwise group image",
|
help="Image to display: uses consumer group order image if set, otherwise group image",
|
||||||
|
|
@ -222,6 +249,7 @@ class GroupOrder(models.Model):
|
||||||
record.display_image = False
|
record.display_image = False
|
||||||
|
|
||||||
available_products_count = fields.Integer(
|
available_products_count = fields.Integer(
|
||||||
|
string="Available Products Count",
|
||||||
compute="_compute_available_products_count",
|
compute="_compute_available_products_count",
|
||||||
store=False,
|
store=False,
|
||||||
help="Total count of available products from all sources",
|
help="Total count of available products from all sources",
|
||||||
|
|
@ -241,15 +269,8 @@ class GroupOrder(models.Model):
|
||||||
for group in record.group_ids:
|
for group in record.group_ids:
|
||||||
if group.company_id and group.company_id != record.company_id:
|
if group.company_id and group.company_id != record.company_id:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
self.env._(
|
f"Group {group.name} belongs to company "
|
||||||
"Group %(group)s belongs to company %(group_company)s, "
|
f"{group.company_id.name}, not to {record.company_id.name}."
|
||||||
"not to %(record_company)s."
|
|
||||||
)
|
|
||||||
% {
|
|
||||||
"group": group.name,
|
|
||||||
"group_company": group.company_id.name,
|
|
||||||
"record_company": record.company_id.name,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.constrains("start_date", "end_date")
|
@api.constrains("start_date", "end_date")
|
||||||
|
|
@ -257,9 +278,7 @@ class GroupOrder(models.Model):
|
||||||
for record in self:
|
for record in self:
|
||||||
if record.start_date and record.end_date:
|
if record.start_date and record.end_date:
|
||||||
if record.start_date > record.end_date:
|
if record.start_date > record.end_date:
|
||||||
raise ValidationError(
|
raise ValidationError("Start date cannot be greater than end date")
|
||||||
self.env._("Start date cannot be greater than end date")
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_open(self):
|
def action_open(self):
|
||||||
"""Open order for purchases."""
|
"""Open order for purchases."""
|
||||||
|
|
@ -484,11 +503,10 @@ class GroupOrder(models.Model):
|
||||||
# Calculate days to NEXT occurrence of cutoff_day
|
# Calculate days to NEXT occurrence of cutoff_day
|
||||||
days_ahead = target_weekday - current_weekday
|
days_ahead = target_weekday - current_weekday
|
||||||
|
|
||||||
if days_ahead < 0:
|
if days_ahead <= 0:
|
||||||
# Target day already passed this week
|
# Target day already passed this week or is today
|
||||||
# Jump to next week's occurrence
|
# Jump to next week's occurrence
|
||||||
days_ahead += 7
|
days_ahead += 7
|
||||||
# If days_ahead == 0, cutoff is today (allowed)
|
|
||||||
|
|
||||||
record.cutoff_date = reference_date + timedelta(days=days_ahead)
|
record.cutoff_date = reference_date + timedelta(days=days_ahead)
|
||||||
_logger.info(
|
_logger.info(
|
||||||
|
|
@ -516,65 +534,3 @@ class GroupOrder(models.Model):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
record.delivery_date = None
|
record.delivery_date = None
|
||||||
|
|
||||||
# === Constraints ===
|
|
||||||
|
|
||||||
@api.constrains("cutoff_day", "pickup_day", "period")
|
|
||||||
def _check_cutoff_before_pickup(self):
|
|
||||||
"""Validate that pickup_day comes after or equals cutoff_day in weekly orders.
|
|
||||||
|
|
||||||
For weekly orders, if pickup_day < cutoff_day numerically, it means pickup
|
|
||||||
would be scheduled BEFORE cutoff in the same week cycle, which is illogical.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- cutoff_day=3 (Thursday), pickup_day=1 (Tuesday): INVALID
|
|
||||||
(pickup Tuesday would be before cutoff Thursday)
|
|
||||||
- cutoff_day=1 (Tuesday), pickup_day=5 (Saturday): VALID
|
|
||||||
(pickup Saturday is after cutoff Tuesday)
|
|
||||||
- cutoff_day=5 (Saturday), pickup_day=5 (Saturday): VALID
|
|
||||||
(same day allowed)
|
|
||||||
"""
|
|
||||||
for record in self:
|
|
||||||
if record.cutoff_day and record.pickup_day and record.period == "weekly":
|
|
||||||
cutoff = int(record.cutoff_day)
|
|
||||||
pickup = int(record.pickup_day)
|
|
||||||
if pickup < cutoff:
|
|
||||||
pickup_name = dict(self._get_day_selection())[str(pickup)]
|
|
||||||
cutoff_name = dict(self._get_day_selection())[str(cutoff)]
|
|
||||||
raise ValidationError(
|
|
||||||
self.env._(
|
|
||||||
"For weekly orders, pickup day (%(pickup)s) must be after or equal to "
|
|
||||||
"cutoff day (%(cutoff)s) in the same week. Current configuration would "
|
|
||||||
"put pickup before cutoff, which is illogical."
|
|
||||||
)
|
|
||||||
% {"pickup": pickup_name, "cutoff": cutoff_name}
|
|
||||||
)
|
|
||||||
|
|
||||||
# === Onchange Methods ===
|
|
||||||
|
|
||||||
@api.onchange("cutoff_day", "start_date")
|
|
||||||
def _onchange_cutoff_day(self):
|
|
||||||
"""Force recompute cutoff_date on UI change for immediate feedback."""
|
|
||||||
self._compute_cutoff_date()
|
|
||||||
|
|
||||||
@api.onchange("pickup_day", "cutoff_day", "start_date")
|
|
||||||
def _onchange_pickup_day(self):
|
|
||||||
"""Force recompute pickup_date on UI change for immediate feedback."""
|
|
||||||
self._compute_pickup_date()
|
|
||||||
|
|
||||||
# === Cron Methods ===
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _cron_update_dates(self):
|
|
||||||
"""Cron job to recalculate dates for active orders daily.
|
|
||||||
|
|
||||||
This ensures that computed dates stay up-to-date as time passes.
|
|
||||||
Only updates orders in 'draft' or 'open' states.
|
|
||||||
"""
|
|
||||||
orders = self.search([("state", "in", ["draft", "open"])])
|
|
||||||
_logger.info("Cron: Updating dates for %d active group orders", len(orders))
|
|
||||||
for order in orders:
|
|
||||||
order._compute_cutoff_date()
|
|
||||||
order._compute_pickup_date()
|
|
||||||
order._compute_delivery_date()
|
|
||||||
_logger.info("Cron: Date update completed")
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -26,150 +27,144 @@ def _register_translations():
|
||||||
# ========================
|
# ========================
|
||||||
# Action Labels
|
# Action Labels
|
||||||
# ========================
|
# ========================
|
||||||
_("Save Cart")
|
_('Save Cart')
|
||||||
_("Reload Cart")
|
_('Reload Cart')
|
||||||
_("Browse Product Categories")
|
_('Browse Product Categories')
|
||||||
_("Proceed to Checkout")
|
_('Proceed to Checkout')
|
||||||
_("Confirm Order")
|
_('Confirm Order')
|
||||||
_("Back to Cart")
|
_('Back to Cart')
|
||||||
_("Remove Item")
|
_('Remove Item')
|
||||||
_("Add to Cart")
|
_('Add to Cart')
|
||||||
_("Save as Draft")
|
_('Save as Draft')
|
||||||
_("Load Draft")
|
_('Load Draft')
|
||||||
_("Browse Product Categories")
|
_('Browse Product Categories')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Draft Modal Labels
|
# Draft Modal Labels
|
||||||
# ========================
|
# ========================
|
||||||
_("Draft Already Exists")
|
_('Draft Already Exists')
|
||||||
_("A saved draft already exists for this week.")
|
_('A saved draft already exists for this week.')
|
||||||
_("You have two options:")
|
_('You have two options:')
|
||||||
_("Option 1: Merge with Existing Draft")
|
_('Option 1: Merge with Existing Draft')
|
||||||
_("Combine your current cart with the existing draft.")
|
_('Combine your current cart with the existing draft.')
|
||||||
_("Existing draft has")
|
_('Existing draft has')
|
||||||
_("Current cart has")
|
_('Current cart has')
|
||||||
_("item(s)")
|
_('item(s)')
|
||||||
_(
|
_('Products will be merged by adding quantities. If a product exists in both, quantities will be combined.')
|
||||||
"Products will be merged by adding quantities. If a product exists in both, quantities will be combined."
|
_('Option 2: Replace with Current Cart')
|
||||||
)
|
_('Delete the old draft and save only the current cart items.')
|
||||||
_("Option 2: Replace with Current Cart")
|
_('The existing draft will be permanently deleted.')
|
||||||
_("Delete the old draft and save only the current cart items.")
|
_('Merge')
|
||||||
_("The existing draft will be permanently deleted.")
|
_('Replace')
|
||||||
_("Merge")
|
|
||||||
_("Replace")
|
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Draft Save/Load Confirmations
|
# Draft Save/Load Confirmations
|
||||||
# ========================
|
# ========================
|
||||||
_("Are you sure you want to save this cart as draft? Items to save: ")
|
_('Are you sure you want to save this cart as draft? Items to save: ')
|
||||||
_("You will be able to reload this cart later.")
|
_('You will be able to reload this cart later.')
|
||||||
_("Are you sure you want to load your last saved draft?")
|
_('Are you sure you want to load your last saved draft?')
|
||||||
_("This will replace the current items in your cart")
|
_('This will replace the current items in your cart')
|
||||||
_("with the saved draft.")
|
_('with the saved draft.')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Cart Messages (All Variations)
|
# Cart Messages (All Variations)
|
||||||
# ========================
|
# ========================
|
||||||
_("Your cart is empty")
|
_('Your cart is empty')
|
||||||
_("This order's cart is empty.")
|
_('This order\'s cart is empty.')
|
||||||
_("This order's cart is empty")
|
_('This order\'s cart is empty')
|
||||||
_("added to cart")
|
_('added to cart')
|
||||||
_("items")
|
_('items')
|
||||||
_("Your cart has been restored")
|
_('Your cart has been restored')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Confirmation & Validation
|
# Confirmation & Validation
|
||||||
# ========================
|
# ========================
|
||||||
_("Confirmation")
|
_('Confirmation')
|
||||||
_("Confirm")
|
_('Confirm')
|
||||||
_("Cancel")
|
_('Cancel')
|
||||||
_("Please enter a valid quantity")
|
_('Please enter a valid quantity')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Error Messages
|
# Error Messages
|
||||||
# ========================
|
# ========================
|
||||||
_("Error: Order ID not found")
|
_('Error: Order ID not found')
|
||||||
_("No draft orders found for this week")
|
_('No draft orders found for this week')
|
||||||
_("Connection error")
|
_('Connection error')
|
||||||
_("Error loading order")
|
_('Error loading order')
|
||||||
_("Error loading draft")
|
_('Error loading draft')
|
||||||
_("Unknown error")
|
_('Unknown error')
|
||||||
_("Error saving cart")
|
_('Error saving cart')
|
||||||
_("Error processing response")
|
_('Error processing response')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Success Messages
|
# Success Messages
|
||||||
# ========================
|
# ========================
|
||||||
_("Cart saved as draft successfully")
|
_('Cart saved as draft successfully')
|
||||||
_("Draft order loaded successfully")
|
_('Draft order loaded successfully')
|
||||||
_("Draft merged successfully")
|
_('Draft merged successfully')
|
||||||
_("Draft replaced successfully")
|
_('Draft replaced successfully')
|
||||||
_("Order loaded")
|
_('Order loaded')
|
||||||
_("Thank you! Your order has been confirmed.")
|
_('Thank you! Your order has been confirmed.')
|
||||||
_("Quantity updated")
|
_('Quantity updated')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Field Labels
|
# Field Labels
|
||||||
# ========================
|
# ========================
|
||||||
_("Product")
|
_('Product')
|
||||||
_("Supplier")
|
_('Supplier')
|
||||||
_("Price")
|
_('Price')
|
||||||
_("Quantity")
|
_('Quantity')
|
||||||
_("Subtotal")
|
_('Subtotal')
|
||||||
_("Total")
|
_('Total')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Checkout Page Labels
|
# Checkout Page Labels
|
||||||
# ========================
|
# ========================
|
||||||
_("Home Delivery")
|
_('Home Delivery')
|
||||||
_("Delivery Information")
|
_('Delivery Information')
|
||||||
_(
|
_('Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}')
|
||||||
"Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}"
|
_('Your order will be delivered the day after pickup between 11:00 - 14:00')
|
||||||
)
|
_('Important')
|
||||||
_("Your order will be delivered the day after pickup between 11:00 - 14:00")
|
_('Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.')
|
||||||
_("Important")
|
|
||||||
_(
|
|
||||||
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming."
|
|
||||||
)
|
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Search & Filter Labels
|
# Search & Filter Labels
|
||||||
# ========================
|
# ========================
|
||||||
_("Search")
|
_('Search')
|
||||||
_("Search products...")
|
_('Search products...')
|
||||||
_("No products found")
|
_('No products found')
|
||||||
_("Categories")
|
_('Categories')
|
||||||
_("All categories")
|
_('All categories')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Category Labels
|
# Category Labels
|
||||||
# ========================
|
# ========================
|
||||||
_("Order Type")
|
_('Order Type')
|
||||||
_("Order Period")
|
_('Order Period')
|
||||||
_("Cutoff Day")
|
_('Cutoff Day')
|
||||||
_("Pickup Day")
|
_('Pickup Day')
|
||||||
_("Store Pickup Day")
|
_('Store Pickup Day')
|
||||||
_("Open until")
|
_('Open until')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Portal Page Labels (New)
|
# Portal Page Labels (New)
|
||||||
# ========================
|
# ========================
|
||||||
_("Load in Cart")
|
_('Load in Cart')
|
||||||
_("Consumer Group")
|
_('Consumer Group')
|
||||||
_("Delivery Information")
|
_('Delivery Information')
|
||||||
_("Delivery Date:")
|
_('Delivery Date:')
|
||||||
_("Pickup Date:")
|
_('Pickup Date:')
|
||||||
_("Delivery Notice:")
|
_('Delivery Notice:')
|
||||||
_("No special delivery instructions")
|
_('No special delivery instructions')
|
||||||
_("Pickup Location:")
|
_('Pickup Location:')
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
# Day Names (Required for translations)
|
# Day Names (Required for translations)
|
||||||
# ========================
|
# ========================
|
||||||
_("Monday")
|
_('Monday')
|
||||||
_("Tuesday")
|
_('Tuesday')
|
||||||
_("Wednesday")
|
_('Wednesday')
|
||||||
_("Thursday")
|
_('Thursday')
|
||||||
_("Friday")
|
_('Friday')
|
||||||
_("Saturday")
|
_('Saturday')
|
||||||
_("Sunday")
|
_('Sunday')
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,20 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from odoo import _
|
from odoo import _, api, fields, models
|
||||||
from odoo import api
|
|
||||||
from odoo import fields
|
|
||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class ProductProduct(models.Model):
|
class ProductProduct(models.Model):
|
||||||
_inherit = "product.product"
|
_inherit = 'product.product'
|
||||||
|
|
||||||
group_order_ids = fields.Many2many(
|
group_order_ids = fields.Many2many(
|
||||||
"group.order",
|
'group.order',
|
||||||
"group_order_product_rel",
|
'group_order_product_rel',
|
||||||
"product_id",
|
'product_id',
|
||||||
"order_id",
|
'order_id',
|
||||||
string="Group Orders",
|
string='Group Orders',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
help="Group orders where this product is available",
|
help='Group orders where this product is available',
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|
@ -28,25 +25,26 @@ class ProductProduct(models.Model):
|
||||||
responsibilities together. Keep this wrapper so existing callers
|
responsibilities together. Keep this wrapper so existing callers
|
||||||
on `product.product` keep working.
|
on `product.product` keep working.
|
||||||
"""
|
"""
|
||||||
order = self.env["group.order"].browse(order_id)
|
order = self.env['group.order'].browse(order_id)
|
||||||
if not order.exists():
|
if not order.exists():
|
||||||
return self.browse()
|
return self.browse()
|
||||||
return order._get_products_for_group_order(order.id)
|
return order._get_products_for_group_order(order.id)
|
||||||
|
|
||||||
|
|
||||||
class ProductTemplate(models.Model):
|
class ProductTemplate(models.Model):
|
||||||
_inherit = "product.template"
|
_inherit = 'product.template'
|
||||||
|
|
||||||
group_order_ids = fields.Many2many(
|
group_order_ids = fields.Many2many(
|
||||||
"group.order",
|
'group.order',
|
||||||
compute="_compute_group_order_ids",
|
compute='_compute_group_order_ids',
|
||||||
string="Consumer Group Orders",
|
string='Consumer Group Orders',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
help="Consumer group orders where variants of this product are available",
|
help='Consumer group orders where variants of this product are available',
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends("product_variant_ids.group_order_ids")
|
@api.depends('product_variant_ids.group_order_ids')
|
||||||
def _compute_group_order_ids(self):
|
def _compute_group_order_ids(self):
|
||||||
for template in self:
|
for template in self:
|
||||||
variants = template.product_variant_ids
|
variants = template.product_variant_ids
|
||||||
template.group_order_ids = variants.mapped("group_order_ids")
|
template.group_order_ids = variants.mapped('group_order_ids')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,37 @@
|
||||||
# Copyright 2025-Today Criptomart
|
# Copyright 2025-Today Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from odoo import _
|
from odoo import _, fields, models
|
||||||
from odoo import fields
|
|
||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class ResPartner(models.Model):
|
class ResPartner(models.Model):
|
||||||
_inherit = "res.partner"
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
# Campo para identificar si un partner es un grupo
|
# Campo para identificar si un partner es un grupo
|
||||||
is_group = fields.Boolean(
|
is_group = fields.Boolean(
|
||||||
string="Is a Consumer Group?",
|
string='Is a Consumer Group?',
|
||||||
help="Check this box if the partner represents a group of users",
|
help='Check this box if the partner represents a group of users',
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Relación para los miembros de un grupo (si is_group es True)
|
# Relación para los miembros de un grupo (si is_group es True)
|
||||||
member_ids = fields.Many2many(
|
member_ids = fields.Many2many(
|
||||||
"res.partner",
|
'res.partner',
|
||||||
"res_partner_group_members_rel",
|
'res_partner_group_members_rel',
|
||||||
"group_id",
|
'group_id',
|
||||||
"member_id",
|
'member_id',
|
||||||
domain=[("is_group", "=", True)],
|
domain=[('is_group', '=', True)],
|
||||||
string="Consumer Groups",
|
string='Consumer Groups',
|
||||||
help="Consumer Groups this partner belongs to",
|
help='Consumer Groups this partner belongs to',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Inverse relation: group orders this group participates in
|
# Inverse relation: group orders this group participates in
|
||||||
group_order_ids = fields.Many2many(
|
group_order_ids = fields.Many2many(
|
||||||
"group.order",
|
'group.order',
|
||||||
"group_order_group_rel",
|
'group_order_group_rel',
|
||||||
"group_id",
|
'group_id',
|
||||||
"order_id",
|
'order_id',
|
||||||
string="Consumer Group Orders",
|
string='Consumer Group Orders',
|
||||||
help="Group orders this consumer group participates in",
|
help='Group orders this consumer group participates in',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,46 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from odoo import fields
|
from odoo import _, fields, models
|
||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class SaleOrder(models.Model):
|
class SaleOrder(models.Model):
|
||||||
_inherit = "sale.order"
|
_inherit = 'sale.order'
|
||||||
|
|
||||||
def _get_pickup_day_selection(self):
|
@staticmethod
|
||||||
|
def _get_pickup_day_selection(records):
|
||||||
"""Return pickup day selection options with translations."""
|
"""Return pickup day selection options with translations."""
|
||||||
return [
|
return [
|
||||||
("0", self.env._("Monday")),
|
('0', _('Monday')),
|
||||||
("1", self.env._("Tuesday")),
|
('1', _('Tuesday')),
|
||||||
("2", self.env._("Wednesday")),
|
('2', _('Wednesday')),
|
||||||
("3", self.env._("Thursday")),
|
('3', _('Thursday')),
|
||||||
("4", self.env._("Friday")),
|
('4', _('Friday')),
|
||||||
("5", self.env._("Saturday")),
|
('5', _('Saturday')),
|
||||||
("6", self.env._("Sunday")),
|
('6', _('Sunday')),
|
||||||
]
|
]
|
||||||
|
|
||||||
pickup_day = fields.Selection(
|
pickup_day = fields.Selection(
|
||||||
selection=_get_pickup_day_selection,
|
selection=_get_pickup_day_selection,
|
||||||
help="Day of week when this order will be picked up (inherited from group order)",
|
string='Pickup Day',
|
||||||
|
help='Day of week when this order will be picked up (inherited from group order)',
|
||||||
)
|
)
|
||||||
|
|
||||||
group_order_id = fields.Many2one(
|
group_order_id = fields.Many2one(
|
||||||
"group.order",
|
'group.order',
|
||||||
help="Reference to the consumer group order that originated this sale order",
|
string='Consumer Group Order',
|
||||||
|
help='Reference to the consumer group order that originated this sale order',
|
||||||
)
|
)
|
||||||
|
|
||||||
pickup_date = fields.Date(
|
pickup_date = fields.Date(
|
||||||
help="Calculated pickup/delivery date (inherited from consumer group order)",
|
string='Pickup Date',
|
||||||
|
help='Calculated pickup/delivery date (inherited from consumer group order)',
|
||||||
)
|
)
|
||||||
|
|
||||||
home_delivery = fields.Boolean(
|
home_delivery = fields.Boolean(
|
||||||
|
string='Home Delivery',
|
||||||
default=False,
|
default=False,
|
||||||
help="Whether this order includes home delivery (inherited from consumer group order)",
|
help='Whether this order includes home delivery (inherited from consumer group order)',
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_name_portal_content_view(self):
|
def _get_name_portal_content_view(self):
|
||||||
|
|
@ -48,5 +52,5 @@ class SaleOrder(models.Model):
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if self.group_order_id:
|
if self.group_order_id:
|
||||||
return "website_sale_aplicoop.sale_order_portal_content_aplicoop"
|
return 'website_sale_aplicoop.sale_order_portal_content_aplicoop'
|
||||||
return super()._get_name_portal_content_view()
|
return super()._get_name_portal_content_view()
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ The implementation follows OCA standards for:
|
||||||
- Code quality and testing (26 passing tests)
|
- Code quality and testing (26 passing tests)
|
||||||
- Documentation structure and multilingual support
|
- Documentation structure and multilingual support
|
||||||
- Security and access control
|
- Security and access control
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,4 @@
|
||||||
|
|
||||||
- `start_date` must be ≤ `end_date` (when both filled)
|
- `start_date` must be ≤ `end_date` (when both filled)
|
||||||
- Empty end_date = permanent order
|
- Empty end_date = permanent order
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ access_group_order_user,group.order user,model_group_order,website_sale_aplicoop
|
||||||
access_group_order_manager,group.order manager,model_group_order,website_sale_aplicoop.group_group_order_manager,1,1,1,1
|
access_group_order_manager,group.order manager,model_group_order,website_sale_aplicoop.group_group_order_manager,1,1,1,1
|
||||||
access_group_order_portal,group.order portal,model_group_order,base.group_portal,1,0,0,0
|
access_group_order_portal,group.order portal,model_group_order,base.group_portal,1,0,0,0
|
||||||
access_product_supplierinfo_portal,product.supplierinfo portal,product.model_product_supplierinfo,base.group_portal,1,0,0,0
|
access_product_supplierinfo_portal,product.supplierinfo portal,product.model_product_supplierinfo,base.group_portal,1,0,0,0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from setuptools import find_packages
|
from setuptools import setup, find_packages
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
with open("README.rst", encoding="utf-8") as fh:
|
with open("README.rst", "r", encoding="utf-8") as fh:
|
||||||
long_description = fh.read()
|
long_description = fh.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,7 @@
|
||||||
--border-dark: #718096;
|
--border-dark: #718096;
|
||||||
|
|
||||||
/* ========== TYPOGRAPHY ========== */
|
/* ========== TYPOGRAPHY ========== */
|
||||||
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
Arial, sans-serif;
|
|
||||||
--font-weight-light: 300;
|
--font-weight-light: 300;
|
||||||
--font-weight-normal: 400;
|
--font-weight-normal: 400;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
|
|
@ -58,7 +57,7 @@
|
||||||
|
|
||||||
/* ========== TRANSITIONS ========== */
|
/* ========== TRANSITIONS ========== */
|
||||||
--transition-fast: 200ms ease;
|
--transition-fast: 200ms ease;
|
||||||
--transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
--transition-normal: 320ms cubic-bezier(.2, .9, .2, 1);
|
||||||
--transition-slow: 500ms ease;
|
--transition-slow: 500ms ease;
|
||||||
|
|
||||||
/* ========== Z-INDEX ========== */
|
/* ========== Z-INDEX ========== */
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@
|
||||||
border: 1px solid rgba(90, 103, 216, 0.12);
|
border: 1px solid rgba(90, 103, 216, 0.12);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06);
|
box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06);
|
||||||
transition: transform 320ms cubic-bezier(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms,
|
transition: transform 320ms cubic-bezier(.2, .9, .2, 1), box-shadow 320ms, border-color 320ms, background 320ms;
|
||||||
background 320ms;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -140,7 +139,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.eskaera-order-card .btn::before {
|
.eskaera-order-card .btn::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-card:hover .card-body {
|
.product-card:hover .card-body {
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg, rgba(108, 117, 125, 0.10) 0%, rgba(108, 117, 125, 0.08) 100%);
|
||||||
135deg,
|
|
||||||
rgba(108, 117, 125, 0.1) 0%,
|
|
||||||
rgba(108, 117, 125, 0.08) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-card .card-title {
|
.product-card .card-title {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,13 @@
|
||||||
* Page backgrounds and main layout structures
|
* Page backgrounds and main layout structures
|
||||||
*/
|
*/
|
||||||
|
|
||||||
html,
|
html, body {
|
||||||
body {
|
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.website_published {
|
body.website_published {
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg,
|
||||||
135deg,
|
|
||||||
color-mix(in srgb, var(--primary-color) 30%, white),
|
color-mix(in srgb, var(--primary-color) 30%, white),
|
||||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||||
) !important;
|
) !important;
|
||||||
|
|
@ -34,24 +32,21 @@ body.website_published .eskaera-checkout-page {
|
||||||
|
|
||||||
.eskaera-page,
|
.eskaera-page,
|
||||||
.eskaera-generic-page {
|
.eskaera-generic-page {
|
||||||
background: linear-gradient(
|
background: linear-gradient(180deg,
|
||||||
180deg,
|
|
||||||
color-mix(in srgb, var(--primary-color) 10%, white),
|
color-mix(in srgb, var(--primary-color) 10%, white),
|
||||||
color-mix(in srgb, var(--primary-color) 70%, black)
|
color-mix(in srgb, var(--primary-color) 70%, black)
|
||||||
) !important;
|
) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eskaera-shop-page {
|
.eskaera-shop-page {
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg,
|
||||||
135deg,
|
|
||||||
color-mix(in srgb, var(--primary-color) 10%, white),
|
color-mix(in srgb, var(--primary-color) 10%, white),
|
||||||
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135))
|
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135))
|
||||||
) !important;
|
) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eskaera-checkout-page {
|
.eskaera-checkout-page {
|
||||||
background: linear-gradient(
|
background: linear-gradient(-135deg,
|
||||||
-135deg,
|
|
||||||
color-mix(in srgb, var(--primary-color) 0%, white),
|
color-mix(in srgb, var(--primary-color) 0%, white),
|
||||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||||
) !important;
|
) !important;
|
||||||
|
|
@ -59,54 +54,29 @@ body.website_published .eskaera-checkout-page {
|
||||||
|
|
||||||
.eskaera-page::before,
|
.eskaera-page::before,
|
||||||
.eskaera-generic-page::before {
|
.eskaera-generic-page::before {
|
||||||
background-image: radial-gradient(
|
background-image:
|
||||||
circle at 20% 50%,
|
radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%),
|
||||||
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
|
radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%),
|
||||||
transparent 50%
|
radial-gradient(circle at 40% 20%, color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%, transparent 50%);
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 80% 80%,
|
|
||||||
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
|
|
||||||
transparent 50%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 40% 20%,
|
|
||||||
color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%,
|
|
||||||
transparent 50%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eskaera-shop-page::before {
|
.eskaera-shop-page::before {
|
||||||
background-image: radial-gradient(
|
background-image:
|
||||||
circle at 15% 30%,
|
radial-gradient(circle at 15% 30%, color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%, transparent 50%),
|
||||||
color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%,
|
radial-gradient(circle at 85% 70%, color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, transparent 50%);
|
||||||
transparent 50%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 85% 70%,
|
|
||||||
color-mix(in srgb, var(--primary-color) 22%, transparent) 0%,
|
|
||||||
transparent 50%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eskaera-checkout-page::before {
|
.eskaera-checkout-page::before {
|
||||||
background-image: radial-gradient(
|
background-image:
|
||||||
circle at 20% 50%,
|
radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%),
|
||||||
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
|
radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%);
|
||||||
transparent 50%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
|
||||||
circle at 80% 80%,
|
|
||||||
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
|
|
||||||
transparent 50%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eskaera-page::before,
|
.eskaera-page::before,
|
||||||
.eskaera-shop-page::before,
|
.eskaera-shop-page::before,
|
||||||
.eskaera-generic-page::before,
|
.eskaera-generic-page::before,
|
||||||
.eskaera-checkout-page::before {
|
.eskaera-checkout-page::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
||||||
|
|
@ -15,36 +15,36 @@
|
||||||
/* ============================================
|
/* ============================================
|
||||||
1. BASE & VARIABLES
|
1. BASE & VARIABLES
|
||||||
============================================ */
|
============================================ */
|
||||||
@import "base/variables.css";
|
@import 'base/variables.css';
|
||||||
@import "base/utilities.css";
|
@import 'base/utilities.css';
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
2. LAYOUT & PAGES
|
2. LAYOUT & PAGES
|
||||||
============================================ */
|
============================================ */
|
||||||
@import "layout/pages.css";
|
@import 'layout/pages.css';
|
||||||
@import "layout/header.css";
|
@import 'layout/header.css';
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
3. COMPONENTS (Reusable UI elements)
|
3. COMPONENTS (Reusable UI elements)
|
||||||
============================================ */
|
============================================ */
|
||||||
@import "components/product-card.css";
|
@import 'components/product-card.css';
|
||||||
@import "components/order-card.css";
|
@import 'components/order-card.css';
|
||||||
@import "components/cart.css";
|
@import 'components/cart.css';
|
||||||
@import "components/buttons.css";
|
@import 'components/buttons.css';
|
||||||
@import "components/quantity-control.css";
|
@import 'components/quantity-control.css';
|
||||||
@import "components/forms.css";
|
@import 'components/forms.css';
|
||||||
@import "components/alerts.css";
|
@import 'components/alerts.css';
|
||||||
@import "components/tag-filter.css";
|
@import 'components/tag-filter.css';
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
4. SECTIONS (Page-specific layouts)
|
4. SECTIONS (Page-specific layouts)
|
||||||
============================================ */
|
============================================ */
|
||||||
@import "sections/products-grid.css";
|
@import 'sections/products-grid.css';
|
||||||
@import "sections/order-list.css";
|
@import 'sections/order-list.css';
|
||||||
@import "sections/checkout.css";
|
@import 'sections/checkout.css';
|
||||||
@import "sections/info-cards.css";
|
@import 'sections/info-cards.css';
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
5. RESPONSIVE DESIGN (Media queries)
|
5. RESPONSIVE DESIGN (Media queries)
|
||||||
============================================ */
|
============================================ */
|
||||||
@import "layout/responsive.css";
|
@import 'layout/responsive.css';
|
||||||
|
|
|
||||||
|
|
@ -5,104 +5,93 @@
|
||||||
* before rendering the checkout summary.
|
* before rendering the checkout summary.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function() {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
console.log("[CHECKOUT] Script loaded");
|
console.log('[CHECKOUT] Script loaded');
|
||||||
|
|
||||||
// Get order ID from button
|
// Get order ID from button
|
||||||
var confirmBtn = document.getElementById("confirm-order-btn");
|
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||||
if (!confirmBtn) {
|
if (!confirmBtn) {
|
||||||
console.log("[CHECKOUT] No confirm button found");
|
console.log('[CHECKOUT] No confirm button found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var orderId = confirmBtn.getAttribute("data-order-id");
|
var orderId = confirmBtn.getAttribute('data-order-id');
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
console.log("[CHECKOUT] No order ID found");
|
console.log('[CHECKOUT] No order ID found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[CHECKOUT] Order ID:", orderId);
|
console.log('[CHECKOUT] Order ID:', orderId);
|
||||||
|
|
||||||
// Get summary div
|
// Get summary div
|
||||||
var summaryDiv = document.getElementById("checkout-summary");
|
var summaryDiv = document.getElementById('checkout-summary');
|
||||||
if (!summaryDiv) {
|
if (!summaryDiv) {
|
||||||
console.log("[CHECKOUT] No summary div found");
|
console.log('[CHECKOUT] No summary div found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to fetch labels and render checkout
|
// Function to fetch labels and render checkout
|
||||||
var fetchLabelsAndRender = function () {
|
var fetchLabelsAndRender = function() {
|
||||||
console.log("[CHECKOUT] Fetching labels...");
|
console.log('[CHECKOUT] Fetching labels...');
|
||||||
|
|
||||||
// Wait for window.groupOrderShop.labels to be initialized (contains hardcoded labels)
|
// Wait for window.groupOrderShop.labels to be initialized (contains hardcoded labels)
|
||||||
var waitForLabels = function (callback, maxWait = 3000, checkInterval = 50) {
|
var waitForLabels = function(callback, maxWait = 3000, checkInterval = 50) {
|
||||||
var startTime = Date.now();
|
var startTime = Date.now();
|
||||||
var checkLabels = function () {
|
var checkLabels = function() {
|
||||||
if (
|
if (window.groupOrderShop && window.groupOrderShop.labels && Object.keys(window.groupOrderShop.labels).length > 0) {
|
||||||
window.groupOrderShop &&
|
console.log('[CHECKOUT] ✅ Hardcoded labels found, proceeding');
|
||||||
window.groupOrderShop.labels &&
|
|
||||||
Object.keys(window.groupOrderShop.labels).length > 0
|
|
||||||
) {
|
|
||||||
console.log("[CHECKOUT] ✅ Hardcoded labels found, proceeding");
|
|
||||||
callback();
|
callback();
|
||||||
} else if (Date.now() - startTime < maxWait) {
|
} else if (Date.now() - startTime < maxWait) {
|
||||||
setTimeout(checkLabels, checkInterval);
|
setTimeout(checkLabels, checkInterval);
|
||||||
} else {
|
} else {
|
||||||
console.log("[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway");
|
console.log('[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway');
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkLabels();
|
checkLabels();
|
||||||
};
|
};
|
||||||
|
|
||||||
waitForLabels(function () {
|
waitForLabels(function() {
|
||||||
// Now fetch additional labels from server
|
// Now fetch additional labels from server
|
||||||
// Detect current language from document or navigator
|
// Detect current language from document or navigator
|
||||||
var currentLang =
|
var currentLang = document.documentElement.lang ||
|
||||||
document.documentElement.lang ||
|
document.documentElement.getAttribute('lang') ||
|
||||||
document.documentElement.getAttribute("lang") ||
|
|
||||||
navigator.language ||
|
navigator.language ||
|
||||||
"es_ES";
|
'es_ES';
|
||||||
console.log("[CHECKOUT] Detected language:", currentLang);
|
console.log('[CHECKOUT] Detected language:', currentLang);
|
||||||
|
|
||||||
fetch("/eskaera/labels", {
|
fetch('/eskaera/labels', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
lang: currentLang,
|
lang: currentLang
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.then(function (response) {
|
})
|
||||||
console.log("[CHECKOUT] Response status:", response.status);
|
.then(function(response) {
|
||||||
|
console.log('[CHECKOUT] Response status:', response.status);
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function(data) {
|
||||||
console.log("[CHECKOUT] Response data:", data);
|
console.log('[CHECKOUT] Response data:', data);
|
||||||
var serverLabels = data.result || data;
|
var serverLabels = data.result || data;
|
||||||
console.log(
|
console.log('[CHECKOUT] Server labels count:', Object.keys(serverLabels).length);
|
||||||
"[CHECKOUT] Server labels count:",
|
console.log('[CHECKOUT] Sample server labels:', {
|
||||||
Object.keys(serverLabels).length
|
|
||||||
);
|
|
||||||
console.log("[CHECKOUT] Sample server labels:", {
|
|
||||||
draft_merged_success: serverLabels.draft_merged_success,
|
draft_merged_success: serverLabels.draft_merged_success,
|
||||||
home_delivery: serverLabels.home_delivery,
|
home_delivery: serverLabels.home_delivery
|
||||||
});
|
});
|
||||||
|
|
||||||
// CRITICAL: Merge server labels with existing hardcoded labels
|
// CRITICAL: Merge server labels with existing hardcoded labels
|
||||||
// Hardcoded labels MUST take precedence over server labels
|
// Hardcoded labels MUST take precedence over server labels
|
||||||
if (window.groupOrderShop && window.groupOrderShop.labels) {
|
if (window.groupOrderShop && window.groupOrderShop.labels) {
|
||||||
var existingLabels = window.groupOrderShop.labels;
|
var existingLabels = window.groupOrderShop.labels;
|
||||||
console.log(
|
console.log('[CHECKOUT] Existing hardcoded labels count:', Object.keys(existingLabels).length);
|
||||||
"[CHECKOUT] Existing hardcoded labels count:",
|
console.log('[CHECKOUT] Sample existing labels:', {
|
||||||
Object.keys(existingLabels).length
|
|
||||||
);
|
|
||||||
console.log("[CHECKOUT] Sample existing labels:", {
|
|
||||||
draft_merged_success: existingLabels.draft_merged_success,
|
draft_merged_success: existingLabels.draft_merged_success,
|
||||||
home_delivery: existingLabels.home_delivery,
|
home_delivery: existingLabels.home_delivery
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start with server labels, then overwrite with hardcoded ones
|
// Start with server labels, then overwrite with hardcoded ones
|
||||||
|
|
@ -110,26 +99,23 @@
|
||||||
Object.assign(mergedLabels, existingLabels);
|
Object.assign(mergedLabels, existingLabels);
|
||||||
|
|
||||||
window.groupOrderShop.labels = mergedLabels;
|
window.groupOrderShop.labels = mergedLabels;
|
||||||
console.log(
|
console.log('[CHECKOUT] ✅ Merged labels - final count:', Object.keys(mergedLabels).length);
|
||||||
"[CHECKOUT] ✅ Merged labels - final count:",
|
console.log('[CHECKOUT] Verification:', {
|
||||||
Object.keys(mergedLabels).length
|
|
||||||
);
|
|
||||||
console.log("[CHECKOUT] Verification:", {
|
|
||||||
draft_merged_success: mergedLabels.draft_merged_success,
|
draft_merged_success: mergedLabels.draft_merged_success,
|
||||||
home_delivery: mergedLabels.home_delivery,
|
home_delivery: mergedLabels.home_delivery
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If no existing labels, use server labels as fallback
|
// If no existing labels, use server labels as fallback
|
||||||
if (window.groupOrderShop) {
|
if (window.groupOrderShop) {
|
||||||
window.groupOrderShop.labels = serverLabels;
|
window.groupOrderShop.labels = serverLabels;
|
||||||
}
|
}
|
||||||
console.log("[CHECKOUT] ⚠️ No existing labels, using server labels");
|
console.log('[CHECKOUT] ⚠️ No existing labels, using server labels');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.renderCheckoutSummary(window.groupOrderShop.labels);
|
window.renderCheckoutSummary(window.groupOrderShop.labels);
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
console.error("[CHECKOUT] Error:", error);
|
console.error('[CHECKOUT] Error:', error);
|
||||||
// Fallback to translated labels
|
// Fallback to translated labels
|
||||||
window.renderCheckoutSummary(window.getCheckoutLabels());
|
window.renderCheckoutSummary(window.getCheckoutLabels());
|
||||||
});
|
});
|
||||||
|
|
@ -139,24 +125,20 @@
|
||||||
// Listen for cart ready event instead of polling
|
// Listen for cart ready event instead of polling
|
||||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||||
// Cart already initialized, render immediately
|
// Cart already initialized, render immediately
|
||||||
console.log("[CHECKOUT] Cart already ready");
|
console.log('[CHECKOUT] Cart already ready');
|
||||||
fetchLabelsAndRender();
|
fetchLabelsAndRender();
|
||||||
} else {
|
} else {
|
||||||
// Wait for cart initialization event
|
// Wait for cart initialization event
|
||||||
console.log("[CHECKOUT] Waiting for cart ready event...");
|
console.log('[CHECKOUT] Waiting for cart ready event...');
|
||||||
document.addEventListener(
|
document.addEventListener('groupOrderCartReady', function() {
|
||||||
"groupOrderCartReady",
|
console.log('[CHECKOUT] Cart ready event received');
|
||||||
function () {
|
|
||||||
console.log("[CHECKOUT] Cart ready event received");
|
|
||||||
fetchLabelsAndRender();
|
fetchLabelsAndRender();
|
||||||
},
|
}, { once: true });
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fallback timeout in case event never fires
|
// Fallback timeout in case event never fires
|
||||||
setTimeout(function () {
|
setTimeout(function() {
|
||||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||||
console.log("[CHECKOUT] Fallback timeout triggered");
|
console.log('[CHECKOUT] Fallback timeout triggered');
|
||||||
fetchLabelsAndRender();
|
fetchLabelsAndRender();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
@ -166,88 +148,67 @@
|
||||||
* Render order summary table or empty message
|
* Render order summary table or empty message
|
||||||
* Exposed globally so other scripts can call it
|
* Exposed globally so other scripts can call it
|
||||||
*/
|
*/
|
||||||
window.renderCheckoutSummary = function (labels) {
|
window.renderCheckoutSummary = function(labels) {
|
||||||
labels = labels || window.getCheckoutLabels();
|
labels = labels || window.getCheckoutLabels();
|
||||||
|
|
||||||
var summaryDiv = document.getElementById("checkout-summary");
|
var summaryDiv = document.getElementById('checkout-summary');
|
||||||
if (!summaryDiv) return;
|
if (!summaryDiv) return;
|
||||||
|
|
||||||
var cartKey =
|
var cartKey = 'eskaera_' + (document.getElementById('confirm-order-btn') ? document.getElementById('confirm-order-btn').getAttribute('data-order-id') : '1') + '_cart';
|
||||||
"eskaera_" +
|
var cart = JSON.parse(localStorage.getItem(cartKey) || '{}');
|
||||||
(document.getElementById("confirm-order-btn")
|
|
||||||
? document.getElementById("confirm-order-btn").getAttribute("data-order-id")
|
|
||||||
: "1") +
|
|
||||||
"_cart";
|
|
||||||
var cart = JSON.parse(localStorage.getItem(cartKey) || "{}");
|
|
||||||
|
|
||||||
var summaryTable = summaryDiv.querySelector(".checkout-summary-table");
|
var summaryTable = summaryDiv.querySelector('.checkout-summary-table');
|
||||||
var tbody = summaryDiv.querySelector("#checkout-summary-tbody");
|
var tbody = summaryDiv.querySelector('#checkout-summary-tbody');
|
||||||
var totalSection = summaryDiv.querySelector(".checkout-total-section");
|
var totalSection = summaryDiv.querySelector('.checkout-total-section');
|
||||||
|
|
||||||
// If no table found, create it with headers (shouldn't happen, but fallback)
|
// If no table found, create it with headers (shouldn't happen, but fallback)
|
||||||
if (!summaryTable) {
|
if (!summaryTable) {
|
||||||
var html =
|
var html = '<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
|
||||||
'<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
|
'<th scope="col" class="col-name">' + escapeHtml(labels.product) + '</th>' +
|
||||||
'<th scope="col" class="col-name">' +
|
'<th scope="col" class="col-qty text-center">' + escapeHtml(labels.quantity) + '</th>' +
|
||||||
escapeHtml(labels.product) +
|
'<th scope="col" class="col-price text-right">' + escapeHtml(labels.price) + '</th>' +
|
||||||
"</th>" +
|
'<th scope="col" class="col-subtotal text-right">' + escapeHtml(labels.subtotal) + '</th>' +
|
||||||
'<th scope="col" class="col-qty text-center">' +
|
|
||||||
escapeHtml(labels.quantity) +
|
|
||||||
"</th>" +
|
|
||||||
'<th scope="col" class="col-price text-right">' +
|
|
||||||
escapeHtml(labels.price) +
|
|
||||||
"</th>" +
|
|
||||||
'<th scope="col" class="col-subtotal text-right">' +
|
|
||||||
escapeHtml(labels.subtotal) +
|
|
||||||
"</th>" +
|
|
||||||
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
|
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
|
||||||
'<div class="checkout-total-section"><div class="total-row">' +
|
'<div class="checkout-total-section"><div class="total-row">' +
|
||||||
'<span class="total-label">' +
|
'<span class="total-label">' + escapeHtml(labels.total) + '</span>' +
|
||||||
escapeHtml(labels.total) +
|
|
||||||
"</span>" +
|
|
||||||
'<span class="total-amount" id="checkout-total-amount">€0.00</span>' +
|
'<span class="total-amount" id="checkout-total-amount">€0.00</span>' +
|
||||||
"</div></div>";
|
'</div></div>';
|
||||||
summaryDiv.innerHTML = html;
|
summaryDiv.innerHTML = html;
|
||||||
summaryTable = summaryDiv.querySelector(".checkout-summary-table");
|
summaryTable = summaryDiv.querySelector('.checkout-summary-table');
|
||||||
tbody = summaryDiv.querySelector("#checkout-summary-tbody");
|
tbody = summaryDiv.querySelector('#checkout-summary-tbody');
|
||||||
totalSection = summaryDiv.querySelector(".checkout-total-section");
|
totalSection = summaryDiv.querySelector('.checkout-total-section');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear only tbody, preserve headers
|
// Clear only tbody, preserve headers
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if (Object.keys(cart).length === 0) {
|
if (Object.keys(cart).length === 0) {
|
||||||
// Show empty message if cart is empty
|
// Show empty message if cart is empty
|
||||||
var emptyRow = document.createElement("tr");
|
var emptyRow = document.createElement('tr');
|
||||||
emptyRow.id = "checkout-empty-row";
|
emptyRow.id = 'checkout-empty-row';
|
||||||
emptyRow.className = "empty-message";
|
emptyRow.className = 'empty-message';
|
||||||
emptyRow.innerHTML =
|
emptyRow.innerHTML = '<td colspan="4" class="text-center text-muted py-4">' +
|
||||||
'<td colspan="4" class="text-center text-muted py-4">' +
|
|
||||||
'<i class="fa fa-inbox fa-2x mb-2"></i>' +
|
'<i class="fa fa-inbox fa-2x mb-2"></i>' +
|
||||||
"<p>" +
|
'<p>' + escapeHtml(labels.empty) + '</p>' +
|
||||||
escapeHtml(labels.empty) +
|
'</td>';
|
||||||
"</p>" +
|
|
||||||
"</td>";
|
|
||||||
tbody.appendChild(emptyRow);
|
tbody.appendChild(emptyRow);
|
||||||
|
|
||||||
// Hide total section
|
// Hide total section
|
||||||
totalSection.style.display = "none";
|
totalSection.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
// Hide empty row if visible
|
// Hide empty row if visible
|
||||||
var emptyRow = tbody.querySelector("#checkout-empty-row");
|
var emptyRow = tbody.querySelector('#checkout-empty-row');
|
||||||
if (emptyRow) emptyRow.remove();
|
if (emptyRow) emptyRow.remove();
|
||||||
|
|
||||||
// Get delivery product ID from page data
|
// Get delivery product ID from page data
|
||||||
var checkoutPage = document.querySelector(".eskaera-checkout-page");
|
var checkoutPage = document.querySelector('.eskaera-checkout-page');
|
||||||
var deliveryProductId = checkoutPage
|
var deliveryProductId = checkoutPage ? checkoutPage.getAttribute('data-delivery-product-id') : null;
|
||||||
? checkoutPage.getAttribute("data-delivery-product-id")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Separate normal products from delivery product
|
// Separate normal products from delivery product
|
||||||
var normalProducts = [];
|
var normalProducts = [];
|
||||||
var deliveryProduct = null;
|
var deliveryProduct = null;
|
||||||
|
|
||||||
Object.keys(cart).forEach(function (productId) {
|
Object.keys(cart).forEach(function(productId) {
|
||||||
if (productId === deliveryProductId) {
|
if (productId === deliveryProductId) {
|
||||||
deliveryProduct = { id: productId, item: cart[productId] };
|
deliveryProduct = { id: productId, item: cart[productId] };
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -256,14 +217,14 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort normal products numerically
|
// Sort normal products numerically
|
||||||
normalProducts.sort(function (a, b) {
|
normalProducts.sort(function(a, b) {
|
||||||
return parseInt(a.id) - parseInt(b.id);
|
return parseInt(a.id) - parseInt(b.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
var total = 0;
|
var total = 0;
|
||||||
|
|
||||||
// Render normal products first
|
// Render normal products first
|
||||||
normalProducts.forEach(function (product) {
|
normalProducts.forEach(function(product) {
|
||||||
var item = product.item;
|
var item = product.item;
|
||||||
var qty = parseFloat(item.quantity || item.qty || 1);
|
var qty = parseFloat(item.quantity || item.qty || 1);
|
||||||
if (isNaN(qty)) qty = 1;
|
if (isNaN(qty)) qty = 1;
|
||||||
|
|
@ -272,20 +233,11 @@
|
||||||
var subtotal = qty * price;
|
var subtotal = qty * price;
|
||||||
total += subtotal;
|
total += subtotal;
|
||||||
|
|
||||||
var row = document.createElement("tr");
|
var row = document.createElement('tr');
|
||||||
row.innerHTML =
|
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
|
||||||
"<td>" +
|
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
|
||||||
escapeHtml(item.name) +
|
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
|
||||||
"</td>" +
|
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
|
||||||
'<td class="text-center">' +
|
|
||||||
qty.toFixed(2).replace(/\.?0+$/, "") +
|
|
||||||
"</td>" +
|
|
||||||
'<td class="text-right">€' +
|
|
||||||
price.toFixed(2) +
|
|
||||||
"</td>" +
|
|
||||||
'<td class="text-right">€' +
|
|
||||||
subtotal.toFixed(2) +
|
|
||||||
"</td>";
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -299,41 +251,32 @@
|
||||||
var subtotal = qty * price;
|
var subtotal = qty * price;
|
||||||
total += subtotal;
|
total += subtotal;
|
||||||
|
|
||||||
var row = document.createElement("tr");
|
var row = document.createElement('tr');
|
||||||
row.innerHTML =
|
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
|
||||||
"<td>" +
|
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
|
||||||
escapeHtml(item.name) +
|
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
|
||||||
"</td>" +
|
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
|
||||||
'<td class="text-center">' +
|
|
||||||
qty.toFixed(2).replace(/\.?0+$/, "") +
|
|
||||||
"</td>" +
|
|
||||||
'<td class="text-right">€' +
|
|
||||||
price.toFixed(2) +
|
|
||||||
"</td>" +
|
|
||||||
'<td class="text-right">€' +
|
|
||||||
subtotal.toFixed(2) +
|
|
||||||
"</td>";
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total
|
// Update total
|
||||||
var totalAmount = summaryDiv.querySelector("#checkout-total-amount");
|
var totalAmount = summaryDiv.querySelector('#checkout-total-amount');
|
||||||
if (totalAmount) {
|
if (totalAmount) {
|
||||||
totalAmount.textContent = "€" + total.toFixed(2);
|
totalAmount.textContent = '€' + total.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show total section
|
// Show total section
|
||||||
totalSection.style.display = "block";
|
totalSection.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[CHECKOUT] Summary rendered");
|
console.log('[CHECKOUT] Summary rendered');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape HTML to prevent XSS
|
* Escape HTML to prevent XSS
|
||||||
*/
|
*/
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
var div = document.createElement("div");
|
var div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
* This file is kept for backwards compatibility but is no longer needed.
|
* This file is kept for backwards compatibility but is no longer needed.
|
||||||
* The main renderSummary() logic is in checkout_labels.js
|
* The main renderSummary() logic is in checkout_labels.js
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function() {
|
||||||
"use strict";
|
'use strict';
|
||||||
// Checkout rendering is handled by checkout_labels.js
|
// Checkout rendering is handled by checkout_labels.js
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,65 +3,56 @@
|
||||||
* Manages home delivery checkbox and product addition/removal
|
* Manages home delivery checkbox and product addition/removal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function() {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
var HomeDeliveryManager = {
|
var HomeDeliveryManager = {
|
||||||
deliveryProductId: null,
|
deliveryProductId: null,
|
||||||
deliveryProductPrice: 5.74,
|
deliveryProductPrice: 5.74,
|
||||||
deliveryProductName: "Home Delivery", // Default fallback
|
deliveryProductName: 'Home Delivery', // Default fallback
|
||||||
orderId: null,
|
orderId: null,
|
||||||
homeDeliveryEnabled: false,
|
homeDeliveryEnabled: false,
|
||||||
|
|
||||||
init: function () {
|
init: function() {
|
||||||
// Get delivery product info from data attributes
|
// Get delivery product info from data attributes
|
||||||
var checkoutPage = document.querySelector(".eskaera-checkout-page");
|
var checkoutPage = document.querySelector('.eskaera-checkout-page');
|
||||||
if (checkoutPage) {
|
if (checkoutPage) {
|
||||||
this.deliveryProductId = checkoutPage.getAttribute("data-delivery-product-id");
|
this.deliveryProductId = checkoutPage.getAttribute('data-delivery-product-id');
|
||||||
console.log(
|
console.log('[HomeDelivery] deliveryProductId from attribute:', this.deliveryProductId, 'type:', typeof this.deliveryProductId);
|
||||||
"[HomeDelivery] deliveryProductId from attribute:",
|
|
||||||
this.deliveryProductId,
|
|
||||||
"type:",
|
|
||||||
typeof this.deliveryProductId
|
|
||||||
);
|
|
||||||
|
|
||||||
var price = checkoutPage.getAttribute("data-delivery-product-price");
|
var price = checkoutPage.getAttribute('data-delivery-product-price');
|
||||||
if (price) {
|
if (price) {
|
||||||
this.deliveryProductPrice = parseFloat(price);
|
this.deliveryProductPrice = parseFloat(price);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get translated product name from data attribute (auto-translated by Odoo server)
|
// Get translated product name from data attribute (auto-translated by Odoo server)
|
||||||
var productName = checkoutPage.getAttribute("data-delivery-product-name");
|
var productName = checkoutPage.getAttribute('data-delivery-product-name');
|
||||||
if (productName) {
|
if (productName) {
|
||||||
this.deliveryProductName = productName;
|
this.deliveryProductName = productName;
|
||||||
console.log(
|
console.log('[HomeDelivery] Using translated product name from server:', this.deliveryProductName);
|
||||||
"[HomeDelivery] Using translated product name from server:",
|
|
||||||
this.deliveryProductName
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if home delivery is enabled for this order
|
// Check if home delivery is enabled for this order
|
||||||
var homeDeliveryAttr = checkoutPage.getAttribute("data-home-delivery-enabled");
|
var homeDeliveryAttr = checkoutPage.getAttribute('data-home-delivery-enabled');
|
||||||
this.homeDeliveryEnabled =
|
this.homeDeliveryEnabled = homeDeliveryAttr === 'true' || homeDeliveryAttr === 'True';
|
||||||
homeDeliveryAttr === "true" || homeDeliveryAttr === "True";
|
console.log('[HomeDelivery] Home delivery enabled:', this.homeDeliveryEnabled);
|
||||||
console.log("[HomeDelivery] Home delivery enabled:", this.homeDeliveryEnabled);
|
|
||||||
|
|
||||||
// Show/hide home delivery section based on configuration
|
// Show/hide home delivery section based on configuration
|
||||||
this.toggleHomeDeliverySection();
|
this.toggleHomeDeliverySection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get order ID from confirm button
|
// Get order ID from confirm button
|
||||||
var confirmBtn = document.getElementById("confirm-order-btn");
|
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||||
if (confirmBtn) {
|
if (confirmBtn) {
|
||||||
this.orderId = confirmBtn.getAttribute("data-order-id");
|
this.orderId = confirmBtn.getAttribute('data-order-id');
|
||||||
console.log("[HomeDelivery] orderId from button:", this.orderId);
|
console.log('[HomeDelivery] orderId from button:', this.orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkbox = document.getElementById("home-delivery-checkbox");
|
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||||
if (!checkbox) return;
|
if (!checkbox) return;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
checkbox.addEventListener("change", function () {
|
checkbox.addEventListener('change', function() {
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
self.addDeliveryProduct();
|
self.addDeliveryProduct();
|
||||||
self.showDeliveryInfo();
|
self.showDeliveryInfo();
|
||||||
|
|
@ -75,44 +66,42 @@
|
||||||
this.checkDeliveryInCart();
|
this.checkDeliveryInCart();
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleHomeDeliverySection: function () {
|
toggleHomeDeliverySection: function() {
|
||||||
var homeDeliverySection = document.querySelector(
|
var homeDeliverySection = document.querySelector('[id*="home-delivery"], [class*="home-delivery"]');
|
||||||
'[id*="home-delivery"], [class*="home-delivery"]'
|
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||||
);
|
var homeDeliveryContainer = document.getElementById('home-delivery-container');
|
||||||
var checkbox = document.getElementById("home-delivery-checkbox");
|
|
||||||
var homeDeliveryContainer = document.getElementById("home-delivery-container");
|
|
||||||
|
|
||||||
if (this.homeDeliveryEnabled) {
|
if (this.homeDeliveryEnabled) {
|
||||||
// Show home delivery option
|
// Show home delivery option
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.closest(".form-check").style.display = "block";
|
checkbox.closest('.form-check').style.display = 'block';
|
||||||
}
|
}
|
||||||
if (homeDeliveryContainer) {
|
if (homeDeliveryContainer) {
|
||||||
homeDeliveryContainer.style.display = "block";
|
homeDeliveryContainer.style.display = 'block';
|
||||||
}
|
}
|
||||||
console.log("[HomeDelivery] Home delivery option shown");
|
console.log('[HomeDelivery] Home delivery option shown');
|
||||||
} else {
|
} else {
|
||||||
// Hide home delivery option and delivery info alert
|
// Hide home delivery option and delivery info alert
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.closest(".form-check").style.display = "none";
|
checkbox.closest('.form-check').style.display = 'none';
|
||||||
checkbox.checked = false;
|
checkbox.checked = false;
|
||||||
}
|
}
|
||||||
if (homeDeliveryContainer) {
|
if (homeDeliveryContainer) {
|
||||||
homeDeliveryContainer.style.display = "none";
|
homeDeliveryContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
// Also hide the delivery info alert when home delivery is disabled
|
// Also hide the delivery info alert when home delivery is disabled
|
||||||
this.hideDeliveryInfo();
|
this.hideDeliveryInfo();
|
||||||
this.removeDeliveryProduct();
|
this.removeDeliveryProduct();
|
||||||
console.log("[HomeDelivery] Home delivery option and delivery info hidden");
|
console.log('[HomeDelivery] Home delivery option and delivery info hidden');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkDeliveryInCart: function () {
|
checkDeliveryInCart: function() {
|
||||||
if (!this.deliveryProductId) return;
|
if (!this.deliveryProductId) return;
|
||||||
|
|
||||||
var cart = this.getCart();
|
var cart = this.getCart();
|
||||||
if (cart[this.deliveryProductId]) {
|
if (cart[this.deliveryProductId]) {
|
||||||
var checkbox = document.getElementById("home-delivery-checkbox");
|
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.checked = true;
|
checkbox.checked = true;
|
||||||
this.showDeliveryInfo();
|
this.showDeliveryInfo();
|
||||||
|
|
@ -120,103 +109,93 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getCart: function () {
|
getCart: function() {
|
||||||
if (!this.orderId) return {};
|
if (!this.orderId) return {};
|
||||||
var cartKey = "eskaera_" + this.orderId + "_cart";
|
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||||
var cartStr = localStorage.getItem(cartKey);
|
var cartStr = localStorage.getItem(cartKey);
|
||||||
return cartStr ? JSON.parse(cartStr) : {};
|
return cartStr ? JSON.parse(cartStr) : {};
|
||||||
},
|
},
|
||||||
|
|
||||||
saveCart: function (cart) {
|
saveCart: function(cart) {
|
||||||
if (!this.orderId) return;
|
if (!this.orderId) return;
|
||||||
var cartKey = "eskaera_" + this.orderId + "_cart";
|
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||||
localStorage.setItem(cartKey, JSON.stringify(cart));
|
localStorage.setItem(cartKey, JSON.stringify(cart));
|
||||||
|
|
||||||
// Re-render checkout summary without reloading
|
// Re-render checkout summary without reloading
|
||||||
var self = this;
|
var self = this;
|
||||||
setTimeout(function () {
|
setTimeout(function() {
|
||||||
// Use the global function from checkout_labels.js
|
// Use the global function from checkout_labels.js
|
||||||
if (typeof window.renderCheckoutSummary === "function") {
|
if (typeof window.renderCheckoutSummary === 'function') {
|
||||||
window.renderCheckoutSummary();
|
window.renderCheckoutSummary();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderCheckoutSummary: function () {
|
renderCheckoutSummary: function() {
|
||||||
// Stub - now handled by global window.renderCheckoutSummary
|
// Stub - now handled by global window.renderCheckoutSummary
|
||||||
},
|
},
|
||||||
|
|
||||||
addDeliveryProduct: function () {
|
addDeliveryProduct: function() {
|
||||||
if (!this.deliveryProductId) {
|
if (!this.deliveryProductId) {
|
||||||
console.warn("[HomeDelivery] Delivery product ID not found");
|
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log('[HomeDelivery] Adding delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||||
"[HomeDelivery] Adding delivery product - deliveryProductId:",
|
|
||||||
this.deliveryProductId,
|
|
||||||
"orderId:",
|
|
||||||
this.orderId
|
|
||||||
);
|
|
||||||
var cart = this.getCart();
|
var cart = this.getCart();
|
||||||
console.log("[HomeDelivery] Current cart before adding:", cart);
|
console.log('[HomeDelivery] Current cart before adding:', cart);
|
||||||
|
|
||||||
cart[this.deliveryProductId] = {
|
cart[this.deliveryProductId] = {
|
||||||
id: this.deliveryProductId,
|
id: this.deliveryProductId,
|
||||||
name: this.deliveryProductName,
|
name: this.deliveryProductName,
|
||||||
price: this.deliveryProductPrice,
|
price: this.deliveryProductPrice,
|
||||||
qty: 1,
|
qty: 1
|
||||||
};
|
};
|
||||||
console.log("[HomeDelivery] Cart after adding delivery:", cart);
|
console.log('[HomeDelivery] Cart after adding delivery:', cart);
|
||||||
this.saveCart(cart);
|
this.saveCart(cart);
|
||||||
console.log("[HomeDelivery] Delivery product added to localStorage");
|
console.log('[HomeDelivery] Delivery product added to localStorage');
|
||||||
},
|
},
|
||||||
|
|
||||||
removeDeliveryProduct: function () {
|
removeDeliveryProduct: function() {
|
||||||
if (!this.deliveryProductId) {
|
if (!this.deliveryProductId) {
|
||||||
console.warn("[HomeDelivery] Delivery product ID not found");
|
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log('[HomeDelivery] Removing delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||||
"[HomeDelivery] Removing delivery product - deliveryProductId:",
|
|
||||||
this.deliveryProductId,
|
|
||||||
"orderId:",
|
|
||||||
this.orderId
|
|
||||||
);
|
|
||||||
var cart = this.getCart();
|
var cart = this.getCart();
|
||||||
console.log("[HomeDelivery] Current cart before removing:", cart);
|
console.log('[HomeDelivery] Current cart before removing:', cart);
|
||||||
|
|
||||||
if (cart[this.deliveryProductId]) {
|
if (cart[this.deliveryProductId]) {
|
||||||
delete cart[this.deliveryProductId];
|
delete cart[this.deliveryProductId];
|
||||||
console.log("[HomeDelivery] Cart after removing delivery:", cart);
|
console.log('[HomeDelivery] Cart after removing delivery:', cart);
|
||||||
}
|
}
|
||||||
this.saveCart(cart);
|
this.saveCart(cart);
|
||||||
console.log("[HomeDelivery] Delivery product removed from localStorage");
|
console.log('[HomeDelivery] Delivery product removed from localStorage');
|
||||||
},
|
},
|
||||||
|
|
||||||
showDeliveryInfo: function () {
|
showDeliveryInfo: function() {
|
||||||
var alert = document.getElementById("delivery-info-alert");
|
var alert = document.getElementById('delivery-info-alert');
|
||||||
if (alert) {
|
if (alert) {
|
||||||
console.log("[HomeDelivery] Showing delivery info alert");
|
console.log('[HomeDelivery] Showing delivery info alert');
|
||||||
alert.classList.remove("d-none");
|
alert.classList.remove('d-none');
|
||||||
alert.style.display = "block";
|
alert.style.display = 'block';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hideDeliveryInfo: function () {
|
hideDeliveryInfo: function() {
|
||||||
var alert = document.getElementById("delivery-info-alert");
|
var alert = document.getElementById('delivery-info-alert');
|
||||||
if (alert) {
|
if (alert) {
|
||||||
console.log("[HomeDelivery] Hiding delivery info alert");
|
console.log('[HomeDelivery] Hiding delivery info alert');
|
||||||
alert.classList.add("d-none");
|
alert.classList.add('d-none');
|
||||||
alert.style.display = "none";
|
alert.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize on DOM ready
|
// Initialize on DOM ready
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
HomeDeliveryManager.init();
|
HomeDeliveryManager.init();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,15 @@
|
||||||
* License AGPL-3.0 or later
|
* License AGPL-3.0 or later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function() {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
// Keep legacy functions as wrappers for backwards compatibility
|
// Keep legacy functions as wrappers for backwards compatibility
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead
|
* DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead
|
||||||
*/
|
*/
|
||||||
window.getCheckoutLabels = function (key) {
|
window.getCheckoutLabels = function(key) {
|
||||||
if (window.i18nManager && window.i18nManager.initialized) {
|
if (window.i18nManager && window.i18nManager.initialized) {
|
||||||
if (key) {
|
if (key) {
|
||||||
return window.i18nManager.get(key);
|
return window.i18nManager.get(key);
|
||||||
|
|
@ -38,29 +38,30 @@
|
||||||
/**
|
/**
|
||||||
* DEPRECATED - Use i18nManager.getAll() instead
|
* DEPRECATED - Use i18nManager.getAll() instead
|
||||||
*/
|
*/
|
||||||
window.getSearchLabels = function () {
|
window.getSearchLabels = function() {
|
||||||
if (window.i18nManager && window.i18nManager.initialized) {
|
if (window.i18nManager && window.i18nManager.initialized) {
|
||||||
return {
|
return {
|
||||||
searchPlaceholder: window.i18nManager.get("search_products"),
|
'searchPlaceholder': window.i18nManager.get('search_products'),
|
||||||
noResults: window.i18nManager.get("no_results"),
|
'noResults': window.i18nManager.get('no_results')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
searchPlaceholder: "Search products...",
|
'searchPlaceholder': 'Search products...',
|
||||||
noResults: "No products found",
|
'noResults': 'No products found'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DEPRECATED - Use i18nManager.formatCurrency(amount) instead
|
* DEPRECATED - Use i18nManager.formatCurrency(amount) instead
|
||||||
*/
|
*/
|
||||||
window.formatCurrency = function (amount) {
|
window.formatCurrency = function(amount) {
|
||||||
if (window.i18nManager) {
|
if (window.i18nManager) {
|
||||||
return window.i18nManager.formatCurrency(amount);
|
return window.i18nManager.formatCurrency(amount);
|
||||||
}
|
}
|
||||||
// Fallback
|
// Fallback
|
||||||
return "€" + parseFloat(amount).toFixed(2);
|
return '€' + parseFloat(amount).toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[i18n_helpers] DEPRECATED - Use i18n_manager.js instead");
|
console.log('[i18n_helpers] DEPRECATED - Use i18n_manager.js instead');
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
* License AGPL-3.0 or later
|
* License AGPL-3.0 or later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function() {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
window.i18nManager = {
|
window.i18nManager = {
|
||||||
labels: null,
|
labels: null,
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
* Initialize by fetching translations from server
|
* Initialize by fetching translations from server
|
||||||
* Returns a Promise that resolves when translations are loaded
|
* Returns a Promise that resolves when translations are loaded
|
||||||
*/
|
*/
|
||||||
init: function () {
|
init: function() {
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -38,40 +38,36 @@
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Detect user's language from document or fallback to en_US
|
// Detect user's language from document or fallback to en_US
|
||||||
var detectedLang = document.documentElement.lang || "es_ES";
|
var detectedLang = document.documentElement.lang || 'es_ES';
|
||||||
console.log("[i18nManager] Detected language:", detectedLang);
|
console.log('[i18nManager] Detected language:', detectedLang);
|
||||||
|
|
||||||
// Fetch translations from server
|
// Fetch translations from server
|
||||||
this.initPromise = fetch("/eskaera/i18n", {
|
this.initPromise = fetch('/eskaera/i18n', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ lang: detectedLang }),
|
body: JSON.stringify({ lang: detectedLang })
|
||||||
})
|
})
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("HTTP error, status = " + response.status);
|
throw new Error('HTTP error, status = ' + response.status);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function(data) {
|
||||||
// Handle JSON-RPC response format
|
// Handle JSON-RPC response format
|
||||||
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
|
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
|
||||||
// Extract the actual labels from the result property
|
// Extract the actual labels from the result property
|
||||||
var labels = data.result || data;
|
var labels = data.result || data;
|
||||||
|
|
||||||
console.log(
|
console.log('[i18nManager] ✓ Loaded', Object.keys(labels).length, 'translation labels');
|
||||||
"[i18nManager] ✓ Loaded",
|
|
||||||
Object.keys(labels).length,
|
|
||||||
"translation labels"
|
|
||||||
);
|
|
||||||
self.labels = labels;
|
self.labels = labels;
|
||||||
self.initialized = true;
|
self.initialized = true;
|
||||||
return labels;
|
return labels;
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
console.error("[i18nManager] Error loading translations:", error);
|
console.error('[i18nManager] Error loading translations:', error);
|
||||||
// Fallback to empty object so app doesn't crash
|
// Fallback to empty object so app doesn't crash
|
||||||
self.labels = {};
|
self.labels = {};
|
||||||
self.initialized = true;
|
self.initialized = true;
|
||||||
|
|
@ -85,9 +81,9 @@
|
||||||
* Get a specific translation label
|
* Get a specific translation label
|
||||||
* Returns the translated string or the key if not found
|
* Returns the translated string or the key if not found
|
||||||
*/
|
*/
|
||||||
get: function (key) {
|
get: function(key) {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
console.warn("[i18nManager] Not yet initialized. Call init() first.");
|
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
return this.labels[key] || key;
|
return this.labels[key] || key;
|
||||||
|
|
@ -96,9 +92,9 @@
|
||||||
/**
|
/**
|
||||||
* Get all translation labels as object
|
* Get all translation labels as object
|
||||||
*/
|
*/
|
||||||
getAll: function () {
|
getAll: function() {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
console.warn("[i18nManager] Not yet initialized. Call init() first.");
|
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return this.labels;
|
return this.labels;
|
||||||
|
|
@ -107,7 +103,7 @@
|
||||||
/**
|
/**
|
||||||
* Check if a specific label exists
|
* Check if a specific label exists
|
||||||
*/
|
*/
|
||||||
has: function (key) {
|
has: function(key) {
|
||||||
if (!this.initialized) return false;
|
if (!this.initialized) return false;
|
||||||
return key in this.labels;
|
return key in this.labels;
|
||||||
},
|
},
|
||||||
|
|
@ -115,42 +111,43 @@
|
||||||
/**
|
/**
|
||||||
* Format currency to Euro format
|
* Format currency to Euro format
|
||||||
*/
|
*/
|
||||||
formatCurrency: function (amount) {
|
formatCurrency: function(amount) {
|
||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(document.documentElement.lang || "es_ES", {
|
return new Intl.NumberFormat(document.documentElement.lang || 'es_ES', {
|
||||||
style: "currency",
|
style: 'currency',
|
||||||
currency: "EUR",
|
currency: 'EUR'
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback to simple Euro format
|
// Fallback to simple Euro format
|
||||||
return "€" + parseFloat(amount).toFixed(2);
|
return '€' + parseFloat(amount).toFixed(2);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape HTML to prevent XSS
|
* Escape HTML to prevent XSS
|
||||||
*/
|
*/
|
||||||
escapeHtml: function (text) {
|
escapeHtml: function(text) {
|
||||||
if (!text) return "";
|
if (!text) return '';
|
||||||
var div = document.createElement("div");
|
var div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-initialize on DOM ready
|
// Auto-initialize on DOM ready
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
i18nManager.init().catch(function (err) {
|
i18nManager.init().catch(function(err) {
|
||||||
console.error("[i18nManager] Auto-init failed:", err);
|
console.error('[i18nManager] Auto-init failed:', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// DOM already loaded
|
// DOM already loaded
|
||||||
setTimeout(function () {
|
setTimeout(function() {
|
||||||
i18nManager.init().catch(function (err) {
|
i18nManager.init().catch(function(err) {
|
||||||
console.error("[i18nManager] Auto-init failed:", err);
|
console.error('[i18nManager] Auto-init failed:', err);
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -7,105 +7,25 @@
|
||||||
|
|
||||||
console.log("[INFINITE_SCROLL] Script loaded!");
|
console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
|
|
||||||
// DEBUG: Add MutationObserver to detect WHO is clearing the products grid
|
// Visual indicator for debugging
|
||||||
(function () {
|
if (typeof document !== "undefined") {
|
||||||
var setupGridObserver = function () {
|
try {
|
||||||
var grid = document.getElementById("products-grid");
|
var debugDiv = document.createElement("div");
|
||||||
if (!grid) {
|
debugDiv.innerHTML = "[INFINITE_SCROLL LOADED]";
|
||||||
console.log("[MUTATION_DEBUG] products-grid not found yet, will retry...");
|
debugDiv.style.position = "fixed";
|
||||||
setTimeout(setupGridObserver, 100);
|
debugDiv.style.top = "0";
|
||||||
return;
|
debugDiv.style.right = "0";
|
||||||
|
debugDiv.style.backgroundColor = "#00ff00";
|
||||||
|
debugDiv.style.color = "#000";
|
||||||
|
debugDiv.style.padding = "5px 10px";
|
||||||
|
debugDiv.style.fontSize = "12px";
|
||||||
|
debugDiv.style.zIndex = "99999";
|
||||||
|
debugDiv.id = "infinite-scroll-debug";
|
||||||
|
document.body.appendChild(debugDiv);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[INFINITE_SCROLL] Error adding debug div:", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
console.log("[MUTATION_DEBUG] 🔍 Setting up MutationObserver on products-grid");
|
|
||||||
console.log("[MUTATION_DEBUG] Initial child count:", grid.children.length);
|
|
||||||
console.log("[MUTATION_DEBUG] Grid innerHTML length:", grid.innerHTML.length);
|
|
||||||
|
|
||||||
// Watch the grid itself for child changes
|
|
||||||
var gridObserver = new MutationObserver(function (mutations) {
|
|
||||||
mutations.forEach(function (mutation) {
|
|
||||||
if (mutation.type === "childList") {
|
|
||||||
if (mutation.removedNodes.length > 0) {
|
|
||||||
console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS REMOVED FROM GRID!");
|
|
||||||
console.log(
|
|
||||||
"[MUTATION_DEBUG] Removed nodes count:",
|
|
||||||
mutation.removedNodes.length
|
|
||||||
);
|
|
||||||
console.log("[MUTATION_DEBUG] Stack trace:");
|
|
||||||
console.trace();
|
|
||||||
}
|
|
||||||
if (mutation.addedNodes.length > 0) {
|
|
||||||
console.log("[MUTATION_DEBUG] Products added:", mutation.addedNodes.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
gridObserver.observe(grid, { childList: true, subtree: false });
|
|
||||||
|
|
||||||
// ALSO watch the parent for the grid element itself being replaced/removed
|
|
||||||
var parent = grid.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
console.log(
|
|
||||||
"[MUTATION_DEBUG] 🔍 Also watching parent element:",
|
|
||||||
parent.tagName,
|
|
||||||
parent.className
|
|
||||||
);
|
|
||||||
var parentObserver = new MutationObserver(function (mutations) {
|
|
||||||
mutations.forEach(function (mutation) {
|
|
||||||
if (mutation.type === "childList") {
|
|
||||||
mutation.removedNodes.forEach(function (node) {
|
|
||||||
if (
|
|
||||||
node.id === "products-grid" ||
|
|
||||||
(node.querySelector && node.querySelector("#products-grid"))
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
"[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS-GRID ELEMENT ITSELF WAS REMOVED!"
|
|
||||||
);
|
|
||||||
console.log("[MUTATION_DEBUG] Stack trace:");
|
|
||||||
console.trace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
parentObserver.observe(parent, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poll to detect innerHTML being cleared (as backup)
|
|
||||||
var lastChildCount = grid.children.length;
|
|
||||||
setInterval(function () {
|
|
||||||
var currentGrid = document.getElementById("products-grid");
|
|
||||||
if (!currentGrid) {
|
|
||||||
console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID ELEMENT NO LONGER EXISTS!");
|
|
||||||
console.trace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var currentChildCount = currentGrid.children.length;
|
|
||||||
if (currentChildCount !== lastChildCount) {
|
|
||||||
console.log(
|
|
||||||
"[MUTATION_DEBUG] 📊 Child count changed: " +
|
|
||||||
lastChildCount +
|
|
||||||
" → " +
|
|
||||||
currentChildCount
|
|
||||||
);
|
|
||||||
if (currentChildCount === 0 && lastChildCount > 0) {
|
|
||||||
console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID WAS EMPTIED!");
|
|
||||||
console.trace();
|
|
||||||
}
|
|
||||||
lastChildCount = currentChildCount;
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
console.log("[MUTATION_DEBUG] ✅ Observers attached (grid + parent + polling)");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start observing as soon as possible
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", setupGridObserver);
|
|
||||||
} else {
|
|
||||||
setupGridObserver();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
@ -125,16 +45,10 @@ console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
config: {},
|
config: {},
|
||||||
|
|
||||||
init: function () {
|
init: function () {
|
||||||
console.log("[INFINITE_SCROLL] 🔧 init() called");
|
|
||||||
|
|
||||||
// Get configuration from page data
|
// Get configuration from page data
|
||||||
var configEl = document.getElementById("eskaera-config");
|
var configEl = document.getElementById("eskaera-config");
|
||||||
console.log("[INFINITE_SCROLL] eskaera-config element:", configEl);
|
|
||||||
|
|
||||||
if (!configEl) {
|
if (!configEl) {
|
||||||
console.error(
|
console.log("[INFINITE_SCROLL] No eskaera-config found, lazy loading disabled");
|
||||||
"[INFINITE_SCROLL] ❌ No eskaera-config found, lazy loading disabled"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,33 +58,15 @@ console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20;
|
this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20;
|
||||||
this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1;
|
this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1;
|
||||||
|
|
||||||
console.log("[INFINITE_SCROLL] Config loaded:", {
|
|
||||||
orderId: this.orderId,
|
|
||||||
searchQuery: this.searchQuery,
|
|
||||||
category: this.category,
|
|
||||||
perPage: this.perPage,
|
|
||||||
currentPage: this.currentPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if there are more products to load from data attribute
|
// Check if there are more products to load from data attribute
|
||||||
var hasNextAttr = configEl.getAttribute("data-has-next");
|
var hasNextAttr = configEl.getAttribute("data-has-next");
|
||||||
this.hasMore = hasNextAttr === "true" || hasNextAttr === "True";
|
this.hasMore = hasNextAttr === "true" || hasNextAttr === "True";
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] hasMore=" +
|
|
||||||
this.hasMore +
|
|
||||||
" (data-has-next=" +
|
|
||||||
hasNextAttr +
|
|
||||||
")"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.hasMore) {
|
if (!this.hasMore) {
|
||||||
console.log(
|
console.log(
|
||||||
"[INFINITE_SCROLL] ⚠️ No more pages available, but keeping initialized for filter handling (has_next=" +
|
"[INFINITE_SCROLL] No more products to load (has_next=" + hasNextAttr + ")"
|
||||||
hasNextAttr +
|
|
||||||
")"
|
|
||||||
);
|
);
|
||||||
// Don't return - we need to stay initialized so realtime_search can call resetWithFilters()
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[INFINITE_SCROLL] Initialized with:", {
|
console.log("[INFINITE_SCROLL] Initialized with:", {
|
||||||
|
|
@ -181,50 +77,36 @@ console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
currentPage: this.currentPage,
|
currentPage: this.currentPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only attach scroll listener if there are more pages to load
|
|
||||||
if (this.hasMore) {
|
|
||||||
this.attachScrollListener();
|
this.attachScrollListener();
|
||||||
|
// Also keep the button listener as fallback
|
||||||
this.attachFallbackButtonListener();
|
this.attachFallbackButtonListener();
|
||||||
} else {
|
|
||||||
console.log("[INFINITE_SCROLL] Skipping scroll listener (no more pages)");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
attachScrollListener: function () {
|
attachScrollListener: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
var scrollThreshold = 300; // Load when within 300px of the bottom of the grid
|
var scrollThreshold = 0.8; // Load when 80% scrolled
|
||||||
|
|
||||||
window.addEventListener("scroll", function () {
|
window.addEventListener("scroll", function () {
|
||||||
if (self.isLoading || !self.hasMore) {
|
if (self.isLoading || !self.hasMore) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var grid = document.getElementById("products-grid");
|
var scrollHeight = document.documentElement.scrollHeight;
|
||||||
if (!grid) {
|
var scrollTop = window.scrollY;
|
||||||
return;
|
var clientHeight = window.innerHeight;
|
||||||
}
|
var scrollPercent = (scrollTop + clientHeight) / scrollHeight;
|
||||||
|
|
||||||
// Calculate distance from bottom of grid to bottom of viewport
|
if (scrollPercent >= scrollThreshold) {
|
||||||
var gridRect = grid.getBoundingClientRect();
|
|
||||||
var gridBottom = gridRect.bottom;
|
|
||||||
var viewportBottom = window.innerHeight;
|
|
||||||
var distanceFromBottom = gridBottom - viewportBottom;
|
|
||||||
|
|
||||||
// Load more if we're within threshold pixels of the grid bottom
|
|
||||||
if (distanceFromBottom <= scrollThreshold && distanceFromBottom > 0) {
|
|
||||||
console.log(
|
console.log(
|
||||||
"[INFINITE_SCROLL] Near grid bottom (distance: " +
|
"[INFINITE_SCROLL] Scroll threshold reached, loading next page"
|
||||||
Math.round(distanceFromBottom) +
|
|
||||||
"px), loading next page"
|
|
||||||
);
|
);
|
||||||
self.loadNextPage();
|
self.loadNextPage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[INFINITE_SCROLL] Scroll listener attached (threshold: " +
|
"[INFINITE_SCROLL] Scroll listener attached (threshold:",
|
||||||
scrollThreshold +
|
scrollThreshold * 100 + "%)"
|
||||||
"px from grid bottom)"
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -252,53 +134,20 @@ console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
/**
|
/**
|
||||||
* Reset infinite scroll to page 1 with new filters and reload products.
|
* Reset infinite scroll to page 1 with new filters and reload products.
|
||||||
* Called by realtime_search when filters change.
|
* Called by realtime_search when filters change.
|
||||||
*
|
|
||||||
* WARNING: This clears the grid! Only call when filters actually change.
|
|
||||||
*/
|
*/
|
||||||
console.log(
|
console.log(
|
||||||
"[INFINITE_SCROLL] ⚠️⚠️⚠️ resetWithFilters CALLED - search=" +
|
"[INFINITE_SCROLL] Resetting with filters: search=" +
|
||||||
searchQuery +
|
searchQuery +
|
||||||
" category=" +
|
" category=" +
|
||||||
categoryId
|
categoryId
|
||||||
);
|
);
|
||||||
console.trace("[INFINITE_SCROLL] ⚠️⚠️⚠️ WHO CALLED resetWithFilters? Call stack:");
|
|
||||||
|
|
||||||
// Normalize values: empty string to "", null to "0" for category
|
this.searchQuery = searchQuery || "";
|
||||||
var newSearchQuery = (searchQuery || "").trim();
|
this.category = categoryId || "0";
|
||||||
var newCategory = (categoryId || "").trim() || "0";
|
|
||||||
|
|
||||||
// CHECK IF VALUES ACTUALLY CHANGED before clearing grid!
|
|
||||||
if (newSearchQuery === this.searchQuery && newCategory === this.category) {
|
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] ✅ NO CHANGE - Skipping reset (values are identical)"
|
|
||||||
);
|
|
||||||
return; // Don't clear grid if nothing changed!
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] 🔥 VALUES CHANGED - Old: search=" +
|
|
||||||
this.searchQuery +
|
|
||||||
" category=" +
|
|
||||||
this.category +
|
|
||||||
" → New: search=" +
|
|
||||||
newSearchQuery +
|
|
||||||
" category=" +
|
|
||||||
newCategory
|
|
||||||
);
|
|
||||||
|
|
||||||
this.searchQuery = newSearchQuery;
|
|
||||||
this.category = newCategory;
|
|
||||||
this.currentPage = 0; // Set to 0 so loadNextPage() increments to 1
|
this.currentPage = 0; // Set to 0 so loadNextPage() increments to 1
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.hasMore = true;
|
this.hasMore = true;
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] After normalization: search=" +
|
|
||||||
this.searchQuery +
|
|
||||||
" category=" +
|
|
||||||
this.category
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the config element data attributes for consistency
|
// Update the config element data attributes for consistency
|
||||||
var configEl = document.getElementById("eskaera-config");
|
var configEl = document.getElementById("eskaera-config");
|
||||||
if (configEl) {
|
if (configEl) {
|
||||||
|
|
@ -306,58 +155,26 @@ console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
configEl.setAttribute("data-category", this.category);
|
configEl.setAttribute("data-category", this.category);
|
||||||
configEl.setAttribute("data-current-page", "1");
|
configEl.setAttribute("data-current-page", "1");
|
||||||
configEl.setAttribute("data-has-next", "true");
|
configEl.setAttribute("data-has-next", "true");
|
||||||
console.log("[INFINITE_SCROLL] Updated eskaera-config attributes");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the grid and reload from page 1
|
// Clear the grid and reload from page 1
|
||||||
var grid = document.getElementById("products-grid");
|
var grid = document.getElementById("products-grid");
|
||||||
if (grid) {
|
if (grid) {
|
||||||
console.log("[INFINITE_SCROLL] 🗑️ CLEARING GRID NOW!");
|
|
||||||
grid.innerHTML = "";
|
grid.innerHTML = "";
|
||||||
console.log("[INFINITE_SCROLL] Grid cleared");
|
console.log("[INFINITE_SCROLL] Grid cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load first page with new filters
|
// Load first page with new filters
|
||||||
console.log("[INFINITE_SCROLL] Calling loadNextPage()...");
|
|
||||||
this.loadNextPage();
|
this.loadNextPage();
|
||||||
},
|
},
|
||||||
|
|
||||||
loadNextPage: function () {
|
loadNextPage: function () {
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] 🚀 loadNextPage() CALLED - currentPage=" +
|
|
||||||
this.currentPage +
|
|
||||||
" isLoading=" +
|
|
||||||
this.isLoading +
|
|
||||||
" hasMore=" +
|
|
||||||
this.hasMore
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.isLoading || !this.hasMore) {
|
|
||||||
console.log("[INFINITE_SCROLL] ❌ ABORTING - already loading or no more pages");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
// Only increment if we're not loading first page (currentPage will be 0 after reset)
|
|
||||||
if (this.currentPage === 0) {
|
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] ✅ Incrementing from 0 to 1 (first page after reset)"
|
|
||||||
);
|
|
||||||
this.currentPage = 1;
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"[INFINITE_SCROLL] ✅ Incrementing page " +
|
|
||||||
this.currentPage +
|
|
||||||
" → " +
|
|
||||||
(this.currentPage + 1)
|
|
||||||
);
|
|
||||||
this.currentPage += 1;
|
this.currentPage += 1;
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[INFINITE_SCROLL] 📡 About to fetch page",
|
"[INFINITE_SCROLL] Loading page",
|
||||||
this.currentPage,
|
this.currentPage,
|
||||||
"for order",
|
"for order",
|
||||||
this.orderId
|
this.orderId
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,6 @@
|
||||||
_attachEventListeners: function () {
|
_attachEventListeners: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Flag to prevent filtering during initialization
|
|
||||||
self.isInitializing = true;
|
|
||||||
|
|
||||||
// Initialize available tags from DOM
|
// Initialize available tags from DOM
|
||||||
self._initializeAvailableTags();
|
self._initializeAvailableTags();
|
||||||
|
|
||||||
|
|
@ -145,68 +142,8 @@
|
||||||
self.originalTagColors = {}; // Maps tag ID to original color
|
self.originalTagColors = {}; // Maps tag ID to original color
|
||||||
|
|
||||||
// Store last values at instance level so polling can access them
|
// Store last values at instance level so polling can access them
|
||||||
// Initialize to current values to avoid triggering reset on first poll
|
|
||||||
self.lastSearchValue = self.searchInput.value.trim();
|
|
||||||
self.lastCategoryValue = self.categorySelect.value;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[realtimeSearch] Initial values stored - search:",
|
|
||||||
JSON.stringify(self.lastSearchValue),
|
|
||||||
"category:",
|
|
||||||
JSON.stringify(self.lastCategoryValue)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear search button
|
|
||||||
self.clearSearchBtn = document.getElementById("clear-search-btn");
|
|
||||||
if (self.clearSearchBtn) {
|
|
||||||
console.log("[realtimeSearch] Clear search button found, attaching listeners");
|
|
||||||
|
|
||||||
// Show/hide button based on input content (passive, no filtering)
|
|
||||||
// This listener is separate from the filtering listener
|
|
||||||
self.searchInput.addEventListener("input", function () {
|
|
||||||
if (self.searchInput.value.trim().length > 0) {
|
|
||||||
self.clearSearchBtn.style.display = "block";
|
|
||||||
} else {
|
|
||||||
self.clearSearchBtn.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear search when button clicked
|
|
||||||
self.clearSearchBtn.addEventListener("click", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
console.log("[realtimeSearch] Clear search button clicked");
|
|
||||||
|
|
||||||
// Clear the input
|
|
||||||
self.searchInput.value = "";
|
|
||||||
self.clearSearchBtn.style.display = "none";
|
|
||||||
|
|
||||||
// Update last stored value to prevent polling from detecting "change"
|
|
||||||
self.lastSearchValue = "";
|
self.lastSearchValue = "";
|
||||||
|
self.lastCategoryValue = "";
|
||||||
// Reset infinite scroll to reload all products from server
|
|
||||||
if (
|
|
||||||
window.infiniteScroll &&
|
|
||||||
typeof window.infiniteScroll.resetWithFilters === "function"
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
"[realtimeSearch] Resetting infinite scroll to show all products"
|
|
||||||
);
|
|
||||||
window.infiniteScroll.resetWithFilters("", self.lastCategoryValue);
|
|
||||||
} else if (!self.isInitializing) {
|
|
||||||
// Fallback: filter locally
|
|
||||||
self._filterProducts();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus back to search input
|
|
||||||
self.searchInput.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial check - don't show if empty
|
|
||||||
if (self.searchInput.value.trim().length > 0) {
|
|
||||||
self.clearSearchBtn.style.display = "block";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent form submission completely
|
// Prevent form submission completely
|
||||||
var form = self.searchInput.closest("form");
|
var form = self.searchInput.closest("form");
|
||||||
|
|
@ -232,11 +169,6 @@
|
||||||
// Search input: listen to 'input' for real-time filtering
|
// Search input: listen to 'input' for real-time filtering
|
||||||
self.searchInput.addEventListener("input", function (e) {
|
self.searchInput.addEventListener("input", function (e) {
|
||||||
try {
|
try {
|
||||||
// Skip filtering during initialization
|
|
||||||
if (self.isInitializing) {
|
|
||||||
console.log("[realtimeSearch] INPUT event during init - skipping filter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
|
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
|
||||||
self._filterProducts();
|
self._filterProducts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -247,11 +179,6 @@
|
||||||
// Also keep 'keyup' for extra compatibility
|
// Also keep 'keyup' for extra compatibility
|
||||||
self.searchInput.addEventListener("keyup", function (e) {
|
self.searchInput.addEventListener("keyup", function (e) {
|
||||||
try {
|
try {
|
||||||
// Skip filtering during initialization
|
|
||||||
if (self.isInitializing) {
|
|
||||||
console.log("[realtimeSearch] KEYUP event during init - skipping filter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"');
|
console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"');
|
||||||
self._filterProducts();
|
self._filterProducts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -262,11 +189,6 @@
|
||||||
// Category select
|
// Category select
|
||||||
self.categorySelect.addEventListener("change", function (e) {
|
self.categorySelect.addEventListener("change", function (e) {
|
||||||
try {
|
try {
|
||||||
// Skip filtering during initialization
|
|
||||||
if (self.isInitializing) {
|
|
||||||
console.log("[realtimeSearch] CHANGE event during init - skipping filter");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(
|
console.log(
|
||||||
'[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"'
|
'[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"'
|
||||||
);
|
);
|
||||||
|
|
@ -393,10 +315,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter products (independent of search/category state)
|
// Filter products (independent of search/category state)
|
||||||
// Skip during initialization
|
|
||||||
if (!self.isInitializing) {
|
|
||||||
self._filterProducts();
|
self._filterProducts();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -409,11 +328,6 @@
|
||||||
var pollingCounter = 0;
|
var pollingCounter = 0;
|
||||||
var pollInterval = setInterval(function () {
|
var pollInterval = setInterval(function () {
|
||||||
try {
|
try {
|
||||||
// Skip polling during initialization to avoid clearing products
|
|
||||||
if (self.isInitializing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pollingCounter++;
|
pollingCounter++;
|
||||||
|
|
||||||
// Try multiple ways to get the search value
|
// Try multiple ways to get the search value
|
||||||
|
|
@ -504,15 +418,12 @@
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: filter locally (but this only filters loaded products)
|
// Fallback: filter locally (but this only filters loaded products)
|
||||||
// Skip during initialization
|
|
||||||
if (!self.isInitializing) {
|
|
||||||
console.log(
|
console.log(
|
||||||
"[realtimeSearch] infiniteScroll not available, filtering locally only"
|
"[realtimeSearch] infiniteScroll not available, filtering locally only"
|
||||||
);
|
);
|
||||||
self._filterProducts();
|
self._filterProducts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[realtimeSearch] ❌ Error in polling:", error.message);
|
console.error("[realtimeSearch] ❌ Error in polling:", error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -521,10 +432,6 @@
|
||||||
console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval);
|
console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval);
|
||||||
|
|
||||||
console.log("[realtimeSearch] Event listeners attached with polling fallback");
|
console.log("[realtimeSearch] Event listeners attached with polling fallback");
|
||||||
|
|
||||||
// Initialization complete - allow filtering now
|
|
||||||
self.isInitializing = false;
|
|
||||||
console.log("[realtimeSearch] ✅ Initialization complete - filtering enabled");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_initializeAvailableTags: function () {
|
_initializeAvailableTags: function () {
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,6 @@
|
||||||
|
|
||||||
console.log("Initializing cart for order:", this.orderId);
|
console.log("Initializing cart for order:", this.orderId);
|
||||||
|
|
||||||
// Attach event listeners FIRST (doesn't depend on translations)
|
|
||||||
this._attachEventListeners();
|
|
||||||
console.log("[groupOrderShop] Event listeners attached");
|
|
||||||
|
|
||||||
// Wait for i18nManager to load translations from server
|
// Wait for i18nManager to load translations from server
|
||||||
i18nManager
|
i18nManager
|
||||||
.init()
|
.init()
|
||||||
|
|
@ -56,6 +52,8 @@
|
||||||
console.log("[groupOrderShop] Translations loaded from server");
|
console.log("[groupOrderShop] Translations loaded from server");
|
||||||
self.labels = i18nManager.getAll();
|
self.labels = i18nManager.getAll();
|
||||||
|
|
||||||
|
// Initialize event listeners and state after translations are ready
|
||||||
|
self._attachEventListeners();
|
||||||
self._loadCart();
|
self._loadCart();
|
||||||
self._checkConfirmationMessage();
|
self._checkConfirmationMessage();
|
||||||
self._initializeTooltips();
|
self._initializeTooltips();
|
||||||
|
|
@ -605,82 +603,12 @@
|
||||||
_attachEventListeners: function () {
|
_attachEventListeners: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Helper function to round decimals correctly
|
|
||||||
function roundDecimal(value, decimals) {
|
|
||||||
var factor = Math.pow(10, decimals);
|
|
||||||
return Math.round(value * factor) / factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ ATTACH CHECKOUT BUTTONS (ALWAYS, on any page) ============
|
|
||||||
// These buttons exist on checkout page, not on cart pages
|
|
||||||
if (!this._cartCheckoutListenersAttached) {
|
|
||||||
console.log("[_attachEventListeners] Attaching checkout button listeners...");
|
|
||||||
|
|
||||||
// Button to save as draft (in checkout page)
|
|
||||||
var saveBtn = document.getElementById("save-order-btn");
|
|
||||||
console.log("[_attachEventListeners] save-order-btn found:", !!saveBtn);
|
|
||||||
|
|
||||||
if (saveBtn) {
|
|
||||||
saveBtn.addEventListener("click", function (e) {
|
|
||||||
console.log("[CLICK] save-order-btn clicked");
|
|
||||||
e.preventDefault();
|
|
||||||
self._saveOrderDraft();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm order button (in checkout page)
|
|
||||||
var confirmBtn = document.getElementById("confirm-order-btn");
|
|
||||||
console.log("[_attachEventListeners] confirm-order-btn found:", !!confirmBtn);
|
|
||||||
|
|
||||||
if (confirmBtn) {
|
|
||||||
confirmBtn.addEventListener("click", function (e) {
|
|
||||||
console.log("[CLICK] confirm-order-btn clicked");
|
|
||||||
e.preventDefault();
|
|
||||||
self._confirmOrder();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button to reload from draft (in My Cart header - cart pages)
|
|
||||||
var reloadCartBtn = document.getElementById("reload-cart-btn");
|
|
||||||
console.log("[_attachEventListeners] reload-cart-btn found:", !!reloadCartBtn);
|
|
||||||
|
|
||||||
if (reloadCartBtn) {
|
|
||||||
reloadCartBtn.addEventListener("click", function (e) {
|
|
||||||
console.log("[CLICK] reload-cart-btn clicked");
|
|
||||||
e.preventDefault();
|
|
||||||
self._loadDraftCart();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button to save cart as draft (in My Cart header - shop pages)
|
|
||||||
var saveCartBtn = document.getElementById("save-cart-btn");
|
|
||||||
console.log("[_attachEventListeners] save-cart-btn found:", !!saveCartBtn);
|
|
||||||
|
|
||||||
if (saveCartBtn) {
|
|
||||||
saveCartBtn.addEventListener("click", function (e) {
|
|
||||||
console.log("[CLICK] save-cart-btn clicked");
|
|
||||||
e.preventDefault();
|
|
||||||
self._saveCartAsDraft();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._cartCheckoutListenersAttached = true;
|
|
||||||
console.log("[_attachEventListeners] Checkout listeners attached (one-time)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ LAZY LOADING: Load More Button ============
|
// ============ LAZY LOADING: Load More Button ============
|
||||||
this._attachLoadMoreListener();
|
this._attachLoadMoreListener();
|
||||||
|
|
||||||
// ============ USE EVENT DELEGATION FOR QUANTITY & CART BUTTONS ============
|
// Adjust quantity step based on UoM category
|
||||||
// This way, new products loaded via AJAX will automatically have listeners
|
// Categories without decimals (per unit): "Unit", "Units", etc.
|
||||||
var productsGrid = document.getElementById("products-grid");
|
// Categories with decimals: "Weight", "Volume", "Length", etc.
|
||||||
|
|
||||||
if (!productsGrid) {
|
|
||||||
console.log("[_attachEventListeners] No products-grid found (checkout page?)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, adjust quantity steps for all existing inputs
|
|
||||||
var unitInputs = document.querySelectorAll(".product-qty");
|
var unitInputs = document.querySelectorAll(".product-qty");
|
||||||
console.log("=== ADJUSTING QUANTITY STEPS ===");
|
console.log("=== ADJUSTING QUANTITY STEPS ===");
|
||||||
console.log("Found " + unitInputs.length + " quantity inputs");
|
console.log("Found " + unitInputs.length + " quantity inputs");
|
||||||
|
|
@ -710,24 +638,32 @@
|
||||||
}
|
}
|
||||||
console.log("=== END ADJUSTING QUANTITY STEPS ===");
|
console.log("=== END ADJUSTING QUANTITY STEPS ===");
|
||||||
|
|
||||||
// IMPORTANT: Do NOT clone the grid node - this destroys all products!
|
// Botones + y - para aumentar/disminuir cantidad
|
||||||
// Instead, use a flag to prevent adding duplicate event listeners
|
// Helper function to round decimals correctly
|
||||||
if (productsGrid._delegationListenersAttached) {
|
function roundDecimal(value, decimals) {
|
||||||
console.log(
|
var factor = Math.pow(10, decimals);
|
||||||
"[_attachEventListeners] Grid delegation listeners already attached, skipping"
|
return Math.round(value * factor) / factor;
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
productsGrid._delegationListenersAttached = true;
|
|
||||||
console.log("[_attachEventListeners] Attaching grid delegation listeners (one-time)");
|
|
||||||
|
|
||||||
// Quantity decrease button (via event delegation)
|
// Remove old listeners by cloning elements (to avoid duplication)
|
||||||
productsGrid.addEventListener("click", function (e) {
|
var decreaseButtons = document.querySelectorAll(".qty-decrease");
|
||||||
var decreaseBtn = e.target.closest(".qty-decrease");
|
for (var k = 0; k < decreaseButtons.length; k++) {
|
||||||
if (!decreaseBtn) return;
|
var newBtn = decreaseButtons[k].cloneNode(true);
|
||||||
|
decreaseButtons[k].parentNode.replaceChild(newBtn, decreaseButtons[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var increaseButtons = document.querySelectorAll(".qty-increase");
|
||||||
|
for (var k = 0; k < increaseButtons.length; k++) {
|
||||||
|
var newBtn = increaseButtons[k].cloneNode(true);
|
||||||
|
increaseButtons[k].parentNode.replaceChild(newBtn, increaseButtons[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ahora asignar nuevos listeners
|
||||||
|
decreaseButtons = document.querySelectorAll(".qty-decrease");
|
||||||
|
for (var k = 0; k < decreaseButtons.length; k++) {
|
||||||
|
decreaseButtons[k].addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var productId = decreaseBtn.getAttribute("data-product-id");
|
var productId = this.getAttribute("data-product-id");
|
||||||
var input = document.getElementById("qty_" + productId);
|
var input = document.getElementById("qty_" + productId);
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
|
|
@ -743,14 +679,13 @@
|
||||||
input.value = newValue;
|
input.value = newValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Quantity increase button (via event delegation)
|
increaseButtons = document.querySelectorAll(".qty-increase");
|
||||||
productsGrid.addEventListener("click", function (e) {
|
for (var k = 0; k < increaseButtons.length; k++) {
|
||||||
var increaseBtn = e.target.closest(".qty-increase");
|
increaseButtons[k].addEventListener("click", function (e) {
|
||||||
if (!increaseBtn) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var productId = increaseBtn.getAttribute("data-product-id");
|
var productId = this.getAttribute("data-product-id");
|
||||||
var input = document.getElementById("qty_" + productId);
|
var input = document.getElementById("qty_" + productId);
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
|
|
@ -765,14 +700,14 @@
|
||||||
input.value = newValue;
|
input.value = newValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add to cart button (via event delegation)
|
// Botones de agregar al carrito
|
||||||
productsGrid.addEventListener("click", function (e) {
|
var buttons = document.querySelectorAll(".add-to-cart-btn");
|
||||||
var cartBtn = e.target.closest(".add-to-cart-btn");
|
for (var i = 0; i < buttons.length; i++) {
|
||||||
if (!cartBtn) return;
|
buttons[i].addEventListener("click", function (e) {
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var form = cartBtn.closest(".add-to-cart-form");
|
var form = this.closest(".add-to-cart-form");
|
||||||
var productId = form.getAttribute("data-product-id");
|
var productId = form.getAttribute("data-product-id");
|
||||||
var productName = form.getAttribute("data-product-name") || "Product";
|
var productName = form.getAttribute("data-product-name") || "Product";
|
||||||
var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0;
|
var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0;
|
||||||
|
|
@ -796,6 +731,49 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button to save cart as draft (in My Cart header)
|
||||||
|
var savCartBtn = document.getElementById("save-cart-btn");
|
||||||
|
if (savCartBtn) {
|
||||||
|
// Remove old listeners by cloning
|
||||||
|
var savCartBtnNew = savCartBtn.cloneNode(true);
|
||||||
|
savCartBtn.parentNode.replaceChild(savCartBtnNew, savCartBtn);
|
||||||
|
savCartBtnNew.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._saveCartAsDraft();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button to reload from draft (in My Cart header)
|
||||||
|
var reloadCartBtn = document.getElementById("reload-cart-btn");
|
||||||
|
if (reloadCartBtn) {
|
||||||
|
// Remove old listeners by cloning
|
||||||
|
var reloadCartBtnNew = reloadCartBtn.cloneNode(true);
|
||||||
|
reloadCartBtn.parentNode.replaceChild(reloadCartBtnNew, reloadCartBtn);
|
||||||
|
reloadCartBtnNew.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._loadDraftCart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button to save as draft
|
||||||
|
var saveBtn = document.getElementById("save-order-btn");
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._saveOrderDraft();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm order button
|
||||||
|
var confirmBtn = document.getElementById("confirm-order-btn");
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._confirmOrder();
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_addToCart: function (productId, productName, productPrice, quantity) {
|
_addToCart: function (productId, productName, productPrice, quantity) {
|
||||||
|
|
@ -1497,7 +1475,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
_saveOrderDraft: function () {
|
_saveOrderDraft: function () {
|
||||||
console.log("[_saveOrderDraft] Starting - this.orderId:", this.orderId);
|
console.log("Saving order as draft:", this.orderId);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
@ -2001,10 +1979,13 @@
|
||||||
console.log("cart-items-container found:", !!cartContainer);
|
console.log("cart-items-container found:", !!cartContainer);
|
||||||
console.log("confirm-order-btn found:", !!confirmBtn);
|
console.log("confirm-order-btn found:", !!confirmBtn);
|
||||||
|
|
||||||
// Always initialize - it handles both cart pages and checkout pages
|
if (cartContainer || confirmBtn) {
|
||||||
console.log("Calling init()");
|
console.log("Calling init()");
|
||||||
var result = window.groupOrderShop.init();
|
var result = window.groupOrderShop.init();
|
||||||
console.log("init() result:", result);
|
console.log("init() result:", result);
|
||||||
|
} else {
|
||||||
|
console.warn("No elements found to initialize cart");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle confirm order buttons in portal (My Orders page)
|
// Handle confirm order buttons in portal (My Orders page)
|
||||||
|
|
|
||||||
|
|
@ -3,174 +3,144 @@
|
||||||
* Tests core cart functionality (add, remove, update, calculate)
|
* Tests core cart functionality (add, remove, update, calculate)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
odoo.define("website_sale_aplicoop.test_cart_functions", function (require) {
|
odoo.define('website_sale_aplicoop.test_cart_functions', function (require) {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
var QUnit = window.QUnit;
|
var QUnit = window.QUnit;
|
||||||
|
|
||||||
QUnit.module(
|
QUnit.module('website_sale_aplicoop', {
|
||||||
"website_sale_aplicoop",
|
beforeEach: function() {
|
||||||
{
|
|
||||||
beforeEach: function () {
|
|
||||||
// Setup: Initialize groupOrderShop object
|
// Setup: Initialize groupOrderShop object
|
||||||
window.groupOrderShop = {
|
window.groupOrderShop = {
|
||||||
orderId: "1",
|
orderId: '1',
|
||||||
cart: {},
|
cart: {},
|
||||||
labels: {
|
labels: {
|
||||||
save_cart: "Save Cart",
|
'save_cart': 'Save Cart',
|
||||||
reload_cart: "Reload Cart",
|
'reload_cart': 'Reload Cart',
|
||||||
checkout: "Checkout",
|
'checkout': 'Checkout',
|
||||||
confirm_order: "Confirm Order",
|
'confirm_order': 'Confirm Order',
|
||||||
back_to_cart: "Back to Cart",
|
'back_to_cart': 'Back to Cart'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear localStorage
|
// Clear localStorage
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
},
|
},
|
||||||
afterEach: function () {
|
afterEach: function() {
|
||||||
// Cleanup
|
// Cleanup
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
delete window.groupOrderShop;
|
delete window.groupOrderShop;
|
||||||
},
|
}
|
||||||
},
|
}, function() {
|
||||||
function () {
|
|
||||||
QUnit.test("groupOrderShop object initializes correctly", function (assert) {
|
QUnit.test('groupOrderShop object initializes correctly', function(assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
|
|
||||||
assert.ok(window.groupOrderShop, "groupOrderShop object exists");
|
assert.ok(window.groupOrderShop, 'groupOrderShop object exists');
|
||||||
assert.equal(window.groupOrderShop.orderId, "1", "orderId is set");
|
assert.equal(window.groupOrderShop.orderId, '1', 'orderId is set');
|
||||||
assert.ok(typeof window.groupOrderShop.cart === "object", "cart is an object");
|
assert.ok(typeof window.groupOrderShop.cart === 'object', 'cart is an object');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("cart starts empty", function (assert) {
|
QUnit.test('cart starts empty', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
var cartKeys = Object.keys(window.groupOrderShop.cart);
|
var cartKeys = Object.keys(window.groupOrderShop.cart);
|
||||||
assert.equal(cartKeys.length, 0, "cart has no items initially");
|
assert.equal(cartKeys.length, 0, 'cart has no items initially');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("can add item to cart", function (assert) {
|
QUnit.test('can add item to cart', function(assert) {
|
||||||
assert.expect(4);
|
assert.expect(4);
|
||||||
|
|
||||||
// Add a product to cart
|
// Add a product to cart
|
||||||
var productId = "123";
|
var productId = '123';
|
||||||
var productData = {
|
var productData = {
|
||||||
name: "Test Product",
|
name: 'Test Product',
|
||||||
price: 10.5,
|
price: 10.50,
|
||||||
quantity: 2,
|
quantity: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
window.groupOrderShop.cart[productId] = productData;
|
window.groupOrderShop.cart[productId] = productData;
|
||||||
|
|
||||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, "cart has 1 item");
|
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'cart has 1 item');
|
||||||
assert.ok(window.groupOrderShop.cart[productId], "product exists in cart");
|
assert.ok(window.groupOrderShop.cart[productId], 'product exists in cart');
|
||||||
assert.equal(
|
assert.equal(window.groupOrderShop.cart[productId].name, 'Test Product', 'product name is correct');
|
||||||
window.groupOrderShop.cart[productId].name,
|
assert.equal(window.groupOrderShop.cart[productId].quantity, 2, 'product quantity is correct');
|
||||||
"Test Product",
|
|
||||||
"product name is correct"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
window.groupOrderShop.cart[productId].quantity,
|
|
||||||
2,
|
|
||||||
"product quantity is correct"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("can remove item from cart", function (assert) {
|
QUnit.test('can remove item from cart', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Add then remove
|
// Add then remove
|
||||||
var productId = "123";
|
var productId = '123';
|
||||||
window.groupOrderShop.cart[productId] = {
|
window.groupOrderShop.cart[productId] = {
|
||||||
name: "Test Product",
|
name: 'Test Product',
|
||||||
price: 10.5,
|
price: 10.50,
|
||||||
quantity: 2,
|
quantity: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'cart has 1 item after add');
|
||||||
Object.keys(window.groupOrderShop.cart).length,
|
|
||||||
1,
|
|
||||||
"cart has 1 item after add"
|
|
||||||
);
|
|
||||||
|
|
||||||
delete window.groupOrderShop.cart[productId];
|
delete window.groupOrderShop.cart[productId];
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(Object.keys(window.groupOrderShop.cart).length, 0, 'cart is empty after remove');
|
||||||
Object.keys(window.groupOrderShop.cart).length,
|
|
||||||
0,
|
|
||||||
"cart is empty after remove"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("can update item quantity", function (assert) {
|
QUnit.test('can update item quantity', function(assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
|
|
||||||
var productId = "123";
|
var productId = '123';
|
||||||
window.groupOrderShop.cart[productId] = {
|
window.groupOrderShop.cart[productId] = {
|
||||||
name: "Test Product",
|
name: 'Test Product',
|
||||||
price: 10.5,
|
price: 10.50,
|
||||||
quantity: 2,
|
quantity: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(window.groupOrderShop.cart[productId].quantity, 2, 'initial quantity is 2');
|
||||||
window.groupOrderShop.cart[productId].quantity,
|
|
||||||
2,
|
|
||||||
"initial quantity is 2"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update quantity
|
// Update quantity
|
||||||
window.groupOrderShop.cart[productId].quantity = 5;
|
window.groupOrderShop.cart[productId].quantity = 5;
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(window.groupOrderShop.cart[productId].quantity, 5, 'quantity updated to 5');
|
||||||
window.groupOrderShop.cart[productId].quantity,
|
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'still only 1 item in cart');
|
||||||
5,
|
|
||||||
"quantity updated to 5"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
Object.keys(window.groupOrderShop.cart).length,
|
|
||||||
1,
|
|
||||||
"still only 1 item in cart"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("cart total calculates correctly", function (assert) {
|
QUnit.test('cart total calculates correctly', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
// Add multiple products
|
// Add multiple products
|
||||||
window.groupOrderShop.cart["123"] = {
|
window.groupOrderShop.cart['123'] = {
|
||||||
name: "Product 1",
|
name: 'Product 1',
|
||||||
price: 10.0,
|
price: 10.00,
|
||||||
quantity: 2,
|
quantity: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
window.groupOrderShop.cart["456"] = {
|
window.groupOrderShop.cart['456'] = {
|
||||||
name: "Product 2",
|
name: 'Product 2',
|
||||||
price: 5.5,
|
price: 5.50,
|
||||||
quantity: 3,
|
quantity: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total manually
|
// Calculate total manually
|
||||||
var total = 0;
|
var total = 0;
|
||||||
Object.keys(window.groupOrderShop.cart).forEach(function (productId) {
|
Object.keys(window.groupOrderShop.cart).forEach(function(productId) {
|
||||||
var item = window.groupOrderShop.cart[productId];
|
var item = window.groupOrderShop.cart[productId];
|
||||||
total += item.price * item.quantity;
|
total += item.price * item.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expected: (10.00 * 2) + (5.50 * 3) = 20.00 + 16.50 = 36.50
|
// Expected: (10.00 * 2) + (5.50 * 3) = 20.00 + 16.50 = 36.50
|
||||||
assert.equal(total.toFixed(2), "36.50", "cart total is correct");
|
assert.equal(total.toFixed(2), '36.50', 'cart total is correct');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("localStorage saves cart correctly", function (assert) {
|
QUnit.test('localStorage saves cart correctly', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
var cartKey = "eskaera_1_cart";
|
var cartKey = 'eskaera_1_cart';
|
||||||
var testCart = {
|
var testCart = {
|
||||||
123: {
|
'123': {
|
||||||
name: "Test Product",
|
name: 'Test Product',
|
||||||
price: 10.5,
|
price: 10.50,
|
||||||
quantity: 2,
|
quantity: 2
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save to localStorage
|
// Save to localStorage
|
||||||
|
|
@ -179,97 +149,76 @@ odoo.define("website_sale_aplicoop.test_cart_functions", function (require) {
|
||||||
// Retrieve and verify
|
// Retrieve and verify
|
||||||
var savedCart = JSON.parse(localStorage.getItem(cartKey));
|
var savedCart = JSON.parse(localStorage.getItem(cartKey));
|
||||||
|
|
||||||
assert.ok(savedCart, "cart was saved to localStorage");
|
assert.ok(savedCart, 'cart was saved to localStorage');
|
||||||
assert.equal(savedCart["123"].name, "Test Product", "cart data is correct");
|
assert.equal(savedCart['123'].name, 'Test Product', 'cart data is correct');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("labels object is initialized", function (assert) {
|
QUnit.test('labels object is initialized', function(assert) {
|
||||||
assert.expect(5);
|
assert.expect(5);
|
||||||
|
|
||||||
assert.ok(window.groupOrderShop.labels, "labels object exists");
|
assert.ok(window.groupOrderShop.labels, 'labels object exists');
|
||||||
assert.equal(
|
assert.equal(window.groupOrderShop.labels['save_cart'], 'Save Cart', 'save_cart label exists');
|
||||||
window.groupOrderShop.labels["save_cart"],
|
assert.equal(window.groupOrderShop.labels['reload_cart'], 'Reload Cart', 'reload_cart label exists');
|
||||||
"Save Cart",
|
assert.equal(window.groupOrderShop.labels['checkout'], 'Checkout', 'checkout label exists');
|
||||||
"save_cart label exists"
|
assert.equal(window.groupOrderShop.labels['confirm_order'], 'Confirm Order', 'confirm_order label exists');
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
window.groupOrderShop.labels["reload_cart"],
|
|
||||||
"Reload Cart",
|
|
||||||
"reload_cart label exists"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
window.groupOrderShop.labels["checkout"],
|
|
||||||
"Checkout",
|
|
||||||
"checkout label exists"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
window.groupOrderShop.labels["confirm_order"],
|
|
||||||
"Confirm Order",
|
|
||||||
"confirm_order label exists"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("cart handles decimal quantities correctly", function (assert) {
|
QUnit.test('cart handles decimal quantities correctly', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
window.groupOrderShop.cart["123"] = {
|
window.groupOrderShop.cart['123'] = {
|
||||||
name: "Weight Product",
|
name: 'Weight Product',
|
||||||
price: 8.99,
|
price: 8.99,
|
||||||
quantity: 1.5,
|
quantity: 1.5
|
||||||
};
|
};
|
||||||
|
|
||||||
var item = window.groupOrderShop.cart["123"];
|
var item = window.groupOrderShop.cart['123'];
|
||||||
var subtotal = item.price * item.quantity;
|
var subtotal = item.price * item.quantity;
|
||||||
|
|
||||||
assert.equal(item.quantity, 1.5, "decimal quantity stored correctly");
|
assert.equal(item.quantity, 1.5, 'decimal quantity stored correctly');
|
||||||
assert.equal(
|
assert.equal(subtotal.toFixed(2), '13.49', 'subtotal with decimal quantity is correct');
|
||||||
subtotal.toFixed(2),
|
|
||||||
"13.49",
|
|
||||||
"subtotal with decimal quantity is correct"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("cart handles zero quantity", function (assert) {
|
QUnit.test('cart handles zero quantity', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
window.groupOrderShop.cart["123"] = {
|
window.groupOrderShop.cart['123'] = {
|
||||||
name: "Test Product",
|
name: 'Test Product',
|
||||||
price: 10.0,
|
price: 10.00,
|
||||||
quantity: 0,
|
quantity: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
var item = window.groupOrderShop.cart["123"];
|
var item = window.groupOrderShop.cart['123'];
|
||||||
var subtotal = item.price * item.quantity;
|
var subtotal = item.price * item.quantity;
|
||||||
|
|
||||||
assert.equal(subtotal, 0, "zero quantity results in zero subtotal");
|
assert.equal(subtotal, 0, 'zero quantity results in zero subtotal');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("cart handles multiple items with same price", function (assert) {
|
QUnit.test('cart handles multiple items with same price', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
window.groupOrderShop.cart["123"] = {
|
window.groupOrderShop.cart['123'] = {
|
||||||
name: "Product A",
|
name: 'Product A',
|
||||||
price: 10.0,
|
price: 10.00,
|
||||||
quantity: 2,
|
quantity: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
window.groupOrderShop.cart["456"] = {
|
window.groupOrderShop.cart['456'] = {
|
||||||
name: "Product B",
|
name: 'Product B',
|
||||||
price: 10.0,
|
price: 10.00,
|
||||||
quantity: 3,
|
quantity: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
var total = 0;
|
var total = 0;
|
||||||
Object.keys(window.groupOrderShop.cart).forEach(function (productId) {
|
Object.keys(window.groupOrderShop.cart).forEach(function(productId) {
|
||||||
var item = window.groupOrderShop.cart[productId];
|
var item = window.groupOrderShop.cart[productId];
|
||||||
total += item.price * item.quantity;
|
total += item.price * item.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 2, "cart has 2 items");
|
assert.equal(Object.keys(window.groupOrderShop.cart).length, 2, 'cart has 2 items');
|
||||||
assert.equal(total.toFixed(2), "50.00", "total is correct with same prices");
|
assert.equal(total.toFixed(2), '50.00', 'total is correct with same prices');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,15 @@
|
||||||
* Tests product filtering and search behavior
|
* Tests product filtering and search behavior
|
||||||
*/
|
*/
|
||||||
|
|
||||||
odoo.define("website_sale_aplicoop.test_realtime_search", function (require) {
|
odoo.define('website_sale_aplicoop.test_realtime_search', function (require) {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
var QUnit = window.QUnit;
|
var QUnit = window.QUnit;
|
||||||
|
|
||||||
QUnit.module(
|
QUnit.module('website_sale_aplicoop.realtime_search', {
|
||||||
"website_sale_aplicoop.realtime_search",
|
beforeEach: function() {
|
||||||
{
|
|
||||||
beforeEach: function () {
|
|
||||||
// Setup: Create test DOM with product cards
|
// Setup: Create test DOM with product cards
|
||||||
this.$fixture = $("#qunit-fixture");
|
this.$fixture = $('#qunit-fixture');
|
||||||
|
|
||||||
this.$fixture.append(
|
this.$fixture.append(
|
||||||
'<input type="text" id="realtime-search-input" />' +
|
'<input type="text" id="realtime-search-input" />' +
|
||||||
|
|
@ -21,7 +19,7 @@ odoo.define("website_sale_aplicoop.test_realtime_search", function (require) {
|
||||||
'<option value="">All Categories</option>' +
|
'<option value="">All Categories</option>' +
|
||||||
'<option value="1">Category 1</option>' +
|
'<option value="1">Category 1</option>' +
|
||||||
'<option value="2">Category 2</option>' +
|
'<option value="2">Category 2</option>' +
|
||||||
"</select>" +
|
'</select>' +
|
||||||
'<div class="product-card" data-product-name="Cabbage" data-category-id="1"></div>' +
|
'<div class="product-card" data-product-name="Cabbage" data-category-id="1"></div>' +
|
||||||
'<div class="product-card" data-product-name="Carrot" data-category-id="1"></div>' +
|
'<div class="product-card" data-product-name="Carrot" data-category-id="1"></div>' +
|
||||||
'<div class="product-card" data-product-name="Apple" data-category-id="2"></div>' +
|
'<div class="product-card" data-product-name="Apple" data-category-id="2"></div>' +
|
||||||
|
|
@ -30,220 +28,214 @@ odoo.define("website_sale_aplicoop.test_realtime_search", function (require) {
|
||||||
|
|
||||||
// Initialize search object
|
// Initialize search object
|
||||||
window.realtimeSearch = {
|
window.realtimeSearch = {
|
||||||
searchInput: document.getElementById("realtime-search-input"),
|
searchInput: document.getElementById('realtime-search-input'),
|
||||||
categorySelect: document.getElementById("realtime-category-select"),
|
categorySelect: document.getElementById('realtime-category-select'),
|
||||||
productCards: document.querySelectorAll(".product-card"),
|
productCards: document.querySelectorAll('.product-card'),
|
||||||
|
|
||||||
filterProducts: function () {
|
filterProducts: function() {
|
||||||
var searchTerm = this.searchInput.value.toLowerCase().trim();
|
var searchTerm = this.searchInput.value.toLowerCase().trim();
|
||||||
var selectedCategory = this.categorySelect.value;
|
var selectedCategory = this.categorySelect.value;
|
||||||
|
|
||||||
var visibleCount = 0;
|
var visibleCount = 0;
|
||||||
var hiddenCount = 0;
|
var hiddenCount = 0;
|
||||||
|
|
||||||
this.productCards.forEach(function (card) {
|
this.productCards.forEach(function(card) {
|
||||||
var productName = card.getAttribute("data-product-name").toLowerCase();
|
var productName = card.getAttribute('data-product-name').toLowerCase();
|
||||||
var categoryId = card.getAttribute("data-category-id");
|
var categoryId = card.getAttribute('data-category-id');
|
||||||
|
|
||||||
var matchesSearch = !searchTerm || productName.includes(searchTerm);
|
var matchesSearch = !searchTerm || productName.includes(searchTerm);
|
||||||
var matchesCategory =
|
var matchesCategory = !selectedCategory || categoryId === selectedCategory;
|
||||||
!selectedCategory || categoryId === selectedCategory;
|
|
||||||
|
|
||||||
if (matchesSearch && matchesCategory) {
|
if (matchesSearch && matchesCategory) {
|
||||||
card.classList.remove("d-none");
|
card.classList.remove('d-none');
|
||||||
visibleCount++;
|
visibleCount++;
|
||||||
} else {
|
} else {
|
||||||
card.classList.add("d-none");
|
card.classList.add('d-none');
|
||||||
hiddenCount++;
|
hiddenCount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { visible: visibleCount, hidden: hiddenCount };
|
return { visible: visibleCount, hidden: hiddenCount };
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
afterEach: function () {
|
afterEach: function() {
|
||||||
// Cleanup
|
// Cleanup
|
||||||
this.$fixture.empty();
|
this.$fixture.empty();
|
||||||
delete window.realtimeSearch;
|
delete window.realtimeSearch;
|
||||||
},
|
}
|
||||||
},
|
}, function() {
|
||||||
function () {
|
|
||||||
QUnit.test("search input element exists", function (assert) {
|
QUnit.test('search input element exists', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
var searchInput = document.getElementById("realtime-search-input");
|
var searchInput = document.getElementById('realtime-search-input');
|
||||||
assert.ok(searchInput, "search input element exists");
|
assert.ok(searchInput, 'search input element exists');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("category select element exists", function (assert) {
|
QUnit.test('category select element exists', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
var categorySelect = document.getElementById("realtime-category-select");
|
var categorySelect = document.getElementById('realtime-category-select');
|
||||||
assert.ok(categorySelect, "category select element exists");
|
assert.ok(categorySelect, 'category select element exists');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("product cards are found", function (assert) {
|
QUnit.test('product cards are found', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
var productCards = document.querySelectorAll(".product-card");
|
var productCards = document.querySelectorAll('.product-card');
|
||||||
assert.equal(productCards.length, 4, "found 4 product cards");
|
assert.equal(productCards.length, 4, 'found 4 product cards');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("search filters by product name", function (assert) {
|
QUnit.test('search filters by product name', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Search for "cab"
|
// Search for "cab"
|
||||||
window.realtimeSearch.searchInput.value = "cab";
|
window.realtimeSearch.searchInput.value = 'cab';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 1, "1 product visible (Cabbage)");
|
assert.equal(result.visible, 1, '1 product visible (Cabbage)');
|
||||||
assert.equal(result.hidden, 3, "3 products hidden");
|
assert.equal(result.hidden, 3, '3 products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("search is case insensitive", function (assert) {
|
QUnit.test('search is case insensitive', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Search for "CARROT" in uppercase
|
// Search for "CARROT" in uppercase
|
||||||
window.realtimeSearch.searchInput.value = "CARROT";
|
window.realtimeSearch.searchInput.value = 'CARROT';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 1, "1 product visible (Carrot)");
|
assert.equal(result.visible, 1, '1 product visible (Carrot)');
|
||||||
assert.equal(result.hidden, 3, "3 products hidden");
|
assert.equal(result.hidden, 3, '3 products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("empty search shows all products", function (assert) {
|
QUnit.test('empty search shows all products', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
window.realtimeSearch.searchInput.value = "";
|
window.realtimeSearch.searchInput.value = '';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 4, "all 4 products visible");
|
assert.equal(result.visible, 4, 'all 4 products visible');
|
||||||
assert.equal(result.hidden, 0, "no products hidden");
|
assert.equal(result.hidden, 0, 'no products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("category filter works", function (assert) {
|
QUnit.test('category filter works', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Select category 1
|
// Select category 1
|
||||||
window.realtimeSearch.categorySelect.value = "1";
|
window.realtimeSearch.categorySelect.value = '1';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 2, "2 products visible (Cabbage, Carrot)");
|
assert.equal(result.visible, 2, '2 products visible (Cabbage, Carrot)');
|
||||||
assert.equal(result.hidden, 2, "2 products hidden (Apple, Banana)");
|
assert.equal(result.hidden, 2, '2 products hidden (Apple, Banana)');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("search and category filter work together", function (assert) {
|
QUnit.test('search and category filter work together', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Search for "ca" in category 1
|
// Search for "ca" in category 1
|
||||||
window.realtimeSearch.searchInput.value = "ca";
|
window.realtimeSearch.searchInput.value = 'ca';
|
||||||
window.realtimeSearch.categorySelect.value = "1";
|
window.realtimeSearch.categorySelect.value = '1';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
// Should show: Cabbage, Carrot (both in category 1 and match "ca")
|
// Should show: Cabbage, Carrot (both in category 1 and match "ca")
|
||||||
assert.equal(result.visible, 2, "2 products visible");
|
assert.equal(result.visible, 2, '2 products visible');
|
||||||
assert.equal(result.hidden, 2, "2 products hidden");
|
assert.equal(result.hidden, 2, '2 products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("search for non-existent product shows none", function (assert) {
|
QUnit.test('search for non-existent product shows none', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
window.realtimeSearch.searchInput.value = "xyz123";
|
window.realtimeSearch.searchInput.value = 'xyz123';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 0, "no products visible");
|
assert.equal(result.visible, 0, 'no products visible');
|
||||||
assert.equal(result.hidden, 4, "all 4 products hidden");
|
assert.equal(result.hidden, 4, 'all 4 products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("partial match works", function (assert) {
|
QUnit.test('partial match works', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Search for "an" should match "Banana"
|
// Search for "an" should match "Banana"
|
||||||
window.realtimeSearch.searchInput.value = "an";
|
window.realtimeSearch.searchInput.value = 'an';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 1, "1 product visible (Banana)");
|
assert.equal(result.visible, 1, '1 product visible (Banana)');
|
||||||
assert.equal(result.hidden, 3, "3 products hidden");
|
assert.equal(result.hidden, 3, '3 products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("search trims whitespace", function (assert) {
|
QUnit.test('search trims whitespace', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// Search with extra whitespace
|
// Search with extra whitespace
|
||||||
window.realtimeSearch.searchInput.value = " apple ";
|
window.realtimeSearch.searchInput.value = ' apple ';
|
||||||
var result = window.realtimeSearch.filterProducts();
|
var result = window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
assert.equal(result.visible, 1, "1 product visible (Apple)");
|
assert.equal(result.visible, 1, '1 product visible (Apple)');
|
||||||
assert.equal(result.hidden, 3, "3 products hidden");
|
assert.equal(result.hidden, 3, '3 products hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("d-none class is added to hidden products", function (assert) {
|
QUnit.test('d-none class is added to hidden products', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
window.realtimeSearch.searchInput.value = "cabbage";
|
window.realtimeSearch.searchInput.value = 'cabbage';
|
||||||
window.realtimeSearch.filterProducts();
|
window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
var productCards = document.querySelectorAll(".product-card");
|
var productCards = document.querySelectorAll('.product-card');
|
||||||
var hiddenCards = Array.from(productCards).filter(function (card) {
|
var hiddenCards = Array.from(productCards).filter(function(card) {
|
||||||
return card.classList.contains("d-none");
|
return card.classList.contains('d-none');
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(hiddenCards.length, 3, "3 cards have d-none class");
|
assert.equal(hiddenCards.length, 3, '3 cards have d-none class');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("d-none class is removed from visible products", function (assert) {
|
QUnit.test('d-none class is removed from visible products', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// First hide all
|
// First hide all
|
||||||
window.realtimeSearch.searchInput.value = "xyz";
|
window.realtimeSearch.searchInput.value = 'xyz';
|
||||||
window.realtimeSearch.filterProducts();
|
window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
var allHidden = Array.from(window.realtimeSearch.productCards).every(function (
|
var allHidden = Array.from(window.realtimeSearch.productCards).every(function(card) {
|
||||||
card
|
return card.classList.contains('d-none');
|
||||||
) {
|
|
||||||
return card.classList.contains("d-none");
|
|
||||||
});
|
});
|
||||||
assert.ok(allHidden, "all cards hidden initially");
|
assert.ok(allHidden, 'all cards hidden initially');
|
||||||
|
|
||||||
// Then show all
|
// Then show all
|
||||||
window.realtimeSearch.searchInput.value = "";
|
window.realtimeSearch.searchInput.value = '';
|
||||||
window.realtimeSearch.filterProducts();
|
window.realtimeSearch.filterProducts();
|
||||||
|
|
||||||
var allVisible = Array.from(window.realtimeSearch.productCards).every(function (
|
var allVisible = Array.from(window.realtimeSearch.productCards).every(function(card) {
|
||||||
card
|
return !card.classList.contains('d-none');
|
||||||
) {
|
|
||||||
return !card.classList.contains("d-none");
|
|
||||||
});
|
});
|
||||||
assert.ok(allVisible, "all cards visible after clearing search");
|
assert.ok(allVisible, 'all cards visible after clearing search');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("filterProducts returns correct counts", function (assert) {
|
QUnit.test('filterProducts returns correct counts', function(assert) {
|
||||||
assert.expect(4);
|
assert.expect(4);
|
||||||
|
|
||||||
// All visible
|
// All visible
|
||||||
window.realtimeSearch.searchInput.value = "";
|
window.realtimeSearch.searchInput.value = '';
|
||||||
var result1 = window.realtimeSearch.filterProducts();
|
var result1 = window.realtimeSearch.filterProducts();
|
||||||
assert.equal(result1.visible + result1.hidden, 4, "total count is 4");
|
assert.equal(result1.visible + result1.hidden, 4, 'total count is 4');
|
||||||
|
|
||||||
// 1 visible
|
// 1 visible
|
||||||
window.realtimeSearch.searchInput.value = "apple";
|
window.realtimeSearch.searchInput.value = 'apple';
|
||||||
var result2 = window.realtimeSearch.filterProducts();
|
var result2 = window.realtimeSearch.filterProducts();
|
||||||
assert.equal(result2.visible, 1, "visible count is 1");
|
assert.equal(result2.visible, 1, 'visible count is 1');
|
||||||
|
|
||||||
// None visible
|
// None visible
|
||||||
window.realtimeSearch.searchInput.value = "xyz";
|
window.realtimeSearch.searchInput.value = 'xyz';
|
||||||
var result3 = window.realtimeSearch.filterProducts();
|
var result3 = window.realtimeSearch.filterProducts();
|
||||||
assert.equal(result3.visible, 0, "visible count is 0");
|
assert.equal(result3.visible, 0, 'visible count is 0');
|
||||||
|
|
||||||
// Category filter
|
// Category filter
|
||||||
window.realtimeSearch.searchInput.value = "";
|
window.realtimeSearch.searchInput.value = '';
|
||||||
window.realtimeSearch.categorySelect.value = "2";
|
window.realtimeSearch.categorySelect.value = '2';
|
||||||
var result4 = window.realtimeSearch.filterProducts();
|
var result4 = window.realtimeSearch.filterProducts();
|
||||||
assert.equal(result4.visible, 2, "category filter shows 2 products");
|
assert.equal(result4.visible, 2, 'category filter shows 2 products');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
odoo.define("website_sale_aplicoop.test_suite", function (require) {
|
odoo.define('website_sale_aplicoop.test_suite', function (require) {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
// Import all test modules
|
// Import all test modules
|
||||||
require("website_sale_aplicoop.test_cart_functions");
|
require('website_sale_aplicoop.test_cart_functions');
|
||||||
require("website_sale_aplicoop.test_tooltips_labels");
|
require('website_sale_aplicoop.test_tooltips_labels');
|
||||||
require("website_sale_aplicoop.test_realtime_search");
|
require('website_sale_aplicoop.test_realtime_search');
|
||||||
|
|
||||||
// Test suite is automatically registered by importing modules
|
// Test suite is automatically registered by importing modules
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,15 @@
|
||||||
* Tests tooltip initialization and label loading
|
* Tests tooltip initialization and label loading
|
||||||
*/
|
*/
|
||||||
|
|
||||||
odoo.define("website_sale_aplicoop.test_tooltips_labels", function (require) {
|
odoo.define('website_sale_aplicoop.test_tooltips_labels', function (require) {
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
var QUnit = window.QUnit;
|
var QUnit = window.QUnit;
|
||||||
|
|
||||||
QUnit.module(
|
QUnit.module('website_sale_aplicoop.tooltips_labels', {
|
||||||
"website_sale_aplicoop.tooltips_labels",
|
beforeEach: function() {
|
||||||
{
|
|
||||||
beforeEach: function () {
|
|
||||||
// Setup: Create test DOM elements
|
// Setup: Create test DOM elements
|
||||||
this.$fixture = $("#qunit-fixture");
|
this.$fixture = $('#qunit-fixture');
|
||||||
|
|
||||||
// Add test buttons with tooltip labels
|
// Add test buttons with tooltip labels
|
||||||
this.$fixture.append(
|
this.$fixture.append(
|
||||||
|
|
@ -24,193 +22,166 @@ odoo.define("website_sale_aplicoop.test_tooltips_labels", function (require) {
|
||||||
|
|
||||||
// Initialize groupOrderShop
|
// Initialize groupOrderShop
|
||||||
window.groupOrderShop = {
|
window.groupOrderShop = {
|
||||||
orderId: "1",
|
orderId: '1',
|
||||||
cart: {},
|
cart: {},
|
||||||
labels: {
|
labels: {
|
||||||
save_cart: "Guardar Carrito",
|
'save_cart': 'Guardar Carrito',
|
||||||
reload_cart: "Recargar Carrito",
|
'reload_cart': 'Recargar Carrito',
|
||||||
checkout: "Proceder al Pago",
|
'checkout': 'Proceder al Pago',
|
||||||
confirm_order: "Confirmar Pedido",
|
'confirm_order': 'Confirmar Pedido',
|
||||||
back_to_cart: "Volver al Carrito",
|
'back_to_cart': 'Volver al Carrito'
|
||||||
},
|
},
|
||||||
_initTooltips: function () {
|
_initTooltips: function() {
|
||||||
var labels = window.groupOrderShop.labels || this.labels || {};
|
var labels = window.groupOrderShop.labels || this.labels || {};
|
||||||
var tooltipElements = document.querySelectorAll("[data-tooltip-label]");
|
var tooltipElements = document.querySelectorAll('[data-tooltip-label]');
|
||||||
|
|
||||||
tooltipElements.forEach(function (el) {
|
tooltipElements.forEach(function(el) {
|
||||||
var labelKey = el.getAttribute("data-tooltip-label");
|
var labelKey = el.getAttribute('data-tooltip-label');
|
||||||
if (labelKey && labels[labelKey]) {
|
if (labelKey && labels[labelKey]) {
|
||||||
el.setAttribute("title", labels[labelKey]);
|
el.setAttribute('title', labels[labelKey]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
afterEach: function () {
|
afterEach: function() {
|
||||||
// Cleanup
|
// Cleanup
|
||||||
this.$fixture.empty();
|
this.$fixture.empty();
|
||||||
delete window.groupOrderShop;
|
delete window.groupOrderShop;
|
||||||
},
|
}
|
||||||
},
|
}, function() {
|
||||||
function () {
|
|
||||||
QUnit.test("tooltips are initialized from labels", function (assert) {
|
QUnit.test('tooltips are initialized from labels', function(assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
|
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
|
|
||||||
var btn1 = document.getElementById("test-btn-1");
|
var btn1 = document.getElementById('test-btn-1');
|
||||||
var btn2 = document.getElementById("test-btn-2");
|
var btn2 = document.getElementById('test-btn-2');
|
||||||
var btn3 = document.getElementById("test-btn-3");
|
var btn3 = document.getElementById('test-btn-3');
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(btn1.getAttribute('title'), 'Guardar Carrito', 'save_cart tooltip is correct');
|
||||||
btn1.getAttribute("title"),
|
assert.equal(btn2.getAttribute('title'), 'Proceder al Pago', 'checkout tooltip is correct');
|
||||||
"Guardar Carrito",
|
assert.equal(btn3.getAttribute('title'), 'Recargar Carrito', 'reload_cart tooltip is correct');
|
||||||
"save_cart tooltip is correct"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
btn2.getAttribute("title"),
|
|
||||||
"Proceder al Pago",
|
|
||||||
"checkout tooltip is correct"
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
btn3.getAttribute("title"),
|
|
||||||
"Recargar Carrito",
|
|
||||||
"reload_cart tooltip is correct"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("tooltips handle missing labels gracefully", function (assert) {
|
QUnit.test('tooltips handle missing labels gracefully', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
// Add button with non-existent label
|
// Add button with non-existent label
|
||||||
this.$fixture.append(
|
this.$fixture.append('<button id="test-btn-4" data-tooltip-label="non_existent">Test</button>');
|
||||||
'<button id="test-btn-4" data-tooltip-label="non_existent">Test</button>'
|
|
||||||
);
|
|
||||||
|
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
|
|
||||||
var btn4 = document.getElementById("test-btn-4");
|
var btn4 = document.getElementById('test-btn-4');
|
||||||
var title = btn4.getAttribute("title");
|
var title = btn4.getAttribute('title');
|
||||||
|
|
||||||
// Should be null or empty since label doesn't exist
|
// Should be null or empty since label doesn't exist
|
||||||
assert.ok(!title || title === "", "missing label does not set tooltip");
|
assert.ok(!title || title === '', 'missing label does not set tooltip');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("labels object contains expected keys", function (assert) {
|
QUnit.test('labels object contains expected keys', function(assert) {
|
||||||
assert.expect(5);
|
assert.expect(5);
|
||||||
|
|
||||||
var labels = window.groupOrderShop.labels;
|
var labels = window.groupOrderShop.labels;
|
||||||
|
|
||||||
assert.ok("save_cart" in labels, "has save_cart label");
|
assert.ok('save_cart' in labels, 'has save_cart label');
|
||||||
assert.ok("reload_cart" in labels, "has reload_cart label");
|
assert.ok('reload_cart' in labels, 'has reload_cart label');
|
||||||
assert.ok("checkout" in labels, "has checkout label");
|
assert.ok('checkout' in labels, 'has checkout label');
|
||||||
assert.ok("confirm_order" in labels, "has confirm_order label");
|
assert.ok('confirm_order' in labels, 'has confirm_order label');
|
||||||
assert.ok("back_to_cart" in labels, "has back_to_cart label");
|
assert.ok('back_to_cart' in labels, 'has back_to_cart label');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("labels are strings", function (assert) {
|
QUnit.test('labels are strings', function(assert) {
|
||||||
assert.expect(5);
|
assert.expect(5);
|
||||||
|
|
||||||
var labels = window.groupOrderShop.labels;
|
var labels = window.groupOrderShop.labels;
|
||||||
|
|
||||||
assert.equal(typeof labels.save_cart, "string", "save_cart is string");
|
assert.equal(typeof labels.save_cart, 'string', 'save_cart is string');
|
||||||
assert.equal(typeof labels.reload_cart, "string", "reload_cart is string");
|
assert.equal(typeof labels.reload_cart, 'string', 'reload_cart is string');
|
||||||
assert.equal(typeof labels.checkout, "string", "checkout is string");
|
assert.equal(typeof labels.checkout, 'string', 'checkout is string');
|
||||||
assert.equal(typeof labels.confirm_order, "string", "confirm_order is string");
|
assert.equal(typeof labels.confirm_order, 'string', 'confirm_order is string');
|
||||||
assert.equal(typeof labels.back_to_cart, "string", "back_to_cart is string");
|
assert.equal(typeof labels.back_to_cart, 'string', 'back_to_cart is string');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("_initTooltips uses window.groupOrderShop.labels", function (assert) {
|
QUnit.test('_initTooltips uses window.groupOrderShop.labels', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
// Update global labels
|
// Update global labels
|
||||||
window.groupOrderShop.labels = {
|
window.groupOrderShop.labels = {
|
||||||
save_cart: "Updated Label",
|
'save_cart': 'Updated Label',
|
||||||
checkout: "Updated Checkout",
|
'checkout': 'Updated Checkout',
|
||||||
reload_cart: "Updated Reload",
|
'reload_cart': 'Updated Reload'
|
||||||
};
|
};
|
||||||
|
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
|
|
||||||
var btn1 = document.getElementById("test-btn-1");
|
var btn1 = document.getElementById('test-btn-1');
|
||||||
assert.equal(
|
assert.equal(btn1.getAttribute('title'), 'Updated Label', 'uses updated global labels');
|
||||||
btn1.getAttribute("title"),
|
|
||||||
"Updated Label",
|
|
||||||
"uses updated global labels"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("tooltips can be reinitialized", function (assert) {
|
QUnit.test('tooltips can be reinitialized', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
// First initialization
|
// First initialization
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
var btn1 = document.getElementById("test-btn-1");
|
var btn1 = document.getElementById('test-btn-1');
|
||||||
assert.equal(btn1.getAttribute("title"), "Guardar Carrito", "first init correct");
|
assert.equal(btn1.getAttribute('title'), 'Guardar Carrito', 'first init correct');
|
||||||
|
|
||||||
// Update labels and reinitialize
|
// Update labels and reinitialize
|
||||||
window.groupOrderShop.labels.save_cart = "New Translation";
|
window.groupOrderShop.labels.save_cart = 'New Translation';
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(btn1.getAttribute('title'), 'New Translation', 'reinitialized with new label');
|
||||||
btn1.getAttribute("title"),
|
|
||||||
"New Translation",
|
|
||||||
"reinitialized with new label"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("elements without data-tooltip-label are ignored", function (assert) {
|
QUnit.test('elements without data-tooltip-label are ignored', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
this.$fixture.append('<button id="test-btn-no-label">No Label</button>');
|
this.$fixture.append('<button id="test-btn-no-label">No Label</button>');
|
||||||
|
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
|
|
||||||
var btnNoLabel = document.getElementById("test-btn-no-label");
|
var btnNoLabel = document.getElementById('test-btn-no-label');
|
||||||
var title = btnNoLabel.getAttribute("title");
|
var title = btnNoLabel.getAttribute('title');
|
||||||
|
|
||||||
assert.ok(!title || title === "", "button without data-tooltip-label has no title");
|
assert.ok(!title || title === '', 'button without data-tooltip-label has no title');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("querySelectorAll finds all tooltip elements", function (assert) {
|
QUnit.test('querySelectorAll finds all tooltip elements', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
var tooltipElements = document.querySelectorAll("[data-tooltip-label]");
|
var tooltipElements = document.querySelectorAll('[data-tooltip-label]');
|
||||||
|
|
||||||
// We have 3 buttons with data-tooltip-label
|
// We have 3 buttons with data-tooltip-label
|
||||||
assert.equal(
|
assert.equal(tooltipElements.length, 3, 'finds all 3 elements with data-tooltip-label');
|
||||||
tooltipElements.length,
|
|
||||||
3,
|
|
||||||
"finds all 3 elements with data-tooltip-label"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("labels survive JSON serialization", function (assert) {
|
QUnit.test('labels survive JSON serialization', function(assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
var labels = window.groupOrderShop.labels;
|
var labels = window.groupOrderShop.labels;
|
||||||
var serialized = JSON.stringify(labels);
|
var serialized = JSON.stringify(labels);
|
||||||
var deserialized = JSON.parse(serialized);
|
var deserialized = JSON.parse(serialized);
|
||||||
|
|
||||||
assert.ok(serialized, "labels can be serialized to JSON");
|
assert.ok(serialized, 'labels can be serialized to JSON');
|
||||||
assert.deepEqual(deserialized, labels, "deserialized labels match original");
|
assert.deepEqual(deserialized, labels, 'deserialized labels match original');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("empty labels object does not break initialization", function (assert) {
|
QUnit.test('empty labels object does not break initialization', function(assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
window.groupOrderShop.labels = {};
|
window.groupOrderShop.labels = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.groupOrderShop._initTooltips();
|
window.groupOrderShop._initTooltips();
|
||||||
assert.ok(true, "initialization with empty labels does not throw error");
|
assert.ok(true, 'initialization with empty labels does not throw error');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
assert.ok(false, "initialization threw error: " + e.message);
|
assert.ok(false, 'initialization threw error: ' + e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,26 @@
|
||||||
# Copyright 2026 Criptomart
|
# Copyright 2026 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from odoo import fields
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
from odoo.tests.common import tagged
|
from odoo import fields
|
||||||
|
|
||||||
|
|
||||||
@tagged("post_install", "date_calculations")
|
|
||||||
class TestDateCalculations(TransactionCase):
|
class TestDateCalculations(TransactionCase):
|
||||||
"""Test suite for date calculation methods in group.order model."""
|
'''Test suite for date calculation methods in group.order model.'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# Create a test group
|
# Create a test group
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_compute_pickup_date_basic(self):
|
def test_compute_pickup_date_basic(self):
|
||||||
"""Test pickup_date calculation returns next occurrence of pickup day."""
|
'''Test pickup_date calculation returns next occurrence of pickup day.'''
|
||||||
# Use today as reference and calculate next Tuesday
|
# Use today as reference and calculate next Tuesday
|
||||||
today = fields.Date.today()
|
today = fields.Date.today()
|
||||||
# Find next Sunday (weekday 6) from today
|
# Find next Sunday (weekday 6) from today
|
||||||
|
|
@ -37,15 +32,13 @@ class TestDateCalculations(TransactionCase):
|
||||||
|
|
||||||
# Create order with pickup_day = Tuesday (1), starting on Sunday
|
# Create order with pickup_day = Tuesday (1), starting on Sunday
|
||||||
# NO cutoff_day to avoid dependency on cutoff_date
|
# NO cutoff_day to avoid dependency on cutoff_date
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': start_date, # Sunday
|
||||||
"start_date": start_date, # Sunday
|
'pickup_day': '1', # Tuesday
|
||||||
"pickup_day": "1", # Tuesday
|
'cutoff_day': False, # Disable to avoid cutoff_date interference
|
||||||
"cutoff_day": False, # Disable to avoid cutoff_date interference
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Force computation
|
# Force computation
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
|
|
@ -55,11 +48,11 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
order.pickup_date,
|
order.pickup_date,
|
||||||
expected_date,
|
expected_date,
|
||||||
f"Expected {expected_date}, got {order.pickup_date}",
|
f"Expected {expected_date}, got {order.pickup_date}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_compute_pickup_date_same_day(self):
|
def test_compute_pickup_date_same_day(self):
|
||||||
"""Test pickup_date when start_date is same weekday as pickup_day."""
|
'''Test pickup_date when start_date is same weekday as pickup_day.'''
|
||||||
# Find next Tuesday from today
|
# Find next Tuesday from today
|
||||||
today = fields.Date.today()
|
today = fields.Date.today()
|
||||||
days_until_tuesday = (1 - today.weekday()) % 7
|
days_until_tuesday = (1 - today.weekday()) % 7
|
||||||
|
|
@ -69,14 +62,12 @@ class TestDateCalculations(TransactionCase):
|
||||||
start_date = today + timedelta(days=days_until_tuesday)
|
start_date = today + timedelta(days=days_until_tuesday)
|
||||||
|
|
||||||
# Start on Tuesday, pickup also Tuesday - should return next week's Tuesday
|
# Start on Tuesday, pickup also Tuesday - should return next week's Tuesday
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order Same Day',
|
||||||
"name": "Test Order Same Day",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': start_date, # Tuesday
|
||||||
"start_date": start_date, # Tuesday
|
'pickup_day': '1', # Tuesday
|
||||||
"pickup_day": "1", # Tuesday
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
|
|
||||||
|
|
@ -85,15 +76,13 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(order.pickup_date, expected_date)
|
self.assertEqual(order.pickup_date, expected_date)
|
||||||
|
|
||||||
def test_compute_pickup_date_no_start_date(self):
|
def test_compute_pickup_date_no_start_date(self):
|
||||||
"""Test pickup_date calculation when no start_date is set."""
|
'''Test pickup_date calculation when no start_date is set.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order No Start',
|
||||||
"name": "Test Order No Start",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': False,
|
||||||
"start_date": False,
|
'pickup_day': '1', # Tuesday
|
||||||
"pickup_day": "1", # Tuesday
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
|
|
||||||
|
|
@ -104,43 +93,32 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(order.pickup_date.weekday(), 1) # 1 = Tuesday
|
self.assertEqual(order.pickup_date.weekday(), 1) # 1 = Tuesday
|
||||||
|
|
||||||
def test_compute_pickup_date_without_pickup_day(self):
|
def test_compute_pickup_date_without_pickup_day(self):
|
||||||
"""Test pickup_date is None when pickup_day is not set."""
|
'''Test pickup_date is None when pickup_day is not set.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order No Pickup Day',
|
||||||
"name": "Test Order No Pickup Day",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': fields.Date.today(),
|
||||||
"start_date": fields.Date.today(),
|
'pickup_day': False,
|
||||||
"pickup_day": False,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
# In Odoo, computed Date fields return False (not None) when no value
|
# In Odoo, computed Date fields return False (not None) when no value
|
||||||
self.assertFalse(order.pickup_date)
|
self.assertFalse(order.pickup_date)
|
||||||
|
|
||||||
def test_compute_pickup_date_all_weekdays(self):
|
def test_compute_pickup_date_all_weekdays(self):
|
||||||
"""Test pickup_date calculation for each day of the week."""
|
'''Test pickup_date calculation for each day of the week.'''
|
||||||
base_date = fields.Date.from_string("2026-02-02") # Monday
|
base_date = fields.Date.from_string('2026-02-02') # Monday
|
||||||
|
|
||||||
for day_num in range(7):
|
for day_num in range(7):
|
||||||
day_name = [
|
day_name = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
||||||
"Monday",
|
'Friday', 'Saturday', 'Sunday'][day_num]
|
||||||
"Tuesday",
|
|
||||||
"Wednesday",
|
|
||||||
"Thursday",
|
|
||||||
"Friday",
|
|
||||||
"Saturday",
|
|
||||||
"Sunday",
|
|
||||||
][day_num]
|
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': f'Test Order {day_name}',
|
||||||
"name": f"Test Order {day_name}",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': base_date,
|
||||||
"start_date": base_date,
|
'pickup_day': str(day_num),
|
||||||
"pickup_day": str(day_num),
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
|
|
||||||
|
|
@ -148,14 +126,14 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
order.pickup_date.weekday(),
|
order.pickup_date.weekday(),
|
||||||
day_num,
|
day_num,
|
||||||
f"Pickup date weekday should be {day_num} ({day_name})",
|
f"Pickup date weekday should be {day_num} ({day_name})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify it's after start_date
|
# Verify it's after start_date
|
||||||
self.assertGreater(order.pickup_date, base_date)
|
self.assertGreater(order.pickup_date, base_date)
|
||||||
|
|
||||||
def test_compute_delivery_date_basic(self):
|
def test_compute_delivery_date_basic(self):
|
||||||
"""Test delivery_date is pickup_date + 1 day."""
|
'''Test delivery_date is pickup_date + 1 day.'''
|
||||||
# Find next Sunday from today
|
# Find next Sunday from today
|
||||||
today = fields.Date.today()
|
today = fields.Date.today()
|
||||||
days_until_sunday = (6 - today.weekday()) % 7
|
days_until_sunday = (6 - today.weekday()) % 7
|
||||||
|
|
@ -164,14 +142,12 @@ class TestDateCalculations(TransactionCase):
|
||||||
else:
|
else:
|
||||||
start_date = today + timedelta(days=days_until_sunday)
|
start_date = today + timedelta(days=days_until_sunday)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Delivery Date',
|
||||||
"name": "Test Delivery Date",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': start_date, # Sunday
|
||||||
"start_date": start_date, # Sunday
|
'pickup_day': '1', # Tuesday = start_date + 2 days
|
||||||
"pickup_day": "1", # Tuesday = start_date + 2 days
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
order._compute_delivery_date()
|
order._compute_delivery_date()
|
||||||
|
|
@ -183,15 +159,13 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(order.delivery_date, expected_delivery)
|
self.assertEqual(order.delivery_date, expected_delivery)
|
||||||
|
|
||||||
def test_compute_delivery_date_without_pickup(self):
|
def test_compute_delivery_date_without_pickup(self):
|
||||||
"""Test delivery_date is None when pickup_date is not set."""
|
'''Test delivery_date is None when pickup_date is not set.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test No Delivery',
|
||||||
"name": "Test No Delivery",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': fields.Date.today(),
|
||||||
"start_date": fields.Date.today(),
|
'pickup_day': False, # No pickup day = no pickup_date
|
||||||
"pickup_day": False, # No pickup day = no pickup_date
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
order._compute_delivery_date()
|
order._compute_delivery_date()
|
||||||
|
|
@ -200,17 +174,15 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertFalse(order.delivery_date)
|
self.assertFalse(order.delivery_date)
|
||||||
|
|
||||||
def test_compute_cutoff_date_basic(self):
|
def test_compute_cutoff_date_basic(self):
|
||||||
"""Test cutoff_date calculation returns next occurrence of cutoff day."""
|
'''Test cutoff_date calculation returns next occurrence of cutoff day.'''
|
||||||
# Create order with cutoff_day = Sunday (6)
|
# Create order with cutoff_day = Sunday (6)
|
||||||
# If today is Sunday, cutoff should be today (days_ahead = 0 is allowed)
|
# If today is Sunday, cutoff should be today (days_ahead = 0 is allowed)
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Cutoff Date',
|
||||||
"name": "Test Cutoff Date",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': fields.Date.from_string('2026-02-01'), # Sunday
|
||||||
"start_date": fields.Date.from_string("2026-02-01"), # Sunday
|
'cutoff_day': '6', # Sunday
|
||||||
"cutoff_day": "6", # Sunday
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_cutoff_date()
|
order._compute_cutoff_date()
|
||||||
|
|
||||||
|
|
@ -221,22 +193,20 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(order.cutoff_date.weekday(), 6) # Sunday
|
self.assertEqual(order.cutoff_date.weekday(), 6) # Sunday
|
||||||
|
|
||||||
def test_compute_cutoff_date_without_cutoff_day(self):
|
def test_compute_cutoff_date_without_cutoff_day(self):
|
||||||
"""Test cutoff_date is None when cutoff_day is not set."""
|
'''Test cutoff_date is None when cutoff_day is not set.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test No Cutoff',
|
||||||
"name": "Test No Cutoff",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': fields.Date.today(),
|
||||||
"start_date": fields.Date.today(),
|
'cutoff_day': False,
|
||||||
"cutoff_day": False,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_cutoff_date()
|
order._compute_cutoff_date()
|
||||||
# In Odoo, computed Date fields return False (not None) when no value
|
# In Odoo, computed Date fields return False (not None) when no value
|
||||||
self.assertFalse(order.cutoff_date)
|
self.assertFalse(order.cutoff_date)
|
||||||
|
|
||||||
def test_date_dependency_chain(self):
|
def test_date_dependency_chain(self):
|
||||||
"""Test that changing start_date triggers recomputation of date fields."""
|
'''Test that changing start_date triggers recomputation of date fields.'''
|
||||||
# Find next Sunday from today
|
# Find next Sunday from today
|
||||||
today = fields.Date.today()
|
today = fields.Date.today()
|
||||||
days_until_sunday = (6 - today.weekday()) % 7
|
days_until_sunday = (6 - today.weekday()) % 7
|
||||||
|
|
@ -245,15 +215,13 @@ class TestDateCalculations(TransactionCase):
|
||||||
else:
|
else:
|
||||||
start_date = today + timedelta(days=days_until_sunday)
|
start_date = today + timedelta(days=days_until_sunday)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Date Chain',
|
||||||
"name": "Test Date Chain",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': start_date, # Dynamic Sunday
|
||||||
"start_date": start_date, # Dynamic Sunday
|
'pickup_day': '1', # Tuesday
|
||||||
"pickup_day": "6", # Sunday (must be >= cutoff_day)
|
'cutoff_day': '6', # Sunday
|
||||||
"cutoff_day": "5", # Saturday
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get initial dates
|
# Get initial dates
|
||||||
initial_pickup = order.pickup_date
|
initial_pickup = order.pickup_date
|
||||||
|
|
@ -262,7 +230,7 @@ class TestDateCalculations(TransactionCase):
|
||||||
|
|
||||||
# Change start_date to a week later
|
# Change start_date to a week later
|
||||||
new_start_date = start_date + timedelta(days=7)
|
new_start_date = start_date + timedelta(days=7)
|
||||||
order.write({"start_date": new_start_date})
|
order.write({'start_date': new_start_date})
|
||||||
|
|
||||||
# Verify pickup and delivery dates changed
|
# Verify pickup and delivery dates changed
|
||||||
self.assertNotEqual(order.pickup_date, initial_pickup)
|
self.assertNotEqual(order.pickup_date, initial_pickup)
|
||||||
|
|
@ -274,12 +242,12 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(delta.days, 1)
|
self.assertEqual(delta.days, 1)
|
||||||
|
|
||||||
def test_pickup_date_no_extra_week_bug(self):
|
def test_pickup_date_no_extra_week_bug(self):
|
||||||
"""Regression test: ensure pickup_date doesn't add extra week incorrectly.
|
'''Regression test: ensure pickup_date doesn't add extra week incorrectly.
|
||||||
|
|
||||||
Bug context: Previously when cutoff_day >= pickup_day numerically,
|
Bug context: Previously when cutoff_day >= pickup_day numerically,
|
||||||
logic incorrectly added 7 extra days even when pickup was already
|
logic incorrectly added 7 extra days even when pickup was already
|
||||||
ahead in the calendar.
|
ahead in the calendar.
|
||||||
"""
|
'''
|
||||||
# Scenario: Pickup Tuesday (1)
|
# Scenario: Pickup Tuesday (1)
|
||||||
# Start: Sunday (dynamic)
|
# Start: Sunday (dynamic)
|
||||||
# Expected pickup: Tuesday (2 days later, NOT +9 days)
|
# Expected pickup: Tuesday (2 days later, NOT +9 days)
|
||||||
|
|
@ -293,15 +261,13 @@ class TestDateCalculations(TransactionCase):
|
||||||
else:
|
else:
|
||||||
start_date = today + timedelta(days=days_until_sunday)
|
start_date = today + timedelta(days=days_until_sunday)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Regression Test Extra Week',
|
||||||
"name": "Regression Test Extra Week",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': start_date, # Sunday (dynamic)
|
||||||
"start_date": start_date, # Sunday (dynamic)
|
'pickup_day': '1', # Tuesday (numerically < 6)
|
||||||
"pickup_day": "1", # Tuesday (numerically < 6)
|
'cutoff_day': False, # Disable to test pure start_date logic
|
||||||
"cutoff_day": False, # Disable to test pure start_date logic
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order._compute_pickup_date()
|
order._compute_pickup_date()
|
||||||
|
|
||||||
|
|
@ -310,30 +276,30 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
order.pickup_date,
|
order.pickup_date,
|
||||||
expected,
|
expected,
|
||||||
f"Bug detected: pickup_date should be {expected} not {order.pickup_date}",
|
f"Bug detected: pickup_date should be {expected} not {order.pickup_date}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify it's exactly 2 days after start_date
|
# Verify it's exactly 2 days after start_date
|
||||||
delta = order.pickup_date - order.start_date
|
delta = order.pickup_date - order.start_date
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
delta.days, 2, "Pickup should be 2 days after Sunday start_date"
|
delta.days,
|
||||||
|
2,
|
||||||
|
"Pickup should be 2 days after Sunday start_date"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_multiple_orders_same_pickup_day(self):
|
def test_multiple_orders_same_pickup_day(self):
|
||||||
"""Test multiple orders with same pickup day get consistent dates."""
|
'''Test multiple orders with same pickup day get consistent dates.'''
|
||||||
start = fields.Date.from_string("2026-02-01")
|
start = fields.Date.from_string('2026-02-01')
|
||||||
pickup_day = "1" # Tuesday
|
pickup_day = '1' # Tuesday
|
||||||
|
|
||||||
orders = []
|
orders = []
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': f'Test Order {i}',
|
||||||
"name": f"Test Order {i}",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': start,
|
||||||
"start_date": start,
|
'pickup_day': pickup_day,
|
||||||
"pickup_day": pickup_day,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
orders.append(order)
|
orders.append(order)
|
||||||
|
|
||||||
# All should have same pickup_date
|
# All should have same pickup_date
|
||||||
|
|
@ -341,229 +307,5 @@ class TestDateCalculations(TransactionCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(set(pickup_dates)),
|
len(set(pickup_dates)),
|
||||||
1,
|
1,
|
||||||
"All orders with same start_date and pickup_day should have same pickup_date",
|
"All orders with same start_date and pickup_day should have same pickup_date"
|
||||||
)
|
|
||||||
|
|
||||||
# === NEW REGRESSION TESTS (v18.0.1.3.1) ===
|
|
||||||
|
|
||||||
def test_cutoff_same_day_as_today_bug_fix(self):
|
|
||||||
"""Regression test: cutoff_date should allow same day as today.
|
|
||||||
|
|
||||||
Bug fixed in v18.0.1.3.1: Previously, if cutoff_day == today.weekday(),
|
|
||||||
the system would incorrectly add 7 days, scheduling cutoff for next week.
|
|
||||||
Now cutoff_date can be today if cutoff_day matches today's weekday.
|
|
||||||
"""
|
|
||||||
today = fields.Date.today()
|
|
||||||
cutoff_day = str(today.weekday()) # Same as today
|
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": "Test Cutoff Today",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": today,
|
|
||||||
"cutoff_day": cutoff_day,
|
|
||||||
"period": "weekly",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# cutoff_date should be TODAY, not next week
|
|
||||||
self.assertEqual(
|
|
||||||
order.cutoff_date,
|
|
||||||
today,
|
|
||||||
f"Expected cutoff_date={today} (today), got {order.cutoff_date}. "
|
|
||||||
"Cutoff should be allowed on the same day.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_delivery_date_stored_correctly(self):
|
|
||||||
"""Regression test: delivery_date must be stored in database.
|
|
||||||
|
|
||||||
Bug fixed in v18.0.1.3.1: delivery_date had store=False, causing
|
|
||||||
inconsistent values and inability to search/filter by this field.
|
|
||||||
Now delivery_date is stored (store=True).
|
|
||||||
"""
|
|
||||||
today = fields.Date.today()
|
|
||||||
# Set pickup for next Monday
|
|
||||||
days_until_monday = (0 - today.weekday()) % 7
|
|
||||||
if days_until_monday == 0:
|
|
||||||
days_until_monday = 7
|
|
||||||
start_date = today + timedelta(days=days_until_monday - 1)
|
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": "Test Delivery Stored",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": start_date,
|
|
||||||
"pickup_day": "0", # Monday
|
|
||||||
"period": "weekly",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Force computation
|
|
||||||
order._compute_pickup_date()
|
|
||||||
order._compute_delivery_date()
|
|
||||||
|
|
||||||
expected_delivery = order.pickup_date + timedelta(days=1)
|
|
||||||
self.assertEqual(
|
|
||||||
order.delivery_date,
|
|
||||||
expected_delivery,
|
|
||||||
f"Expected delivery_date={expected_delivery}, got {order.delivery_date}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify it's stored: read from database
|
|
||||||
order_from_db = self.env["group.order"].browse(order.id)
|
|
||||||
self.assertEqual(
|
|
||||||
order_from_db.delivery_date,
|
|
||||||
expected_delivery,
|
|
||||||
"delivery_date should be persisted in database (store=True)",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_constraint_cutoff_before_pickup_invalid(self):
|
|
||||||
"""Test constraint: pickup_day must be >= cutoff_day for weekly orders.
|
|
||||||
|
|
||||||
New constraint in v18.0.1.3.1: For weekly orders, if pickup_day < cutoff_day
|
|
||||||
numerically, it creates an illogical scenario where pickup would be
|
|
||||||
scheduled before cutoff in the same week cycle.
|
|
||||||
"""
|
|
||||||
today = fields.Date.today()
|
|
||||||
|
|
||||||
# Invalid configuration: pickup (Tuesday=1) < cutoff (Thursday=3)
|
|
||||||
with self.assertRaises(
|
|
||||||
ValidationError,
|
|
||||||
msg="Should raise ValidationError for pickup_day < cutoff_day",
|
|
||||||
):
|
|
||||||
self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": "Invalid Order",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": today,
|
|
||||||
"cutoff_day": "3", # Thursday
|
|
||||||
"pickup_day": "1", # Tuesday (BEFORE Thursday)
|
|
||||||
"period": "weekly",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_constraint_cutoff_before_pickup_valid(self):
|
|
||||||
"""Test constraint allows valid configurations.
|
|
||||||
|
|
||||||
Valid scenarios:
|
|
||||||
- pickup_day > cutoff_day: pickup is after cutoff ✓
|
|
||||||
- pickup_day == cutoff_day: same day allowed ✓
|
|
||||||
"""
|
|
||||||
today = fields.Date.today()
|
|
||||||
|
|
||||||
# Valid: pickup (Saturday=5) > cutoff (Tuesday=1)
|
|
||||||
order1 = self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": "Valid Order 1",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": today,
|
|
||||||
"cutoff_day": "1", # Tuesday
|
|
||||||
"pickup_day": "5", # Saturday (AFTER Tuesday)
|
|
||||||
"period": "weekly",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertTrue(order1.id, "Valid configuration should create order")
|
|
||||||
|
|
||||||
# Valid: pickup == cutoff (same day)
|
|
||||||
order2 = self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": "Valid Order 2",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": today,
|
|
||||||
"cutoff_day": "5", # Saturday
|
|
||||||
"pickup_day": "5", # Saturday (SAME DAY)
|
|
||||||
"period": "weekly",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertTrue(order2.id, "Same day configuration should be allowed")
|
|
||||||
|
|
||||||
def test_all_weekday_combinations_consistency(self):
|
|
||||||
"""Test that all valid weekday combinations produce consistent results.
|
|
||||||
|
|
||||||
This regression test ensures the date calculation logic works correctly
|
|
||||||
for all 49 combinations of start_date × pickup_day (7 × 7).
|
|
||||||
"""
|
|
||||||
today = fields.Date.today()
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for start_offset in range(7): # 7 possible start days
|
|
||||||
start_date = today + timedelta(days=start_offset)
|
|
||||||
|
|
||||||
for pickup_weekday in range(7): # 7 possible pickup days
|
|
||||||
order = self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": f"Test S{start_offset}P{pickup_weekday}",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": start_date,
|
|
||||||
"pickup_day": str(pickup_weekday),
|
|
||||||
"period": "weekly",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate pickup_date is set
|
|
||||||
if not order.pickup_date:
|
|
||||||
errors.append(
|
|
||||||
f"start_offset={start_offset}, pickup_day={pickup_weekday}: "
|
|
||||||
f"pickup_date is None"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Validate pickup_date is in the future or today
|
|
||||||
if order.pickup_date < start_date:
|
|
||||||
errors.append(
|
|
||||||
f"start_offset={start_offset}, pickup_day={pickup_weekday}: "
|
|
||||||
f"pickup_date {order.pickup_date} < start_date {start_date}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate pickup_date weekday matches pickup_day
|
|
||||||
if order.pickup_date.weekday() != pickup_weekday:
|
|
||||||
errors.append(
|
|
||||||
f"start_offset={start_offset}, pickup_day={pickup_weekday}: "
|
|
||||||
f"pickup_date weekday is {order.pickup_date.weekday()}, "
|
|
||||||
f"expected {pickup_weekday}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
len(errors),
|
|
||||||
0,
|
|
||||||
f"Found {len(errors)} errors in weekday combinations:\n"
|
|
||||||
+ "\n".join(errors),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cron_update_dates_executes(self):
|
|
||||||
"""Test that cron job method executes without errors.
|
|
||||||
|
|
||||||
New feature in v18.0.1.3.1: Daily cron job to recalculate dates
|
|
||||||
for active orders to keep them up-to-date as time passes.
|
|
||||||
"""
|
|
||||||
today = fields.Date.today()
|
|
||||||
|
|
||||||
# Create multiple orders in different states
|
|
||||||
orders = []
|
|
||||||
for i, state in enumerate(["draft", "open", "closed"]):
|
|
||||||
order = self.env["group.order"].create(
|
|
||||||
{
|
|
||||||
"name": f"Test Cron Order {state}",
|
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
|
||||||
"start_date": today,
|
|
||||||
"pickup_day": str((i + 1) % 7),
|
|
||||||
"cutoff_day": str(i % 7),
|
|
||||||
"period": "weekly",
|
|
||||||
"state": state,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
orders.append(order)
|
|
||||||
|
|
||||||
# Execute cron method (should not raise errors)
|
|
||||||
try:
|
|
||||||
self.env["group.order"]._cron_update_dates()
|
|
||||||
except Exception as e:
|
|
||||||
self.fail(f"Cron method raised exception: {e}")
|
|
||||||
|
|
||||||
# Verify dates are still valid for active orders
|
|
||||||
active_orders = [o for o in orders if o.state in ["draft", "open"]]
|
|
||||||
for order in active_orders:
|
|
||||||
self.assertIsNotNone(
|
|
||||||
order.pickup_date,
|
|
||||||
f"Pickup date should be set for active order {order.name}",
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ Coverage:
|
||||||
- Draft timeline (very old draft, recent draft)
|
- Draft timeline (very old draft, recent draft)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
@ -24,117 +23,91 @@ class TestSaveDraftOrder(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product1 = self.env["product.product"].create(
|
self.product1 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product 1',
|
||||||
"name": "Product 1",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product2 = self.env["product.product"].create(
|
self.product2 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product 2',
|
||||||
"name": "Product 2",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'pickup_date': start_date + timedelta(days=3),
|
||||||
"pickup_date": start_date + timedelta(days=3),
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
self.group_order.product_ids = [(4, self.product1.id), (4, self.product2.id)]
|
self.group_order.product_ids = [(4, self.product1.id), (4, self.product2.id)]
|
||||||
|
|
||||||
def test_save_draft_with_items(self):
|
def test_save_draft_with_items(self):
|
||||||
"""Test saving draft order with products."""
|
"""Test saving draft order with products."""
|
||||||
draft_order = self.env["sale.order"].create(
|
draft_order = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [
|
||||||
"order_line": [
|
(0, 0, {
|
||||||
(
|
'product_id': self.product1.id,
|
||||||
0,
|
'product_qty': 2,
|
||||||
0,
|
'price_unit': self.product1.list_price,
|
||||||
{
|
}),
|
||||||
"product_id": self.product1.id,
|
(0, 0, {
|
||||||
"product_qty": 2,
|
'product_id': self.product2.id,
|
||||||
"price_unit": self.product1.list_price,
|
'product_qty': 1,
|
||||||
},
|
'price_unit': self.product2.list_price,
|
||||||
),
|
}),
|
||||||
(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"product_id": self.product2.id,
|
|
||||||
"product_qty": 1,
|
|
||||||
"price_unit": self.product2.list_price,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(draft_order.exists())
|
self.assertTrue(draft_order.exists())
|
||||||
self.assertEqual(draft_order.state, "draft")
|
self.assertEqual(draft_order.state, 'draft')
|
||||||
self.assertEqual(len(draft_order.order_line), 2)
|
self.assertEqual(len(draft_order.order_line), 2)
|
||||||
|
|
||||||
def test_save_draft_empty_order(self):
|
def test_save_draft_empty_order(self):
|
||||||
"""Test saving draft order without items."""
|
"""Test saving draft order without items."""
|
||||||
# Edge case: empty draft
|
# Edge case: empty draft
|
||||||
empty_draft = self.env["sale.order"].create(
|
empty_draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [],
|
||||||
"order_line": [],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should be valid (user hasn't added products yet)
|
# Should be valid (user hasn't added products yet)
|
||||||
self.assertTrue(empty_draft.exists())
|
self.assertTrue(empty_draft.exists())
|
||||||
|
|
@ -143,23 +116,15 @@ class TestSaveDraftOrder(TransactionCase):
|
||||||
def test_save_draft_updates_existing(self):
|
def test_save_draft_updates_existing(self):
|
||||||
"""Test that saving draft updates existing draft, not creates new."""
|
"""Test that saving draft updates existing draft, not creates new."""
|
||||||
# Create initial draft
|
# Create initial draft
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [(0, 0, {
|
||||||
"order_line": [
|
'product_id': self.product1.id,
|
||||||
(
|
'product_qty': 1,
|
||||||
0,
|
})],
|
||||||
0,
|
})
|
||||||
{
|
|
||||||
"product_id": self.product1.id,
|
|
||||||
"product_qty": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
draft_id = draft.id
|
draft_id = draft.id
|
||||||
|
|
||||||
|
|
@ -167,33 +132,29 @@ class TestSaveDraftOrder(TransactionCase):
|
||||||
draft.order_line[0].product_qty = 5
|
draft.order_line[0].product_qty = 5
|
||||||
|
|
||||||
# Should be same draft, not new one
|
# Should be same draft, not new one
|
||||||
updated_draft = self.env["sale.order"].browse(draft_id)
|
updated_draft = self.env['sale.order'].browse(draft_id)
|
||||||
self.assertTrue(updated_draft.exists())
|
self.assertTrue(updated_draft.exists())
|
||||||
self.assertEqual(updated_draft.order_line[0].product_qty, 5)
|
self.assertEqual(updated_draft.order_line[0].product_qty, 5)
|
||||||
|
|
||||||
def test_save_draft_preserves_group_order_reference(self):
|
def test_save_draft_preserves_group_order_reference(self):
|
||||||
"""Test that group_order_id is preserved when saving."""
|
"""Test that group_order_id is preserved when saving."""
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Link must be preserved
|
# Link must be preserved
|
||||||
self.assertEqual(draft.group_order_id, self.group_order)
|
self.assertEqual(draft.group_order_id, self.group_order)
|
||||||
|
|
||||||
def test_save_draft_preserves_pickup_date(self):
|
def test_save_draft_preserves_pickup_date(self):
|
||||||
"""Test that pickup_date is preserved in draft."""
|
"""Test that pickup_date is preserved in draft."""
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(draft.pickup_date, self.group_order.pickup_date)
|
self.assertEqual(draft.pickup_date, self.group_order.pickup_date)
|
||||||
|
|
||||||
|
|
@ -203,83 +164,63 @@ class TestLoadDraftOrder(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
|
|
||||||
def test_load_existing_draft(self):
|
def test_load_existing_draft(self):
|
||||||
"""Test loading an existing draft order."""
|
"""Test loading an existing draft order."""
|
||||||
# Create draft
|
# Create draft
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [(0, 0, {
|
||||||
"order_line": [
|
'product_id': self.product.id,
|
||||||
(
|
'product_qty': 3,
|
||||||
0,
|
})],
|
||||||
0,
|
})
|
||||||
{
|
|
||||||
"product_id": self.product.id,
|
|
||||||
"product_qty": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load it
|
# Load it
|
||||||
loaded = self.env["sale.order"].search(
|
loaded = self.env['sale.order'].search([
|
||||||
[
|
('id', '=', draft.id),
|
||||||
("id", "=", draft.id),
|
('partner_id', '=', self.member_partner.id),
|
||||||
("partner_id", "=", self.member_partner.id),
|
('state', '=', 'draft'),
|
||||||
("state", "=", "draft"),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(len(loaded), 1)
|
self.assertEqual(len(loaded), 1)
|
||||||
self.assertEqual(loaded[0].order_line[0].product_qty, 3)
|
self.assertEqual(loaded[0].order_line[0].product_qty, 3)
|
||||||
|
|
@ -287,37 +228,29 @@ class TestLoadDraftOrder(TransactionCase):
|
||||||
def test_load_draft_not_visible_to_other_user(self):
|
def test_load_draft_not_visible_to_other_user(self):
|
||||||
"""Test that draft from one user not accessible to another."""
|
"""Test that draft from one user not accessible to another."""
|
||||||
# Create draft for member_partner
|
# Create draft for member_partner
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create another user/partner
|
# Create another user/partner
|
||||||
other_partner = self.env["res.partner"].create(
|
other_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Other Member',
|
||||||
"name": "Other Member",
|
'email': 'other@test.com',
|
||||||
"email": "other@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
other_user = self.env["res.users"].create(
|
other_user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Other User',
|
||||||
"name": "Other User",
|
'login': 'other@test.com',
|
||||||
"login": "other@test.com",
|
'partner_id': other_partner.id,
|
||||||
"partner_id": other_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Other user should not see original draft
|
# Other user should not see original draft
|
||||||
other_drafts = self.env["sale.order"].search(
|
other_drafts = self.env['sale.order'].search([
|
||||||
[
|
('id', '=', draft.id),
|
||||||
("id", "=", draft.id),
|
('partner_id', '=', other_partner.id),
|
||||||
("partner_id", "=", other_partner.id),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(len(other_drafts), 0)
|
self.assertEqual(len(other_drafts), 0)
|
||||||
|
|
||||||
|
|
@ -327,16 +260,14 @@ class TestLoadDraftOrder(TransactionCase):
|
||||||
self.group_order.action_close()
|
self.group_order.action_close()
|
||||||
|
|
||||||
# Create draft before closure (simulated)
|
# Create draft before closure (simulated)
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draft should still be loadable (but should warn)
|
# Draft should still be loadable (but should warn)
|
||||||
loaded = self.env["sale.order"].browse(draft.id)
|
loaded = self.env['sale.order'].browse(draft.id)
|
||||||
self.assertTrue(loaded.exists())
|
self.assertTrue(loaded.exists())
|
||||||
# Controller should check: group_order.state and warn if closed
|
# Controller should check: group_order.state and warn if closed
|
||||||
|
|
||||||
|
|
@ -346,51 +277,41 @@ class TestDraftConsistency(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 100.0,
|
||||||
"list_price": 100.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
|
|
||||||
def test_draft_price_snapshot(self):
|
def test_draft_price_snapshot(self):
|
||||||
|
|
@ -398,24 +319,16 @@ class TestDraftConsistency(TransactionCase):
|
||||||
original_price = self.product.list_price
|
original_price = self.product.list_price
|
||||||
|
|
||||||
# Save draft with current price
|
# Save draft with current price
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [(0, 0, {
|
||||||
"order_line": [
|
'product_id': self.product.id,
|
||||||
(
|
'product_qty': 1,
|
||||||
0,
|
'price_unit': original_price,
|
||||||
0,
|
})],
|
||||||
{
|
})
|
||||||
"product_id": self.product.id,
|
|
||||||
"product_qty": 1,
|
|
||||||
"price_unit": original_price,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
saved_price = draft.order_line[0].price_unit
|
saved_price = draft.order_line[0].price_unit
|
||||||
|
|
||||||
|
|
@ -429,26 +342,18 @@ class TestDraftConsistency(TransactionCase):
|
||||||
def test_draft_quantity_consistency(self):
|
def test_draft_quantity_consistency(self):
|
||||||
"""Test that quantities are preserved across saves."""
|
"""Test that quantities are preserved across saves."""
|
||||||
# Save draft
|
# Save draft
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [(0, 0, {
|
||||||
"order_line": [
|
'product_id': self.product.id,
|
||||||
(
|
'product_qty': 5,
|
||||||
0,
|
})],
|
||||||
0,
|
})
|
||||||
{
|
|
||||||
"product_id": self.product.id,
|
|
||||||
"product_qty": 5,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Re-load draft
|
# Re-load draft
|
||||||
reloaded = self.env["sale.order"].browse(draft.id)
|
reloaded = self.env['sale.order'].browse(draft.id)
|
||||||
self.assertEqual(reloaded.order_line[0].product_qty, 5)
|
self.assertEqual(reloaded.order_line[0].product_qty, 5)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -457,80 +362,62 @@ class TestProductArchivedInDraft(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'active': True,
|
||||||
"active": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
|
|
||||||
def test_load_draft_with_archived_product(self):
|
def test_load_draft_with_archived_product(self):
|
||||||
"""Test loading draft when product has been archived."""
|
"""Test loading draft when product has been archived."""
|
||||||
# Create draft with active product
|
# Create draft with active product
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
'order_line': [(0, 0, {
|
||||||
"order_line": [
|
'product_id': self.product.id,
|
||||||
(
|
'product_qty': 2,
|
||||||
0,
|
})],
|
||||||
0,
|
})
|
||||||
{
|
|
||||||
"product_id": self.product.id,
|
|
||||||
"product_qty": 2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Archive the product
|
# Archive the product
|
||||||
self.product.active = False
|
self.product.active = False
|
||||||
|
|
||||||
# Load draft - should still work (historical data)
|
# Load draft - should still work (historical data)
|
||||||
loaded = self.env["sale.order"].browse(draft.id)
|
loaded = self.env['sale.order'].browse(draft.id)
|
||||||
self.assertTrue(loaded.exists())
|
self.assertTrue(loaded.exists())
|
||||||
# But product may not be editable/accessible
|
# But product may not be editable/accessible
|
||||||
|
|
||||||
|
|
@ -540,128 +427,108 @@ class TestDraftTimeline(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_draft_from_current_week(self):
|
def test_draft_from_current_week(self):
|
||||||
"""Test draft from current/open group order."""
|
"""Test draft from current/open group order."""
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
current_order = self.env["group.order"].create(
|
current_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Current Order',
|
||||||
"name": "Current Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
current_order.action_open()
|
current_order.action_open()
|
||||||
|
|
||||||
draft = self.env["sale.order"].create(
|
draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': current_order.id,
|
||||||
"group_order_id": current_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should be accessible and valid
|
# Should be accessible and valid
|
||||||
self.assertTrue(draft.exists())
|
self.assertTrue(draft.exists())
|
||||||
self.assertEqual(draft.group_order_id.state, "open")
|
self.assertEqual(draft.group_order_id.state, 'open')
|
||||||
|
|
||||||
def test_draft_from_old_order_6_months_ago(self):
|
def test_draft_from_old_order_6_months_ago(self):
|
||||||
"""Test draft from order that was 6 months ago."""
|
"""Test draft from order that was 6 months ago."""
|
||||||
old_start = datetime.now().date() - timedelta(days=180)
|
old_start = datetime.now().date() - timedelta(days=180)
|
||||||
old_end = old_start + timedelta(days=7)
|
old_end = old_start + timedelta(days=7)
|
||||||
|
|
||||||
old_order = self.env["group.order"].create(
|
old_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Old Order',
|
||||||
"name": "Old Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': old_start,
|
||||||
"start_date": old_start,
|
'end_date': old_end,
|
||||||
"end_date": old_end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
old_order.action_open()
|
old_order.action_open()
|
||||||
old_order.action_close()
|
old_order.action_close()
|
||||||
|
|
||||||
old_draft = self.env["sale.order"].create(
|
old_draft = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': old_order.id,
|
||||||
"group_order_id": old_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should still exist but be inaccessible (order closed)
|
# Should still exist but be inaccessible (order closed)
|
||||||
self.assertTrue(old_draft.exists())
|
self.assertTrue(old_draft.exists())
|
||||||
self.assertEqual(old_order.state, "closed")
|
self.assertEqual(old_order.state, 'closed')
|
||||||
|
|
||||||
def test_draft_order_count_for_user(self):
|
def test_draft_order_count_for_user(self):
|
||||||
"""Test counting total drafts for a user."""
|
"""Test counting total drafts for a user."""
|
||||||
# Create multiple orders and drafts
|
# Create multiple orders and drafts
|
||||||
orders = []
|
orders = []
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
start = datetime.now().date() + timedelta(days=i * 7)
|
start = datetime.now().date() + timedelta(days=i*7)
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': f'Order {i}',
|
||||||
"name": f"Order {i}",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': start + timedelta(days=7),
|
||||||
"end_date": start + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
orders.append(order)
|
orders.append(order)
|
||||||
|
|
||||||
# Create draft for each
|
# Create draft for each
|
||||||
for order in orders:
|
for order in orders:
|
||||||
self.env["sale.order"].create(
|
self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': order.id,
|
||||||
"group_order_id": order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Count drafts for user
|
# Count drafts for user
|
||||||
user_drafts = self.env["sale.order"].search(
|
user_drafts = self.env['sale.order'].search([
|
||||||
[
|
('partner_id', '=', self.member_partner.id),
|
||||||
("partner_id", "=", self.member_partner.id),
|
('state', '=', 'draft'),
|
||||||
("state", "=", "draft"),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(len(user_drafts), 3)
|
self.assertEqual(len(user_drafts), 3)
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,11 @@ Coverage:
|
||||||
- Extreme dates (year 1900, year 2099)
|
- Extreme dates (year 1900, year 2099)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import date
|
from datetime import datetime, timedelta, date
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class TestLeapYearHandling(TransactionCase):
|
class TestLeapYearHandling(TransactionCase):
|
||||||
|
|
@ -27,12 +25,10 @@ class TestLeapYearHandling(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_order_spans_leap_day(self):
|
def test_order_spans_leap_day(self):
|
||||||
"""Test order that includes Feb 29 (leap year)."""
|
"""Test order that includes Feb 29 (leap year)."""
|
||||||
|
|
@ -40,18 +36,16 @@ class TestLeapYearHandling(TransactionCase):
|
||||||
start = date(2024, 2, 25)
|
start = date(2024, 2, 25)
|
||||||
end = date(2024, 3, 3) # Spans Feb 29
|
end = date(2024, 3, 3) # Spans Feb 29
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Leap Year Order',
|
||||||
"name": "Leap Year Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '2', # Wednesday (Feb 28 or 29 depending on week)
|
||||||
"pickup_day": "2", # Wednesday (Feb 28 or 29 depending on week)
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Should correctly calculate pickup date
|
# Should correctly calculate pickup date
|
||||||
|
|
@ -63,18 +57,16 @@ class TestLeapYearHandling(TransactionCase):
|
||||||
start = date(2024, 2, 26) # Monday
|
start = date(2024, 2, 26) # Monday
|
||||||
end = date(2024, 3, 3)
|
end = date(2024, 3, 3)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Feb 29 Pickup',
|
||||||
"name": "Feb 29 Pickup",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3', # Thursday = Feb 29
|
||||||
"pickup_day": "3", # Thursday = Feb 29
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(order.pickup_date, date(2024, 2, 29))
|
self.assertEqual(order.pickup_date, date(2024, 2, 29))
|
||||||
|
|
||||||
|
|
@ -84,18 +76,16 @@ class TestLeapYearHandling(TransactionCase):
|
||||||
start = date(2023, 2, 25)
|
start = date(2023, 2, 25)
|
||||||
end = date(2023, 3, 3)
|
end = date(2023, 3, 3)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Non-Leap Year Order',
|
||||||
"name": "Non-Leap Year Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '2',
|
||||||
"pickup_day": "2",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Pickup should be Feb 28 (last day of Feb)
|
# Pickup should be Feb 28 (last day of Feb)
|
||||||
|
|
@ -107,30 +97,26 @@ class TestLongDurationOrders(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_order_spans_entire_year(self):
|
def test_order_spans_entire_year(self):
|
||||||
"""Test order running for 365 days."""
|
"""Test order running for 365 days."""
|
||||||
start = date(2024, 1, 1)
|
start = date(2024, 1, 1)
|
||||||
end = date(2024, 12, 31)
|
end = date(2024, 12, 31)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Year-Long Order',
|
||||||
"name": "Year-Long Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3', # Same day each week
|
||||||
"pickup_day": "3", # Same day each week
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Should handle 52+ weeks correctly
|
# Should handle 52+ weeks correctly
|
||||||
|
|
@ -142,18 +128,16 @@ class TestLongDurationOrders(TransactionCase):
|
||||||
start = date(2024, 1, 1)
|
start = date(2024, 1, 1)
|
||||||
end = date(2026, 12, 31) # 3 years
|
end = date(2026, 12, 31) # 3 years
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Multi-Year Order',
|
||||||
"name": "Multi-Year Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'monthly',
|
||||||
"period": "monthly",
|
'pickup_day': '15',
|
||||||
"pickup_day": "15",
|
'cutoff_day': '10',
|
||||||
"cutoff_day": "10",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
days_diff = (end - start).days
|
days_diff = (end - start).days
|
||||||
|
|
@ -163,18 +147,16 @@ class TestLongDurationOrders(TransactionCase):
|
||||||
"""Test order with start_date == end_date (single day)."""
|
"""Test order with start_date == end_date (single day)."""
|
||||||
same_day = date(2024, 2, 15)
|
same_day = date(2024, 2, 15)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'One-Day Order',
|
||||||
"name": "One-Day Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'once',
|
||||||
"type": "once",
|
'start_date': same_day,
|
||||||
"start_date": same_day,
|
'end_date': same_day,
|
||||||
"end_date": same_day,
|
'period': 'once',
|
||||||
"period": "once",
|
'pickup_day': '0',
|
||||||
"pickup_day": "0",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
|
|
@ -184,12 +166,10 @@ class TestPickupDayBoundary(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pickup_day_same_as_start_date(self):
|
def test_pickup_day_same_as_start_date(self):
|
||||||
"""Test when pickup_day equals start date (today)."""
|
"""Test when pickup_day equals start date (today)."""
|
||||||
|
|
@ -197,18 +177,16 @@ class TestPickupDayBoundary(TransactionCase):
|
||||||
start = today
|
start = today
|
||||||
end = today + timedelta(days=7)
|
end = today + timedelta(days=7)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Today Pickup',
|
||||||
"name": "Today Pickup",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': str(start.weekday()), # Same as start
|
||||||
"pickup_day": str(start.weekday()), # Same as start
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Pickup should be today
|
# Pickup should be today
|
||||||
|
|
@ -220,18 +198,16 @@ class TestPickupDayBoundary(TransactionCase):
|
||||||
start = date(2024, 1, 24)
|
start = date(2024, 1, 24)
|
||||||
end = date(2024, 2, 1)
|
end = date(2024, 2, 1)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Month-End Pickup',
|
||||||
"name": "Month-End Pickup",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'once',
|
||||||
"period": "once",
|
'pickup_day': '2', # Wednesday = Jan 31
|
||||||
"pickup_day": "2", # Wednesday = Jan 31
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
|
|
@ -241,18 +217,16 @@ class TestPickupDayBoundary(TransactionCase):
|
||||||
start = date(2024, 1, 28)
|
start = date(2024, 1, 28)
|
||||||
end = date(2024, 2, 5)
|
end = date(2024, 2, 5)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Month Boundary Pickup',
|
||||||
"name": "Month Boundary Pickup",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '4', # Friday (Feb 2)
|
||||||
"pickup_day": "4", # Friday (Feb 2)
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Pickup should be in Feb
|
# Pickup should be in Feb
|
||||||
|
|
@ -264,18 +238,16 @@ class TestPickupDayBoundary(TransactionCase):
|
||||||
end = date(2024, 1, 8)
|
end = date(2024, 1, 8)
|
||||||
|
|
||||||
for day_num in range(7):
|
for day_num in range(7):
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': f'Pickup Day {day_num}',
|
||||||
"name": f"Pickup Day {day_num}",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': str(day_num),
|
||||||
"pickup_day": str(day_num),
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Each should have valid pickup_date
|
# Each should have valid pickup_date
|
||||||
|
|
@ -287,12 +259,10 @@ class TestFutureStartDateOrders(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_order_starts_tomorrow(self):
|
def test_order_starts_tomorrow(self):
|
||||||
"""Test order starting tomorrow."""
|
"""Test order starting tomorrow."""
|
||||||
|
|
@ -300,18 +270,16 @@ class TestFutureStartDateOrders(TransactionCase):
|
||||||
start = today + timedelta(days=1)
|
start = today + timedelta(days=1)
|
||||||
end = start + timedelta(days=7)
|
end = start + timedelta(days=7)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Future Order',
|
||||||
"name": "Future Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
self.assertGreater(order.start_date, today)
|
self.assertGreater(order.start_date, today)
|
||||||
|
|
@ -322,18 +290,16 @@ class TestFutureStartDateOrders(TransactionCase):
|
||||||
start = today + relativedelta(months=6)
|
start = today + relativedelta(months=6)
|
||||||
end = start + timedelta(days=30)
|
end = start + timedelta(days=30)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Far Future Order',
|
||||||
"name": "Far Future Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'monthly',
|
||||||
"period": "monthly",
|
'pickup_day': '15',
|
||||||
"pickup_day": "15",
|
'cutoff_day': '10',
|
||||||
"cutoff_day": "10",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
|
|
@ -343,30 +309,26 @@ class TestExtremeDate(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_order_year_2000(self):
|
def test_order_year_2000(self):
|
||||||
"""Test order in year 2000 (Y2K edge case)."""
|
"""Test order in year 2000 (Y2K edge case)."""
|
||||||
start = date(2000, 1, 1)
|
start = date(2000, 1, 1)
|
||||||
end = date(2000, 12, 31)
|
end = date(2000, 12, 31)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Y2K Order',
|
||||||
"name": "Y2K Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
|
|
@ -375,18 +337,16 @@ class TestExtremeDate(TransactionCase):
|
||||||
start = date(2099, 1, 1)
|
start = date(2099, 1, 1)
|
||||||
end = date(2099, 12, 31)
|
end = date(2099, 12, 31)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Far Future Order',
|
||||||
"name": "Far Future Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
|
|
@ -395,18 +355,16 @@ class TestExtremeDate(TransactionCase):
|
||||||
start = date(1999, 12, 26)
|
start = date(1999, 12, 26)
|
||||||
end = date(2000, 1, 2)
|
end = date(2000, 1, 2)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Century Order',
|
||||||
"name": "Century Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '6', # Saturday
|
||||||
"pickup_day": "6", # Saturday
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# Should handle date arithmetic correctly across years
|
# Should handle date arithmetic correctly across years
|
||||||
|
|
@ -419,29 +377,25 @@ class TestOrderWithoutEndDate(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_permanent_order_with_null_end_date(self):
|
def test_permanent_order_with_null_end_date(self):
|
||||||
"""Test order with end_date = NULL (ongoing order)."""
|
"""Test order with end_date = NULL (ongoing order)."""
|
||||||
start = date.today()
|
start = date.today()
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Permanent Order',
|
||||||
"name": "Permanent Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': False, # No end date
|
||||||
"end_date": False, # No end date
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# If supported, should handle gracefully
|
# If supported, should handle gracefully
|
||||||
# Otherwise, may be optional validation
|
# Otherwise, may be optional validation
|
||||||
|
|
@ -452,12 +406,10 @@ class TestPickupCalculationAccuracy(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pickup_date_calculation_multiple_weeks(self):
|
def test_pickup_date_calculation_multiple_weeks(self):
|
||||||
"""Test pickup_date calculation over multiple weeks."""
|
"""Test pickup_date calculation over multiple weeks."""
|
||||||
|
|
@ -465,18 +417,16 @@ class TestPickupCalculationAccuracy(TransactionCase):
|
||||||
start = date(2024, 1, 1)
|
start = date(2024, 1, 1)
|
||||||
end = date(2024, 1, 22)
|
end = date(2024, 1, 22)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Multi-Week Pickup',
|
||||||
"name": "Multi-Week Pickup",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3', # Thursday
|
||||||
"pickup_day": "3", # Thursday
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# First pickup should be first Thursday on or after start
|
# First pickup should be first Thursday on or after start
|
||||||
|
|
@ -488,18 +438,16 @@ class TestPickupCalculationAccuracy(TransactionCase):
|
||||||
start = date(2024, 2, 1)
|
start = date(2024, 2, 1)
|
||||||
end = date(2024, 3, 31)
|
end = date(2024, 3, 31)
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Monthly Order',
|
||||||
"name": "Monthly Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start,
|
||||||
"start_date": start,
|
'end_date': end,
|
||||||
"end_date": end,
|
'period': 'monthly',
|
||||||
"period": "monthly",
|
'pickup_day': '15',
|
||||||
"pickup_day": "15",
|
'cutoff_day': '10',
|
||||||
"cutoff_day": "10",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
# First pickup should be Feb 15
|
# First pickup should be Feb 15
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,11 @@ Coverage:
|
||||||
- /eskaera/labels (GET) - Get translated labels
|
- /eskaera/labels (GET) - Get translated labels
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
import json
|
||||||
|
|
||||||
from odoo.exceptions import AccessError
|
from odoo.tests.common import TransactionCase, HttpCase
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError, AccessError
|
||||||
from odoo.tests.common import HttpCase
|
|
||||||
from odoo.tests.common import TransactionCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestEskaearaListEndpoint(TransactionCase):
|
class TestEskaearaListEndpoint(TransactionCase):
|
||||||
|
|
@ -30,75 +28,63 @@ class TestEskaearaListEndpoint(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create multiple group orders (some open, some closed)
|
# Create multiple group orders (some open, some closed)
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
|
|
||||||
self.open_order = self.env["group.order"].create(
|
self.open_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Open Order',
|
||||||
"name": "Open Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.open_order.action_open()
|
self.open_order.action_open()
|
||||||
|
|
||||||
self.draft_order = self.env["group.order"].create(
|
self.draft_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Draft Order',
|
||||||
"name": "Draft Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date - timedelta(days=14),
|
||||||
"start_date": start_date - timedelta(days=14),
|
'end_date': start_date - timedelta(days=7),
|
||||||
"end_date": start_date - timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
# Stay in draft
|
# Stay in draft
|
||||||
|
|
||||||
self.closed_order = self.env["group.order"].create(
|
self.closed_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Closed Order',
|
||||||
"name": "Closed Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date - timedelta(days=21),
|
||||||
"start_date": start_date - timedelta(days=21),
|
'end_date': start_date - timedelta(days=14),
|
||||||
"end_date": start_date - timedelta(days=14),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.closed_order.action_open()
|
self.closed_order.action_open()
|
||||||
self.closed_order.action_close()
|
self.closed_order.action_close()
|
||||||
|
|
||||||
|
|
@ -106,12 +92,10 @@ class TestEskaearaListEndpoint(TransactionCase):
|
||||||
"""Test that /eskaera shows only open/draft orders, not closed."""
|
"""Test that /eskaera shows only open/draft orders, not closed."""
|
||||||
# In controller context, only open and draft should be visible to members
|
# In controller context, only open and draft should be visible to members
|
||||||
# This is business logic: closed orders are historical
|
# This is business logic: closed orders are historical
|
||||||
visible_orders = self.env["group.order"].search(
|
visible_orders = self.env['group.order'].search([
|
||||||
[
|
('state', 'in', ['open', 'draft']),
|
||||||
("state", "in", ["open", "draft"]),
|
('group_ids', 'in', self.group.id),
|
||||||
("group_ids", "in", self.group.id),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIn(self.open_order, visible_orders)
|
self.assertIn(self.open_order, visible_orders)
|
||||||
self.assertIn(self.draft_order, visible_orders)
|
self.assertIn(self.draft_order, visible_orders)
|
||||||
|
|
@ -119,36 +103,30 @@ class TestEskaearaListEndpoint(TransactionCase):
|
||||||
|
|
||||||
def test_eskaera_list_filters_by_user_groups(self):
|
def test_eskaera_list_filters_by_user_groups(self):
|
||||||
"""Test that user only sees orders from their groups."""
|
"""Test that user only sees orders from their groups."""
|
||||||
other_group = self.env["res.partner"].create(
|
other_group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Other Group',
|
||||||
"name": "Other Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'other@test.com',
|
||||||
"email": "other@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
other_order = self.env["group.order"].create(
|
other_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Other Group Order',
|
||||||
"name": "Other Group Order",
|
'group_ids': [(6, 0, [other_group.id])],
|
||||||
"group_ids": [(6, 0, [other_group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
other_order.action_open()
|
other_order.action_open()
|
||||||
|
|
||||||
# User should not see orders from groups they're not in
|
# User should not see orders from groups they're not in
|
||||||
user_groups = self.member_partner.group_ids
|
user_groups = self.member_partner.group_ids
|
||||||
visible_orders = self.env["group.order"].search(
|
visible_orders = self.env['group.order'].search([
|
||||||
[
|
('state', 'in', ['open', 'draft']),
|
||||||
("state", "in", ["open", "draft"]),
|
('group_ids', 'in', user_groups.ids),
|
||||||
("group_ids", "in", user_groups.ids),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertNotIn(other_order, visible_orders)
|
self.assertNotIn(other_order, visible_orders)
|
||||||
|
|
||||||
|
|
@ -158,75 +136,61 @@ class TestAddToCartEndpoint(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Published product
|
# Published product
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
'is_published': True,
|
||||||
"is_published": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unpublished product (should not be available)
|
# Unpublished product (should not be available)
|
||||||
self.unpublished_product = self.env["product.product"].create(
|
self.unpublished_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Unpublished Product',
|
||||||
"name": "Unpublished Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 15.0,
|
||||||
"list_price": 15.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'sale_ok': False,
|
||||||
"sale_ok": False,
|
'is_published': False,
|
||||||
"is_published": False,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
self.group_order.product_ids = [(4, self.product.id)]
|
self.group_order.product_ids = [(4, self.product.id)]
|
||||||
|
|
||||||
|
|
@ -234,13 +198,13 @@ class TestAddToCartEndpoint(TransactionCase):
|
||||||
"""Test adding published product to cart."""
|
"""Test adding published product to cart."""
|
||||||
# Simulate controller logic
|
# Simulate controller logic
|
||||||
cart_line = {
|
cart_line = {
|
||||||
"product_id": self.product.id,
|
'product_id': self.product.id,
|
||||||
"quantity": 2,
|
'quantity': 2,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
}
|
}
|
||||||
# Should succeed
|
# Should succeed
|
||||||
self.assertTrue(cart_line["product_id"])
|
self.assertTrue(cart_line['product_id'])
|
||||||
|
|
||||||
def test_add_to_cart_zero_quantity(self):
|
def test_add_to_cart_zero_quantity(self):
|
||||||
"""Test that adding zero quantity is rejected."""
|
"""Test that adding zero quantity is rejected."""
|
||||||
|
|
@ -264,13 +228,11 @@ class TestAddToCartEndpoint(TransactionCase):
|
||||||
def test_add_to_cart_product_not_in_order(self):
|
def test_add_to_cart_product_not_in_order(self):
|
||||||
"""Test that products not in the order cannot be added."""
|
"""Test that products not in the order cannot be added."""
|
||||||
# Create a product NOT associated with group_order
|
# Create a product NOT associated with group_order
|
||||||
other_product = self.env["product.product"].create(
|
other_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Other Product',
|
||||||
"name": "Other Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 25.0,
|
||||||
"list_price": 25.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Controller should check: product in group_order.product_ids
|
# Controller should check: product in group_order.product_ids
|
||||||
self.assertNotIn(other_product, self.group_order.product_ids)
|
self.assertNotIn(other_product, self.group_order.product_ids)
|
||||||
|
|
@ -279,7 +241,7 @@ class TestAddToCartEndpoint(TransactionCase):
|
||||||
"""Test that adding to closed order is rejected."""
|
"""Test that adding to closed order is rejected."""
|
||||||
self.group_order.action_close()
|
self.group_order.action_close()
|
||||||
# Controller should check: order.state == 'open'
|
# Controller should check: order.state == 'open'
|
||||||
self.assertEqual(self.group_order.state, "closed")
|
self.assertEqual(self.group_order.state, 'closed')
|
||||||
|
|
||||||
|
|
||||||
class TestCheckoutEndpoint(TransactionCase):
|
class TestCheckoutEndpoint(TransactionCase):
|
||||||
|
|
@ -287,46 +249,38 @@ class TestCheckoutEndpoint(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'pickup_date': start_date + timedelta(days=3),
|
||||||
"pickup_date": start_date + timedelta(days=3),
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
|
|
||||||
def test_checkout_page_loads(self):
|
def test_checkout_page_loads(self):
|
||||||
|
|
@ -347,18 +301,16 @@ class TestCheckoutEndpoint(TransactionCase):
|
||||||
def test_checkout_order_without_products(self):
|
def test_checkout_order_without_products(self):
|
||||||
"""Test checkout when no products available."""
|
"""Test checkout when no products available."""
|
||||||
# Order with empty product_ids
|
# Order with empty product_ids
|
||||||
empty_order = self.env["group.order"].create(
|
empty_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Empty Order',
|
||||||
"name": "Empty Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
empty_order.action_open()
|
empty_order.action_open()
|
||||||
|
|
||||||
# Should handle gracefully
|
# Should handle gracefully
|
||||||
|
|
@ -370,115 +322,95 @@ class TestConfirmOrderEndpoint(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'pickup_date': start_date + timedelta(days=3),
|
||||||
"pickup_date": start_date + timedelta(days=3),
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
self.group_order.product_ids = [(4, self.product.id)]
|
self.group_order.product_ids = [(4, self.product.id)]
|
||||||
|
|
||||||
# Create a draft sale order
|
# Create a draft sale order
|
||||||
self.draft_sale = self.env["sale.order"].create(
|
self.draft_sale = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_confirm_order_creates_sale_order(self):
|
def test_confirm_order_creates_sale_order(self):
|
||||||
"""Test that confirming creates a confirmed sale.order."""
|
"""Test that confirming creates a confirmed sale.order."""
|
||||||
# Controller should change state from draft to sale
|
# Controller should change state from draft to sale
|
||||||
self.draft_sale.action_confirm()
|
self.draft_sale.action_confirm()
|
||||||
self.assertEqual(self.draft_sale.state, "sale")
|
self.assertEqual(self.draft_sale.state, 'sale')
|
||||||
|
|
||||||
def test_confirm_empty_order(self):
|
def test_confirm_empty_order(self):
|
||||||
"""Test confirming order without items fails."""
|
"""Test confirming order without items fails."""
|
||||||
# Order with no order_lines should fail
|
# Order with no order_lines should fail
|
||||||
empty_sale = self.env["sale.order"].create(
|
empty_sale = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should validate: must have at least one line
|
# Should validate: must have at least one line
|
||||||
self.assertEqual(len(empty_sale.order_line), 0)
|
self.assertEqual(len(empty_sale.order_line), 0)
|
||||||
|
|
||||||
def test_confirm_order_wrong_group(self):
|
def test_confirm_order_wrong_group(self):
|
||||||
"""Test that user cannot confirm order from different group."""
|
"""Test that user cannot confirm order from different group."""
|
||||||
other_group = self.env["res.partner"].create(
|
other_group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Other Group',
|
||||||
"name": "Other Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
other_order = self.env["group.order"].create(
|
other_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Other Order',
|
||||||
"name": "Other Order",
|
'group_ids': [(6, 0, [other_group.id])],
|
||||||
"group_ids": [(6, 0, [other_group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# User should not be in other_group
|
# User should not be in other_group
|
||||||
self.assertNotIn(self.member_partner, other_group.member_ids)
|
self.assertNotIn(self.member_partner, other_group.member_ids)
|
||||||
|
|
@ -489,94 +421,76 @@ class TestLoadDraftEndpoint(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member',
|
||||||
"name": "Group Member",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'pickup_date': start_date + timedelta(days=3),
|
||||||
"pickup_date": start_date + timedelta(days=3),
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
self.group_order.product_ids = [(4, self.product.id)]
|
self.group_order.product_ids = [(4, self.product.id)]
|
||||||
|
|
||||||
def test_load_draft_from_history(self):
|
def test_load_draft_from_history(self):
|
||||||
"""Test loading a previous draft order."""
|
"""Test loading a previous draft order."""
|
||||||
# Create old draft sale
|
# Create old draft sale
|
||||||
old_sale = self.env["sale.order"].create(
|
old_sale = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should be able to load
|
# Should be able to load
|
||||||
self.assertTrue(old_sale.exists())
|
self.assertTrue(old_sale.exists())
|
||||||
|
|
||||||
def test_load_draft_not_owned_by_user(self):
|
def test_load_draft_not_owned_by_user(self):
|
||||||
"""Test that user cannot load draft from other user."""
|
"""Test that user cannot load draft from other user."""
|
||||||
other_partner = self.env["res.partner"].create(
|
other_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Other Member',
|
||||||
"name": "Other Member",
|
'email': 'other@test.com',
|
||||||
"email": "other@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
other_sale = self.env["sale.order"].create(
|
other_sale = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': other_partner.id,
|
||||||
"partner_id": other_partner.id,
|
'group_order_id': self.group_order.id,
|
||||||
"group_order_id": self.group_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# User should not be able to load other's draft
|
# User should not be able to load other's draft
|
||||||
self.assertNotEqual(other_sale.partner_id, self.member_partner)
|
self.assertNotEqual(other_sale.partner_id, self.member_partner)
|
||||||
|
|
@ -586,28 +500,24 @@ class TestLoadDraftEndpoint(TransactionCase):
|
||||||
old_start = datetime.now().date() - timedelta(days=30)
|
old_start = datetime.now().date() - timedelta(days=30)
|
||||||
old_end = datetime.now().date() - timedelta(days=23)
|
old_end = datetime.now().date() - timedelta(days=23)
|
||||||
|
|
||||||
expired_order = self.env["group.order"].create(
|
expired_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Expired Order',
|
||||||
"name": "Expired Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': old_start,
|
||||||
"start_date": old_start,
|
'end_date': old_end,
|
||||||
"end_date": old_end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
expired_order.action_open()
|
expired_order.action_open()
|
||||||
expired_order.action_close()
|
expired_order.action_close()
|
||||||
|
|
||||||
old_sale = self.env["sale.order"].create(
|
old_sale = self.env['sale.order'].create({
|
||||||
{
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
'group_order_id': expired_order.id,
|
||||||
"group_order_id": expired_order.id,
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should warn: order expired
|
# Should warn: order expired
|
||||||
self.assertEqual(expired_order.state, "closed")
|
self.assertEqual(expired_order.state, 'closed')
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,127 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
class TestEskaerShop(TransactionCase):
|
class TestEskaerShop(TransactionCase):
|
||||||
"""Test suite para la lógica de eskaera_shop (descubrimiento de productos)."""
|
'''Test suite para la lógica de eskaera_shop (descubrimiento de productos).'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Crear un grupo (res.partner)
|
# Crear un grupo (res.partner)
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo Test Eskaera',
|
||||||
"name": "Grupo Test Eskaera",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo@test.com',
|
||||||
"email": "grupo@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear usuario miembro del grupo
|
# Crear usuario miembro del grupo
|
||||||
user_partner = self.env["res.partner"].create(
|
user_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Usuario Test Partner',
|
||||||
"name": "Usuario Test Partner",
|
'email': 'usuario_test@test.com',
|
||||||
"email": "usuario_test@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Usuario Test',
|
||||||
"name": "Usuario Test",
|
'login': 'usuario_test@test.com',
|
||||||
"login": "usuario_test@test.com",
|
'email': 'usuario_test@test.com',
|
||||||
"email": "usuario_test@test.com",
|
'partner_id': user_partner.id,
|
||||||
"partner_id": user_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Añadir el partner del usuario como miembro del grupo
|
# Añadir el partner del usuario como miembro del grupo
|
||||||
self.group.member_ids = [(4, user_partner.id)]
|
self.group.member_ids = [(4, user_partner.id)]
|
||||||
|
|
||||||
# Crear categorías de producto
|
# Crear categorías de producto
|
||||||
self.category1 = self.env["product.category"].create(
|
self.category1 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Categoría Test 1',
|
||||||
"name": "Categoría Test 1",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category2 = self.env["product.category"].create(
|
self.category2 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Categoría Test 2',
|
||||||
"name": "Categoría Test 2",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear proveedor
|
# Crear proveedor
|
||||||
self.supplier = self.env["res.partner"].create(
|
self.supplier = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Proveedor Test',
|
||||||
"name": "Proveedor Test",
|
'is_company': True,
|
||||||
"is_company": True,
|
'supplier_rank': 1,
|
||||||
"supplier_rank": 1,
|
'email': 'proveedor@test.com',
|
||||||
"email": "proveedor@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear productos
|
# Crear productos
|
||||||
self.product_cat1 = self.env["product.product"].create(
|
self.product_cat1 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Categoría 1',
|
||||||
"name": "Producto Categoría 1",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.category1.id,
|
||||||
"categ_id": self.category1.id,
|
'active': True,
|
||||||
"active": True,
|
})
|
||||||
}
|
self.product_cat1.product_tmpl_id.write({
|
||||||
)
|
'is_published': True,
|
||||||
self.product_cat1.product_tmpl_id.write(
|
'sale_ok': True,
|
||||||
{
|
})
|
||||||
"is_published": True,
|
|
||||||
"sale_ok": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product_cat2 = self.env["product.product"].create(
|
self.product_cat2 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Categoría 2',
|
||||||
"name": "Producto Categoría 2",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
'categ_id': self.category2.id,
|
||||||
"categ_id": self.category2.id,
|
'active': True,
|
||||||
"active": True,
|
})
|
||||||
}
|
self.product_cat2.product_tmpl_id.write({
|
||||||
)
|
'is_published': True,
|
||||||
self.product_cat2.product_tmpl_id.write(
|
'sale_ok': True,
|
||||||
{
|
})
|
||||||
"is_published": True,
|
|
||||||
"sale_ok": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear producto con relación a proveedor
|
# Crear producto con relación a proveedor
|
||||||
self.product_supplier_template = self.env["product.template"].create(
|
self.product_supplier_template = self.env['product.template'].create({
|
||||||
{
|
'name': 'Producto Proveedor',
|
||||||
"name": "Producto Proveedor",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 30.0,
|
||||||
"list_price": 30.0,
|
'categ_id': self.category1.id,
|
||||||
"categ_id": self.category1.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product_supplier = self.product_supplier_template.product_variant_ids[0]
|
self.product_supplier = self.product_supplier_template.product_variant_ids[0]
|
||||||
self.product_supplier.active = True
|
self.product_supplier.active = True
|
||||||
|
|
||||||
# Crear relación con proveedor
|
# Crear relación con proveedor
|
||||||
self.env["product.supplierinfo"].create(
|
self.env['product.supplierinfo'].create({
|
||||||
{
|
'product_tmpl_id': self.product_supplier_template.id,
|
||||||
"product_tmpl_id": self.product_supplier_template.id,
|
'partner_id': self.supplier.id,
|
||||||
"partner_id": self.supplier.id,
|
'min_qty': 1.0,
|
||||||
"min_qty": 1.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product_direct = self.env["product.product"].create(
|
self.product_direct = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Directo',
|
||||||
"name": "Producto Directo",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 40.0,
|
||||||
"list_price": 40.0,
|
'categ_id': self.category1.id,
|
||||||
"categ_id": self.category1.id,
|
'active': True,
|
||||||
"active": True,
|
})
|
||||||
}
|
self.product_direct.product_tmpl_id.write({
|
||||||
)
|
'is_published': True,
|
||||||
self.product_direct.product_tmpl_id.write(
|
'sale_ok': True,
|
||||||
{
|
})
|
||||||
"is_published": True,
|
|
||||||
"sale_ok": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_discovery_direct(self):
|
def test_product_discovery_direct(self):
|
||||||
"""Test que los productos directos se descubren correctamente."""
|
'''Test que los productos directos se descubren correctamente.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Directo',
|
||||||
"name": "Pedido Directo",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'product_ids': [(6, 0, [self.product_direct.id])],
|
||||||
"product_ids": [(6, 0, [self.product_direct.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
|
|
||||||
|
|
@ -162,124 +131,96 @@ class TestEskaerShop(TransactionCase):
|
||||||
self.assertEqual(len(products), 1)
|
self.assertEqual(len(products), 1)
|
||||||
self.assertIn(self.product_direct, products)
|
self.assertIn(self.product_direct, products)
|
||||||
|
|
||||||
products = self.env["product.product"]._get_products_for_group_order(order.id)
|
products = self.env['product.product']._get_products_for_group_order(order.id)
|
||||||
self.assertIn(
|
self.assertIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_discovery_by_category(self):
|
def test_product_discovery_by_category(self):
|
||||||
"""Test que los productos se descubren por categoría cuando no hay directos."""
|
'''Test que los productos se descubren por categoría cuando no hay directos.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido por Categoría',
|
||||||
"name": "Pedido por Categoría",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'category_ids': [(6, 0, [self.category1.id])],
|
||||||
"category_ids": [(6, 0, [self.category1.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
|
|
||||||
# Simular lo que hace eskaera_shop (fallback a categorías)
|
# Simular lo que hace eskaera_shop (fallback a categorías)
|
||||||
products = order.product_ids
|
products = order.product_ids
|
||||||
if not products:
|
if not products:
|
||||||
products = self.env["product.product"].search(
|
products = self.env['product.product'].search([
|
||||||
[
|
('categ_id', 'in', order.category_ids.ids),
|
||||||
("categ_id", "in", order.category_ids.ids),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Debe incluir todos los productos de la categoría 1
|
# Debe incluir todos los productos de la categoría 1
|
||||||
self.assertGreaterEqual(len(products), 2)
|
self.assertGreaterEqual(len(products), 2)
|
||||||
self.assertIn(
|
self.assertIn(self.product_cat1.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_cat1.product_tmpl_id, products.mapped("product_tmpl_id")
|
self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
)
|
self.assertIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.assertIn(
|
|
||||||
self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
order.write({"category_ids": [(4, self.category1.id)]})
|
order.write({'category_ids': [(4, self.category1.id)]})
|
||||||
products = self.env["product.product"]._get_products_for_group_order(order.id)
|
products = self.env['product.product']._get_products_for_group_order(order.id)
|
||||||
self.assertIn(
|
self.assertIn(self.product_cat1.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_cat1.product_tmpl_id, products.mapped("product_tmpl_id")
|
self.assertNotIn(self.product_cat2.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
self.product_cat2.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_discovery_by_supplier(self):
|
def test_product_discovery_by_supplier(self):
|
||||||
"""Test que los productos se descubren por proveedor cuando no hay directos ni categorías."""
|
'''Test que los productos se descubren por proveedor cuando no hay directos ni categorías.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido por Proveedor',
|
||||||
"name": "Pedido por Proveedor",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'supplier_ids': [(6, 0, [self.supplier.id])],
|
||||||
"supplier_ids": [(6, 0, [self.supplier.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
|
|
||||||
# Simular lo que hace eskaera_shop (fallback a proveedores)
|
# Simular lo que hace eskaera_shop (fallback a proveedores)
|
||||||
products = order.product_ids
|
products = order.product_ids
|
||||||
if not products and order.category_ids:
|
if not products and order.category_ids:
|
||||||
products = self.env["product.product"].search(
|
products = self.env['product.product'].search([
|
||||||
[
|
('categ_id', 'in', order.category_ids.ids),
|
||||||
("categ_id", "in", order.category_ids.ids),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not products and order.supplier_ids:
|
if not products and order.supplier_ids:
|
||||||
# Buscar productos que tienen estos proveedores en seller_ids
|
# Buscar productos que tienen estos proveedores en seller_ids
|
||||||
product_templates = self.env["product.template"].search(
|
product_templates = self.env['product.template'].search([
|
||||||
[
|
('seller_ids.partner_id', 'in', order.supplier_ids.ids),
|
||||||
("seller_ids.partner_id", "in", order.supplier_ids.ids),
|
])
|
||||||
]
|
products = product_templates.mapped('product_variant_ids')
|
||||||
)
|
|
||||||
products = product_templates.mapped("product_variant_ids")
|
|
||||||
|
|
||||||
# Debe incluir el producto del proveedor
|
# Debe incluir el producto del proveedor
|
||||||
self.assertEqual(len(products), 1)
|
self.assertEqual(len(products), 1)
|
||||||
self.assertIn(
|
self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
order.write({"supplier_ids": [(4, self.supplier.id)]})
|
order.write({'supplier_ids': [(4, self.supplier.id)]})
|
||||||
products = self.env["product.product"]._get_products_for_group_order(order.id)
|
products = self.env['product.product']._get_products_for_group_order(order.id)
|
||||||
self.assertIn(
|
self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_discovery_priority(self):
|
def test_product_discovery_priority(self):
|
||||||
"""Test que la prioridad de descubrimiento es: directos > categorías > proveedores."""
|
'''Test que la prioridad de descubrimiento es: directos > categorías > proveedores.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido con Todos',
|
||||||
"name": "Pedido con Todos",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'product_ids': [(6, 0, [self.product_direct.id])],
|
||||||
"product_ids": [(6, 0, [self.product_direct.id])],
|
'category_ids': [(6, 0, [self.category1.id, self.category2.id])],
|
||||||
"category_ids": [(6, 0, [self.category1.id, self.category2.id])],
|
'supplier_ids': [(6, 0, [self.supplier.id])],
|
||||||
"supplier_ids": [(6, 0, [self.supplier.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
|
|
||||||
|
|
@ -288,122 +229,94 @@ class TestEskaerShop(TransactionCase):
|
||||||
|
|
||||||
# Debe retornar los productos directos, no los de categoría/proveedor
|
# Debe retornar los productos directos, no los de categoría/proveedor
|
||||||
self.assertEqual(len(products), 1)
|
self.assertEqual(len(products), 1)
|
||||||
self.assertIn(
|
self.assertIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id")
|
self.assertNotIn(self.product_cat1.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
)
|
self.assertNotIn(self.product_cat2.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.assertNotIn(
|
self.assertNotIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_cat1.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
self.product_cat2.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. The canonical helper now returns the UNION of all association
|
# 2. The canonical helper now returns the UNION of all association
|
||||||
# sources (direct products, categories, suppliers). Assert all are
|
# sources (direct products, categories, suppliers). Assert all are
|
||||||
# present to reflect the new behaviour.
|
# present to reflect the new behaviour.
|
||||||
products = self.env["product.product"]._get_products_for_group_order(order.id)
|
products = self.env['product.product']._get_products_for_group_order(order.id)
|
||||||
tmpl_ids = products.mapped("product_tmpl_id")
|
tmpl_ids = products.mapped('product_tmpl_id')
|
||||||
self.assertIn(self.product_direct.product_tmpl_id, tmpl_ids)
|
self.assertIn(self.product_direct.product_tmpl_id, tmpl_ids)
|
||||||
self.assertIn(self.product_cat1.product_tmpl_id, tmpl_ids)
|
self.assertIn(self.product_cat1.product_tmpl_id, tmpl_ids)
|
||||||
self.assertIn(self.product_supplier.product_tmpl_id, tmpl_ids)
|
self.assertIn(self.product_supplier.product_tmpl_id, tmpl_ids)
|
||||||
|
|
||||||
def test_product_discovery_fallback_from_category_to_supplier(self):
|
def test_product_discovery_fallback_from_category_to_supplier(self):
|
||||||
"""Test que si no hay directos ni categorías, usa proveedores."""
|
'''Test que si no hay directos ni categorías, usa proveedores.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Fallback',
|
||||||
"name": "Pedido Fallback",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
|
||||||
# Sin product_ids
|
# Sin product_ids
|
||||||
# Sin category_ids
|
# Sin category_ids
|
||||||
"supplier_ids": [(6, 0, [self.supplier.id])],
|
'supplier_ids': [(6, 0, [self.supplier.id])],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
|
|
||||||
# Simular lo que hace eskaera_shop
|
# Simular lo que hace eskaera_shop
|
||||||
products = order.product_ids
|
products = order.product_ids
|
||||||
if not products and order.category_ids:
|
if not products and order.category_ids:
|
||||||
products = self.env["product.product"].search(
|
products = self.env['product.product'].search([
|
||||||
[
|
('categ_id', 'in', order.category_ids.ids),
|
||||||
("categ_id", "in", order.category_ids.ids),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not products and order.supplier_ids:
|
if not products and order.supplier_ids:
|
||||||
# Buscar productos que tienen estos proveedores en seller_ids
|
# Buscar productos que tienen estos proveedores en seller_ids
|
||||||
product_templates = self.env["product.template"].search(
|
product_templates = self.env['product.template'].search([
|
||||||
[
|
('seller_ids.partner_id', 'in', order.supplier_ids.ids),
|
||||||
("seller_ids.partner_id", "in", order.supplier_ids.ids),
|
])
|
||||||
]
|
products = product_templates.mapped('product_variant_ids')
|
||||||
)
|
|
||||||
products = product_templates.mapped("product_variant_ids")
|
|
||||||
|
|
||||||
# Debe retornar productos del proveedor
|
# Debe retornar productos del proveedor
|
||||||
self.assertEqual(len(products), 1)
|
self.assertEqual(len(products), 1)
|
||||||
self.assertIn(
|
self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clear categories so supplier-only fallback remains active
|
# Clear categories so supplier-only fallback remains active
|
||||||
order.write(
|
order.write({
|
||||||
{
|
'category_ids': [(5, 0, 0)],
|
||||||
"category_ids": [(5, 0, 0)],
|
'supplier_ids': [(4, self.supplier.id)],
|
||||||
"supplier_ids": [(4, self.supplier.id)],
|
})
|
||||||
}
|
products = self.env['product.product']._get_products_for_group_order(order.id)
|
||||||
)
|
self.assertIn(self.product_supplier.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
products = self.env["product.product"]._get_products_for_group_order(order.id)
|
self.assertNotIn(self.product_direct.product_tmpl_id, products.mapped('product_tmpl_id'))
|
||||||
self.assertIn(
|
|
||||||
self.product_supplier.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
self.product_direct.product_tmpl_id, products.mapped("product_tmpl_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_no_products_available(self):
|
def test_no_products_available(self):
|
||||||
"""Test que retorna vacío si no hay productos definidos de ninguna forma."""
|
'''Test que retorna vacío si no hay productos definidos de ninguna forma.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Sin Productos',
|
||||||
"name": "Pedido Sin Productos",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
|
||||||
# Sin product_ids, category_ids, supplier_ids
|
# Sin product_ids, category_ids, supplier_ids
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
|
|
||||||
# Simular lo que hace eskaera_shop
|
# Simular lo que hace eskaera_shop
|
||||||
products = order.product_ids
|
products = order.product_ids
|
||||||
if not products and order.category_ids:
|
if not products and order.category_ids:
|
||||||
products = self.env["product.product"].search(
|
products = self.env['product.product'].search([
|
||||||
[
|
('categ_id', 'in', order.category_ids.ids),
|
||||||
("categ_id", "in", order.category_ids.ids),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not products and order.supplier_ids:
|
if not products and order.supplier_ids:
|
||||||
# Buscar productos que tienen estos proveedores en seller_ids
|
# Buscar productos que tienen estos proveedores en seller_ids
|
||||||
product_templates = self.env["product.template"].search(
|
product_templates = self.env['product.template'].search([
|
||||||
[
|
('seller_ids.partner_id', 'in', order.supplier_ids.ids),
|
||||||
("seller_ids.partner_id", "in", order.supplier_ids.ids),
|
])
|
||||||
]
|
products = product_templates.mapped('product_variant_ids')
|
||||||
)
|
|
||||||
products = product_templates.mapped("product_variant_ids")
|
|
||||||
|
|
||||||
# Debe estar vacío
|
# Debe estar vacío
|
||||||
self.assertEqual(len(products), 0)
|
self.assertEqual(len(products), 0)
|
||||||
|
|
|
||||||
|
|
@ -1,354 +1,310 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from psycopg2 import IntegrityError
|
|
||||||
|
|
||||||
from odoo import fields
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from psycopg2 import IntegrityError
|
||||||
|
from odoo import fields
|
||||||
|
|
||||||
|
|
||||||
class TestGroupOrder(TransactionCase):
|
class TestGroupOrder(TransactionCase):
|
||||||
"""Test suite para el modelo group.order."""
|
'''Test suite para el modelo group.order.'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# Crear un grupo (res.partner)
|
# Crear un grupo (res.partner)
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo Test',
|
||||||
"name": "Grupo Test",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo@test.com',
|
||||||
"email": "grupo@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear productos
|
# Crear productos
|
||||||
self.product1 = self.env["product.product"].create(
|
self.product1 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Test 1',
|
||||||
"name": "Producto Test 1",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product2 = self.env["product.product"].create(
|
self.product2 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Test 2',
|
||||||
"name": "Producto Test 2",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_group_order(self):
|
def test_create_group_order(self):
|
||||||
"""Test crear un pedido de grupo."""
|
'''Test crear un pedido de grupo.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Semanal Test',
|
||||||
"name": "Pedido Semanal Test",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
self.assertEqual(order.state, "draft")
|
self.assertEqual(order.state, 'draft')
|
||||||
self.assertIn(self.group, order.group_ids)
|
self.assertIn(self.group, order.group_ids)
|
||||||
|
|
||||||
def test_group_order_dates_validation(self):
|
def test_group_order_dates_validation(self):
|
||||||
"""Test that start_date must be before end_date"""
|
""" Test that start_date must be before end_date """
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.env["group.order"].create(
|
self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Invalid',
|
||||||
"name": "Pedido Invalid",
|
'start_date': fields.Date.today() + timedelta(days=7),
|
||||||
"start_date": fields.Date.today() + timedelta(days=7),
|
'end_date': fields.Date.today(),
|
||||||
"end_date": fields.Date.today(),
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_group_order_state_transitions(self):
|
def test_group_order_state_transitions(self):
|
||||||
"""Test transiciones de estado."""
|
'''Test transiciones de estado.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido State Test',
|
||||||
"name": "Pedido State Test",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draft -> Open
|
# Draft -> Open
|
||||||
order.action_open()
|
order.action_open()
|
||||||
self.assertEqual(order.state, "open")
|
self.assertEqual(order.state, 'open')
|
||||||
|
|
||||||
# Open -> Closed
|
# Open -> Closed
|
||||||
order.action_close()
|
order.action_close()
|
||||||
self.assertEqual(order.state, "closed")
|
self.assertEqual(order.state, 'closed')
|
||||||
|
|
||||||
def test_group_order_action_cancel(self):
|
def test_group_order_action_cancel(self):
|
||||||
"""Test cancelar un pedido."""
|
'''Test cancelar un pedido.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Cancel Test',
|
||||||
"name": "Pedido Cancel Test",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order.action_cancel()
|
order.action_cancel()
|
||||||
self.assertEqual(order.state, "cancelled")
|
self.assertEqual(order.state, 'cancelled')
|
||||||
|
|
||||||
def test_get_active_orders_for_week(self):
|
def test_get_active_orders_for_week(self):
|
||||||
"""Test obtener pedidos activos para la semana."""
|
'''Test obtener pedidos activos para la semana.'''
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
week_start = today - timedelta(days=today.weekday())
|
week_start = today - timedelta(days=today.weekday())
|
||||||
week_end = week_start + timedelta(days=6)
|
week_end = week_start + timedelta(days=6)
|
||||||
|
|
||||||
# Crear pedido activo esta semana
|
# Crear pedido activo esta semana
|
||||||
active_order = self.env["group.order"].create(
|
active_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Activo',
|
||||||
"name": "Pedido Activo",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': week_start,
|
||||||
"start_date": week_start,
|
'end_date': week_end,
|
||||||
"end_date": week_end,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'state': 'open',
|
||||||
"state": "open",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear pedido inactivo (futuro)
|
# Crear pedido inactivo (futuro)
|
||||||
future_order = self.env["group.order"].create(
|
future_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Futuro',
|
||||||
"name": "Pedido Futuro",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': week_end + timedelta(days=1),
|
||||||
"start_date": week_end + timedelta(days=1),
|
'end_date': week_end + timedelta(days=8),
|
||||||
"end_date": week_end + timedelta(days=8),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'state': 'open',
|
||||||
"state": "open",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
active_orders = self.env["group.order"].search(
|
active_orders = self.env['group.order'].search([
|
||||||
[
|
('state', '=', 'open'),
|
||||||
("state", "=", "open"),
|
'|',
|
||||||
"|",
|
('end_date', '>=', week_start),
|
||||||
("end_date", ">=", week_start),
|
('end_date', '=', False),
|
||||||
("end_date", "=", False),
|
('start_date', '<=', week_end),
|
||||||
("start_date", "<=", week_end),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIn(active_order, active_orders)
|
self.assertIn(active_order, active_orders)
|
||||||
self.assertNotIn(future_order, active_orders)
|
self.assertNotIn(future_order, active_orders)
|
||||||
|
|
||||||
def test_permanent_group_order(self):
|
def test_permanent_group_order(self):
|
||||||
"""Test crear un pedido permanente (sin end_date)."""
|
'''Test crear un pedido permanente (sin end_date).'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Permanente',
|
||||||
"name": "Pedido Permanente",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': False,
|
||||||
"end_date": False,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
self.assertFalse(order.end_date)
|
self.assertFalse(order.end_date)
|
||||||
|
|
||||||
def test_get_active_orders_excludes_draft(self):
|
def test_get_active_orders_excludes_draft(self):
|
||||||
"""Test que get_active_orders_for_week NO incluye pedidos en draft."""
|
'''Test que get_active_orders_for_week NO incluye pedidos en draft.'''
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
|
|
||||||
# Crear pedido en draft (no abierto)
|
# Crear pedido en draft (no abierto)
|
||||||
draft_order = self.env["group.order"].create(
|
draft_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Draft',
|
||||||
"name": "Pedido Draft",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': today,
|
||||||
"start_date": today,
|
'end_date': today + timedelta(days=7),
|
||||||
"end_date": today + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'state': 'draft',
|
||||||
"state": "draft",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
week_start = today - timedelta(days=today.weekday())
|
week_start = today - timedelta(days=today.weekday())
|
||||||
week_end = week_start + timedelta(days=6)
|
week_end = week_start + timedelta(days=6)
|
||||||
active_orders = self.env["group.order"].search(
|
active_orders = self.env['group.order'].search([
|
||||||
[
|
('state', '=', 'open'),
|
||||||
("state", "=", "open"),
|
'|',
|
||||||
"|",
|
('end_date', '>=', week_start),
|
||||||
("end_date", ">=", week_start),
|
('end_date', '=', False),
|
||||||
("end_date", "=", False),
|
('start_date', '<=', week_end),
|
||||||
("start_date", "<=", week_end),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
self.assertNotIn(draft_order, active_orders)
|
self.assertNotIn(draft_order, active_orders)
|
||||||
|
|
||||||
def test_get_active_orders_excludes_closed(self):
|
def test_get_active_orders_excludes_closed(self):
|
||||||
"""Test que get_active_orders_for_week NO incluye pedidos cerrados."""
|
'''Test que get_active_orders_for_week NO incluye pedidos cerrados.'''
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
|
|
||||||
closed_order = self.env["group.order"].create(
|
closed_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Cerrado',
|
||||||
"name": "Pedido Cerrado",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': today,
|
||||||
"start_date": today,
|
'end_date': today + timedelta(days=7),
|
||||||
"end_date": today + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'state': 'closed',
|
||||||
"state": "closed",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
week_start = today - timedelta(days=today.weekday())
|
week_start = today - timedelta(days=today.weekday())
|
||||||
week_end = week_start + timedelta(days=6)
|
week_end = week_start + timedelta(days=6)
|
||||||
active_orders = self.env["group.order"].search(
|
active_orders = self.env['group.order'].search([
|
||||||
[
|
('state', '=', 'open'),
|
||||||
("state", "=", "open"),
|
'|',
|
||||||
"|",
|
('end_date', '>=', week_start),
|
||||||
("end_date", ">=", week_start),
|
('end_date', '=', False),
|
||||||
("end_date", "=", False),
|
('start_date', '<=', week_end),
|
||||||
("start_date", "<=", week_end),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
self.assertNotIn(closed_order, active_orders)
|
self.assertNotIn(closed_order, active_orders)
|
||||||
|
|
||||||
def test_get_active_orders_excludes_cancelled(self):
|
def test_get_active_orders_excludes_cancelled(self):
|
||||||
"""Test que get_active_orders_for_week NO incluye pedidos cancelados."""
|
'''Test que get_active_orders_for_week NO incluye pedidos cancelados.'''
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
|
|
||||||
cancelled_order = self.env["group.order"].create(
|
cancelled_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Cancelado',
|
||||||
"name": "Pedido Cancelado",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': today,
|
||||||
"start_date": today,
|
'end_date': today + timedelta(days=7),
|
||||||
"end_date": today + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
'state': 'cancelled',
|
||||||
"state": "cancelled",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
week_start = today - timedelta(days=today.weekday())
|
week_start = today - timedelta(days=today.weekday())
|
||||||
week_end = week_start + timedelta(days=6)
|
week_end = week_start + timedelta(days=6)
|
||||||
active_orders = self.env["group.order"].search(
|
active_orders = self.env['group.order'].search([
|
||||||
[
|
('state', '=', 'open'),
|
||||||
("state", "=", "open"),
|
'|',
|
||||||
"|",
|
('end_date', '>=', week_start),
|
||||||
("end_date", ">=", week_start),
|
('end_date', '=', False),
|
||||||
("end_date", "=", False),
|
('start_date', '<=', week_end),
|
||||||
("start_date", "<=", week_end),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
self.assertNotIn(cancelled_order, active_orders)
|
self.assertNotIn(cancelled_order, active_orders)
|
||||||
|
|
||||||
def test_state_transition_draft_to_open(self):
|
def test_state_transition_draft_to_open(self):
|
||||||
"""Test que un pedido pasa de draft a open."""
|
'''Test que un pedido pasa de draft a open.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Estado Test',
|
||||||
"name": "Pedido Estado Test",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(order.state, "draft")
|
self.assertEqual(order.state, 'draft')
|
||||||
order.action_open()
|
order.action_open()
|
||||||
self.assertEqual(order.state, "open")
|
self.assertEqual(order.state, 'open')
|
||||||
|
|
||||||
def test_state_transition_open_to_closed(self):
|
def test_state_transition_open_to_closed(self):
|
||||||
"""Test transición válida open -> closed."""
|
'''Test transición válida open -> closed.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Estado Test',
|
||||||
"name": "Pedido Estado Test",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order.action_open()
|
order.action_open()
|
||||||
self.assertEqual(order.state, "open")
|
self.assertEqual(order.state, 'open')
|
||||||
|
|
||||||
order.action_close()
|
order.action_close()
|
||||||
self.assertEqual(order.state, "closed")
|
self.assertEqual(order.state, 'closed')
|
||||||
|
|
||||||
def test_state_transition_any_to_cancelled(self):
|
def test_state_transition_any_to_cancelled(self):
|
||||||
"""Test cancelar desde cualquier estado."""
|
'''Test cancelar desde cualquier estado.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Estado Test',
|
||||||
"name": "Pedido Estado Test",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Desde draft
|
# Desde draft
|
||||||
order.action_cancel()
|
order.action_cancel()
|
||||||
self.assertEqual(order.state, "cancelled")
|
self.assertEqual(order.state, 'cancelled')
|
||||||
|
|
||||||
# Crear otro desde open
|
# Crear otro desde open
|
||||||
order2 = self.env["group.order"].create(
|
order2 = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Estado Test 2',
|
||||||
"name": "Pedido Estado Test 2",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
order2.action_open()
|
order2.action_open()
|
||||||
order2.action_cancel()
|
order2.action_cancel()
|
||||||
self.assertEqual(order2.state, "cancelled")
|
self.assertEqual(order2.state, 'cancelled')
|
||||||
|
|
|
||||||
|
|
@ -1,178 +1,147 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class TestMultiCompanyGroupOrder(TransactionCase):
|
class TestMultiCompanyGroupOrder(TransactionCase):
|
||||||
"""Test suite para el soporte multicompañía en group.order."""
|
'''Test suite para el soporte multicompañía en group.order.'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Crear dos compañías
|
# Crear dos compañías
|
||||||
self.company1 = self.env["res.company"].create(
|
self.company1 = self.env['res.company'].create({
|
||||||
{
|
'name': 'Company 1',
|
||||||
"name": "Company 1",
|
})
|
||||||
}
|
self.company2 = self.env['res.company'].create({
|
||||||
)
|
'name': 'Company 2',
|
||||||
self.company2 = self.env["res.company"].create(
|
})
|
||||||
{
|
|
||||||
"name": "Company 2",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear grupos en diferentes compañías
|
# Crear grupos en diferentes compañías
|
||||||
self.group1 = self.env["res.partner"].create(
|
self.group1 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo Company 1',
|
||||||
"name": "Grupo Company 1",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo1@test.com',
|
||||||
"email": "grupo1@test.com",
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group2 = self.env["res.partner"].create(
|
self.group2 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo Company 2',
|
||||||
"name": "Grupo Company 2",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo2@test.com',
|
||||||
"email": "grupo2@test.com",
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear productos en cada compañía
|
# Crear productos en cada compañía
|
||||||
self.product1 = self.env["product.product"].create(
|
self.product1 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Company 1',
|
||||||
"name": "Producto Company 1",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product2 = self.env["product.product"].create(
|
self.product2 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Producto Company 2',
|
||||||
"name": "Producto Company 2",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_group_order_has_company_id(self):
|
def test_group_order_has_company_id(self):
|
||||||
"""Test que group.order tenga el campo company_id."""
|
'''Test que group.order tenga el campo company_id.'''
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Company 1',
|
||||||
"name": "Pedido Company 1",
|
'group_ids': [(6, 0, [self.group1.id])],
|
||||||
"group_ids": [(6, 0, [self.group1.id])],
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
self.assertEqual(order.company_id, self.company1)
|
self.assertEqual(order.company_id, self.company1)
|
||||||
|
|
||||||
def test_group_order_default_company(self):
|
def test_group_order_default_company(self):
|
||||||
"""Test que company_id por defecto sea la compañía del usuario."""
|
'''Test que company_id por defecto sea la compañía del usuario.'''
|
||||||
# Crear usuario con compañía específica
|
# Crear usuario con compañía específica
|
||||||
user = self.env["res.users"].create(
|
user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser',
|
||||||
"login": "testuser",
|
'password': 'test123',
|
||||||
"password": "test123",
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'company_ids': [(6, 0, [self.company1.id])],
|
||||||
"company_ids": [(6, 0, [self.company1.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order = (
|
order = self.env['group.order'].with_user(user).create({
|
||||||
self.env["group.order"]
|
'name': 'Pedido Default Company',
|
||||||
.with_user(user)
|
'group_ids': [(6, 0, [self.group1.id])],
|
||||||
.create(
|
'type': 'regular',
|
||||||
{
|
'start_date': datetime.now().date(),
|
||||||
"name": "Pedido Default Company",
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"group_ids": [(6, 0, [self.group1.id])],
|
'period': 'weekly',
|
||||||
"type": "regular",
|
'pickup_day': '5',
|
||||||
"start_date": datetime.now().date(),
|
'cutoff_day': '0',
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
})
|
||||||
"period": "weekly",
|
|
||||||
"pickup_day": "5",
|
|
||||||
"cutoff_day": "0",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verificar que se asignó la compañía del usuario
|
# Verificar que se asignó la compañía del usuario
|
||||||
self.assertEqual(order.company_id, self.company1)
|
self.assertEqual(order.company_id, self.company1)
|
||||||
|
|
||||||
def test_group_order_company_constraint(self):
|
def test_group_order_company_constraint(self):
|
||||||
"""Test que solo grupos de la misma compañía se puedan asignar."""
|
'''Test que solo grupos de la misma compañía se puedan asignar.'''
|
||||||
# Intentar asignar un grupo de otra compañía
|
# Intentar asignar un grupo de otra compañía
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.env["group.order"].create(
|
self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Mixed Companies',
|
||||||
"name": "Pedido Mixed Companies",
|
'group_ids': [(6, 0, [self.group1.id, self.group2.id])],
|
||||||
"group_ids": [(6, 0, [self.group1.id, self.group2.id])],
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_group_order_multi_company_filter(self):
|
def test_group_order_multi_company_filter(self):
|
||||||
"""Test que get_active_orders_for_week() respete company_id."""
|
'''Test que get_active_orders_for_week() respete company_id.'''
|
||||||
# Crear órdenes en diferentes compañías
|
# Crear órdenes en diferentes compañías
|
||||||
order1 = self.env["group.order"].create(
|
order1 = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Company 1',
|
||||||
"name": "Pedido Company 1",
|
'group_ids': [(6, 0, [self.group1.id])],
|
||||||
"group_ids": [(6, 0, [self.group1.id])],
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'state': 'open',
|
||||||
"state": "open",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order2 = self.env["group.order"].create(
|
order2 = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Company 2',
|
||||||
"name": "Pedido Company 2",
|
'group_ids': [(6, 0, [self.group2.id])],
|
||||||
"group_ids": [(6, 0, [self.group2.id])],
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'state': 'open',
|
||||||
"state": "open",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Obtener órdenes activas de company1
|
# Obtener órdenes activas de company1
|
||||||
active_orders = (
|
active_orders = self.env['group.order'].with_context(
|
||||||
self.env["group.order"]
|
allowed_company_ids=[self.company1.id]
|
||||||
.with_context(allowed_company_ids=[self.company1.id])
|
).get_active_orders_for_week()
|
||||||
.get_active_orders_for_week()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Debería contener solo order1
|
# Debería contener solo order1
|
||||||
self.assertIn(order1, active_orders)
|
self.assertIn(order1, active_orders)
|
||||||
|
|
@ -180,28 +149,24 @@ class TestMultiCompanyGroupOrder(TransactionCase):
|
||||||
# el filtro de compañía correctamente
|
# el filtro de compañía correctamente
|
||||||
|
|
||||||
def test_product_company_isolation(self):
|
def test_product_company_isolation(self):
|
||||||
"""Test que los productos de diferentes compañías estén aislados."""
|
'''Test que los productos de diferentes compañías estén aislados.'''
|
||||||
# Crear categoría para products
|
# Crear categoría para products
|
||||||
category = self.env["product.category"].create(
|
category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido con Categoría',
|
||||||
"name": "Pedido con Categoría",
|
'group_ids': [(6, 0, [self.group1.id])],
|
||||||
"group_ids": [(6, 0, [self.group1.id])],
|
'category_ids': [(6, 0, [category.id])],
|
||||||
"category_ids": [(6, 0, [category.id])],
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
self.assertEqual(order.company_id, self.company1)
|
self.assertEqual(order.company_id, self.company1)
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,12 @@ Coverage:
|
||||||
- Product price info structure in eskaera_shop
|
- Product price info structure in eskaera_shop
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from odoo.tests import tagged
|
import json
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
|
||||||
@tagged("post_install", "-at_install")
|
@tagged('post_install', '-at_install')
|
||||||
class TestPricingWithPricelist(TransactionCase):
|
class TestPricingWithPricelist(TransactionCase):
|
||||||
"""Test pricing calculations using OCA product_get_price_helper addon."""
|
"""Test pricing calculations using OCA product_get_price_helper addon."""
|
||||||
|
|
||||||
|
|
@ -25,154 +26,118 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Create test company
|
# Create test company
|
||||||
self.company = self.env["res.company"].create(
|
self.company = self.env['res.company'].create({
|
||||||
{
|
'name': 'Test Company Pricing',
|
||||||
"name": "Test Company Pricing",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create test group
|
# Create test group
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group Pricing',
|
||||||
"name": "Test Group Pricing",
|
'is_company': True,
|
||||||
"is_company": True,
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create test user
|
# Create test user
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User Pricing',
|
||||||
"name": "Test User Pricing",
|
'login': 'testpricing@example.com',
|
||||||
"login": "testpricing@example.com",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
'company_ids': [(6, 0, [self.company.id])],
|
||||||
"company_ids": [(6, 0, [self.company.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get or create default tax group
|
# Get or create default tax group
|
||||||
tax_group = self.env["account.tax.group"].search(
|
tax_group = self.env['account.tax.group'].search([
|
||||||
[("company_id", "=", self.company.id)], limit=1
|
('company_id', '=', self.company.id)
|
||||||
)
|
], limit=1)
|
||||||
if not tax_group:
|
if not tax_group:
|
||||||
tax_group = self.env["account.tax.group"].create(
|
tax_group = self.env['account.tax.group'].create({
|
||||||
{
|
'name': 'IVA',
|
||||||
"name": "IVA",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get default country (Spain)
|
# Get default country (Spain)
|
||||||
country_es = self.env.ref("base.es")
|
country_es = self.env.ref('base.es')
|
||||||
|
|
||||||
# Create tax (21% IVA)
|
# Create tax (21% IVA)
|
||||||
self.tax_21 = self.env["account.tax"].create(
|
self.tax_21 = self.env['account.tax'].create({
|
||||||
{
|
'name': 'IVA 21%',
|
||||||
"name": "IVA 21%",
|
'amount': 21.0,
|
||||||
"amount": 21.0,
|
'amount_type': 'percent',
|
||||||
"amount_type": "percent",
|
'type_tax_use': 'sale',
|
||||||
"type_tax_use": "sale",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
'country_id': country_es.id,
|
||||||
"country_id": country_es.id,
|
'tax_group_id': tax_group.id,
|
||||||
"tax_group_id": tax_group.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create tax (10% IVA reducido)
|
# Create tax (10% IVA reducido)
|
||||||
self.tax_10 = self.env["account.tax"].create(
|
self.tax_10 = self.env['account.tax'].create({
|
||||||
{
|
'name': 'IVA 10%',
|
||||||
"name": "IVA 10%",
|
'amount': 10.0,
|
||||||
"amount": 10.0,
|
'amount_type': 'percent',
|
||||||
"amount_type": "percent",
|
'type_tax_use': 'sale',
|
||||||
"type_tax_use": "sale",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
'country_id': country_es.id,
|
||||||
"country_id": country_es.id,
|
'tax_group_id': tax_group.id,
|
||||||
"tax_group_id": tax_group.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create fiscal position (maps 21% to 10%)
|
# Create fiscal position (maps 21% to 10%)
|
||||||
self.fiscal_position = self.env["account.fiscal.position"].create(
|
self.fiscal_position = self.env['account.fiscal.position'].create({
|
||||||
{
|
'name': 'Test Fiscal Position',
|
||||||
"name": "Test Fiscal Position",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
self.env['account.fiscal.position.tax'].create({
|
||||||
)
|
'position_id': self.fiscal_position.id,
|
||||||
self.env["account.fiscal.position.tax"].create(
|
'tax_src_id': self.tax_21.id,
|
||||||
{
|
'tax_dest_id': self.tax_10.id,
|
||||||
"position_id": self.fiscal_position.id,
|
})
|
||||||
"tax_src_id": self.tax_21.id,
|
|
||||||
"tax_dest_id": self.tax_10.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create product category
|
# Create product category
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category Pricing',
|
||||||
"name": "Test Category Pricing",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create test products with different tax configurations
|
# Create test products with different tax configurations
|
||||||
self.product_with_tax = self.env["product.product"].create(
|
self.product_with_tax = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product With 21% Tax',
|
||||||
"name": "Product With 21% Tax",
|
'list_price': 100.0,
|
||||||
"list_price": 100.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'taxes_id': [(6, 0, [self.tax_21.id])],
|
||||||
"taxes_id": [(6, 0, [self.tax_21.id])],
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product_without_tax = self.env["product.product"].create(
|
self.product_without_tax = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product Without Tax',
|
||||||
"name": "Product Without Tax",
|
'list_price': 50.0,
|
||||||
"list_price": 50.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'taxes_id': False,
|
||||||
"taxes_id": False,
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create pricelist with discount
|
# Create pricelist with discount
|
||||||
self.pricelist_with_discount = self.env["product.pricelist"].create(
|
self.pricelist_with_discount = self.env['product.pricelist'].create({
|
||||||
{
|
'name': 'Test Pricelist 10% Discount',
|
||||||
"name": "Test Pricelist 10% Discount",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
'item_ids': [(0, 0, {
|
||||||
"item_ids": [
|
'compute_price': 'percentage',
|
||||||
(
|
'percent_price': 10.0, # 10% discount
|
||||||
0,
|
'applied_on': '3_global',
|
||||||
0,
|
})],
|
||||||
{
|
})
|
||||||
"compute_price": "percentage",
|
|
||||||
"percent_price": 10.0, # 10% discount
|
|
||||||
"applied_on": "3_global",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create pricelist without discount
|
# Create pricelist without discount
|
||||||
self.pricelist_no_discount = self.env["product.pricelist"].create(
|
self.pricelist_no_discount = self.env['product.pricelist'].create({
|
||||||
{
|
'name': 'Test Pricelist No Discount',
|
||||||
"name": "Test Pricelist No Discount",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create group order
|
# Create group order
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order Pricing',
|
||||||
"name": "Test Order Pricing",
|
'state': 'open',
|
||||||
"state": "open",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'product_ids': [(6, 0, [self.product_with_tax.id, self.product_without_tax.id])],
|
||||||
"product_ids": [
|
'company_id': self.company.id,
|
||||||
(6, 0, [self.product_with_tax.id, self.product_without_tax.id])
|
})
|
||||||
],
|
|
||||||
"company_id": self.company.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_to_cart_basic_price_without_tax(self):
|
def test_add_to_cart_basic_price_without_tax(self):
|
||||||
"""Test basic price calculation for product without taxes."""
|
"""Test basic price calculation for product without taxes."""
|
||||||
|
|
@ -183,14 +148,10 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
fposition=False,
|
fposition=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(result['value'], 50.0,
|
||||||
result["value"], 50.0, "Product without tax should have price = list_price"
|
"Product without tax should have price = list_price")
|
||||||
)
|
self.assertEqual(result.get('discount', 0), 0,
|
||||||
self.assertEqual(
|
"No discount pricelist should have 0% discount")
|
||||||
result.get("discount", 0),
|
|
||||||
0,
|
|
||||||
"No discount pricelist should have 0% discount",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_to_cart_with_pricelist_discount(self):
|
def test_add_to_cart_with_pricelist_discount(self):
|
||||||
"""Test that discounted prices are calculated correctly."""
|
"""Test that discounted prices are calculated correctly."""
|
||||||
|
|
@ -204,14 +165,10 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
# OCA addon returns price without taxes by default
|
# OCA addon returns price without taxes by default
|
||||||
expected_price = 100.0 * 0.9 # 90.0
|
expected_price = 100.0 * 0.9 # 90.0
|
||||||
|
|
||||||
self.assertIn("value", result, "Result must contain 'value' key")
|
self.assertIn('value', result, "Result must contain 'value' key")
|
||||||
self.assertIn("tax_included", result, "Result must contain 'tax_included' key")
|
self.assertIn('tax_included', result, "Result must contain 'tax_included' key")
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(result['value'], expected_price, places=2,
|
||||||
result["value"],
|
msg=f"Expected {expected_price}, got {result['value']}")
|
||||||
expected_price,
|
|
||||||
places=2,
|
|
||||||
msg=f"Expected {expected_price}, got {result['value']}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_to_cart_with_fiscal_position(self):
|
def test_add_to_cart_with_fiscal_position(self):
|
||||||
"""Test fiscal position maps taxes correctly (21% -> 10%)."""
|
"""Test fiscal position maps taxes correctly (21% -> 10%)."""
|
||||||
|
|
@ -230,10 +187,10 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
|
|
||||||
# Both should return base price (100.0) without tax by default
|
# Both should return base price (100.0) without tax by default
|
||||||
# Tax mapping only affects tax calculation, not the base price returned
|
# Tax mapping only affects tax calculation, not the base price returned
|
||||||
self.assertIn("value", result_without_fp, "Result must contain 'value'")
|
self.assertIn('value', result_without_fp, "Result must contain 'value'")
|
||||||
self.assertIn("value", result_with_fp, "Result must contain 'value'")
|
self.assertIn('value', result_with_fp, "Result must contain 'value'")
|
||||||
self.assertEqual(result_without_fp["value"], 100.0)
|
self.assertEqual(result_without_fp['value'], 100.0)
|
||||||
self.assertEqual(result_with_fp["value"], 100.0)
|
self.assertEqual(result_with_fp['value'], 100.0)
|
||||||
|
|
||||||
def test_add_to_cart_with_tax_included(self):
|
def test_add_to_cart_with_tax_included(self):
|
||||||
"""Test price calculation returns tax_included flag correctly."""
|
"""Test price calculation returns tax_included flag correctly."""
|
||||||
|
|
@ -245,32 +202,22 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# By default, tax is not included in price
|
# By default, tax is not included in price
|
||||||
self.assertIn("tax_included", result)
|
self.assertIn('tax_included', result)
|
||||||
self.assertEqual(
|
self.assertEqual(result['value'], 100.0, "Price should be base price without tax")
|
||||||
result["value"], 100.0, "Price should be base price without tax"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_to_cart_with_quantity_discount(self):
|
def test_add_to_cart_with_quantity_discount(self):
|
||||||
"""Test quantity-based discounts if applicable."""
|
"""Test quantity-based discounts if applicable."""
|
||||||
# Create pricelist with quantity-based rule
|
# Create pricelist with quantity-based rule
|
||||||
pricelist_qty = self.env["product.pricelist"].create(
|
pricelist_qty = self.env['product.pricelist'].create({
|
||||||
{
|
'name': 'Quantity Discount Pricelist',
|
||||||
"name": "Quantity Discount Pricelist",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
'item_ids': [(0, 0, {
|
||||||
"item_ids": [
|
'compute_price': 'percentage',
|
||||||
(
|
'percent_price': 20.0, # 20% discount
|
||||||
0,
|
'min_quantity': 5.0,
|
||||||
0,
|
'applied_on': '3_global',
|
||||||
{
|
})],
|
||||||
"compute_price": "percentage",
|
})
|
||||||
"percent_price": 20.0, # 20% discount
|
|
||||||
"min_quantity": 5.0,
|
|
||||||
"applied_on": "3_global",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Quantity 1: No discount
|
# Quantity 1: No discount
|
||||||
result_qty_1 = self.product_with_tax._get_price(
|
result_qty_1 = self.product_with_tax._get_price(
|
||||||
|
|
@ -288,8 +235,8 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
|
|
||||||
# Qty 1: 100.0 (no discount, no tax in value)
|
# Qty 1: 100.0 (no discount, no tax in value)
|
||||||
# Qty 5: 100 * 0.8 = 80.0 (with 20% discount, no tax in value)
|
# Qty 5: 100 * 0.8 = 80.0 (with 20% discount, no tax in value)
|
||||||
self.assertAlmostEqual(result_qty_1["value"], 100.0, places=2)
|
self.assertAlmostEqual(result_qty_1['value'], 100.0, places=2)
|
||||||
self.assertAlmostEqual(result_qty_5["value"], 80.0, places=2)
|
self.assertAlmostEqual(result_qty_5['value'], 80.0, places=2)
|
||||||
|
|
||||||
def test_add_to_cart_price_fallback_no_pricelist(self):
|
def test_add_to_cart_price_fallback_no_pricelist(self):
|
||||||
"""Test fallback to list_price when pricelist is not available."""
|
"""Test fallback to list_price when pricelist is not available."""
|
||||||
|
|
@ -304,25 +251,21 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
# Should return list_price with taxes (fallback behavior)
|
# Should return list_price with taxes (fallback behavior)
|
||||||
# This depends on OCA addon implementation
|
# This depends on OCA addon implementation
|
||||||
self.assertIsNotNone(result, "Should not fail when pricelist is False")
|
self.assertIsNotNone(result, "Should not fail when pricelist is False")
|
||||||
self.assertIn("value", result, "Result should contain 'value' key")
|
self.assertIn('value', result, "Result should contain 'value' key")
|
||||||
|
|
||||||
def test_add_to_cart_price_fallback_no_variant(self):
|
def test_add_to_cart_price_fallback_no_variant(self):
|
||||||
"""Test handling when product has no variants."""
|
"""Test handling when product has no variants."""
|
||||||
# Create product template without variants
|
# Create product template without variants
|
||||||
product_template = self.env["product.template"].create(
|
product_template = self.env['product.template'].create({
|
||||||
{
|
'name': 'Product Without Variant',
|
||||||
"name": "Product Without Variant",
|
'list_price': 75.0,
|
||||||
"list_price": 75.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should have auto-created variant
|
# Should have auto-created variant
|
||||||
self.assertTrue(
|
self.assertTrue(product_template.product_variant_ids,
|
||||||
product_template.product_variant_ids,
|
"Product template should have at least one variant")
|
||||||
"Product template should have at least one variant",
|
|
||||||
)
|
|
||||||
|
|
||||||
variant = product_template.product_variant_ids[0]
|
variant = product_template.product_variant_ids[0]
|
||||||
result = variant._get_price(
|
result = variant._get_price(
|
||||||
|
|
@ -332,7 +275,7 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsNotNone(result, "Should handle product with auto-created variant")
|
self.assertIsNotNone(result, "Should handle product with auto-created variant")
|
||||||
self.assertAlmostEqual(result["value"], 75.0, places=2)
|
self.assertAlmostEqual(result['value'], 75.0, places=2)
|
||||||
|
|
||||||
def test_product_price_info_structure(self):
|
def test_product_price_info_structure(self):
|
||||||
"""Test product_price_info dict structure returned by _get_price."""
|
"""Test product_price_info dict structure returned by _get_price."""
|
||||||
|
|
@ -343,17 +286,16 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify structure
|
# Verify structure
|
||||||
self.assertIn("value", result, "Result must contain 'value' key")
|
self.assertIn('value', result, "Result must contain 'value' key")
|
||||||
self.assertIsInstance(
|
self.assertIsInstance(result['value'], (int, float),
|
||||||
result["value"], (int, float), "Price value must be numeric"
|
"Price value must be numeric")
|
||||||
)
|
|
||||||
|
|
||||||
# Optional keys (depends on OCA addon version)
|
# Optional keys (depends on OCA addon version)
|
||||||
if "discount" in result:
|
if 'discount' in result:
|
||||||
self.assertIsInstance(result["discount"], (int, float))
|
self.assertIsInstance(result['discount'], (int, float))
|
||||||
|
|
||||||
if "original_value" in result:
|
if 'original_value' in result:
|
||||||
self.assertIsInstance(result["original_value"], (int, float))
|
self.assertIsInstance(result['original_value'], (int, float))
|
||||||
|
|
||||||
def test_discounted_price_visual_comparison(self):
|
def test_discounted_price_visual_comparison(self):
|
||||||
"""Test comparison of original vs discounted price for UI display."""
|
"""Test comparison of original vs discounted price for UI display."""
|
||||||
|
|
@ -364,14 +306,11 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# When there's a discount, original_value should be higher than value
|
# When there's a discount, original_value should be higher than value
|
||||||
if result.get("discount", 0) > 0:
|
if result.get('discount', 0) > 0:
|
||||||
original = result.get("original_value", result["value"])
|
original = result.get('original_value', result['value'])
|
||||||
discounted = result["value"]
|
discounted = result['value']
|
||||||
self.assertGreater(
|
self.assertGreater(original, discounted,
|
||||||
original,
|
"Original price should be higher than discounted price")
|
||||||
discounted,
|
|
||||||
"Original price should be higher than discounted price",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_price_calculation_with_multiple_taxes(self):
|
def test_price_calculation_with_multiple_taxes(self):
|
||||||
"""Test product with multiple taxes applied."""
|
"""Test product with multiple taxes applied."""
|
||||||
|
|
@ -380,27 +319,23 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
country = self.tax_21.country_id
|
country = self.tax_21.country_id
|
||||||
|
|
||||||
# Create additional tax
|
# Create additional tax
|
||||||
tax_extra = self.env["account.tax"].create(
|
tax_extra = self.env['account.tax'].create({
|
||||||
{
|
'name': 'Extra Tax 5%',
|
||||||
"name": "Extra Tax 5%",
|
'amount': 5.0,
|
||||||
"amount": 5.0,
|
'amount_type': 'percent',
|
||||||
"amount_type": "percent",
|
'type_tax_use': 'sale',
|
||||||
"type_tax_use": "sale",
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
'country_id': country.id,
|
||||||
"country_id": country.id,
|
'tax_group_id': tax_group.id,
|
||||||
"tax_group_id": tax_group.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
product_multi_tax = self.env["product.product"].create(
|
product_multi_tax = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product With Multiple Taxes',
|
||||||
"name": "Product With Multiple Taxes",
|
'list_price': 100.0,
|
||||||
"list_price": 100.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'taxes_id': [(6, 0, [self.tax_21.id, tax_extra.id])],
|
||||||
"taxes_id": [(6, 0, [self.tax_21.id, tax_extra.id])],
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = product_multi_tax._get_price(
|
result = product_multi_tax._get_price(
|
||||||
qty=1.0,
|
qty=1.0,
|
||||||
|
|
@ -409,27 +344,22 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Base price 100.0 (taxes not included in value by default)
|
# Base price 100.0 (taxes not included in value by default)
|
||||||
self.assertEqual(
|
self.assertEqual(result['value'], 100.0,
|
||||||
result["value"],
|
msg="Should return base price (taxes applied separately)")
|
||||||
100.0,
|
|
||||||
msg="Should return base price (taxes applied separately)",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_price_currency_handling(self):
|
def test_price_currency_handling(self):
|
||||||
"""Test price calculation with different currencies."""
|
"""Test price calculation with different currencies."""
|
||||||
# Get or use existing EUR currency
|
# Get or use existing EUR currency
|
||||||
eur = self.env["res.currency"].search([("name", "=", "EUR")], limit=1)
|
eur = self.env['res.currency'].search([('name', '=', 'EUR')], limit=1)
|
||||||
if not eur:
|
if not eur:
|
||||||
self.skipTest("EUR currency not available in test database")
|
self.skipTest("EUR currency not available in test database")
|
||||||
|
|
||||||
# Create pricelist with EUR
|
# Create pricelist with EUR
|
||||||
pricelist_eur = self.env["product.pricelist"].create(
|
pricelist_eur = self.env['product.pricelist'].create({
|
||||||
{
|
'name': 'EUR Pricelist',
|
||||||
"name": "EUR Pricelist",
|
'currency_id': eur.id,
|
||||||
"currency_id": eur.id,
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = self.product_with_tax._get_price(
|
result = self.product_with_tax._get_price(
|
||||||
qty=1.0,
|
qty=1.0,
|
||||||
|
|
@ -438,7 +368,7 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsNotNone(result, "Should handle different currency pricelist")
|
self.assertIsNotNone(result, "Should handle different currency pricelist")
|
||||||
self.assertIn("value", result)
|
self.assertIn('value', result)
|
||||||
|
|
||||||
def test_price_consistency_across_calls(self):
|
def test_price_consistency_across_calls(self):
|
||||||
"""Test that multiple calls with same params return same price."""
|
"""Test that multiple calls with same params return same price."""
|
||||||
|
|
@ -454,22 +384,17 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
fposition=False,
|
fposition=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(result1['value'], result2['value'],
|
||||||
result1["value"],
|
"Price calculation should be deterministic")
|
||||||
result2["value"],
|
|
||||||
"Price calculation should be deterministic",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_zero_price_product(self):
|
def test_zero_price_product(self):
|
||||||
"""Test handling of free products (price = 0)."""
|
"""Test handling of free products (price = 0)."""
|
||||||
free_product = self.env["product.product"].create(
|
free_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Free Product',
|
||||||
"name": "Free Product",
|
'list_price': 0.0,
|
||||||
"list_price": 0.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'company_id': self.company.id,
|
||||||
"company_id": self.company.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = free_product._get_price(
|
result = free_product._get_price(
|
||||||
qty=1.0,
|
qty=1.0,
|
||||||
|
|
@ -477,7 +402,8 @@ class TestPricingWithPricelist(TransactionCase):
|
||||||
fposition=False,
|
fposition=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(result["value"], 0.0, "Free product should have price = 0")
|
self.assertEqual(result['value'], 0.0,
|
||||||
|
"Free product should have price = 0")
|
||||||
|
|
||||||
def test_negative_quantity_handling(self):
|
def test_negative_quantity_handling(self):
|
||||||
"""Test that negative quantities are handled properly."""
|
"""Test that negative quantities are handled properly."""
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ Coverage:
|
||||||
- Ordering and deduplication
|
- Ordering and deduplication
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
@ -28,105 +27,81 @@ class TestProductDiscoveryUnion(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a supplier
|
# Create a supplier
|
||||||
self.supplier = self.env["res.partner"].create(
|
self.supplier = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Supplier',
|
||||||
"name": "Test Supplier",
|
'is_supplier': True,
|
||||||
"is_supplier": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create categories
|
# Create categories
|
||||||
self.category1 = self.env["product.category"].create(
|
self.category1 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Category 1',
|
||||||
"name": "Category 1",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category2 = self.env["product.category"].create(
|
self.category2 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Category 2',
|
||||||
"name": "Category 2",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create products
|
# Create products
|
||||||
# Direct product
|
# Direct product
|
||||||
self.direct_product = self.env["product.product"].create(
|
self.direct_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Direct Product',
|
||||||
"name": "Direct Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Category 1 product
|
# Category 1 product
|
||||||
self.cat1_product = self.env["product.product"].create(
|
self.cat1_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Category 1 Product',
|
||||||
"name": "Category 1 Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
'categ_id': self.category1.id,
|
||||||
"categ_id": self.category1.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Category 2 product
|
# Category 2 product
|
||||||
self.cat2_product = self.env["product.product"].create(
|
self.cat2_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Category 2 Product',
|
||||||
"name": "Category 2 Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 30.0,
|
||||||
"list_price": 30.0,
|
'categ_id': self.category2.id,
|
||||||
"categ_id": self.category2.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Supplier product
|
# Supplier product
|
||||||
self.supplier_product = self.env["product.product"].create(
|
self.supplier_product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Supplier Product',
|
||||||
"name": "Supplier Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 40.0,
|
||||||
"list_price": 40.0,
|
'categ_id': self.category1.id, # Also in category
|
||||||
"categ_id": self.category1.id, # Also in category
|
'seller_ids': [(0, 0, {
|
||||||
"seller_ids": [
|
'partner_id': self.supplier.id,
|
||||||
(
|
'product_name': 'Supplier Product',
|
||||||
0,
|
})],
|
||||||
0,
|
'is_published': True,
|
||||||
{
|
'sale_ok': True,
|
||||||
"partner_id": self.supplier.id,
|
})
|
||||||
"product_name": "Supplier Product",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
"is_published": True,
|
|
||||||
"sale_ok": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_discovery_from_direct_products(self):
|
def test_discovery_from_direct_products(self):
|
||||||
"""Test discovery returns directly linked products."""
|
"""Test discovery returns directly linked products."""
|
||||||
|
|
@ -170,16 +145,14 @@ class TestProductDiscoveryUnion(TransactionCase):
|
||||||
|
|
||||||
def test_discovery_filters_unpublished(self):
|
def test_discovery_filters_unpublished(self):
|
||||||
"""Test that unpublished products are excluded from discovery."""
|
"""Test that unpublished products are excluded from discovery."""
|
||||||
unpublished = self.env["product.product"].create(
|
unpublished = self.env['product.product'].create({
|
||||||
{
|
'name': 'Unpublished Product',
|
||||||
"name": "Unpublished Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 50.0,
|
||||||
"list_price": 50.0,
|
'categ_id': self.category1.id,
|
||||||
"categ_id": self.category1.id,
|
'is_published': False,
|
||||||
"is_published": False,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group_order.category_ids = [(4, self.category1.id)]
|
self.group_order.category_ids = [(4, self.category1.id)]
|
||||||
discovered = self.group_order.product_ids
|
discovered = self.group_order.product_ids
|
||||||
|
|
@ -189,16 +162,14 @@ class TestProductDiscoveryUnion(TransactionCase):
|
||||||
|
|
||||||
def test_discovery_filters_not_for_sale(self):
|
def test_discovery_filters_not_for_sale(self):
|
||||||
"""Test that non-sellable products are excluded."""
|
"""Test that non-sellable products are excluded."""
|
||||||
not_for_sale = self.env["product.product"].create(
|
not_for_sale = self.env['product.product'].create({
|
||||||
{
|
'name': 'Not For Sale',
|
||||||
"name": "Not For Sale",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 60.0,
|
||||||
"list_price": 60.0,
|
'categ_id': self.category1.id,
|
||||||
"categ_id": self.category1.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': False,
|
||||||
"sale_ok": False,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group_order.category_ids = [(4, self.category1.id)]
|
self.group_order.category_ids = [(4, self.category1.id)]
|
||||||
discovered = self.group_order.product_ids
|
discovered = self.group_order.product_ids
|
||||||
|
|
@ -212,96 +183,76 @@ class TestDeepCategoryHierarchies(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create nested category structure:
|
# Create nested category structure:
|
||||||
# Root -> L1 -> L2 -> L3 -> L4
|
# Root -> L1 -> L2 -> L3 -> L4
|
||||||
self.cat_l1 = self.env["product.category"].create(
|
self.cat_l1 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Level 1',
|
||||||
"name": "Level 1",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cat_l2 = self.env["product.category"].create(
|
self.cat_l2 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Level 2',
|
||||||
"name": "Level 2",
|
'parent_id': self.cat_l1.id,
|
||||||
"parent_id": self.cat_l1.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cat_l3 = self.env["product.category"].create(
|
self.cat_l3 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Level 3',
|
||||||
"name": "Level 3",
|
'parent_id': self.cat_l2.id,
|
||||||
"parent_id": self.cat_l2.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cat_l4 = self.env["product.category"].create(
|
self.cat_l4 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Level 4',
|
||||||
"name": "Level 4",
|
'parent_id': self.cat_l3.id,
|
||||||
"parent_id": self.cat_l3.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cat_l5 = self.env["product.category"].create(
|
self.cat_l5 = self.env['product.category'].create({
|
||||||
{
|
'name': 'Level 5',
|
||||||
"name": "Level 5",
|
'parent_id': self.cat_l4.id,
|
||||||
"parent_id": self.cat_l4.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create products at each level
|
# Create products at each level
|
||||||
self.product_l2 = self.env["product.product"].create(
|
self.product_l2 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product L2',
|
||||||
"name": "Product L2",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.cat_l2.id,
|
||||||
"categ_id": self.cat_l2.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product_l4 = self.env["product.product"].create(
|
self.product_l4 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product L4',
|
||||||
"name": "Product L4",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
'categ_id': self.cat_l4.id,
|
||||||
"categ_id": self.cat_l4.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product_l5 = self.env["product.product"].create(
|
self.product_l5 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product L5',
|
||||||
"name": "Product L5",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 30.0,
|
||||||
"list_price": 30.0,
|
'categ_id': self.cat_l5.id,
|
||||||
"categ_id": self.cat_l5.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_discovery_root_category_includes_all_descendants(self):
|
def test_discovery_root_category_includes_all_descendants(self):
|
||||||
"""Test that linking root category discovers all nested products."""
|
"""Test that linking root category discovers all nested products."""
|
||||||
|
|
@ -356,41 +307,33 @@ class TestEmptySourcesDiscovery(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Empty Category',
|
||||||
"name": "Empty Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
# No products in this category
|
# No products in this category
|
||||||
|
|
||||||
self.supplier = self.env["res.partner"].create(
|
self.supplier = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Supplier No Products',
|
||||||
"name": "Supplier No Products",
|
'is_supplier': True,
|
||||||
"is_supplier": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
# No products from this supplier
|
# No products from this supplier
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_discovery_empty_category(self):
|
def test_discovery_empty_category(self):
|
||||||
"""Test discovery from empty category."""
|
"""Test discovery from empty category."""
|
||||||
|
|
@ -428,47 +371,39 @@ class TestProductDiscoveryOrdering(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create products with specific names
|
# Create products with specific names
|
||||||
self.products = []
|
self.products = []
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
product = self.env["product.product"].create(
|
product = self.env['product.product'].create({
|
||||||
{
|
'name': f'Product {chr(65 + i)}', # A, B, C, D, E
|
||||||
"name": f"Product {chr(65 + i)}", # A, B, C, D, E
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': (i + 1) * 10.0,
|
||||||
"list_price": (i + 1) * 10.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
'is_published': True,
|
||||||
"is_published": True,
|
'sale_ok': True,
|
||||||
"sale_ok": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.products.append(product)
|
self.products.append(product)
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_discovery_consistent_ordering(self):
|
def test_discovery_consistent_ordering(self):
|
||||||
"""Test that repeated calls return same order."""
|
"""Test that repeated calls return same order."""
|
||||||
|
|
@ -478,7 +413,10 @@ class TestProductDiscoveryOrdering(TransactionCase):
|
||||||
discovered2 = list(self.group_order.product_ids)
|
discovered2 = list(self.group_order.product_ids)
|
||||||
|
|
||||||
# Order should be consistent
|
# Order should be consistent
|
||||||
self.assertEqual([p.id for p in discovered1], [p.id for p in discovered2])
|
self.assertEqual(
|
||||||
|
[p.id for p in discovered1],
|
||||||
|
[p.id for p in discovered2]
|
||||||
|
)
|
||||||
|
|
||||||
def test_discovery_alphabetical_or_price_order(self):
|
def test_discovery_alphabetical_or_price_order(self):
|
||||||
"""Test that products are ordered predictably."""
|
"""Test that products are ordered predictably."""
|
||||||
|
|
@ -489,6 +427,6 @@ class TestProductDiscoveryOrdering(TransactionCase):
|
||||||
# Should be in some consistent order (name, price, ID, etc)
|
# Should be in some consistent order (name, price, ID, etc)
|
||||||
# Verify they're the same products, regardless of order
|
# Verify they're the same products, regardless of order
|
||||||
self.assertEqual(len(discovered), 5)
|
self.assertEqual(len(discovered), 5)
|
||||||
discovered_ids = {p.id for p in discovered}
|
discovered_ids = set(p.id for p in discovered)
|
||||||
expected_ids = {p.id for p in self.products}
|
expected_ids = set(p.id for p in self.products)
|
||||||
self.assertEqual(discovered_ids, expected_ids)
|
self.assertEqual(discovered_ids, expected_ids)
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,91 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
class TestProductExtension(TransactionCase):
|
class TestProductExtension(TransactionCase):
|
||||||
"""Test suite para las extensiones de product.template."""
|
'''Test suite para las extensiones de product.template.'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super(TestProductExtension, self).setUp()
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
})
|
||||||
}
|
self.order = self.env['group.order'].create({
|
||||||
)
|
'name': 'Test Order',
|
||||||
self.order = self.env["group.order"].create(
|
'product_ids': [(4, self.product.id)]
|
||||||
{"name": "Test Order", "product_ids": [(4, self.product.id)]}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_template_group_order_ids_field_exists(self):
|
def test_product_template_group_order_ids_field_exists(self):
|
||||||
"""Test que el campo group_order_ids existe en product.template."""
|
'''Test que el campo group_order_ids existe en product.template.'''
|
||||||
product_template = self.product.product_tmpl_id
|
product_template = self.product.product_tmpl_id
|
||||||
|
|
||||||
# El campo debe existir y ser readonly
|
# El campo debe existir y ser readonly
|
||||||
self.assertTrue(hasattr(product_template, "group_order_ids"))
|
self.assertTrue(hasattr(product_template, 'group_order_ids'))
|
||||||
|
|
||||||
def test_product_group_order_ids_readonly(self):
|
def test_product_group_order_ids_readonly(self):
|
||||||
"""Test that group_order_ids is a readonly field"""
|
""" Test that group_order_ids is a readonly field """
|
||||||
field = self.env["product.template"]._fields["group_order_ids"]
|
field = self.env['product.template']._fields['group_order_ids']
|
||||||
self.assertTrue(field.readonly)
|
self.assertTrue(field.readonly)
|
||||||
|
|
||||||
def test_product_group_order_ids_reverse_lookup(self):
|
def test_product_group_order_ids_reverse_lookup(self):
|
||||||
"""Test that adding a product to an order reflects in group_order_ids"""
|
""" Test that adding a product to an order reflects in group_order_ids """
|
||||||
related_orders = self.product.product_tmpl_id.group_order_ids
|
related_orders = self.product.product_tmpl_id.group_order_ids
|
||||||
self.assertIn(self.order, related_orders)
|
self.assertIn(self.order, related_orders)
|
||||||
|
|
||||||
def test_product_group_order_ids_empty_by_default(self):
|
def test_product_group_order_ids_empty_by_default(self):
|
||||||
"""Test that a new product has no group orders"""
|
""" Test that a new product has no group orders """
|
||||||
new_product = self.env["product.product"].create({"name": "New Product"})
|
new_product = self.env['product.product'].create({'name': 'New Product'})
|
||||||
self.assertFalse(new_product.product_tmpl_id.group_order_ids)
|
self.assertFalse(new_product.product_tmpl_id.group_order_ids)
|
||||||
|
|
||||||
def test_product_group_order_ids_multiple_orders(self):
|
def test_product_group_order_ids_multiple_orders(self):
|
||||||
"""Test that group_order_ids can contain multiple orders"""
|
""" Test that group_order_ids can contain multiple orders """
|
||||||
order2 = self.env["group.order"].create(
|
order2 = self.env['group.order'].create({
|
||||||
{"name": "Test Order 2", "product_ids": [(4, self.product.id)]}
|
'name': 'Test Order 2',
|
||||||
)
|
'product_ids': [(4, self.product.id)]
|
||||||
|
})
|
||||||
self.assertIn(self.order, self.product.product_tmpl_id.group_order_ids)
|
self.assertIn(self.order, self.product.product_tmpl_id.group_order_ids)
|
||||||
self.assertIn(order2, self.product.product_tmpl_id.group_order_ids)
|
self.assertIn(order2, self.product.product_tmpl_id.group_order_ids)
|
||||||
|
|
||||||
def test_product_group_order_ids_empty_after_remove_from_order(self):
|
def test_product_group_order_ids_empty_after_remove_from_order(self):
|
||||||
"""Test that group_order_ids is empty after removing the product from all orders"""
|
""" Test that group_order_ids is empty after removing the product from all orders """
|
||||||
self.order.write({"product_ids": [(3, self.product.id)]})
|
self.order.write({'product_ids': [(3, self.product.id)]})
|
||||||
self.assertFalse(self.product.product_tmpl_id.group_order_ids)
|
self.assertFalse(self.product.product_tmpl_id.group_order_ids)
|
||||||
|
|
||||||
def test_product_group_order_ids_with_multiple_products(self):
|
def test_product_group_order_ids_with_multiple_products(self):
|
||||||
"""Test group_order_ids with multiple products in one order"""
|
""" Test group_order_ids with multiple products in one order """
|
||||||
product2 = self.env["product.product"].create({"name": "Test Product 2"})
|
product2 = self.env['product.product'].create({'name': 'Test Product 2'})
|
||||||
self.order.write({"product_ids": [(4, self.product.id), (4, product2.id)]})
|
self.order.write({'product_ids': [
|
||||||
|
(4, self.product.id),
|
||||||
|
(4, product2.id)
|
||||||
|
]})
|
||||||
self.assertIn(self.order, self.product.product_tmpl_id.group_order_ids)
|
self.assertIn(self.order, self.product.product_tmpl_id.group_order_ids)
|
||||||
self.assertIn(self.order, product2.product_tmpl_id.group_order_ids)
|
self.assertIn(self.order, product2.product_tmpl_id.group_order_ids)
|
||||||
|
|
||||||
def test_product_with_variants_group_order_ids(self):
|
def test_product_with_variants_group_order_ids(self):
|
||||||
"""Test that group_order_ids works correctly with product variants"""
|
""" Test that group_order_ids works correctly with product variants """
|
||||||
# Create a product template with two variants
|
# Create a product template with two variants
|
||||||
product_template = self.env["product.template"].create(
|
product_template = self.env['product.template'].create({
|
||||||
{
|
'name': 'Product with Variants',
|
||||||
"name": "Product with Variants",
|
'attribute_line_ids': [(0, 0, {
|
||||||
"attribute_line_ids": [
|
'attribute_id': self.env.ref('product.product_attribute_1').id,
|
||||||
(
|
'value_ids': [
|
||||||
0,
|
(4, self.env.ref('product.product_attribute_value_1').id),
|
||||||
0,
|
(4, self.env.ref('product.product_attribute_value_2').id)
|
||||||
{
|
]
|
||||||
"attribute_id": self.env.ref(
|
})]
|
||||||
"product.product_attribute_1"
|
})
|
||||||
).id,
|
|
||||||
"value_ids": [
|
|
||||||
(
|
|
||||||
4,
|
|
||||||
self.env.ref(
|
|
||||||
"product.product_attribute_value_1"
|
|
||||||
).id,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
4,
|
|
||||||
self.env.ref(
|
|
||||||
"product.product_attribute_value_2"
|
|
||||||
).id,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
variant1 = product_template.product_variant_ids[0]
|
variant1 = product_template.product_variant_ids[0]
|
||||||
variant2 = product_template.product_variant_ids[1]
|
variant2 = product_template.product_variant_ids[1]
|
||||||
|
|
||||||
# Add one variant to an order (store variant id, not template id)
|
# Add one variant to an order (store variant id, not template id)
|
||||||
order_with_variant = self.env["group.order"].create(
|
order_with_variant = self.env['group.order'].create({
|
||||||
{"name": "Order with Variant", "product_ids": [(4, variant1.id)]}
|
'name': 'Order with Variant',
|
||||||
)
|
'product_ids': [(4, variant1.id)]
|
||||||
|
})
|
||||||
|
|
||||||
# Check that the order appears in the group_order_ids of the template
|
# Check that the order appears in the group_order_ids of the template
|
||||||
self.assertIn(order_with_variant, product_template.group_order_ids)
|
self.assertIn(order_with_variant, product_template.group_order_ids)
|
||||||
|
|
|
||||||
|
|
@ -1,170 +1,145 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.exceptions import AccessError
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import AccessError
|
||||||
|
|
||||||
|
|
||||||
class TestGroupOrderRecordRules(TransactionCase):
|
class TestGroupOrderRecordRules(TransactionCase):
|
||||||
"""Test suite para record rules de multicompañía en group.order."""
|
'''Test suite para record rules de multicompañía en group.order.'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Crear dos compañías
|
# Crear dos compañías
|
||||||
self.company1 = self.env["res.company"].create(
|
self.company1 = self.env['res.company'].create({
|
||||||
{
|
'name': 'Company 1',
|
||||||
"name": "Company 1",
|
})
|
||||||
}
|
self.company2 = self.env['res.company'].create({
|
||||||
)
|
'name': 'Company 2',
|
||||||
self.company2 = self.env["res.company"].create(
|
})
|
||||||
{
|
|
||||||
"name": "Company 2",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear usuarios para cada compañía
|
# Crear usuarios para cada compañía
|
||||||
self.user_company1 = self.env["res.users"].create(
|
self.user_company1 = self.env['res.users'].create({
|
||||||
{
|
'name': 'User Company 1',
|
||||||
"name": "User Company 1",
|
'login': 'user_c1',
|
||||||
"login": "user_c1",
|
'password': 'pass123',
|
||||||
"password": "pass123",
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'company_ids': [(6, 0, [self.company1.id])],
|
||||||
"company_ids": [(6, 0, [self.company1.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.user_company2 = self.env["res.users"].create(
|
self.user_company2 = self.env['res.users'].create({
|
||||||
{
|
'name': 'User Company 2',
|
||||||
"name": "User Company 2",
|
'login': 'user_c2',
|
||||||
"login": "user_c2",
|
'password': 'pass123',
|
||||||
"password": "pass123",
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
'company_ids': [(6, 0, [self.company2.id])],
|
||||||
"company_ids": [(6, 0, [self.company2.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear admin con acceso a ambas compañías
|
# Crear admin con acceso a ambas compañías
|
||||||
self.admin_user = self.env["res.users"].create(
|
self.admin_user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Admin Both',
|
||||||
"name": "Admin Both",
|
'login': 'admin_both',
|
||||||
"login": "admin_both",
|
'password': 'pass123',
|
||||||
"password": "pass123",
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'company_ids': [(6, 0, [self.company1.id, self.company2.id])],
|
||||||
"company_ids": [(6, 0, [self.company1.id, self.company2.id])],
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear grupos en cada compañía
|
# Crear grupos en cada compañía
|
||||||
self.group1 = self.env["res.partner"].create(
|
self.group1 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo Company 1',
|
||||||
"name": "Grupo Company 1",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo1@test.com',
|
||||||
"email": "grupo1@test.com",
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group2 = self.env["res.partner"].create(
|
self.group2 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo Company 2',
|
||||||
"name": "Grupo Company 2",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo2@test.com',
|
||||||
"email": "grupo2@test.com",
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear órdenes en cada compañía
|
# Crear órdenes en cada compañía
|
||||||
self.order1 = self.env["group.order"].create(
|
self.order1 = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Company 1',
|
||||||
"name": "Pedido Company 1",
|
'group_ids': [(6, 0, [self.group1.id])],
|
||||||
"group_ids": [(6, 0, [self.group1.id])],
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.order2 = self.env["group.order"].create(
|
self.order2 = self.env['group.order'].create({
|
||||||
{
|
'name': 'Pedido Company 2',
|
||||||
"name": "Pedido Company 2",
|
'group_ids': [(6, 0, [self.group2.id])],
|
||||||
"group_ids": [(6, 0, [self.group2.id])],
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': (datetime.now() + timedelta(days=7)).date(),
|
||||||
"end_date": (datetime.now() + timedelta(days=7)).date(),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_user_company1_can_read_own_orders(self):
|
def test_user_company1_can_read_own_orders(self):
|
||||||
"""Test que usuario de Company 1 puede leer sus propias órdenes."""
|
'''Test que usuario de Company 1 puede leer sus propias órdenes.'''
|
||||||
orders = (
|
orders = self.env['group.order'].with_user(
|
||||||
self.env["group.order"]
|
self.user_company1
|
||||||
.with_user(self.user_company1)
|
).search([('company_id', '=', self.company1.id)])
|
||||||
.search([("company_id", "=", self.company1.id)])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIn(self.order1, orders)
|
self.assertIn(self.order1, orders)
|
||||||
|
|
||||||
def test_user_company1_cannot_read_company2_orders(self):
|
def test_user_company1_cannot_read_company2_orders(self):
|
||||||
"""Test que usuario de Company 1 NO puede leer órdenes de Company 2."""
|
'''Test que usuario de Company 1 NO puede leer órdenes de Company 2.'''
|
||||||
orders = (
|
orders = self.env['group.order'].with_user(
|
||||||
self.env["group.order"]
|
self.user_company1
|
||||||
.with_user(self.user_company1)
|
).search([('company_id', '=', self.company2.id)])
|
||||||
.search([("company_id", "=", self.company2.id)])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertNotIn(self.order2, orders)
|
self.assertNotIn(self.order2, orders)
|
||||||
self.assertEqual(len(orders), 0)
|
self.assertEqual(len(orders), 0)
|
||||||
|
|
||||||
def test_admin_can_read_all_orders(self):
|
def test_admin_can_read_all_orders(self):
|
||||||
"""Test que admin con acceso a ambas compañías ve todas las órdenes."""
|
'''Test que admin con acceso a ambas compañías ve todas las órdenes.'''
|
||||||
orders = self.env["group.order"].with_user(self.admin_user).search([])
|
orders = self.env['group.order'].with_user(
|
||||||
|
self.admin_user
|
||||||
|
).search([])
|
||||||
|
|
||||||
self.assertIn(self.order1, orders)
|
self.assertIn(self.order1, orders)
|
||||||
self.assertIn(self.order2, orders)
|
self.assertIn(self.order2, orders)
|
||||||
|
|
||||||
def test_user_cannot_write_other_company_order(self):
|
def test_user_cannot_write_other_company_order(self):
|
||||||
"""Test que usuario no puede escribir en orden de otra compañía."""
|
'''Test que usuario no puede escribir en orden de otra compañía.'''
|
||||||
with self.assertRaises(AccessError):
|
with self.assertRaises(AccessError):
|
||||||
self.order2.with_user(self.user_company1).write(
|
self.order2.with_user(self.user_company1).write({
|
||||||
{
|
'name': 'Intentando cambiar nombre',
|
||||||
"name": "Intentando cambiar nombre",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_record_rule_filters_search(self):
|
def test_record_rule_filters_search(self):
|
||||||
"""Test que búsqueda automáticamente filtra por record rule."""
|
'''Test que búsqueda automáticamente filtra por record rule.'''
|
||||||
# Usuario de Company 1 busca todas las órdenes
|
# Usuario de Company 1 busca todas las órdenes
|
||||||
orders_c1 = (
|
orders_c1 = self.env['group.order'].with_user(
|
||||||
self.env["group.order"]
|
self.user_company1
|
||||||
.with_user(self.user_company1)
|
).search([('state', '=', 'draft')])
|
||||||
.search([("state", "=", "draft")])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Solo debe ver su orden
|
# Solo debe ver su orden
|
||||||
self.assertEqual(len(orders_c1), 1)
|
self.assertEqual(len(orders_c1), 1)
|
||||||
self.assertEqual(orders_c1[0], self.order1)
|
self.assertEqual(orders_c1[0], self.order1)
|
||||||
|
|
||||||
def test_cross_company_access_denied(self):
|
def test_cross_company_access_denied(self):
|
||||||
"""Test que acceso entre compañías es denegado."""
|
'''Test que acceso entre compañías es denegado.'''
|
||||||
# Usuario company1 intenta acceder a orden de company2
|
# Usuario company1 intenta acceder a orden de company2
|
||||||
with self.assertRaises(AccessError):
|
with self.assertRaises(AccessError):
|
||||||
self.order2.with_user(self.user_company1).read()
|
self.order2.with_user(self.user_company1).read()
|
||||||
|
|
||||||
def test_admin_can_bypass_company_restriction(self):
|
def test_admin_can_bypass_company_restriction(self):
|
||||||
"""Test que admin puede acceder a órdenes de cualquier compañía."""
|
'''Test que admin puede acceder a órdenes de cualquier compañía.'''
|
||||||
# Admin lee orden de company2 sin problema
|
# Admin lee orden de company2 sin problema
|
||||||
order2_admin = self.order2.with_user(self.admin_user)
|
order2_admin = self.order2.with_user(self.admin_user)
|
||||||
self.assertEqual(order2_admin.name, "Pedido Company 2")
|
self.assertEqual(order2_admin.name, 'Pedido Company 2')
|
||||||
self.assertEqual(order2_admin.company_id, self.company2)
|
self.assertEqual(order2_admin.company_id, self.company2)
|
||||||
|
|
|
||||||
|
|
@ -5,38 +5,32 @@ from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
class TestResPartnerExtension(TransactionCase):
|
class TestResPartnerExtension(TransactionCase):
|
||||||
"""Test suite para la extensión res.partner (user-group relationship)."""
|
'''Test suite para la extensión res.partner (user-group relationship).'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# Crear grupos (res.partner with is_company=True)
|
# Crear grupos (res.partner with is_company=True)
|
||||||
self.group1 = self.env["res.partner"].create(
|
self.group1 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo 1',
|
||||||
"name": "Grupo 1",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo1@test.com',
|
||||||
"email": "grupo1@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group2 = self.env["res.partner"].create(
|
self.group2 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Grupo 2',
|
||||||
"name": "Grupo 2",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'grupo2@test.com',
|
||||||
"email": "grupo2@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear usuario
|
# Crear usuario
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_partner_can_belong_to_groups(self):
|
def test_partner_can_belong_to_groups(self):
|
||||||
"""Test que un partner (usuario) puede pertenecer a múltiples grupos."""
|
'''Test que un partner (usuario) puede pertenecer a múltiples grupos.'''
|
||||||
partner = self.user.partner_id
|
partner = self.user.partner_id
|
||||||
|
|
||||||
# Agregar partner a grupos (usar campo member_ids)
|
# Agregar partner a grupos (usar campo member_ids)
|
||||||
|
|
@ -48,14 +42,12 @@ class TestResPartnerExtension(TransactionCase):
|
||||||
self.assertEqual(len(partner.member_ids), 2)
|
self.assertEqual(len(partner.member_ids), 2)
|
||||||
|
|
||||||
def test_group_can_have_multiple_users(self):
|
def test_group_can_have_multiple_users(self):
|
||||||
"""Test que un grupo puede tener múltiples usuarios."""
|
'''Test que un grupo puede tener múltiples usuarios.'''
|
||||||
user2 = self.env["res.users"].create(
|
user2 = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User 2',
|
||||||
"name": "Test User 2",
|
'login': 'testuser2@test.com',
|
||||||
"login": "testuser2@test.com",
|
'email': 'testuser2@test.com',
|
||||||
"email": "testuser2@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Agregar usuarios al grupo
|
# Agregar usuarios al grupo
|
||||||
self.group1.user_ids = [(6, 0, [self.user.id, user2.id])]
|
self.group1.user_ids = [(6, 0, [self.user.id, user2.id])]
|
||||||
|
|
@ -66,7 +58,7 @@ class TestResPartnerExtension(TransactionCase):
|
||||||
self.assertEqual(len(self.group1.user_ids), 2)
|
self.assertEqual(len(self.group1.user_ids), 2)
|
||||||
|
|
||||||
def test_user_group_relationship_is_bidirectional(self):
|
def test_user_group_relationship_is_bidirectional(self):
|
||||||
"""Test que se puede modificar la relación desde el lado del partner o el grupo."""
|
'''Test que se puede modificar la relación desde el lado del partner o el grupo.'''
|
||||||
partner = self.user.partner_id
|
partner = self.user.partner_id
|
||||||
|
|
||||||
# Opción 1: Agregar grupo al usuario (desde el lado del usuario/partner)
|
# Opción 1: Agregar grupo al usuario (desde el lado del usuario/partner)
|
||||||
|
|
@ -75,18 +67,16 @@ class TestResPartnerExtension(TransactionCase):
|
||||||
|
|
||||||
# Opción 2: Agregar usuario al grupo (desde el lado del grupo)
|
# Opción 2: Agregar usuario al grupo (desde el lado del grupo)
|
||||||
# Nota: Esto es una relación Many2many independiente
|
# Nota: Esto es una relación Many2many independiente
|
||||||
user2 = self.env["res.users"].create(
|
user2 = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User 2',
|
||||||
"name": "Test User 2",
|
'login': 'testuser2@test.com',
|
||||||
"login": "testuser2@test.com",
|
'email': 'testuser2@test.com',
|
||||||
"email": "testuser2@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.group2.user_ids = [(6, 0, [user2.id])]
|
self.group2.user_ids = [(6, 0, [user2.id])]
|
||||||
self.assertIn(user2, self.group2.user_ids)
|
self.assertIn(user2, self.group2.user_ids)
|
||||||
|
|
||||||
def test_empty_group_ids(self):
|
def test_empty_group_ids(self):
|
||||||
"""Test que un partner sin grupos tiene group_ids vacío."""
|
'''Test que un partner sin grupos tiene group_ids vacío.'''
|
||||||
partner = self.user.partner_id
|
partner = self.user.partner_id
|
||||||
|
|
||||||
# Sin agregar a ningún grupo
|
# Sin agregar a ningún grupo
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ draft sale orders.
|
||||||
See: website_sale_aplicoop/controllers/website_sale.py
|
See: website_sale_aplicoop/controllers/website_sale.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
@ -24,72 +23,60 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Create a consumer group
|
# Create a consumer group
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
'email': 'group@test.com',
|
||||||
"email": "group@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a group member (user partner)
|
# Create a group member (user partner)
|
||||||
self.member_partner = self.env["res.partner"].create(
|
self.member_partner = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Member Partner',
|
||||||
"name": "Group Member Partner",
|
'email': 'member@test.com',
|
||||||
"email": "member@test.com",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add member to group
|
# Add member to group
|
||||||
self.group.member_ids = [(4, self.member_partner.id)]
|
self.group.member_ids = [(4, self.member_partner.id)]
|
||||||
|
|
||||||
# Create test user
|
# Create test user
|
||||||
self.user = self.env["res.users"].create(
|
self.user = self.env['res.users'].create({
|
||||||
{
|
'name': 'Test User',
|
||||||
"name": "Test User",
|
'login': 'testuser@test.com',
|
||||||
"login": "testuser@test.com",
|
'email': 'testuser@test.com',
|
||||||
"email": "testuser@test.com",
|
'partner_id': self.member_partner.id,
|
||||||
"partner_id": self.member_partner.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a group order
|
# Create a group order
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
end_date = start_date + timedelta(days=7)
|
end_date = start_date + timedelta(days=7)
|
||||||
|
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Group Order',
|
||||||
"name": "Test Group Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': end_date,
|
||||||
"end_date": end_date,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3', # Wednesday
|
||||||
"pickup_day": "3", # Wednesday
|
'pickup_date': start_date + timedelta(days=3),
|
||||||
"pickup_date": start_date + timedelta(days=3),
|
'home_delivery': False,
|
||||||
"home_delivery": False,
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Open the group order
|
# Open the group order
|
||||||
self.group_order.action_open()
|
self.group_order.action_open()
|
||||||
|
|
||||||
# Create products for the order
|
# Create products for the order
|
||||||
self.category = self.env["product.category"].create(
|
self.category = self.env['product.category'].create({
|
||||||
{
|
'name': 'Test Category',
|
||||||
"name": "Test Category",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
'categ_id': self.category.id,
|
||||||
"categ_id": self.category.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Associate product with group order
|
# Associate product with group order
|
||||||
self.group_order.product_ids = [(4, self.product.id)]
|
self.group_order.product_ids = [(4, self.product.id)]
|
||||||
|
|
@ -103,16 +90,16 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
"""
|
"""
|
||||||
# Simulate what the controller does: create order with group_order_id
|
# Simulate what the controller does: create order with group_order_id
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"home_delivery": self.group_order.home_delivery,
|
'home_delivery': self.group_order.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify the order was created with group_order_id
|
# Verify the order was created with group_order_id
|
||||||
self.assertIsNotNone(sale_order.id)
|
self.assertIsNotNone(sale_order.id)
|
||||||
|
|
@ -122,34 +109,34 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
def test_save_eskaera_draft_propagates_pickup_day(self):
|
def test_save_eskaera_draft_propagates_pickup_day(self):
|
||||||
"""Test that save_eskaera_draft() propagates pickup_day correctly."""
|
"""Test that save_eskaera_draft() propagates pickup_day correctly."""
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"home_delivery": self.group_order.home_delivery,
|
'home_delivery': self.group_order.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify pickup_day was propagated
|
# Verify pickup_day was propagated
|
||||||
self.assertEqual(sale_order.pickup_day, "3")
|
self.assertEqual(sale_order.pickup_day, '3')
|
||||||
self.assertEqual(sale_order.pickup_day, self.group_order.pickup_day)
|
self.assertEqual(sale_order.pickup_day, self.group_order.pickup_day)
|
||||||
|
|
||||||
def test_save_eskaera_draft_propagates_pickup_date(self):
|
def test_save_eskaera_draft_propagates_pickup_date(self):
|
||||||
"""Test that save_eskaera_draft() propagates pickup_date correctly."""
|
"""Test that save_eskaera_draft() propagates pickup_date correctly."""
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"home_delivery": self.group_order.home_delivery,
|
'home_delivery': self.group_order.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify pickup_date was propagated
|
# Verify pickup_date was propagated
|
||||||
self.assertEqual(sale_order.pickup_date, self.group_order.pickup_date)
|
self.assertEqual(sale_order.pickup_date, self.group_order.pickup_date)
|
||||||
|
|
@ -157,35 +144,33 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
def test_save_eskaera_draft_propagates_home_delivery(self):
|
def test_save_eskaera_draft_propagates_home_delivery(self):
|
||||||
"""Test that save_eskaera_draft() propagates home_delivery correctly."""
|
"""Test that save_eskaera_draft() propagates home_delivery correctly."""
|
||||||
# Create a group order with home_delivery=True
|
# Create a group order with home_delivery=True
|
||||||
group_order_home = self.env["group.order"].create(
|
group_order_home = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Group Order with Home Delivery',
|
||||||
"name": "Test Group Order with Home Delivery",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date(),
|
||||||
"start_date": datetime.now().date(),
|
'end_date': datetime.now().date() + timedelta(days=7),
|
||||||
"end_date": datetime.now().date() + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'pickup_date': datetime.now().date() + timedelta(days=3),
|
||||||
"pickup_date": datetime.now().date() + timedelta(days=3),
|
'home_delivery': True, # Enable home delivery
|
||||||
"home_delivery": True, # Enable home delivery
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
group_order_home.action_open()
|
group_order_home.action_open()
|
||||||
|
|
||||||
# Test with home_delivery=True
|
# Test with home_delivery=True
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": group_order_home.id,
|
'group_order_id': group_order_home.id,
|
||||||
"pickup_day": group_order_home.pickup_day,
|
'pickup_day': group_order_home.pickup_day,
|
||||||
"pickup_date": group_order_home.pickup_date,
|
'pickup_date': group_order_home.pickup_date,
|
||||||
"home_delivery": group_order_home.home_delivery,
|
'home_delivery': group_order_home.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify home_delivery was propagated
|
# Verify home_delivery was propagated
|
||||||
self.assertTrue(sale_order.home_delivery)
|
self.assertTrue(sale_order.home_delivery)
|
||||||
|
|
@ -194,19 +179,19 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
def test_save_eskaera_draft_order_is_draft_state(self):
|
def test_save_eskaera_draft_order_is_draft_state(self):
|
||||||
"""Test that save_eskaera_draft() creates order in draft state."""
|
"""Test that save_eskaera_draft() creates order in draft state."""
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"home_delivery": self.group_order.home_delivery,
|
'home_delivery': self.group_order.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify order is in draft state
|
# Verify order is in draft state
|
||||||
self.assertEqual(sale_order.state, "draft")
|
self.assertEqual(sale_order.state, 'draft')
|
||||||
|
|
||||||
def test_save_eskaera_draft_multiple_fields_together(self):
|
def test_save_eskaera_draft_multiple_fields_together(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -216,23 +201,23 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
all group_order-related fields are propagated together.
|
all group_order-related fields are propagated together.
|
||||||
"""
|
"""
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"home_delivery": self.group_order.home_delivery,
|
'home_delivery': self.group_order.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify all fields together
|
# Verify all fields together
|
||||||
self.assertEqual(sale_order.group_order_id.id, self.group_order.id)
|
self.assertEqual(sale_order.group_order_id.id, self.group_order.id)
|
||||||
self.assertEqual(sale_order.pickup_day, self.group_order.pickup_day)
|
self.assertEqual(sale_order.pickup_day, self.group_order.pickup_day)
|
||||||
self.assertEqual(sale_order.pickup_date, self.group_order.pickup_date)
|
self.assertEqual(sale_order.pickup_date, self.group_order.pickup_date)
|
||||||
self.assertEqual(sale_order.home_delivery, self.group_order.home_delivery)
|
self.assertEqual(sale_order.home_delivery, self.group_order.home_delivery)
|
||||||
self.assertEqual(sale_order.state, "draft")
|
self.assertEqual(sale_order.state, 'draft')
|
||||||
|
|
||||||
def test_save_cart_draft_also_saves_group_order_id(self):
|
def test_save_cart_draft_also_saves_group_order_id(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -243,16 +228,16 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
"""
|
"""
|
||||||
# save_cart_draft should also include group_order_id
|
# save_cart_draft should also include group_order_id
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"pickup_date": self.group_order.pickup_date,
|
'pickup_date': self.group_order.pickup_date,
|
||||||
"home_delivery": self.group_order.home_delivery,
|
'home_delivery': self.group_order.home_delivery,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify all fields
|
# Verify all fields
|
||||||
self.assertEqual(sale_order.group_order_id.id, self.group_order.id)
|
self.assertEqual(sale_order.group_order_id.id, self.group_order.id)
|
||||||
|
|
@ -267,13 +252,13 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
sale orders without associating them to a group order.
|
sale orders without associating them to a group order.
|
||||||
"""
|
"""
|
||||||
order_vals = {
|
order_vals = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
# No group_order_id
|
# No group_order_id
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order = self.env["sale.order"].create(order_vals)
|
sale_order = self.env['sale.order'].create(order_vals)
|
||||||
|
|
||||||
# Verify order was created without group_order_id
|
# Verify order was created without group_order_id
|
||||||
self.assertIsNotNone(sale_order.id)
|
self.assertIsNotNone(sale_order.id)
|
||||||
|
|
@ -286,13 +271,13 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
This is a sanity check to ensure the field is properly defined in the model.
|
This is a sanity check to ensure the field is properly defined in the model.
|
||||||
"""
|
"""
|
||||||
# Verify the field exists in the model
|
# Verify the field exists in the model
|
||||||
sale_order_model = self.env["sale.order"]
|
sale_order_model = self.env['sale.order']
|
||||||
self.assertIn("group_order_id", sale_order_model._fields)
|
self.assertIn('group_order_id', sale_order_model._fields)
|
||||||
|
|
||||||
# Verify it's a Many2one field
|
# Verify it's a Many2one field
|
||||||
field = sale_order_model._fields["group_order_id"]
|
field = sale_order_model._fields['group_order_id']
|
||||||
self.assertEqual(field.type, "many2one")
|
self.assertEqual(field.type, 'many2one')
|
||||||
self.assertEqual(field.comodel_name, "group.order")
|
self.assertEqual(field.comodel_name, 'group.order')
|
||||||
|
|
||||||
def test_different_group_orders_map_to_different_sale_orders(self):
|
def test_different_group_orders_map_to_different_sale_orders(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -302,44 +287,42 @@ class TestSaveOrderEndpoints(TransactionCase):
|
||||||
don't accidentally share the same sale.order.
|
don't accidentally share the same sale.order.
|
||||||
"""
|
"""
|
||||||
# Create a second group order
|
# Create a second group order
|
||||||
group_order_2 = self.env["group.order"].create(
|
group_order_2 = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Group Order 2',
|
||||||
"name": "Test Group Order 2",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': datetime.now().date() + timedelta(days=10),
|
||||||
"start_date": datetime.now().date() + timedelta(days=10),
|
'end_date': datetime.now().date() + timedelta(days=17),
|
||||||
"end_date": datetime.now().date() + timedelta(days=17),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '5',
|
||||||
"pickup_day": "5",
|
'pickup_date': datetime.now().date() + timedelta(days=12),
|
||||||
"pickup_date": datetime.now().date() + timedelta(days=12),
|
'home_delivery': True,
|
||||||
"home_delivery": True,
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
group_order_2.action_open()
|
group_order_2.action_open()
|
||||||
|
|
||||||
# Create order for first group order
|
# Create order for first group order
|
||||||
order_vals_1 = {
|
order_vals_1 = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": self.group_order.id,
|
'group_order_id': self.group_order.id,
|
||||||
"pickup_day": self.group_order.pickup_day,
|
'pickup_day': self.group_order.pickup_day,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order_1 = self.env["sale.order"].create(order_vals_1)
|
sale_order_1 = self.env['sale.order'].create(order_vals_1)
|
||||||
|
|
||||||
# Create order for second group order
|
# Create order for second group order
|
||||||
order_vals_2 = {
|
order_vals_2 = {
|
||||||
"partner_id": self.member_partner.id,
|
'partner_id': self.member_partner.id,
|
||||||
"group_order_id": group_order_2.id,
|
'group_order_id': group_order_2.id,
|
||||||
"pickup_day": group_order_2.pickup_day,
|
'pickup_day': group_order_2.pickup_day,
|
||||||
"order_line": [],
|
'order_line': [],
|
||||||
"state": "draft",
|
'state': 'draft',
|
||||||
}
|
}
|
||||||
|
|
||||||
sale_order_2 = self.env["sale.order"].create(order_vals_2)
|
sale_order_2 = self.env['sale.order'].create(order_vals_2)
|
||||||
|
|
||||||
# Verify they're different orders with different group_order_ids
|
# Verify they're different orders with different group_order_ids
|
||||||
self.assertNotEqual(sale_order_1.id, sale_order_2.id)
|
self.assertNotEqual(sale_order_1.id, sale_order_2.id)
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,77 @@
|
||||||
# Copyright 2025 Criptomart
|
# Copyright 2025 Criptomart
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
from datetime import timedelta
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
|
||||||
from odoo import _
|
from odoo import _
|
||||||
from odoo.tests.common import TransactionCase
|
|
||||||
from odoo.tests.common import tagged
|
|
||||||
|
|
||||||
|
|
||||||
@tagged("post_install", "-at_install")
|
@tagged('post_install', '-at_install')
|
||||||
class TestTemplatesRendering(TransactionCase):
|
class TestTemplatesRendering(TransactionCase):
|
||||||
"""Test suite to verify QWeb templates work with day_names context.
|
'''Test suite to verify QWeb templates work with day_names context.
|
||||||
|
|
||||||
This test covers the fix for the issue where _() function calls
|
This test covers the fix for the issue where _() function calls
|
||||||
in QWeb t-value attributes caused TypeError: 'NoneType' object is not callable.
|
in QWeb t-value attributes caused TypeError: 'NoneType' object is not callable.
|
||||||
The fix moves day_names definition to Python controller and passes it as context.
|
The fix moves day_names definition to Python controller and passes it as context.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up test data: create a test group order."""
|
'''Set up test data: create a test group order.'''
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Create a test supplier
|
# Create a test supplier
|
||||||
self.supplier = self.env["res.partner"].create(
|
self.supplier = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Supplier',
|
||||||
"name": "Test Supplier",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create test products
|
# Create test products
|
||||||
self.product = self.env["product.product"].create(
|
self.product = self.env['product.product'].create({
|
||||||
{
|
'name': 'Test Product',
|
||||||
"name": "Test Product",
|
'type': 'consu', # consumable (consu), service, or storable
|
||||||
"type": "consu", # consumable (consu), service, or storable
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a test group
|
# Create a test group
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a group order
|
# Create a group order
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'state': 'open',
|
||||||
"state": "open",
|
'supplier_ids': [(6, 0, [self.supplier.id])],
|
||||||
"supplier_ids": [(6, 0, [self.supplier.id])],
|
'product_ids': [(6, 0, [self.product.id])],
|
||||||
"product_ids": [(6, 0, [self.product.id])],
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'start_date': date.today(),
|
||||||
"start_date": date.today(),
|
'end_date': date.today() + timedelta(days=7),
|
||||||
"end_date": date.today() + timedelta(days=7),
|
'pickup_day': '5', # Saturday
|
||||||
"pickup_day": "5", # Saturday
|
'cutoff_day': '3', # Thursday
|
||||||
"cutoff_day": "3", # Thursday
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_eskaera_page_template_exists(self):
|
def test_eskaera_page_template_exists(self):
|
||||||
"""Test that eskaera_page template compiles without errors."""
|
'''Test that eskaera_page template compiles without errors.'''
|
||||||
template = self.env.ref("website_sale_aplicoop.eskaera_page")
|
template = self.env.ref('website_sale_aplicoop.eskaera_page')
|
||||||
self.assertIsNotNone(template)
|
self.assertIsNotNone(template)
|
||||||
self.assertEqual(template.type, "qweb")
|
self.assertEqual(template.type, 'qweb')
|
||||||
|
|
||||||
def test_eskaera_shop_template_exists(self):
|
def test_eskaera_shop_template_exists(self):
|
||||||
"""Test that eskaera_shop template compiles without errors."""
|
'''Test that eskaera_shop template compiles without errors.'''
|
||||||
template = self.env.ref("website_sale_aplicoop.eskaera_shop")
|
template = self.env.ref('website_sale_aplicoop.eskaera_shop')
|
||||||
self.assertIsNotNone(template)
|
self.assertIsNotNone(template)
|
||||||
self.assertEqual(template.type, "qweb")
|
self.assertEqual(template.type, 'qweb')
|
||||||
|
|
||||||
def test_eskaera_checkout_template_exists(self):
|
def test_eskaera_checkout_template_exists(self):
|
||||||
"""Test that eskaera_checkout template compiles without errors."""
|
'''Test that eskaera_checkout template compiles without errors.'''
|
||||||
template = self.env.ref("website_sale_aplicoop.eskaera_checkout")
|
template = self.env.ref('website_sale_aplicoop.eskaera_checkout')
|
||||||
self.assertIsNotNone(template)
|
self.assertIsNotNone(template)
|
||||||
self.assertEqual(template.type, "qweb")
|
self.assertEqual(template.type, 'qweb')
|
||||||
|
|
||||||
def test_day_names_context_is_provided(self):
|
def test_day_names_context_is_provided(self):
|
||||||
"""Test that day_names context is provided by the controller method."""
|
'''Test that day_names context is provided by the controller method.'''
|
||||||
# Simulate what the controller does, passing env for test context
|
# Simulate what the controller does, passing env for test context
|
||||||
from odoo.addons.website_sale_aplicoop.controllers.website_sale import (
|
from odoo.addons.website_sale_aplicoop.controllers.website_sale import AplicoopWebsiteSale
|
||||||
AplicoopWebsiteSale,
|
|
||||||
)
|
|
||||||
|
|
||||||
controller = AplicoopWebsiteSale()
|
controller = AplicoopWebsiteSale()
|
||||||
day_names = controller._get_day_names(env=self.env)
|
day_names = controller._get_day_names(env=self.env)
|
||||||
|
|
@ -99,61 +86,45 @@ class TestTemplatesRendering(TransactionCase):
|
||||||
self.assertGreater(len(day_name), 0, f"Day at index {i} is empty string")
|
self.assertGreater(len(day_name), 0, f"Day at index {i} is empty string")
|
||||||
|
|
||||||
def test_day_names_not_using_inline_underscore(self):
|
def test_day_names_not_using_inline_underscore(self):
|
||||||
"""Test that day_names are defined in Python, not in t-value attributes.
|
'''Test that day_names are defined in Python, not in t-value attributes.
|
||||||
|
|
||||||
This test ensures the fix has been applied:
|
This test ensures the fix has been applied:
|
||||||
- day_names MUST be passed from controller context
|
- day_names MUST be passed from controller context
|
||||||
- day_names MUST NOT be defined with _() inside t-value attributes
|
- day_names MUST NOT be defined with _() inside t-value attributes
|
||||||
- Templates use day_names[index] from context, not t-set with _()
|
- Templates use day_names[index] from context, not t-set with _()
|
||||||
"""
|
'''
|
||||||
template = self.env.ref("website_sale_aplicoop.eskaera_page")
|
template = self.env.ref('website_sale_aplicoop.eskaera_page')
|
||||||
# Read the template source to verify it doesn't have inline _() in t-value
|
# Read the template source to verify it doesn't have inline _() in t-value
|
||||||
self.assertIn(
|
self.assertIn('day_names', template.arch_db,
|
||||||
"day_names",
|
"Template must reference day_names from context")
|
||||||
template.arch_db,
|
|
||||||
"Template must reference day_names from context",
|
|
||||||
)
|
|
||||||
# The fix ensures no <t t-set="day_names" t-value="[_(...)]"/> exists
|
# The fix ensures no <t t-set="day_names" t-value="[_(...)]"/> exists
|
||||||
# which was causing the NoneType error
|
# which was causing the NoneType error
|
||||||
|
|
||||||
def test_eskaera_checkout_summary_template_exists(self):
|
def test_eskaera_checkout_summary_template_exists(self):
|
||||||
"""Test that eskaera_checkout_summary sub-template exists."""
|
'''Test that eskaera_checkout_summary sub-template exists.'''
|
||||||
template = self.env.ref("website_sale_aplicoop.eskaera_checkout_summary")
|
template = self.env.ref('website_sale_aplicoop.eskaera_checkout_summary')
|
||||||
self.assertIsNotNone(template)
|
self.assertIsNotNone(template)
|
||||||
self.assertEqual(template.type, "qweb")
|
self.assertEqual(template.type, 'qweb')
|
||||||
# Verify it has the expected structure
|
# Verify it has the expected structure
|
||||||
self.assertIn(
|
self.assertIn('checkout-summary-table', template.arch_db,
|
||||||
"checkout-summary-table",
|
"Template must have checkout-summary-table id")
|
||||||
template.arch_db,
|
self.assertIn('Product', template.arch_db,
|
||||||
"Template must have checkout-summary-table id",
|
"Template must have Product label for translation")
|
||||||
)
|
self.assertIn('Quantity', template.arch_db,
|
||||||
self.assertIn(
|
"Template must have Quantity label for translation")
|
||||||
"Product",
|
self.assertIn('Price', template.arch_db,
|
||||||
template.arch_db,
|
"Template must have Price label for translation")
|
||||||
"Template must have Product label for translation",
|
self.assertIn('Subtotal', template.arch_db,
|
||||||
)
|
"Template must have Subtotal label for translation")
|
||||||
self.assertIn(
|
|
||||||
"Quantity",
|
|
||||||
template.arch_db,
|
|
||||||
"Template must have Quantity label for translation",
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Price", template.arch_db, "Template must have Price label for translation"
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Subtotal",
|
|
||||||
template.arch_db,
|
|
||||||
"Template must have Subtotal label for translation",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_eskaera_checkout_summary_renders(self):
|
def test_eskaera_checkout_summary_renders(self):
|
||||||
"""Test that eskaera_checkout_summary renders without errors."""
|
'''Test that eskaera_checkout_summary renders without errors.'''
|
||||||
template = self.env.ref("website_sale_aplicoop.eskaera_checkout_summary")
|
template = self.env.ref('website_sale_aplicoop.eskaera_checkout_summary')
|
||||||
# Render the template with empty context
|
# Render the template with empty context
|
||||||
html = template._render_template(template.xml_id, {})
|
html = template._render_template(template.xml_id, {})
|
||||||
# Should contain the basic table structure
|
# Should contain the basic table structure
|
||||||
self.assertIn("<table", html)
|
self.assertIn('<table', html)
|
||||||
self.assertIn("checkout-summary-table", html)
|
self.assertIn('checkout-summary-table', html)
|
||||||
self.assertIn("Product", html)
|
self.assertIn('Product', html)
|
||||||
self.assertIn("Quantity", html)
|
self.assertIn('Quantity', html)
|
||||||
self.assertIn("This order's cart is empty", html)
|
self.assertIn("This order's cart is empty", html)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,10 @@ Coverage:
|
||||||
- group.order state transitions: illegal transitions
|
- group.order state transitions: illegal transitions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from odoo.exceptions import UserError
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError, UserError
|
||||||
|
|
||||||
|
|
||||||
class TestGroupOrderValidations(TransactionCase):
|
class TestGroupOrderValidations(TransactionCase):
|
||||||
|
|
@ -27,27 +25,21 @@ class TestGroupOrderValidations(TransactionCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.company1 = self.env.company
|
self.company1 = self.env.company
|
||||||
self.company2 = self.env["res.company"].create(
|
self.company2 = self.env['res.company'].create({
|
||||||
{
|
'name': 'Company 2',
|
||||||
"name": "Company 2",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group_c1 = self.env["res.partner"].create(
|
self.group_c1 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Company 1',
|
||||||
"name": "Group Company 1",
|
'is_company': True,
|
||||||
"is_company": True,
|
'company_id': self.company1.id,
|
||||||
"company_id": self.company1.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.group_c2 = self.env["res.partner"].create(
|
self.group_c2 = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Group Company 2',
|
||||||
"name": "Group Company 2",
|
'is_company': True,
|
||||||
"is_company": True,
|
'company_id': self.company2.id,
|
||||||
"company_id": self.company2.id,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_group_order_same_company_constraint(self):
|
def test_group_order_same_company_constraint(self):
|
||||||
"""Test that all groups in an order must be from same company."""
|
"""Test that all groups in an order must be from same company."""
|
||||||
|
|
@ -55,36 +47,32 @@ class TestGroupOrderValidations(TransactionCase):
|
||||||
|
|
||||||
# Creating order with groups from different companies should fail
|
# Creating order with groups from different companies should fail
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.env["group.order"].create(
|
self.env['group.order'].create({
|
||||||
{
|
'name': 'Multi-Company Order',
|
||||||
"name": "Multi-Company Order",
|
'group_ids': [(6, 0, [self.group_c1.id, self.group_c2.id])],
|
||||||
"group_ids": [(6, 0, [self.group_c1.id, self.group_c2.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_group_order_same_company_mixed_single(self):
|
def test_group_order_same_company_mixed_single(self):
|
||||||
"""Test that single company group is valid."""
|
"""Test that single company group is valid."""
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
|
|
||||||
# Single company should pass
|
# Single company should pass
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Single Company Order',
|
||||||
"name": "Single Company Order",
|
'group_ids': [(6, 0, [self.group_c1.id])],
|
||||||
"group_ids": [(6, 0, [self.group_c1.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
def test_group_order_date_validation_start_after_end(self):
|
def test_group_order_date_validation_start_after_end(self):
|
||||||
|
|
@ -93,35 +81,31 @@ class TestGroupOrderValidations(TransactionCase):
|
||||||
end_date = start_date - timedelta(days=1) # End before start
|
end_date = start_date - timedelta(days=1) # End before start
|
||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
self.env["group.order"].create(
|
self.env['group.order'].create({
|
||||||
{
|
'name': 'Bad Dates Order',
|
||||||
"name": "Bad Dates Order",
|
'group_ids': [(6, 0, [self.group_c1.id])],
|
||||||
"group_ids": [(6, 0, [self.group_c1.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': end_date,
|
||||||
"end_date": end_date,
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_group_order_date_validation_same_date(self):
|
def test_group_order_date_validation_same_date(self):
|
||||||
"""Test that start_date == end_date is allowed (single-day order)."""
|
"""Test that start_date == end_date is allowed (single-day order)."""
|
||||||
same_date = datetime.now().date()
|
same_date = datetime.now().date()
|
||||||
|
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Same Day Order',
|
||||||
"name": "Same Day Order",
|
'group_ids': [(6, 0, [self.group_c1.id])],
|
||||||
"group_ids": [(6, 0, [self.group_c1.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': same_date,
|
||||||
"start_date": same_date,
|
'end_date': same_date,
|
||||||
"end_date": same_date,
|
'period': 'once',
|
||||||
"period": "once",
|
'pickup_day': '0',
|
||||||
"pickup_day": "0",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertTrue(order.exists())
|
self.assertTrue(order.exists())
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -130,31 +114,27 @@ class TestGroupOrderImageFallback(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_image_fallback_order_image_first(self):
|
def test_image_fallback_order_image_first(self):
|
||||||
"""Test that order image takes priority over group image."""
|
"""Test that order image takes priority over group image."""
|
||||||
# Set both order and group image
|
# Set both order and group image
|
||||||
test_image = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
test_image = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
|
||||||
|
|
||||||
self.group_order.image_1920 = test_image
|
self.group_order.image_1920 = test_image
|
||||||
self.group.image_1920 = test_image
|
self.group.image_1920 = test_image
|
||||||
|
|
@ -165,7 +145,7 @@ class TestGroupOrderImageFallback(TransactionCase):
|
||||||
|
|
||||||
def test_image_fallback_group_image_when_no_order_image(self):
|
def test_image_fallback_group_image_when_no_order_image(self):
|
||||||
"""Test fallback to group image when order has no image."""
|
"""Test fallback to group image when order has no image."""
|
||||||
test_image = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
test_image = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
|
||||||
|
|
||||||
# Only set group image
|
# Only set group image
|
||||||
self.group_order.image_1920 = False
|
self.group_order.image_1920 = False
|
||||||
|
|
@ -191,42 +171,34 @@ class TestGroupOrderProductCount(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.group_order = self.env["group.order"].create(
|
self.group_order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product1 = self.env["product.product"].create(
|
self.product1 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product 1',
|
||||||
"name": "Product 1",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 10.0,
|
||||||
"list_price": 10.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.product2 = self.env["product.product"].create(
|
self.product2 = self.env['product.product'].create({
|
||||||
{
|
'name': 'Product 2',
|
||||||
"name": "Product 2",
|
'type': 'consu',
|
||||||
"type": "consu",
|
'list_price': 20.0,
|
||||||
"list_price": 20.0,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_product_count_initial_zero(self):
|
def test_product_count_initial_zero(self):
|
||||||
"""Test that new order has zero products."""
|
"""Test that new order has zero products."""
|
||||||
|
|
@ -260,31 +232,27 @@ class TestStateTransitions(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
self.order = self.env["group.order"].create(
|
self.order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_illegal_transition_draft_to_closed(self):
|
def test_illegal_transition_draft_to_closed(self):
|
||||||
"""Test that Draft -> Closed transition is not allowed."""
|
"""Test that Draft -> Closed transition is not allowed."""
|
||||||
# Should not allow skipping Open state
|
# Should not allow skipping Open state
|
||||||
self.assertEqual(self.order.state, "draft")
|
self.assertEqual(self.order.state, 'draft')
|
||||||
|
|
||||||
# Calling action_close() without action_open() should fail
|
# Calling action_close() without action_open() should fail
|
||||||
with self.assertRaises((ValidationError, UserError)):
|
with self.assertRaises((ValidationError, UserError)):
|
||||||
|
|
@ -293,7 +261,7 @@ class TestStateTransitions(TransactionCase):
|
||||||
def test_illegal_transition_cancelled_to_open(self):
|
def test_illegal_transition_cancelled_to_open(self):
|
||||||
"""Test that Cancelled -> Open transition is not allowed."""
|
"""Test that Cancelled -> Open transition is not allowed."""
|
||||||
self.order.action_cancel()
|
self.order.action_cancel()
|
||||||
self.assertEqual(self.order.state, "cancelled")
|
self.assertEqual(self.order.state, 'cancelled')
|
||||||
|
|
||||||
# Should not allow re-opening cancelled order
|
# Should not allow re-opening cancelled order
|
||||||
with self.assertRaises((ValidationError, UserError)):
|
with self.assertRaises((ValidationError, UserError)):
|
||||||
|
|
@ -301,28 +269,28 @@ class TestStateTransitions(TransactionCase):
|
||||||
|
|
||||||
def test_legal_transition_draft_open_closed(self):
|
def test_legal_transition_draft_open_closed(self):
|
||||||
"""Test that Draft -> Open -> Closed is allowed."""
|
"""Test that Draft -> Open -> Closed is allowed."""
|
||||||
self.assertEqual(self.order.state, "draft")
|
self.assertEqual(self.order.state, 'draft')
|
||||||
|
|
||||||
self.order.action_open()
|
self.order.action_open()
|
||||||
self.assertEqual(self.order.state, "open")
|
self.assertEqual(self.order.state, 'open')
|
||||||
|
|
||||||
self.order.action_close()
|
self.order.action_close()
|
||||||
self.assertEqual(self.order.state, "closed")
|
self.assertEqual(self.order.state, 'closed')
|
||||||
|
|
||||||
def test_transition_draft_to_cancelled(self):
|
def test_transition_draft_to_cancelled(self):
|
||||||
"""Test that Draft -> Cancelled is allowed."""
|
"""Test that Draft -> Cancelled is allowed."""
|
||||||
self.assertEqual(self.order.state, "draft")
|
self.assertEqual(self.order.state, 'draft')
|
||||||
|
|
||||||
self.order.action_cancel()
|
self.order.action_cancel()
|
||||||
self.assertEqual(self.order.state, "cancelled")
|
self.assertEqual(self.order.state, 'cancelled')
|
||||||
|
|
||||||
def test_transition_open_to_cancelled(self):
|
def test_transition_open_to_cancelled(self):
|
||||||
"""Test that Open -> Cancelled is allowed (emergency stop)."""
|
"""Test that Open -> Cancelled is allowed (emergency stop)."""
|
||||||
self.order.action_open()
|
self.order.action_open()
|
||||||
self.assertEqual(self.order.state, "open")
|
self.assertEqual(self.order.state, 'open')
|
||||||
|
|
||||||
self.order.action_cancel()
|
self.order.action_cancel()
|
||||||
self.assertEqual(self.order.state, "cancelled")
|
self.assertEqual(self.order.state, 'cancelled')
|
||||||
|
|
||||||
|
|
||||||
class TestUserPartnerValidation(TransactionCase):
|
class TestUserPartnerValidation(TransactionCase):
|
||||||
|
|
@ -330,37 +298,31 @@ class TestUserPartnerValidation(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.group = self.env["res.partner"].create(
|
self.group = self.env['res.partner'].create({
|
||||||
{
|
'name': 'Test Group',
|
||||||
"name": "Test Group",
|
'is_company': True,
|
||||||
"is_company": True,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create user without partner (edge case)
|
# Create user without partner (edge case)
|
||||||
self.user_no_partner = self.env["res.users"].create(
|
self.user_no_partner = self.env['res.users'].create({
|
||||||
{
|
'name': 'User No Partner',
|
||||||
"name": "User No Partner",
|
'login': 'noparnter@test.com',
|
||||||
"login": "noparnter@test.com",
|
'partner_id': False, # Explicitly no partner
|
||||||
"partner_id": False, # Explicitly no partner
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_user_without_partner_cannot_access_order(self):
|
def test_user_without_partner_cannot_access_order(self):
|
||||||
"""Test that user without partner_id has no access to orders."""
|
"""Test that user without partner_id has no access to orders."""
|
||||||
start_date = datetime.now().date()
|
start_date = datetime.now().date()
|
||||||
order = self.env["group.order"].create(
|
order = self.env['group.order'].create({
|
||||||
{
|
'name': 'Test Order',
|
||||||
"name": "Test Order",
|
'group_ids': [(6, 0, [self.group.id])],
|
||||||
"group_ids": [(6, 0, [self.group.id])],
|
'type': 'regular',
|
||||||
"type": "regular",
|
'start_date': start_date,
|
||||||
"start_date": start_date,
|
'end_date': start_date + timedelta(days=7),
|
||||||
"end_date": start_date + timedelta(days=7),
|
'period': 'weekly',
|
||||||
"period": "weekly",
|
'pickup_day': '3',
|
||||||
"pickup_day": "3",
|
'cutoff_day': '0',
|
||||||
"cutoff_day": "0",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# User without partner should not have access
|
# User without partner should not have access
|
||||||
# This should be validated in controller
|
# This should be validated in controller
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,6 @@
|
||||||
<field name="delivery_product_id" invisible="not home_delivery" required="home_delivery" help="Product to use for home delivery"/>
|
<field name="delivery_product_id" invisible="not home_delivery" required="home_delivery" help="Product to use for home delivery"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Calculated Dates" name="calculated_dates">
|
|
||||||
<group>
|
|
||||||
<field name="cutoff_date" readonly="1" help="Automatically calculated cutoff date"/>
|
|
||||||
<field name="pickup_date" readonly="1" help="Automatically calculated pickup date"/>
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="delivery_date" readonly="1" help="Automatically calculated delivery date (pickup + 1 day)"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<group string="Description">
|
<group string="Description">
|
||||||
<field name="description" placeholder="Free text description..." nolabel="1"/>
|
<field name="description" placeholder="Free text description..." nolabel="1"/>
|
||||||
</group>
|
</group>
|
||||||
|
|
|
||||||
|
|
@ -473,12 +473,7 @@
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<!-- CRITICAL: This input is NOT inside a form to prevent Odoo from transforming it -->
|
<!-- CRITICAL: This input is NOT inside a form to prevent Odoo from transforming it -->
|
||||||
<!-- It must remain a pure HTML input element for realtime_search.js to detect value changes -->
|
<!-- It must remain a pure HTML input element for realtime_search.js to detect value changes -->
|
||||||
<div style="position: relative;">
|
<input type="text" id="realtime-search-input" class="form-control realtime-search-box search-input-styled" placeholder="Search products..." autocomplete="off" />
|
||||||
<input type="text" id="realtime-search-input" class="form-control realtime-search-box search-input-styled" placeholder="Search products..." autocomplete="off" style="padding-right: 40px;" />
|
|
||||||
<button type="button" id="clear-search-btn" class="btn btn-link" style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%); padding: 0; width: 30px; height: 30px; display: none; color: #6c757d; text-decoration: none; font-size: 1.5rem; line-height: 1;" aria-label="Clear search" title="Clear search">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<select name="category" id="realtime-category-select" class="form-select">
|
<select name="category" id="realtime-category-select" class="form-select">
|
||||||
|
|
@ -556,18 +551,7 @@
|
||||||
<t t-call="website_sale_aplicoop.eskaera_shop_products" />
|
<t t-call="website_sale_aplicoop.eskaera_shop_products" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data attributes for infinite scroll configuration (ALWAYS present for JavaScript) -->
|
<!-- Infinite scroll container -->
|
||||||
<div id="eskaera-config"
|
|
||||||
t-attf-data-order-id="{{ group_order.id }}"
|
|
||||||
t-attf-data-search="{{ search_query }}"
|
|
||||||
t-attf-data-category="{{ selected_category }}"
|
|
||||||
t-attf-data-per-page="{{ per_page }}"
|
|
||||||
t-attf-data-current-page="{{ current_page }}"
|
|
||||||
t-attf-data-has-next="{{ 'true' if has_next else 'false' }}"
|
|
||||||
class="d-none">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Infinite scroll container (only if enabled and has more pages) -->
|
|
||||||
<t t-if="lazy_loading_enabled and has_next">
|
<t t-if="lazy_loading_enabled and has_next">
|
||||||
<div id="infinite-scroll-container" class="row mt-4">
|
<div id="infinite-scroll-container" class="row mt-4">
|
||||||
<div class="col-12 text-center">
|
<div class="col-12 text-center">
|
||||||
|
|
@ -594,6 +578,17 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Data attributes for infinite scroll configuration -->
|
||||||
|
<div id="eskaera-config"
|
||||||
|
t-attf-data-order-id="{{ group_order.id }}"
|
||||||
|
t-attf-data-search="{{ search_query }}"
|
||||||
|
t-attf-data-category="{{ selected_category }}"
|
||||||
|
t-attf-data-per-page="{{ per_page }}"
|
||||||
|
t-attf-data-current-page="{{ current_page }}"
|
||||||
|
t-attf-data-has-next="{{ 'true' if has_next else 'false' }}"
|
||||||
|
class="d-none">
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
|
|
@ -662,9 +657,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scripts are loaded from web.assets_frontend in __manifest__.py
|
<!-- Scripts (in dependency order) -->
|
||||||
(i18n_manager, i18n_helpers, website_sale, checkout_labels,
|
<!-- Load i18n_manager first - fetches translations from server -->
|
||||||
home_delivery, realtime_search, infinite_scroll) -->
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_manager.js" />
|
||||||
|
<!-- Keep legacy helpers for backwards compatibility -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_helpers.js" />
|
||||||
|
<!-- Main shop functionality (depends on i18nManager) -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/website_sale.js" />
|
||||||
|
<!-- UI enhancements -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/realtime_search.js" />
|
||||||
|
<!-- Infinite scroll for lazy loading products -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/infinite_scroll.js" />
|
||||||
|
|
||||||
<!-- Initialize tooltips using native title attribute -->
|
<!-- Initialize tooltips using native title attribute -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
@ -999,8 +1004,17 @@
|
||||||
console.log('[LABELS] Initialized from server:', window.groupOrderShop.labels);
|
console.log('[LABELS] Initialized from server:', window.groupOrderShop.labels);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<!-- Scripts are loaded from web.assets_frontend in __manifest__.py -->
|
<!-- Scripts (in dependency order) -->
|
||||||
<!-- (i18n_manager, i18n_helpers, website_sale, checkout_labels, home_delivery, checkout_summary) -->
|
<!-- Load i18n_manager first - fetches translations from server -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_manager.js" />
|
||||||
|
<!-- Keep legacy helpers for backwards compatibility -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_helpers.js" />
|
||||||
|
<!-- Main shop functionality (depends on i18nManager) -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/website_sale.js" />
|
||||||
|
<!-- UI enhancements -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_summary.js" />
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Auto-load cart from localStorage when accessing checkout directly
|
// Auto-load cart from localStorage when accessing checkout directly
|
||||||
(function() {
|
(function() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue