Enable Dark Mode!
how-to-build-a-dynamic-record-selector-for-odoo-17-websites.jpg
By: Aswin AK

How to Build a Dynamic Record Selector for Odoo 17 Websites

Technical Odoo 17 Website&E-commerce

In Odoo 17, creating a dynamic and reusable record selector for your website can significantly enhance user experience, especially for tasks like selecting partners, products, or other records interactively. This blog walks you through implementing a Dynamic Record Selector widget for the Odoo website, focusing on making the model dynamic while maintaining a user-friendly and efficient interface. It supports any model dynamically.

Why a Dynamic Record Selector?

A record selector allows users to search and select records (e.g., partners, products, or employees) directly from a website interface. Making it dynamic means the widget can work with any Odoo model, reducing code duplication and improving reusability. Key features include:

* Infinite scrolling for seamless record loading.

* Real-time search to filter records as users type.

* Dynamic model support to work with any Odoo model (e.g., res.partner, product.template, etc.).

* User-friendly UI with loading indicators and notifications.

This implementation is particularly useful for e-commerce websites, customer portals, or any Odoo website requiring interactive record selection.

Implementation

1. Allow the widget to accept any model name dynamically.

2. Make the search and update logic model-agnostic.

3. Update the backend routes to handle dynamic models.

4. Ensure the UI remains intuitive and responsive.

Step-by-Step Implementation

Let’s create the DynamicRecordSelector to support dynamic models in Odoo 17. Below is the code with explanations.

Module Structure

+-- __init__.py
+-- __manifest__.py
+-- controllers/
¦   +-- __init__.py
¦   +-- dynamic_record_controller.py
+-- static/
¦   +-- src/
¦   Â¦   +-- js/
¦   Â¦       +-- dynamic_record_selector.js
¦   Â¦   +-- css/
¦   Â¦       +-- dynamic_record_selector.css
+-- views/
¦   +-- dynamic_record_selector.xml

__manifest__.py

{
    'name': 'Website Dynamic Record Selector',
    'version': '1.0',
    'category': 'Website',
    'summary': 'A dynamic record selector widget for Odoo 17 websites',
    'description': """
        This module provides a reusable dynamic record selector widget for Odoo 17 websites.
        It allows users to search and select records from any model (e.g., res.partner, product.template)
        with infinite scrolling, real-time search, and a user-friendly interface.
    """,
    'author': 'Your Name',
    'depends': [
        'website'
    ],
    'data': [
        'views/dynamic_record_selector.xml',
    ],
    'assets': {
        'web.assets_frontend': [
            'website_dynamic_record_selector/static/src/js/dynamic_record_selector.js',
            'website_dynamic_record_selector/static/src/css/dynamic_record_selector.css',
        ],
    },
    'installable': True,
    'auto_install': False,
    'application': False,
}

1. Update the Frontend Widget

The widget needs to accept a model name and field names dynamically. We’ll modify the JavaScript to make it generic.

dynamic_record_selector.js

/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import {_t} from "@web/core/l10n/translation";
import {debounce} from "@web/core/utils/timing";
publicWidget.registry.DynamicRecordSelector = publicWidget.Widget.extend({
    selector: '.js_dynamic_record_selector',
    events: {
        'focus .search-input': '_onFocus',
        'input .search-input': '_onSearchInput',
        'click .record-list li': '_onRecordSelect',
        'blur .search-input': '_onBlur',
        'mousedown .record-dropdown': '_onDropdownMouseDown',
    },
    init: function () {
        this._super.apply(this, arguments);
        this.rpc = this.bindService("rpc");
    },
    start: function () {
        this.limit = 10;
        this.offset = 0;
        this.selectedRecord = null;
        this.isLoading = false;
        this.hasMore = true;
        this.$searchInput = this.$('.search-input');
        this.$recordList = this.$('.record-list');
        this.$recordDropdown = this.$('.record-dropdown');
        this.isClickingInside = false;
        // Get model and field from data attributes
        this.model = this.$el.data('model') || 'res.partner';
        this.displayField = this.$el.data('display-field') || 'name';
        this.searchField = this.$el.data('search-field') || 'name';
        // Create and append the loading indicator
        this.$loadingIndicator = $('<div class="loading-indicator">Loading...</div>').hide();
        this.$recordDropdown.append(this.$loadingIndicator);
        this._setupInfiniteScroll();
        return this._super.apply(this, arguments);
    },
    _setupInfiniteScroll: function () {
        this.$recordDropdown.on('scroll', debounce(this._onScroll.bind(this), 200));
    },
    _onScroll: function () {
        if (this.isLoading || !this.hasMore) return;
        var scrollHeight = this.$recordList[0].scrollHeight;
        var scrollTop = this.$recordList.scrollTop();
        var clientHeight = this.$recordList.innerHeight();
        if (scrollTop + clientHeight >= scrollHeight - 50) {
            this._fetchRecords(true);
        }
    },
    _onFocus: function () {
        if (this.$recordList.children().length === 0) {
            this._fetchRecords();
        }
        this.$recordDropdown.addClass('show');
    },
    _onBlur: function (ev) {
        if (!this.isClickingInside) {
            this.$recordDropdown.removeClass('show');
        }
    },
    _onDropdownMouseDown: function (ev) {
        this.isClickingInside = true;
        setTimeout(() => {
            this.isClickingInside = false;
        }, 0);
    },
    _onSearchInput: function () {
        this.offset = 0;
        this.hasMore = true;
        this._fetchRecords();
    },
    _onRecordSelect: function (ev) {
        var $target = $(ev.currentTarget);
        var recordId = $target.data('record-id');
        if (!recordId) {
            this.$recordDropdown.removeClass('show');
            return;
        }
        var recordName = $target.text();
        this.selectedRecord = { id: recordId, name: recordName };
        this.$searchInput.val(recordName);
        this.$('input[type="hidden"]').val(recordId);
        this.$recordDropdown.removeClass('show');
        //TODO: You can add your logic here
    },
    _fetchRecords: function (append) {
        if (this.isLoading) return;
        this.isLoading = true;
        this._showLoading();
        var self = this;
        var searchTerm = this.$searchInput.val();
        this.rpc('/website/dynamic_record/search', {
            model: this.model,
            term: searchTerm,
            limit: this.limit,
            offset: this.offset,
            display_field: this.displayField,
            search_field: this.searchField,
        }).then(function (result) {
            if (!append) {
                self.$recordList.empty();
            }
            if (result.records.length > 0) {
                var fragment = document.createDocumentFragment();
                result.records.forEach(function (record) {
                    var li = document.createElement('li');
                    li.dataset.recordId = record.id;
                    li.textContent = record[self.displayField];
                    fragment.appendChild(li);
                });
                self.$recordList[0].appendChild(fragment);
                self.offset += result.records.length;
                self.hasMore = result.has_more;
            } else if (!append) {
                var li = document.createElement('li');
                li.textContent = _t("No records found");
                self.$recordList[0].appendChild(li);
                self.hasMore = false;
            }
            self.$recordDropdown.addClass('show');
            self.isLoading = false;
            self._hideLoading();
        }).catch(function (error) {
            console.error("Error fetching records:", error);
            self.isLoading = false;
            self._hideLoading();
        });
    },
    _showLoading: function () {
        this.$loadingIndicator.show();
    },
    _hideLoading: function () {
        this.$loadingIndicator.hide();
    },
});

