bmad-method-odoo
Version:
BMAD-METHOD expansion pack for comprehensive Odoo ERP development workflows
637 lines (545 loc) • 19.7 kB
Markdown
# Create Odoo Addon
You are tasked with creating a new Odoo addon based on business requirements and technical specifications. This task involves generating a complete addon structure following OCA standards and Odoo best practices.
## Task Objectives
### Primary Goals
- Create a complete addon structure with all necessary files
- Implement business logic according to specifications
- Follow OCA development standards and guidelines
- ensure compatibility with target Odoo version
- Provide comprehensive documentation and testing
### Success Criteria
- Addon installs without errors on target Odoo version
- All functional requirements are implemented
- Code passes OCA quality checks (flake8, pylint-odoo)
- Basic unit tests are included
- Documentation is complete and accurate
## Prerequisites
### Required Information
- **Business Requirements**: Clear functional specifications
- **Technical Architecture**: Data model and integration design
- **Target Odoo Version**: Specific version compatibility (13.0-18.0)
- **Module Dependencies**: Required standard and OCA modules
- **Integration Points**: External systems or APIs to connect
### Development Environment
- Doodba-based development environment
- Access to OCA module templates and tools
- Git repository for version control
- Testing database and environment
## Implementation Process
### 1. Addon Structure Creation
#### Manifest File (`__manifest__.py`)
```python
{
'name': 'Module Name',
'version': '16.0.1.0.0',
'category': 'Category',
'summary': 'Brief module description',
'description': """
Long description of the module functionality.
Include:
- Key features
- Business use cases
- Integration points
- Configuration requirements
""",
'author': 'Your Company, Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/project-repo',
'license': 'AGPL-3',
'depends': [
'base',
'web',
# Add required dependencies
],
'data': [
'security/ir.model.access.csv',
'security/security.xml',
'views/model_views.xml',
'views/menu_items.xml',
'data/initial_data.xml',
],
'demo': [
'demo/demo_data.xml',
],
'installable': True,
'auto_install': False,
'application': False,
'external_dependencies': {
'python': [],
'bin': [],
},
}
```
#### Directory Structure
```
addon_name/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── model_name.py
├── views/
│ ├── model_views.xml
│ └── menu_items.xml
├── security/
│ ├── ir.model.access.csv
│ └── security.xml
├── data/
│ └── initial_data.xml
├── demo/
│ └── demo_data.xml
├── tests/
│ ├── __init__.py
│ └── test_model.py
├── static/
│ ├── description/
│ │ ├── icon.png
│ │ └── index.html
│ └── src/
│ └── js/
├── wizards/
│ ├── __init__.py
│ └── wizard_name.py
└── README.rst
```
### 2. Model Implementation
#### Base Model Structure
```python
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
class CustomModel(models.Model):
_name = 'custom.model'
_description = 'Custom Model Description'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'sequence, name'
_rec_name = 'name'
# Basic fields
name = fields.Char(
string='Name',
required=True,
translate=True,
tracking=True
)
description = fields.Text(
string='Description',
translate=True
)
sequence = fields.Integer(
string='Sequence',
default=10,
help='Used to order records'
)
active = fields.Boolean(
string='Active',
default=True,
tracking=True
)
# State management
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
('cancelled', 'Cancelled')
], string='Status', default='draft', tracking=True)
# Relationships
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
required=True
)
user_id = fields.Many2one(
'res.users',
string='Responsible',
default=lambda self: self.env.user,
tracking=True
)
# Computed fields
@api.depends('field1', 'field2')
def _compute_calculated_field(self):
for record in self:
record.calculated_field = record.field1 + record.field2
calculated_field = fields.Float(
string='Calculated Field',
compute='_compute_calculated_field',
store=True
)
# Constraints
@api.constrains('field1')
def _check_field1(self):
for record in self:
if record.field1 < 0:
raise ValidationError(_('Field1 must be positive'))
# CRUD hooks
@api.model
def create(self, vals):
# Custom creation logic
if 'sequence' not in vals:
vals['sequence'] = self._get_next_sequence()
return super().create(vals)
def write(self, vals):
# Custom update logic
if 'state' in vals and vals['state'] == 'confirmed':
self._validate_confirmation()
return super().write(vals)
def unlink(self):
# Custom deletion logic
if any(record.state == 'confirmed' for record in self):
raise UserError(_('Cannot delete confirmed records'))
return super().unlink()
# Business methods
def action_confirm(self):
self.ensure_one()
if self.state != 'draft':
raise UserError(_('Only draft records can be confirmed'))
self.state = 'confirmed'
self.message_post(body=_('Record confirmed'))
def action_cancel(self):
self.ensure_one()
if self.state == 'done':
raise UserError(_('Cannot cancel completed records'))
self.state = 'cancelled'
self.message_post(body=_('Record cancelled'))
# Private methods
def _get_next_sequence(self):
last_record = self.search([], order='sequence desc', limit=1)
return (last_record.sequence or 0) + 10
def _validate_confirmation(self):
# Validation logic for confirmation
if not self.name:
raise ValidationError(_('Name is required for confirmation'))
```
### 3. View Implementation
#### Form View
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_custom_model_form" model="ir.ui.view">
<field name="name">custom.model.form</field>
<field name="model">custom.model</field>
<field name="arch" type="xml">
<form string="Custom Model">
<header>
<button name="action_confirm" type="object"
string="Confirm" class="oe_highlight"
invisible="state != 'draft'"/>
<button name="action_cancel" type="object"
string="Cancel"
invisible="state in ('done', 'cancelled')"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,confirmed,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Enter name..."/>
</h1>
</div>
<group>
<group>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="sequence"/>
<field name="active"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description" nolabel="1"/>
</page>
<page string="Additional Info">
<group>
<field name="calculated_field"/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_custom_model_tree" model="ir.ui.view">
<field name="name">custom.model.tree</field>
<field name="model">custom.model</field>
<field name="arch" type="xml">
<tree string="Custom Models"
decoration-info="state=='draft'"
decoration-success="state=='done'"
decoration-muted="state=='cancelled'">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="user_id"/>
<field name="calculated_field"/>
<field name="state"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- Search View -->
<record id="view_custom_model_search" model="ir.ui.view">
<field name="name">custom.model.search</field>
<field name="model">custom.model</field>
<field name="arch" type="xml">
<search string="Search Custom Models">
<field name="name"/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<separator/>
<filter name="my_records" string="My Records"
domain="[('user_id', '=', uid)]"/>
<filter name="active" string="Active"
domain="[('active', '=', True)]"/>
<separator/>
<filter name="draft" string="Draft"
domain="[('state', '=', 'draft')]"/>
<filter name="confirmed" string="Confirmed"
domain="[('state', '=', 'confirmed')]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Responsible" name="group_user"
context="{'group_by': 'user_id'}"/>
<filter string="Company" name="group_company"
context="{'group_by': 'company_id'}"
groups="base.group_multi_company"/>
</group>
</search>
</field>
</record>
</odoo>
```
### 4. Security Implementation
#### Access Rights (`security/ir.model.access.csv`)
```csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_custom_model_user,custom.model.user,model_custom_model,base.group_user,1,1,1,1
access_custom_model_manager,custom.model.manager,model_custom_model,base.group_system,1,1,1,1
```
#### Security Groups (`security/security.xml`)
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Security Groups -->
<record model="res.groups" id="group_custom_model_user">
<field name="name">Custom Model User</field>
<field name="category_id" ref="base.module_category_operations"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record model="res.groups" id="group_custom_model_manager">
<field name="name">Custom Model Manager</field>
<field name="category_id" ref="base.module_category_operations"/>
<field name="implied_ids" eval="[(4, ref('group_custom_model_user'))]"/>
</record>
<!-- Record Rules -->
<record id="custom_model_rule_user" model="ir.rule">
<field name="name">Custom Model: User Access</field>
<field name="model_id" ref="model_custom_model"/>
<field name="domain_force">[
'|',
('user_id', '=', user.id),
('company_id', 'in', user.company_ids.ids)
]</field>
<field name="groups" eval="[(4, ref('group_custom_model_user'))]"/>
</record>
<record id="custom_model_rule_manager" model="ir.rule">
<field name="name">Custom Model: Manager Access</field>
<field name="model_id" ref="model_custom_model"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_custom_model_manager'))]"/>
</record>
</odoo>
```
### 5. Menu and Actions
#### Menu Items (`views/menu_items.xml`)
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Actions -->
<record id="action_custom_model" model="ir.actions.act_window">
<field name="name">Custom Models</field>
<field name="res_model">custom.model</field>
<field name="view_mode">tree,form</field>
<field name="context">{}</field>
<field name="domain">[]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first custom model!
</p>
<p>
Custom models help you manage your business processes
efficiently with powerful features and integrations.
</p>
</field>
</record>
<!-- Menu Items -->
<menuitem id="menu_custom_root"
name="Custom Module"
sequence="10"/>
<menuitem id="menu_custom_models"
name="Custom Models"
parent="menu_custom_root"
action="action_custom_model"
sequence="10"/>
</odoo>
```
### 6. Testing Implementation
#### Unit Tests (`tests/test_model.py`)
```python
from odoo.tests import TransactionCase
from odoo.exceptions import ValidationError, UserError
class TestCustomModel(TransactionCase):
def setUp(self):
super().setUp()
self.CustomModel = self.env['custom.model']
self.user = self.env.ref('base.user_demo')
def test_create_model(self):
"""Test model creation with required fields"""
model = self.CustomModel.create({
'name': 'Test Model',
'user_id': self.user.id,
})
self.assertEqual(model.name, 'Test Model')
self.assertEqual(model.state, 'draft')
self.assertEqual(model.user_id, self.user)
self.assertTrue(model.active)
def test_confirm_action(self):
"""Test confirmation workflow"""
model = self.CustomModel.create({
'name': 'Test Model',
'user_id': self.user.id,
})
# Test confirmation
model.action_confirm()
self.assertEqual(model.state, 'confirmed')
# Test cannot confirm again
with self.assertRaises(UserError):
model.action_confirm()
def test_validation_constraints(self):
"""Test model validation constraints"""
with self.assertRaises(ValidationError):
self.CustomModel.create({
'name': 'Test Model',
'field1': -1, # Should fail constraint
})
def test_computed_fields(self):
"""Test computed field calculation"""
model = self.CustomModel.create({
'name': 'Test Model',
'field1': 10,
'field2': 20,
})
self.assertEqual(model.calculated_field, 30)
def test_sequence_generation(self):
"""Test automatic sequence generation"""
model1 = self.CustomModel.create({'name': 'Model 1'})
model2 = self.CustomModel.create({'name': 'Model 2'})
self.assertGreater(model2.sequence, model1.sequence)
```
### 7. Documentation
#### README.rst
```rst
============
Custom Model
============
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
|badge1|
This module provides custom model functionality for managing business processes.
**Table of contents**
.. contents::
:local:
Features
========
* Custom model creation and management
* Workflow management with state transitions
* Multi-company support
* Activity and messaging integration
* Comprehensive security model
Usage
=====
To use this module:
1. Go to Custom Module menu
2. Create a new custom model
3. Fill in required information
4. Use workflow actions to manage state
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/project-repo/issues>`_.
Credits
=======
Authors
~~~~~~~
* Your Company
Contributors
~~~~~~~~~~~~
* Your Name <your.email@example.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/project-repo <https://github.com/OCA/project-repo/tree/16.0/custom_model>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
```
## Quality Assurance
### Pre-commit Hooks
```yaml
# .pre-commit-config.yaml
repos:
- repo: https://github.com/OCA/odoo-pre-commit-hooks
rev: v1.0.0
hooks:
- id: oca-checks-odoo-module
- id: oca-checks-po
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear==21.9.2]
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort except __init__.py
exclude: /__init__\.py$
```
### Testing Checklist
- [ ] Module installs without errors
- [ ] All views render correctly
- [ ] Security rules work as expected
- [ ] Unit tests pass
- [ ] Code passes linting checks
- [ ] Documentation is complete
- [ ] Demo data loads correctly
## Deployment Instructions
### Installation Steps
1. Copy addon to Odoo addons path
2. Update addon list: `invoke install --modules=custom_model`
3. Install addon from Apps menu
4. Configure security groups if needed
5. Import initial data if required
### Post-Installation
1. Verify addon functionality
2. Train users on new features
3. Monitor performance and usage
4. Set up backup procedures
Remember: This task creates the foundation for Odoo addon development. Focus on code quality, security, and user experience while following OCA standards for community compatibility.