- How
website_saleextendsproduct.templatefor the shop - Publishing products and managing shop visibility
- How the cart and checkout flow works technically
- Pricelists and how they affect displayed prices
- Extending the shop controller with custom logic
The website_sale Module
website_sale depends on website, sale, and account. It adds:
- Website fields on
product.template(published, website description, ribbons) - The
/shopcontroller with product listing, filtering, and search - Cart management at
/shop/cart - Checkout flow at
/shop/checkout - Payment integration via the
paymentmodule - eCommerce categories (
product.public.category)
Product Website Fields
Key fields added by website_sale to product.template:
| Field | Type | Purpose |
|---|---|---|
| is_published | Boolean | Visible in the shop |
| website_description | Html | Long description shown on product page |
| website_id | Many2one | Restrict to a specific website (null = all) |
| public_categ_ids | Many2many | eCommerce categories for filtering |
| website_sequence | Integer | Sort order in the shop |
| website_ribbon_id | Many2one | "New", "Sale" badge on product card |
Products are only visible if is_published = True and website_id matches the current website (or is False).
Cart and Checkout Flow
The cart is a sale order in draft state (state='draft') linked to the current visitor's session. Key controllers:
| URL | Action |
|---|---|
| /shop/cart/update | Add/update product quantity (JSON) |
| /shop/cart | Cart page — shows current order lines |
| /shop/checkout | Address entry and delivery method selection |
| /shop/payment | Payment method selection and acquirer redirect |
| /shop/payment/validate | Post-payment callback — confirms the sale order |
The draft order is retrieved or created via request.website.sale_get_order(). After payment, it is confirmed automatically and becomes a confirmed sale order in the backend.
Pricelists and Pricing
Displayed prices in the shop depend on the active pricelist. The active pricelist is determined by:
- The visitor's country (geo-IP based)
- The portal user's partner pricelist
- A pricelist code in the URL (
?pl=SUMMER20) - The website's default pricelist
# Get current pricelist in a controller
pricelist = request.website.get_current_pricelist()
# Get price for a product
product = request.env['product.template'].browse(product_id)
price = product.with_context(pricelist=pricelist.id).price
Extending the Shop Controller
Extend the shop listing or product detail page by subclassing WebsiteSale:
from odoo.addons.website_sale.controllers.main import WebsiteSale
class ExtendedShop(WebsiteSale):
def _get_search_domain(self, search, category, attrib_values,
search_in_description=True):
"""Add custom filter: only show products in stock."""
domain = super()._get_search_domain(
search, category, attrib_values, search_in_description
)
domain += [('qty_available', '>', 0)]
return domain
def _prepare_product_values(self, product, category, search, **kwargs):
"""Add extra context to the product detail page."""
values = super()._prepare_product_values(
product, category, search, **kwargs
)
values['related_books'] = request.env['library.book'].sudo().search([
('author', '=', product.name),
], limit=4)
return values
Common Customizations
Frequent eCommerce customization patterns:
| Feature | Approach |
|---|---|
| Custom product attributes | Add product.attribute records and product.attribute.value via data XML |
| Custom cart validation | Override _cart_update() in a shop controller subclass |
| Post-order hooks | Override action_confirm() on sale.order |
| Custom checkout steps | Override checkout_values() and add extra template blocks |
| Minimum order quantity | Set sale_line_warn_msg or override _cart_update() |
website_salemanages shop products viais_publishedandwebsite_idfields onproduct.template- The cart is a draft
sale.order— retrieved viarequest.website.sale_get_order() - Pricelists control displayed prices; the active pricelist is resolved from geo-IP, user, or URL parameter
- Extend the shop by subclassing
WebsiteSaleand overriding specific methods
Frequently Asked Questions
How do I add a product variant selector to the product page?
Product variants are handled automatically by website_sale when a product has multiple attribute values. Set up product.attribute and product.attribute.value records, link them to the product template, and the variant selector appears automatically on the product page. No controller changes needed.
Can I add custom fields to the checkout form?
Yes — extend the checkout template via XPath to add extra fields, and override the checkout_values() method to process and validate the extra data. Store custom values on the sale order or a linked model using a Python controller override of _checkout_form_save().
How does Odoo handle multi-currency in the shop?
Currency is controlled by the active pricelist. Each pricelist has a currency. If your website uses multiple currencies, create one pricelist per currency. The visitor's pricelist is selected based on country, session, or URL parameter. Price display uses the pricelist's currency automatically.
What happens to the cart if a visitor doesn't complete checkout?
The cart remains as a draft sale order indefinitely. Odoo has a built-in scheduled action ("Sales / Cancel Quotations") that can cancel old quotations after a configurable number of days. You can also create a custom cron job to clean up abandoned carts by filtering sale.order with state='draft' and website_id != False beyond a certain age.