Odoo 16.0 Implementing Real-Time Product Updates
Purpose
The goal is to refresh product information dynamically when there are changes.
Implementation
sequenceDiagram
autonumber
participant DB as Odoo Backend
participant WS as WebSocket (bus.bus)
participant PY as Python Controller
participant JS as JavaScript (Frontend)
participant HTML as Website UI
DB->>WS: Notify WebSocket on Product Update
WS->>JS: Send Notification with Product ID
JS->>PY: Call /product/tree/sync with Product ID
PY->>DB: Fetch Updated Product Data
DB->>PY: Return Product Data
PY->>JS: Send Rendered HTML
JS->>HTML: Replace Product Section in DOM
1. Python Controller Backend
Render Website Products
This method is responsible for rendering the product information using the specified template.
def _render_website_products(self, product_obj):
html_return = request.env["ir.ui.view"]._render_template(
'website_sale.inherited_products_description_website_sale2', {
'website': request.env['website'].browse(1),
'product': product_obj,
}
)
return {
'html': html_return,
}
Product Sync Endpoint
This method is exposed as a public route to allow the JavaScript code to request updated product information.
@http.route(['/product/tree/sync'], type='json', auth="public", website=True, csrf=False)
def product_tree_sync(self, product_id):
product_obj = request.env['product.template'].sudo().browse(int(product_id))
return self._render_website_products(product_obj)
2. Python Backend
This method sends a notification to the WebSocket service when there is an update in the product status.
def notify_websocket(self):
self.ensure_one()
self.env['bus.bus']._sendone('product_updates', 'notification', {
'product_tmpl_id': self.product_tmpl_id.id,
})
3. JavaScript Frontend
Define a JavaScript module to handle WebSocket connections and update the DOM when there are notifications.
/** @odoo-module */
import { registry } from "@web/core/registry";
import websiteSaleList from "website_sale.website_sale_list";
export const websiteSaleTreeWs = {
dependencies: ['bus_service', 'rpc'],
start(env, { bus_service, rpc }) {
if (!document.querySelector('.oe_website_sale') || !document.querySelector('.oe_product_cart')) {
return;
}
this.product_ids = Array.from(document.querySelectorAll('div[data-product_id]')).map(product => parseInt(product.getAttribute('data-product_id')));
this.rpc = rpc;
this.busService = bus_service;
this.busService.addChannel('product_updates');
this.busService.addEventListener('notification', this.onMessage.bind(this));
this.busService.start();
},
destroy() {
this.busService.removeEventListener('notification', this.onMessage.bind(this));
this.busService.stop();
},
async onMessage({ detail: notifications }) {
for (const notification of notifications) {
if (this.product_ids.includes(notification['payload']['product_tmpl_id'])) {
const result = await this.rpc('/product/tree/sync', {
product_id: notification['payload']['product_tmpl_id'],
});
if (result) {
let product = document.querySelector(`.oe_product_cart[data-product_id="${notification['payload']['product_tmpl_id']}"]`);
product = product && product.querySelector('.js_replace_me_please');
if (product) {
product.parentElement.innerHTML = result.html;
}
}
}
}
}
}
registry.category("services").add("websiteSaleTreeWs", websiteSaleTreeWs);
4. XML Templates
Product Description Template
This template contains the structure for rendering product information.
<template id="inherited_products_description_website_sale2" name="Website Sale Products 2">
<div> <!-- this div is important for js replacement -->
<div class="js_replace_me_please">
<div>
<h6>
<a t-field="product.name"/>
</h6>
</div>
</div>
</div>
</template>
Inherited Product Template
This template extends the existing product template to include data attributes.
<template id="inherited_products_description_website_sale" name="Website Sale Products" inherit_id="website_sale.products_item">
<xpath expr="//form" position="replace">
<div t-att-data-product_id="product.id">
<t t-call="website_sale.inherited_products_description_website_sale2"/>
</div>
</xpath>
</template>