The separate /eskaera/<slug>/payment step is gone. Members review the summary, choose home delivery and pick a payment method on the checkout, in one screen; the old URL redirects there so bookmarks and sessions that were mid-flow do not hit a 404. The checkout now renders the member's draft sale.order instead of the localStorage cart. That is what fixes the products appearing "out of nowhere" between the two pages: the summary was a snapshot of localStorage taken at page load, and `_autoLoadDraftOnInit` then pulled the draft back into localStorage without re-rendering. Deleting a product in the shop removed it from the cart but left the line on the draft, so the autoload resurrected it, the confirm button sent it back, and it only became visible one page later. The checkout no longer auto-loads the draft — it renders it, and what it shows is what the payment form charges. "Proceed to Checkout" pushes the cart to that draft before navigating. Saving is idempotent: `_merge_or_replace_draft` reuses the cycle's draft and, through the new `_draft_matches_lines`, rewrites `order_line` only when the lines actually differ — replacing them unlinks and recreates every one of them, which is pure churn when nothing changed. The home delivery checkbox goes through the new /eskaera/set-home-delivery so the delivery line moves on the order itself. Writing only to localStorage would have changed the summary and left the amount alone, which with online payment on is the amount being charged. Also fixes the confirmation notice nobody ever saw: saving answered with the payment step URL and the frontend followed it immediately, destroying the toast in the same tick. Saving no longer navigates; the caller decides whether it is staying or moving on. Along the way: checkout_labels.js and the eskaera_checkout_summary / eskaera_payment templates are removed, superseded by the server-rendered summary and checkout, and the stale sessionStorage delivery preference no longer overrides the checkbox the order just rendered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
6.9 KiB
Python
180 lines
6.9 KiB
Python
# Copyright 2025 Criptomart
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
from datetime import date
|
|
from datetime import timedelta
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.tests.common import tagged
|
|
|
|
|
|
@tagged("post_install", "-at_install")
|
|
class TestTemplatesRendering(TransactionCase):
|
|
"""Test suite to verify QWeb templates work with day_names context.
|
|
|
|
This test covers the fix for the issue where _() function calls
|
|
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.
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""Set up test data: create a test group order."""
|
|
super().setUp()
|
|
|
|
# Create a test supplier
|
|
self.supplier = self.env["res.partner"].create(
|
|
{
|
|
"name": "Test Supplier",
|
|
"is_company": True,
|
|
}
|
|
)
|
|
|
|
# Create test products
|
|
self.product = self.env["product.product"].create(
|
|
{
|
|
"name": "Test Product",
|
|
"type": "consu", # consumable (consu), service, or storable
|
|
}
|
|
)
|
|
|
|
# Create a test group
|
|
self.group = self.env["res.partner"].create(
|
|
{
|
|
"name": "Test Group",
|
|
"is_company": True,
|
|
}
|
|
)
|
|
|
|
# Create a group order
|
|
self.group_order = self.env["group.order"].create(
|
|
{
|
|
"name": "Test Order",
|
|
"state": "open",
|
|
"supplier_ids": [(6, 0, [self.supplier.id])],
|
|
"product_ids": [(6, 0, [self.product.id])],
|
|
"group_ids": [(6, 0, [self.group.id])],
|
|
"start_date": date.today(),
|
|
"end_date": date.today() + timedelta(days=7),
|
|
"pickup_day": "5", # Saturday
|
|
"cutoff_day": "3", # Thursday
|
|
}
|
|
)
|
|
|
|
def test_eskaera_page_template_exists(self):
|
|
"""Test that eskaera_page template compiles without errors."""
|
|
template = self.env.ref("website_sale_aplicoop.eskaera_page")
|
|
self.assertIsNotNone(template)
|
|
self.assertEqual(template.type, "qweb")
|
|
|
|
def test_eskaera_shop_template_exists(self):
|
|
"""Test that eskaera_shop template compiles without errors."""
|
|
template = self.env.ref("website_sale_aplicoop.eskaera_shop")
|
|
self.assertIsNotNone(template)
|
|
self.assertEqual(template.type, "qweb")
|
|
|
|
def test_eskaera_checkout_template_exists(self):
|
|
"""Test that eskaera_checkout template compiles without errors."""
|
|
template = self.env.ref("website_sale_aplicoop.eskaera_checkout")
|
|
self.assertIsNotNone(template)
|
|
self.assertEqual(template.type, "qweb")
|
|
|
|
def test_day_names_context_is_provided(self):
|
|
"""Test that day_names context is provided by the controller method."""
|
|
# Simulate what the controller does, passing env for test context
|
|
from ..controllers.website_sale import AplicoopWebsiteSale
|
|
|
|
controller = AplicoopWebsiteSale()
|
|
day_names = controller._get_day_names(env=self.env)
|
|
|
|
# Verify we have exactly 7 days
|
|
self.assertEqual(len(day_names), 7)
|
|
|
|
# Verify all are strings and not None
|
|
for i, day_name in enumerate(day_names):
|
|
self.assertIsNotNone(day_name, f"Day at index {i} is None")
|
|
self.assertIsInstance(day_name, str, f"Day at index {i} is not a string")
|
|
self.assertGreater(len(day_name), 0, f"Day at index {i} is empty string")
|
|
|
|
def test_day_names_not_using_inline_underscore(self):
|
|
"""Test that day_names are defined in Python, not in t-value attributes.
|
|
|
|
This test ensures the fix has been applied:
|
|
- day_names MUST be passed from controller context
|
|
- day_names MUST NOT be defined with _() inside t-value attributes
|
|
- Templates use day_names[index] from context, not t-set with _()
|
|
|
|
Note: day_names can be used in called sub-templates (t-call), not just the main template.
|
|
We verify the sub-template that actually uses day_names (eskaera_order_card_meta).
|
|
"""
|
|
# Verify that the sub-template that uses day_names is properly structured
|
|
template = self.env.ref("website_sale_aplicoop.eskaera_order_card_meta")
|
|
self.assertIn(
|
|
"day_names",
|
|
template.arch_db,
|
|
"Sub-template eskaera_order_card_meta must reference day_names from context",
|
|
)
|
|
# The fix ensures no <t t-set="day_names" t-value="[_(...)]"/> exists
|
|
# which was causing the NoneType error
|
|
|
|
def test_order_lines_summary_template_exists(self):
|
|
"""The checkout summary sub-template exists and reads the sale.order."""
|
|
template = self.env.ref("website_sale_aplicoop.eskaera_order_lines_summary")
|
|
self.assertIsNotNone(template)
|
|
self.assertEqual(template.type, "qweb")
|
|
# Verify it has the expected structure
|
|
self.assertIn(
|
|
"checkout-summary-table",
|
|
template.arch_db,
|
|
"Template must have checkout-summary-table id",
|
|
)
|
|
# The summary is server-side on purpose: what it shows is what the
|
|
# payment form charges, so it can never drift from the localStorage
|
|
# cart the way the old client-rendered table did.
|
|
self.assertIn(
|
|
"sale_order.order_line",
|
|
template.arch_db,
|
|
"Template must read the order lines, not a client-side cart",
|
|
)
|
|
self.assertIn(
|
|
"Product",
|
|
template.arch_db,
|
|
"Template must have Product label for translation",
|
|
)
|
|
self.assertIn(
|
|
"Quantity",
|
|
template.arch_db,
|
|
"Template must have Quantity label for translation",
|
|
)
|
|
self.assertIn(
|
|
"Subtotal",
|
|
template.arch_db,
|
|
"Template must have Subtotal label for translation",
|
|
)
|
|
|
|
def test_order_lines_summary_renders(self):
|
|
"""The summary renders the lines and total of a real order."""
|
|
order = self.env["sale.order"].create(
|
|
{
|
|
"partner_id": self.supplier.id,
|
|
"order_line": [
|
|
(
|
|
0,
|
|
0,
|
|
{
|
|
"product_id": self.product.id,
|
|
"product_uom_qty": 2,
|
|
"price_unit": 5.0,
|
|
},
|
|
)
|
|
],
|
|
}
|
|
)
|
|
template = self.env.ref("website_sale_aplicoop.eskaera_order_lines_summary")
|
|
|
|
html = template._render_template(template.xml_id, {"sale_order": order})
|
|
|
|
self.assertIn("<table", html)
|
|
self.assertIn("checkout-summary-table", html)
|
|
self.assertIn("Product", html)
|
|
self.assertIn("Quantity", html)
|
|
self.assertIn("Test Product", html)
|