Odoo eCommerce: A Store Wired Into the ERP
Odoo's eCommerce (the "Website Sale" app) turns the website into an online shop — but its defining feature is that the storefront shares the same database as Inventory, Sales, and Accounting, so there's no syncing between "the shop" and "the business."
| Storefront thing | Is also an Odoo… |
|---|---|
| Product on the site | product in Inventory (same record) |
| Cart / checkout | a draft Sales order |
| Completed order | confirmed Sales order → invoice |
| Stock shown online | live Inventory quantity |
The integration is the value: in a typical stack you run a separate store platform and constantly sync products, stock, and orders with your back office. In Odoo it's one system — a product's price and stock on the website are the same records Inventory manages, a checkout creates a real Sales order, and confirming it flows straight into invoicing and stock deduction. No integration middleware, no data drift. Core concepts: product variants (size/color) map to Odoo's variant system; pricelists drive dynamic pricing and promotions; delivery and payment methods plug into shipping and payment-provider modules; taxes come from the accounting configuration. For developers: customizing eCommerce means QWeb templates (product pages, checkout steps), controllers for custom flows, and snippets for merchandising — plus respecting that these are public pages handling money, so security (validate input, correct pricing server-side, never trust the client cart totals) and correctness matter. Payment security in particular: prices and totals must be computed/verified server-side, never taken from the browser. The takeaway: Odoo eCommerce is best understood not as a bolt-on store but as the ERP's data exposed and transacted through a web storefront.
🏋️ Practical Exercise
- Enable eCommerce.
- Publish a product to the shop.
- Configure a product variant.
- Set up a test payment provider.
- Place a test order.
🔥 Challenge Exercise
Set up a basic Odoo eCommerce shop: publish products, configure variants and prices, enable a test payment provider, and complete a test checkout. Explain how the eCommerce module ties the website to the Sales and Inventory backend models.
- 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
Interview Questions
- What does the Odoo eCommerce module do?
- How do you publish a product to the online shop?
- How do product variants work?
- How does eCommerce connect to Inventory and Sales?
- What is a payment provider/acquirer?
Related Topics
FAQ
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.
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().
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.
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.

