Coding Standards¶
Style guidelines for JDX Odoo modules and Field PWA development.
Python (Odoo Modules)¶
General Rules¶
- Follow PEP 8 style guide
- Maximum line length: 120 characters
- Use 4 spaces for indentation (no tabs)
- UTF-8 encoding for all files
Imports¶
# Standard library
import logging
from datetime import datetime
# Third-party
import requests
# Odoo
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
# Local
from .utils import helper_function
Order: Standard library → Third-party → Odoo → Local
Model Definitions¶
class ResPartner(models.Model):
_inherit = 'res.partner'
_description = 'Partner Extension'
# Fields (ordered by type)
# 1. Basic fields
custom_code = fields.Char(string='Custom Code', size=20)
is_vip = fields.Boolean(string='VIP Customer', default=False)
credit_limit = fields.Float(string='Credit Limit', digits=(12, 2))
# 2. Relational fields
sales_rep_id = fields.Many2one('res.users', string='Sales Rep')
tag_ids = fields.Many2many('res.partner.category', string='Tags')
# 3. Computed fields
total_orders = fields.Integer(compute='_compute_total_orders', store=True)
# Constraints
_sql_constraints = [
('custom_code_unique', 'UNIQUE(custom_code)', 'Custom code must be unique!')
]
@api.constrains('credit_limit')
def _check_credit_limit(self):
for record in self:
if record.credit_limit < 0:
raise ValidationError("Credit limit cannot be negative.")
# Compute methods
@api.depends('sale_order_ids')
def _compute_total_orders(self):
for record in self:
record.total_orders = len(record.sale_order_ids)
# CRUD overrides
@api.model
def create(self, vals):
if not vals.get('custom_code'):
vals['custom_code'] = self.env['ir.sequence'].next_by_code('res.partner.custom')
return super().create(vals)
# Business methods
def action_mark_vip(self):
"""Mark selected partners as VIP."""
self.write({'is_vip': True})
Method Order in Models¶
- Private attributes (
_name,_inherit,_description) - Fields definitions
- SQL constraints
- Python constraints (
@api.constrains) - Compute methods
- Onchange methods
- CRUD overrides (
create,write,unlink) - Action methods
- Business methods
Naming Conventions¶
| Type | Convention | Example |
|---|---|---|
| Model | dot.separated |
sale.order.line |
| Field | snake_case |
total_amount |
| Method | snake_case |
compute_total |
| Compute method | _compute_* |
_compute_total |
| Onchange method | _onchange_* |
_onchange_partner_id |
| Action method | action_* |
action_confirm |
| Private method | _* |
_prepare_invoice |
Docstrings¶
def calculate_discount(self, amount, percentage):
"""Calculate discount amount.
Args:
amount (float): Original amount
percentage (float): Discount percentage (0-100)
Returns:
float: Discounted amount
Raises:
ValidationError: If percentage is out of range
"""
if not 0 <= percentage <= 100:
raise ValidationError("Percentage must be between 0 and 100")
return amount * (1 - percentage / 100)
Logging¶
_logger = logging.getLogger(__name__)
# Usage
_logger.debug("Processing partner: %s", partner.name)
_logger.info("Order %s confirmed", order.name)
_logger.warning("Credit limit exceeded for %s", partner.name)
_logger.error("Failed to send SMS: %s", str(e))
Python (Flask PWA)¶
Project Structure¶
pwa/
├── app.py # Application factory
├── config.py # Configuration
├── routes/
│ ├── __init__.py
│ ├── auth.py # Authentication routes
│ ├── jobs.py # Jobs blueprint
│ └── api.py # API endpoints
├── services/
│ ├── odoo.py # Odoo API client
│ └── s3.py # S3 service
├── templates/
└── static/
Flask Patterns¶
# Blueprint definition
from flask import Blueprint, render_template, jsonify
jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs')
@jobs_bp.route('/')
def list_jobs():
"""List active jobs for current user."""
try:
jobs = odoo_service.get_jobs(current_user.id)
return render_template('jobs/list.html', jobs=jobs)
except OdooAPIError as e:
flash(f"Error loading jobs: {e}", 'error')
return redirect(url_for('dashboard.index'))
@jobs_bp.route('/api/jobs')
def api_list_jobs():
"""API endpoint for jobs list."""
jobs = odoo_service.get_jobs(current_user.id)
return jsonify({'jobs': jobs})
Error Handling¶
from flask import jsonify
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Not found'}), 404
@app.errorhandler(500)
def server_error(error):
app.logger.error(f"Server error: {error}")
return jsonify({'error': 'Internal server error'}), 500
JavaScript¶
General Rules¶
- ES6+ syntax
- Use
constby default,letwhen reassignment needed - No
var - Semicolons required
- Single quotes for strings
Code Style¶
// Constants
const API_BASE_URL = '/api';
const MAX_RETRIES = 3;
// Functions
async function fetchJobs() {
try {
const response = await fetch(`${API_BASE_URL}/jobs`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch jobs:', error);
throw error;
}
}
// Event handlers
document.getElementById('submit-btn').addEventListener('click', async (e) => {
e.preventDefault();
await submitForm();
});
// Classes
class JobManager {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.jobs = [];
}
async loadJobs() {
this.jobs = await fetchJobs();
this.render();
}
render() {
const container = document.getElementById('jobs-container');
container.innerHTML = this.jobs.map(job => this.renderJob(job)).join('');
}
renderJob(job) {
return `
<div class="job-card" data-id="${job.id}">
<h3>${job.name}</h3>
<p>${job.status}</p>
</div>
`;
}
}
Offline Handling (PWA)¶
// Check online status
function isOnline() {
return navigator.onLine;
}
// Queue actions for offline
const offlineQueue = [];
async function saveJob(jobData) {
if (isOnline()) {
return await fetch('/api/jobs', {
method: 'POST',
body: JSON.stringify(jobData)
});
} else {
offlineQueue.push({ type: 'saveJob', data: jobData });
localStorage.setItem('offlineQueue', JSON.stringify(offlineQueue));
showNotification('Saved offline. Will sync when online.');
}
}
// Sync when back online
window.addEventListener('online', async () => {
const queue = JSON.parse(localStorage.getItem('offlineQueue') || '[]');
for (const item of queue) {
await processQueueItem(item);
}
localStorage.removeItem('offlineQueue');
});
HTML/CSS¶
HTML Structure¶
<!-- Use semantic elements -->
<header class="app-header">
<nav class="main-nav">...</nav>
</header>
<main class="content">
<section class="jobs-list">
<h1>Active Jobs</h1>
<article class="job-card">...</article>
</section>
</main>
<footer class="app-footer">...</footer>
CSS Naming (BEM)¶
/* Block */
.job-card { }
/* Element */
.job-card__title { }
.job-card__status { }
/* Modifier */
.job-card--urgent { }
.job-card--completed { }
SQL¶
Safe Queries in Odoo¶
# GOOD - Parameterized query
self.env.cr.execute("""
SELECT id, name FROM res_partner
WHERE company_id = %s AND active = %s
""", (company_id, True))
# BAD - SQL injection risk
self.env.cr.execute(f"""
SELECT * FROM res_partner WHERE name = '{user_input}'
""")
Use ORM When Possible¶
# Prefer ORM
partners = self.env['res.partner'].search([
('company_id', '=', company_id),
('active', '=', True)
])
# Use SQL only for performance-critical queries
File Headers¶
Python Files¶
# -*- coding: utf-8 -*-
# Part of JDX Odoo. See LICENSE file for full copyright.
"""
Module description here.
"""
JavaScript Files¶
Code Review Checklist¶
- Follows naming conventions
- Has appropriate docstrings/comments
- No hardcoded values (use config/constants)
- Proper error handling
- No SQL injection vulnerabilities
- Logging for important operations
- Tests added for new functionality