Wasting $52K on Slow Dev? Call JSON RPC in Odoo 18 Web Controllers
By Braincuber Team
Published on December 22, 2025
Frontend dev builds custom portal page. Needs to fetch product data from Odoo backend. Tries direct database query—security blocked. Tries ORM from JavaScript—impossible. Hardcodes API endpoint with XML-RPC—takes 3 days, breaks on Odoo update. Product manager asks: "Why does simple data fetch take week?" Dev replies: "Odoo backend communication is complicated." Meanwhile, competitor ships same feature in 2 hours using JSON RPC.
Your frontend-backend communication disaster: Custom web pages can't talk to Odoo backend cleanly. Devs resort to hacky solutions (direct SQL queries = security nightmare). XML-RPC over-complicated (verbose syntax, hard to debug). REST API requires custom module setup every time. No standard way to call Python methods from JavaScript. Frontend stuck with static data (can't update dynamically). Every feature requiring backend data = 3-5 days dev time vs should be 2 hours.
Cost: Slow feature development = 3 days per feature × 23 features yearly × $95/hour × 8 hours = $52,440 wasted. Developer frustration = high turnover (lost 2 frontend devs last year, $147,000 recruitment + training). Bugs from hacky workarounds = 17 production incidents × $8,400 downtime = $142,800. Can't build interactive features competitors have (lost 8 deals = $376,000 revenue). Technical debt from workarounds = unmaintainable codebase.
Odoo 18 JSON RPC fixes this: Standard protocol for JavaScript → Python communication. Create web controller in Python (define route, write method logic). Call from JavaScript using rpc() function—clean, simple, fast. Pass parameters as JSON objects. Receive responses as JSON. Async/await support (modern JavaScript). Debugging easy (see requests in browser Network tab). Build interactive dashboards, real-time data updates, custom portals—all talking to Odoo backend seamlessly. Here's how to implement JSON RPC calls so you stop losing $52K annually to slow frontend development.
You're Losing Money If:
What JSON RPC Does
JSON RPC (Remote Procedure Call) = Standard protocol for calling Python backend methods from JavaScript frontend. Send JSON request → Python method executes → JSON response returned.
| Without JSON RPC (Hacky Methods) | With JSON RPC (Standard) |
|---|---|
| Direct SQL from JS (security risk!) | Secure Python controller with access rights |
| XML-RPC verbose syntax, hard debug | Clean JSON syntax, browser DevTools debugging |
| Custom REST API module per feature | One controller, many methods (reusable) |
| Dev time: 3-5 days per feature | Dev time: 2 hours per feature |
| Static frontend (no dynamic data) | Real-time updates, interactive dashboards |
💡 JSON RPC Flow:
- Frontend (JS): User clicks "Load Products" button
- JavaScript: Calls
rpc('/my/get_products', {category_id: 5}) - HTTP Request: JSON data sent to Odoo server
- Web Controller (Python): Method receives request, queries database
- Python Logic: Fetches products, formats data
- HTTP Response: JSON data returned
- JavaScript: Receives response, updates DOM with product list
- User sees: Products displayed instantly
Understanding JSON RPC in Odoo
JSON-RPC = Lightweight remote procedure call protocol using JSON for data exchange over HTTP.
Key Concepts:
- Remote Procedure Call: Execute Python function on server from JavaScript on client
- JSON Format: Data sent/received as JSON objects (easy to work with in JS)
- HTTP Protocol: Uses standard HTTP POST requests (works everywhere)
- Async Operations: Non-blocking (page doesn't freeze while waiting for response)
- Type Safety: Route type='json' tells Odoo to expect JSON data
Step 1: Create Web Controller (Python Backend)
Create Controller File
Build Python controller to handle JSON RPC requests.
from odoo import http
from odoo.http import request
class ProductController(http.Controller):
"""Controller for product-related JSON RPC calls."""
@http.route('/my/get_products', type='json', auth='user', methods=['POST'])
def get_products(self, category_id=None, limit=10):
"""
Fetch products from database.
Args:
category_id (int, optional): Filter by category
limit (int): Maximum products to return
Returns:
dict: {'success': True, 'products': [...]}
"""
try:
# Build domain filter
domain = []
if category_id:
domain.append(('categ_id', '=', category_id))
# Query products
products = request.env['product.product'].search(
domain,
limit=limit,
order='name'
)
# Format response
product_list = []
for product in products:
product_list.append({
'id': product.id,
'name': product.name,
'price': product.list_price,
'qty_available': product.qty_available,
'image_url': f'/web/image/product.product/{product.id}/image_128'
})
return {
'success': True,
'products': product_list,
'count': len(product_list)
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
Route Decorator Explained:
@http.route('/my/get_products', ...)
URL endpoint: https://yourodoo.com/my/get_products
type='json'
Tells Odoo: Expect JSON data, return JSON response
auth='user'
Requires logged-in user (auth='public' for anonymous access)
methods=['POST']
Only accept POST requests (standard for JSON RPC)
Register Controller in __init__.py
from . import main
from . import controllers
from . import models
Step 2: Call from JavaScript Frontend
Basic RPC Call
/** @odoo-module **/
import { rpc } from "@web/core/network/rpc";
// Simple RPC call
async function loadProducts() {
try {
const result = await rpc('/my/get_products', {
category_id: 5,
limit: 20
});
if (result.success) {
console.log('Products loaded:', result.products);
console.log('Count:', result.count);
// Use product data
displayProducts(result.products);
} else {
console.error('Error:', result.error);
}
} catch (error) {
console.error('RPC failed:', error);
}
}
function displayProducts(products) {
const container = document.querySelector('#product-list');
products.forEach(product => {
const productHTML = `
${product.name}
Price: $${product.price}
Stock: ${product.qty_available}
`;
container.innerHTML += productHTML;
});
}
Using Public Widget (Website Pages)
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { rpc } from "@web/core/network/rpc";
publicWidget.registry.ProductLoader = publicWidget.Widget.extend({
selector: '.o_product_widget',
events: {
'click .load-products-btn': '_onLoadProducts',
'change .category-filter': '_onCategoryChange',
},
start: function () {
// Initialize widget
return this._super(...arguments).then(() => {
// Load products on page load
this._loadProducts();
});
},
async _loadProducts(categoryId = null) {
const $loader = this.$('.product-loader');
$loader.show();
try {
const result = await rpc('/my/get_products', {
category_id: categoryId,
limit: 50
});
if (result.success) {
this._renderProducts(result.products);
} else {
this._showError(result.error);
}
} catch (error) {
this._showError('Failed to load products');
} finally {
$loader.hide();
}
},
_renderProducts: function (products) {
const $container = this.$('.product-list');
$container.empty();
products.forEach(product => {
const $productCard = $(`
${product.name}
$${product.price}
`);
$container.append($productCard);
});
},
_onLoadProducts: function (ev) {
ev.preventDefault();
this._loadProducts();
},
_onCategoryChange: function (ev) {
const categoryId = parseInt($(ev.currentTarget).val());
this._loadProducts(categoryId);
},
_showError: function (message) {
alert('Error: ' + message);
}
});
export default publicWidget.registry.ProductLoader;
Step 3: Advanced Examples
Example 1: Submit Form Data
@http.route('/my/create_lead', type='json', auth='public', methods=['POST'])
def create_lead(self, name, email, phone, message):
"""Create CRM lead from contact form."""
try:
lead = request.env['crm.lead'].sudo().create({
'name': name,
'email_from': email,
'phone': phone,
'description': message,
'type': 'lead',
})
return {
'success': True,
'lead_id': lead.id,
'message': 'Thank you! We will contact you soon.'
}
except Exception as e:
return {
'success': False,
'error': 'Failed to submit form. Please try again.'
}
async function submitContactForm(event) {
event.preventDefault();
const formData = {
name: document.querySelector('#name').value,
email: document.querySelector('#email').value,
phone: document.querySelector('#phone').value,
message: document.querySelector('#message').value,
};
const submitBtn = document.querySelector('.submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
try {
const result = await rpc('/my/create_lead', formData);
if (result.success) {
alert(result.message);
document.querySelector('#contact-form').reset();
} else {
alert(result.error);
}
} catch (error) {
alert('Network error. Please try again.');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Submit';
}
}
Example 2: Real-Time Dashboard Updates
@http.route('/my/dashboard_stats', type='json', auth='user', methods=['POST'])
def get_dashboard_stats(self):
"""Get real-time dashboard statistics."""
user = request.env.user
# Calculate stats
open_leads = request.env['crm.lead'].search_count([
('user_id', '=', user.id),
('type', '=', 'lead'),
('active', '=', True)
])
won_opportunities = request.env['crm.lead'].search_count([
('user_id', '=', user.id),
('type', '=', 'opportunity'),
('stage_id.is_won', '=', True)
])
total_revenue = sum(request.env['sale.order'].search([
('user_id', '=', user.id),
('state', '=', 'sale')
]).mapped('amount_total'))
return {
'success': True,
'stats': {
'open_leads': open_leads,
'won_opportunities': won_opportunities,
'total_revenue': total_revenue,
'username': user.name,
}
}
class DashboardWidget {
constructor() {
this.refreshInterval = null;
this.init();
}
init() {
this.loadStats();
// Auto-refresh every 30 seconds
this.refreshInterval = setInterval(() => {
this.loadStats();
}, 30000);
}
async loadStats() {
try {
const result = await rpc('/my/dashboard_stats', {});
if (result.success) {
this.updateUI(result.stats);
}
} catch (error) {
console.error('Failed to load stats:', error);
}
}
updateUI(stats) {
document.querySelector('#open-leads').textContent = stats.open_leads;
document.querySelector('#won-opps').textContent = stats.won_opportunities;
document.querySelector('#total-revenue').textContent =
`$${stats.total_revenue.toLocaleString()}`;
document.querySelector('#username').textContent = stats.username;
}
destroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
}
}
// Initialize dashboard
const dashboard = new DashboardWidget();
Error Handling Best Practices
Python Controller Error Handling:
from odoo.exceptions import AccessError, UserError, ValidationError
import logging
_logger = logging.getLogger(__name__)
@http.route('/my/safe_method', type='json', auth='user')
def safe_method(self, record_id):
try:
# Validate input
if not record_id or not isinstance(record_id, int):
return {
'success': False,
'error': 'Invalid record ID'
}
# Check access rights
record = request.env['product.product'].browse(record_id)
record.check_access_rights('read')
record.check_access_rule('read')
# Business logic
data = {
'id': record.id,
'name': record.name,
}
return {'success': True, 'data': data}
except AccessError:
return {
'success': False,
'error': 'Access denied'
}
except ValidationError as e:
return {
'success': False,
'error': str(e)
}
except Exception as e:
_logger.exception("Unexpected error in safe_method")
return {
'success': False,
'error': 'An unexpected error occurred'
}
Debugging JSON RPC Calls
Browser DevTools (Network Tab):
- Open Browser DevTools (F12)
- Go to Network tab
- Filter by XHR or Fetch
- Trigger RPC call in your app
- Click request to see:
- Headers: Request URL, method, status code
- Payload: JSON data sent (parameters)
- Response: JSON data received
- Timing: How long request took
- Status 200 = Success, 500 = Server error, 403 = Access denied
Python Logging:
import logging
_logger = logging.getLogger(__name__)
@http.route('/my/debug_route', type='json', auth='user')
def debug_route(self, param1, param2):
_logger.info('Route called with param1=%s, param2=%s', param1, param2)
result = some_calculation(param1, param2)
_logger.info('Calculation result: %s', result)
return {'success': True, 'result': result}
# View logs: Odoo server console or Settings → Technical → Logs
Security Considerations
⚠️ Security Checklist:
- Authentication: Use
auth='user'unless public endpoint required - Access Rights: Check
record.check_access_rights('read')before data access - Input Validation: Validate all parameters (type, range, format)
- SQL Injection: Use ORM methods, NEVER raw SQL from user input
- XSS Prevention: Sanitize data returned to frontend if rendering HTML
- Rate Limiting: Consider limiting requests from single user/IP
Real-World Impact
E-commerce Company Example:
Before JSON RPC:
- Custom product filter: 3 days dev time (XML-RPC complexity)
- Real-time inventory: Impossible (page refresh required)
- Add to cart: Full page reload (slow UX)
- Developer frustration: High (quit after 6 months)
After Implementing JSON RPC:
- Product filter: 2 hours (clean RPC call)
- Real-time inventory: Implemented (30-second auto-refresh)
- Add to cart: Instant (AJAX, no reload)
- Development speed: 15x faster for interactive features
- Conversion rate: +23% (better UX)
Financial Impact:
- Dev time saved: 3 days → 2 hours per feature × 23 features = $51,680/year
- Retained developer: Saved $147K recruitment cost
- Conversion increase: 23% × $2.4M revenue = $552K additional revenue
- Total impact: $750,680 annually
Pro Tip: Start simple. First JSON RPC endpoint: Return hardcoded data. Confirm JS can call it. Then add database queries. Then add parameters. Don't try to build complex controller Day 1—iterate. Most bugs happen in parameter passing (check types in browser DevTools → Network → Payload).
Wasting $52K Annually on Slow Frontend Development?
We build JSON RPC controllers for Odoo 18: Clean Python backend endpoints, JavaScript integration, real-time dashboards, interactive web apps. Reduce dev time from 3 days to 2 hours.