2. Update the Backend Routes

The backend needs to handle dynamic models for searching and updating records. Below are the updated controller routes.

dynamic_record_controller.py

from odoo import http
from odoo.http import request
class DynamicRecordController(http.Controller):
    @http.route('/website/dynamic_record/search', type='json', auth="public", website=True)
    def search_records(self, model, term='', limit=10, offset=0, display_field='name', search_field='name', **kwargs):
        try:
            # Build domain for search
            domain = [(search_field, 'ilike', term)]
            records = request.env[model].sudo().search_read(
                domain=domain,
                fields=['id', display_field],
                limit=limit,
                offset=offset,
                order=f'{display_field} asc'
            )
            total_count = request.env[model].sudo().search_count(domain)
            return {
                'records': records,
                'has_more': (offset + limit) < total_count
            }
        except Exception as e:
            return {'error': str(e)}

3. Update the HTML Template

The widget needs an HTML structure with data attributes to specify the model and fields. Here’s an example template.

dynamic_record_selector.xml

<template id="dynamic_record_selector_template" name="Dynamic Record Selector">
    <div class="js_dynamic_record_selector" t-att-data-model="model" t-att-data-display-field="display_field" t-att-data-search-field="search_field">
        <input type="text" class="search-input form-control" placeholder="Search records..."/>
        <input type="hidden" name="record_id"/>
        <div class="record-dropdown dropdown-menu">
            <ul class="record-list list-group"></ul>
        </div>
    </div>
</template>

4. Styling the Widget

To ensure a polished look, add some CSS for the dropdown and loading indicator.

dynamic_record_selector.css

.js_dynamic_record_selector {
    position: relative;
}
.record-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 1000;
    max-height: 300px;
    overflow-y: auto;
}
.record-dropdown.show {
    display: block;
}
.record-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.record-list li {
    padding: 8px 12px;
    cursor: pointer;
}
.record-list li:hover {
    background-color: #f8f9fa;
}
.loading-indicator {
    text-align: center;
    padding: 10px;
    color: #666;

How to Use the Dynamic Record Selector:

In your QWeb template, include the selector with the desired model and fields by inheriting the template and placing the code exactly where you need it.

<t t-call="your_module.dynamic_record_selector_template">
    <t t-set="model" t-value="'product.template'"/>
    <t t-set="display_field" t-value="'name'"/>
    <t t-set="search_field" t-value="'name'"/>
</t>

Benefits of the Dynamic Approach

* Reusability: Use the same widget for any model (e.g., res.partner, product.template, hr.employee).

* Scalability: Infinite scrolling ensures performance with large datasets.

* Flexibility: Configurable display and search fields adapt to different use cases.

* User-Friendly: Real-time search, loading indicators, and notifications enhance UX.

Conclusion

The Dynamic Record Selector is a powerful addition to Odoo 17 websites, enabling seamless record selection for any model. By making the widget model-agnostic, we’ve created a reusable and scalable solution that enhances user interaction. Try implementing this in your Odoo module and customize it to fit your specific needs!

To read more about How to add Dynamic Record Stages in Odoo 17, refer to our blog How to add Dynamic Record Stages in Odoo 17.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message