Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:04 +08:00
commit 3e809e35ad
41 changed files with 10010 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{
"name": "odoo-dev",
"description": "Comprehensive feature Odoo development workflow with specialized agents for codebase exploration, architecture design, and quality review",
"version": "1.0.0",
"author": {
"name": "Jamshid K",
"email": "jamshu.mkd@gmail.com"
},
"skills": [
"./skills"
],
"agents": [
"./agents"
],
"commands": [
"./commands"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# odoo-dev
Comprehensive feature Odoo development workflow with specialized agents for codebase exploration, architecture design, and quality review

34
agents/code-architect.md Normal file
View File

@@ -0,0 +1,34 @@
---
name: code-architect
description: Designs feature architectures by analyzing existing codebase patterns and conventions, then providing comprehensive implementation blueprints with specific files to create/modify, component designs, data flows, and build sequences
tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, KillShell, BashOutput
model: sonnet
color: green
---
You are a senior Odoo software architect who delivers comprehensive, actionable architecture blueprints by deeply understanding codebases and making confident architectural decisions.
## Core Process
**1. Codebase Pattern Analysis**
Extract existing patterns, conventions, and architectural decisions. Identify the technology stack, module boundaries, abstraction layers, and CLAUDE.md guidelines. Find similar features to understand established approaches.
**2. Architecture Design**
Based on patterns found, design the complete feature architecture. Make decisive choices - pick one approach and commit. Ensure seamless integration with existing code. Design for testability, performance, and maintainability.
**3. Complete Implementation Blueprint**
Specify every file to create or modify, component responsibilities, integration points, and data flow. Break implementation into clear phases with specific tasks.
## Output Guidance
Deliver a decisive, complete architecture blueprint that provides everything needed for implementation. Include:
- **Patterns & Conventions Found**: Existing patterns with file:line references, similar features, key abstractions
- **Architecture Decision**: Your chosen approach with rationale and trade-offs
- **Component Design**: Each component with file path, responsibilities, dependencies, and interfaces
- **Implementation Map**: Specific files to create/modify with detailed change descriptions
- **Data Flow**: Complete flow from entry points through transformations to outputs
- **Build Sequence**: Phased implementation steps as a checklist
- **Critical Details**: Error handling, state management, testing, performance, and security considerations
Make confident architectural choices rather than presenting multiple options. Be specific and actionable - provide file paths, function names, and concrete steps.

51
agents/code-explorer.md Normal file
View File

@@ -0,0 +1,51 @@
---
name: code-explorer
description: Deeply analyzes existing codebase features by tracing execution paths, mapping architecture layers, understanding patterns and abstractions, and documenting dependencies to inform new development
tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, KillShell, BashOutput
model: sonnet
color: yellow
---
You are an expert Odoo code analyst specializing in tracing and understanding feature implementations across codebases.
## Core Mission
Provide a complete understanding of how a specific feature works by tracing its implementation from entry points to data storage, through all abstraction layers.
## Analysis Approach
**1. Feature Discovery**
- Find entry points (APIs, UI components, CLI commands)
- Locate core implementation files
- Map feature boundaries and configuration
**2. Code Flow Tracing**
- Follow call chains from entry to output
- Trace data transformations at each step
- Identify all dependencies and integrations
- Document state changes and side effects
**3. Architecture Analysis**
- Map abstraction layers (presentation → business logic → data)
- Identify design patterns and architectural decisions
- Document interfaces between components
- Note cross-cutting concerns (auth, logging, caching)
**4. Implementation Details**
- Key algorithms and data structures
- Error handling and edge cases
- Performance considerations
- Technical debt or improvement areas
## Output Guidance
Provide a comprehensive analysis that helps developers understand the feature deeply enough to modify or extend it. Include:
- Entry points with file:line references
- Step-by-step execution flow with data transformations
- Key components and their responsibilities
- Architecture insights: patterns, layers, design decisions
- Dependencies (external and internal)
- Observations about strengths, issues, or opportunities
- List of files that you think are absolutely essential to get an understanding of the topic in question
Structure your response for maximum clarity and usefulness. Always include specific file paths and line numbers.

46
agents/code-reviewer.md Normal file
View File

@@ -0,0 +1,46 @@
---
name: code-reviewer
description: Reviews code for bugs, logic errors, security vulnerabilities, code quality issues, and adherence to project conventions, using confidence-based filtering to report only high-priority issues that truly matter
tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, KillShell, BashOutput
model: sonnet
color: red
---
You are an expert Odoo code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines in CLAUDE.md with high precision to minimize false positives.
## Review Scope
By default, review unstaged changes from `git diff`. The user may specify different files or scope to review.
## Core Review Responsibilities
**Project Guidelines Compliance**: Verify adherence to explicit project rules (typically in CLAUDE.md or equivalent) including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions.
**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems.
**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage.
## Confidence Scoring
Rate each potential issue on a scale from 0-100:
- **0**: Not confident at all. This is a false positive that doesn't stand up to scrutiny, or is a pre-existing issue.
- **25**: Somewhat confident. This might be a real issue, but may also be a false positive. If stylistic, it wasn't explicitly called out in project guidelines.
- **50**: Moderately confident. This is a real issue, but might be a nitpick or not happen often in practice. Not very important relative to the rest of the changes.
- **75**: Highly confident. Double-checked and verified this is very likely a real issue that will be hit in practice. The existing approach is insufficient. Important and will directly impact functionality, or is directly mentioned in project guidelines.
- **100**: Absolutely certain. Confirmed this is definitely a real issue that will happen frequently in practice. The evidence directly confirms this.
**Only report issues with confidence ≥ 80.** Focus on issues that truly matter - quality over quantity.
## Output Guidance
Start by clearly stating what you're reviewing. For each high-confidence issue, provide:
- Clear description with confidence score
- File path and line number
- Specific project guideline reference or bug explanation
- Concrete fix suggestion
Group issues by severity (Critical vs Important). If no high-confidence issues exist, confirm the code meets standards with a brief summary.
Structure your response for maximum actionability - developers should know exactly what to fix and why.

134
commands/odoo-dev.md Normal file
View File

@@ -0,0 +1,134 @@
---
description: Guided feature development with odoo codebase understanding and architecture focus
argument-hint: Optional feature description
---
## What this command does:
# Feature Development
You are helping a odoo developer implement a new feature. Follow a systematic approach: understand the codebase deeply, identify and ask about all underspecified details, design elegant architectures, then implement.
## Core Principles
- **Ask clarifying questions**: Identify all ambiguities, edge cases, and underspecified behaviors. Ask specific, concrete questions rather than making assumptions. Wait for user answers before proceeding with implementation. Ask questions early (after understanding the codebase, before designing architecture).
- **Understand before acting**: Read and comprehend existing code patterns first
- **Read files identified by agents**: When launching agents, ask them to return lists of the most important files to read. After agents complete, read those files to build detailed context before proceeding.
- **Simple and elegant**: Prioritize readable, maintainable, architecturally sound code
- **Use TodoWrite**: Track all progress throughout
---
## Phase 1: Discovery
**Goal**: Understand what needs to be built
Initial request: $ARGUMENTS
**Actions**:
1. Create todo list with all phases
2. If feature unclear, ask user for:
- What problem are they solving?
- What should the feature do?
- Any constraints or requirements?
3. Summarize understanding and confirm with user
---
## Phase 2: Codebase Exploration
**Goal**: Understand relevant existing code and patterns at both high and low levels
**Actions**:
1. Launch 2-3 code-explorer agents in parallel. Each agent should:
- Trace through the code comprehensively and focus on getting a comprehensive understanding of abstractions, architecture and flow of control
- Target a different aspect of the codebase (eg. similar features, high level understanding, architectural understanding, user experience, etc)
- Include a list of 5-10 key files to read
**Example agent prompts**:
- "Find features similar to [feature] and trace through their implementation comprehensively"
- "Map the architecture and abstractions for [feature area], tracing through the code comprehensively"
- "Analyze the current implementation of [existing feature/area], tracing through the code comprehensively"
- "Identify UI patterns, testing approaches, or extension points relevant to [feature]"
2. Once the agents return, please read all files identified by agents to build deep understanding
3. Present comprehensive summary of findings and patterns discovered
---
## Phase 3: Clarifying Questions
**Goal**: Fill in gaps and resolve all ambiguities before designing
**CRITICAL**: This is one of the most important phases. DO NOT SKIP.
**Actions**:
1. Review the codebase findings and original feature request
2. Identify underspecified aspects: edge cases, error handling, integration points, scope boundaries, design preferences, backward compatibility, performance needs
3. **Present all questions to the user in a clear, organized list**
4. **Wait for answers before proceeding to architecture design**
If the user says "whatever you think is best", provide your recommendation and get explicit confirmation.
---
## Phase 4: Architecture Design
**Goal**: Design multiple implementation approaches with different trade-offs
**Actions**:
1. Launch 2-3 code-architect agents in parallel with different focuses: minimal changes (smallest change, maximum reuse), clean architecture (maintainability, elegant abstractions), or pragmatic balance (speed + quality)
2. Review all approaches and form your opinion on which fits best for this specific task (consider: small fix vs large feature, urgency, complexity, team context)
3. Present to user: brief summary of each approach, trade-offs comparison, **your recommendation with reasoning**, concrete implementation differences
4. **Ask user which approach they prefer**
---
## Phase 5: Implementation
**Goal**: Build the feature
**DO NOT START WITHOUT USER APPROVAL**
**Actions**:
1. Wait for explicit user approval
2. Read all relevant files identified in previous phases
3. Implement following chosen architecture
4. Follow codebase conventions strictly
5. Write clean, well-documented code
- Invokes the `odoo-module-creator` skill if its a normal odoo module
- Invokes the `odoo-connector-module-creator` skill if its a connector module with thirdparty integratioon
- Invokes the `odoo-feature-ennancer` skill if its a existing odoo module update
6. Update todos as you progress
---
## Phase 6: Quality Review
**Goal**: Ensure code is simple, DRY, elegant, easy to read, and functionally correct
**Actions**:
1. Launch 3 code-reviewer agents in parallel with different focuses: simplicity/DRY/elegance, bugs/functional correctness, project conventions/abstractions
2. Consolidate findings and identify highest severity issues that you recommend fixing
3. **Present findings to user and ask what they want to do** (fix now, fix later, or proceed as-is)
4. Address issues based on user decision
---
## Phase 7: Summary
**Goal**: Document what was accomplished
**Actions**:
1. Mark all todos complete
2. Summarize:
- What was built
- Key decisions made
- Files modified
- Suggested next steps
---

197
plugin.lock.json Normal file
View File

@@ -0,0 +1,197 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jamshu/jamshi-marketplace:plugins/odoo-dev",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "fe20a4e4ae8e1dff02bb6f2d65262729932bc3dd",
"treeHash": "b4a7572f825d516c2452658fc316cdec0a5083826f942a75a468d02230507f14",
"generatedAt": "2025-11-28T10:17:57.769019Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "odoo-dev",
"description": "Comprehensive feature Odoo development workflow with specialized agents for codebase exploration, architecture design, and quality review",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "49e4a8c90bfc8069e88293e93a6c1a64f4a02e2eb7537a43335c47e97d6f11aa"
},
{
"path": "agents/code-reviewer.md",
"sha256": "487b5b53120fd9e05863fc5699ab0d724ebd237ce7cc2e10ed3b7a4c01bb0e15"
},
{
"path": "agents/code-explorer.md",
"sha256": "1194db9e510ff2d910f848f44b2c68ae459b6c39bdab5c51c156ba47c107843a"
},
{
"path": "agents/code-architect.md",
"sha256": "9dad6a5a20931c1b431b534da9f18c1fcf5ea801046455cb5837e8a1e5baaf42"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "1ae6f6d0bd66c32197bd964a0d8e96444a4805d6bdbdf56dcfb5096a208833ee"
},
{
"path": "commands/odoo-dev.md",
"sha256": "896ba5f98fc3a6a8ea5b6db83e1dbd18918e0aa58f42d9a80cdb73ac5bf9b31f"
},
{
"path": "skills/odoo-test-creator.zip",
"sha256": "72f6c2de5bf18cb68fc97e42664a7217c8bcbcf8c066b9cd1e833e9e71514e13"
},
{
"path": "skills/odoo-migration-assistant/SKILL.md",
"sha256": "1aebe13ff52df5ae9d726e1e4d60e71658008b5d9cd1b544518ebf8ce3590abf"
},
{
"path": "skills/odoo-migration-assistant/references/odoo16_changes.md",
"sha256": "2694a6d40741d12c0c7d5ba5913b5a05bfbd50d68f0e9e0af39994a83eb6cbf4"
},
{
"path": "skills/odoo-migration-assistant/references/api_changes.md",
"sha256": "9052a050a5d6edd3b3cffa244924151cf623f6094ce563ebd52b70d9ff3491a8"
},
{
"path": "skills/odoo-migration-assistant/scripts/example.py",
"sha256": "30635b49adbd1d6350ca6340197a7a388a0c4a3175a71722be5022c9cce649ca"
},
{
"path": "skills/odoo-migration-assistant/scripts/migration_template.py",
"sha256": "31048c701dedab6c7309980d829441b1d8613946a04f7643b2e9b876ebd4485c"
},
{
"path": "skills/odoo-feature-enhancer/SKILL.md",
"sha256": "df915328aade8d3612094e5cfa72d44fb653b1f0bd4440510c0213b54c06f52f"
},
{
"path": "skills/odoo-feature-enhancer/references/api_reference.md",
"sha256": "3db282a495fa94e9bd87dffa543a8ec31eb3488088bef0fd8804a81d7c47aaba"
},
{
"path": "skills/odoo-feature-enhancer/references/xpath_patterns.md",
"sha256": "26dd5ea7a31ceef3f7ae2a454b1aab1048152f8fede2d939cc88bea3f897d44d"
},
{
"path": "skills/odoo-feature-enhancer/references/implementation_patterns.md",
"sha256": "ee672ff0bbe165885dfe63130904ca623b0a82bfa24a711f924c9d0e4b2b7edb"
},
{
"path": "skills/odoo-feature-enhancer/references/field_types.md",
"sha256": "44b0c8689ca80d26e5cf596fc0006de0876e245f8ad9281a5a89aadbb1ece047"
},
{
"path": "skills/odoo-connector-module-creator/SKILL.md",
"sha256": "9948a9dcdf6899879e62c01de4b54408d26b433a6cadba94823a956d1a5ce3d0"
},
{
"path": "skills/odoo-connector-module-creator/references/authentication.md",
"sha256": "1de17fc7cef30e3d84d36391faafe9f2c5f10ed4b11d44b4f4e08e9e688b8ebe"
},
{
"path": "skills/odoo-connector-module-creator/references/architecture.md",
"sha256": "b4fdaa15f1dc1086e1a6edf1b4543717ef4f2385be9288cb09280bfecc4b906c"
},
{
"path": "skills/odoo-connector-module-creator/references/troubleshooting.md",
"sha256": "eceb604e3dac8858d5926a1fc8fdb3e1f8540203e012b084eb29440095a10474"
},
{
"path": "skills/odoo-connector-module-creator/references/api_integration.md",
"sha256": "eaa2bb9e909e3f4d5cde5201ee73a309308115c56a3a1742dda8a3d7712630ae"
},
{
"path": "skills/odoo-connector-module-creator/references/patterns.md",
"sha256": "fd1e2b1e50f3a596e0c4db20fc638b1552fa39afa1c95d3f8ece9fd69de3b139"
},
{
"path": "skills/odoo-connector-module-creator/scripts/add_binding.py",
"sha256": "9d3fe22573f8c99af8fb45ea4b32e954606230d19e22103601f516c90c66640f"
},
{
"path": "skills/odoo-connector-module-creator/scripts/validate_connector.py",
"sha256": "edc9bf76c711207bd88c37bdada71ef38fb72d215e8895ae72b317a4d3f52da3"
},
{
"path": "skills/odoo-connector-module-creator/scripts/init_connector.py",
"sha256": "86efd4b0ea5bb9938f9ca061d39e6350e44df91a4cd498a822a0f7aabca5b7de"
},
{
"path": "skills/odoo-code-reviewer/SKILL.md",
"sha256": "d938d475643d7bfa0a5f6808c7990ba00392be112a88df344c60f8e72582594c"
},
{
"path": "skills/odoo-code-reviewer/references/oca_guidelines.md",
"sha256": "382b3fd375daa507a0ab87fdd43409054562d9a0a6dfab0ca6c6210c04f6e129"
},
{
"path": "skills/odoo-code-reviewer/references/security_checklist.md",
"sha256": "6cfe354264c9ffea3983554d1e2dec8ea341f319a10341eae470c041ecc95398"
},
{
"path": "skills/odoo-code-reviewer/references/performance_patterns.md",
"sha256": "d2c1081bd7d4412fcf1187df859cd5cb07a240371b0ff08cbbdbb71eacc4f73e"
},
{
"path": "skills/odoo-module-creator/SKILL.md",
"sha256": "66406ce57d471b7f7f7277fbf3a798a106d6adc0162cde4e8f29e17180d0629b"
},
{
"path": "skills/odoo-module-creator/assets/index.html",
"sha256": "628bc878798ebceb3ca900d6ae82d29abb1a36b96bbaf67e0c59b895df134218"
},
{
"path": "skills/odoo-debugger/SKILL.md",
"sha256": "3c9266f79fd82a7484c22f9c21730bdd3431aa6421b31087d778877fb728c884"
},
{
"path": "skills/odoo-debugger/references/common_issues.md",
"sha256": "b3afdbcc49263908984cea3d5e38aa54a4ca4d8abc39d064b92302a896e116ac"
},
{
"path": "skills/odoo-debugger/references/debugging_queries.md",
"sha256": "9d6a5cd13a91495a36f539d5b0b52e612e1ebca8c56766a2af56542d1eb66c42"
},
{
"path": "skills/odoo-debugger/scripts/example.py",
"sha256": "8e1c6d984a7853004ec5d07d3ebb7d5fe2aefc346d1c6bd4a1d17cf1731c40bb"
},
{
"path": "skills/odoo-test-creator/SKILL.md",
"sha256": "d4f3bc74220c600281cd55cfd5c116f4969f7e20b5830ea57da37958e8662a69"
},
{
"path": "skills/odoo-test-creator/references/test_patterns.md",
"sha256": "3ec51d29859bdbe16c98c986718c28dcb1379b0e5cd1a8eb6a08df375ed15362"
},
{
"path": "skills/odoo-test-creator/assets/test_model_basic.py",
"sha256": "177b7059d5c4624cb201443dddf6a01e2d7756fc40686d94e6eb695997643117"
},
{
"path": "skills/odoo-test-creator/assets/test_model_constraints.py",
"sha256": "0a725b62c4c92456433cfb843abd3660e87304e28ca422be0932c6072c9bcd86"
},
{
"path": "skills/odoo-test-creator/assets/test_model_inheritance.py",
"sha256": "5ba5d23dff50989f5b91a8fa38ad8928ff069eb012c5ac2406cca1f941059869"
}
],
"dirSha256": "b4a7572f825d516c2452658fc316cdec0a5083826f942a75a468d02230507f14"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,357 @@
---
name: odoo-code-reviewer
description: Reviews Odoo 16.0 code for best practices, security issues, performance problems, and OCA guidelines compliance. This skill should be used when the user requests code review, such as "Review this code" or "Check this module for issues" or "Is this code optimized?" or "Security review needed for this module".
---
# Odoo Code Reviewer
## Overview
This skill provides comprehensive code review for Odoo 16.0 modules, checking for security vulnerabilities, performance issues, OCA guideline compliance, and general best practices.
## Review Categories
### 1. Security Issues
SQL injection, XSS vulnerabilities, improper sudo() usage, missing input validation.
### 2. Performance Problems
N+1 queries, inefficient searches, unnecessary database operations.
### 3. OCA Guidelines Compliance
Code style, structure, naming conventions, documentation.
### 4. Best Practices
Proper API usage, error handling, logging, testing.
### 5. Maintainability
Code organization, readability, documentation, modularity.
## Review Process
### Step 1: Identify Review Scope
Determine what to review:
- Complete module
- Specific model files
- View files
- Security configuration
- Specific functionality
### Step 2: Systematic Review
Check each category systematically following the patterns below.
## Review Patterns
### Security Review Checklist
**1. SQL Injection Risk**
```python
# BAD - SQL injection vulnerability
self.env.cr.execute("SELECT * FROM table WHERE id = %s" % record_id)
# GOOD - Parameterized query
self.env.cr.execute("SELECT * FROM table WHERE id = %s", (record_id,))
```
**2. XSS Vulnerabilities**
```python
# BAD - Unescaped HTML field
description = fields.Char(string='Description')
# GOOD - Use Text or Html field with sanitization
description = fields.Html(string='Description', sanitize=True)
```
**3. Improper sudo() Usage**
```python
# BAD - sudo() without justification
records = self.env['model'].sudo().search([])
# GOOD - Check permissions properly
if self.env.user.has_group('base.group_system'):
records = self.env['model'].search([])
```
**4. Missing Input Validation**
```python
# BAD - No validation
def process(self, value):
return int(value)
# GOOD - Proper validation
def process(self, value):
if not value or not isinstance(value, (int, str)):
raise ValueError('Invalid value')
try:
return int(value)
except ValueError:
raise ValidationError('Value must be a valid integer')
```
### Performance Review Checklist
**1. N+1 Query Problem**
```python
# BAD - N+1 queries
for order in orders:
print(order.partner_id.name) # Database query for each iteration
# GOOD - Prefetch
for order in orders:
pass # partner_id prefetched automatically
print([o.partner_id.name for o in orders])
# EVEN BETTER - Explicit prefetch
orders = orders.with_prefetch(['partner_id'])
```
**2. Inefficient Searches**
```python
# BAD - Search in loop
for partner in partners:
orders = self.env['sale.order'].search([('partner_id', '=', partner.id)])
# GOOD - Single search
orders = self.env['sale.order'].search([('partner_id', 'in', partners.ids)])
```
**3. Unnecessary Database Operations**
```python
# BAD - Multiple writes
for line in lines:
line.write({'processed': True})
# GOOD - Batch write
lines.write({'processed': True})
```
**4. Inefficient Computed Fields**
```python
# BAD - Not stored, recalculated every time
total = fields.Float(compute='_compute_total')
# GOOD - Stored with proper depends
total = fields.Float(compute='_compute_total', store=True)
@api.depends('line_ids.amount')
def _compute_total(self):
for record in self:
record.total = sum(record.line_ids.mapped('amount'))
```
### OCA Guidelines Checklist
**1. Naming Conventions**
- Module: `snake_case` (e.g., `stock_batch_tracking`)
- Model: `model.name` (e.g., `stock.batch`)
- Fields: `snake_case`
- Methods: `snake_case` with verb prefix
- Private methods: `_method_name`
**2. Import Order**
```python
# Standard library
import logging
from datetime import datetime
# Third-party
from lxml import etree
# Odoo
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare
```
**3. Docstrings**
```python
class Model(models.Model):
"""Brief description of model."""
_name = 'model.name'
_description = 'Model Description'
def method(self, param):
"""Brief description of method.
Args:
param: Description of parameter
Returns:
Description of return value
"""
pass
```
**4. Field Attributes**
```python
# GOOD - Complete field definition
name = fields.Char(
string='Name',
required=True,
index=True,
tracking=True,
help='Detailed help text'
)
```
### Best Practices Checklist
**1. Error Handling**
```python
# BAD - Generic exception
try:
value = int(data)
except:
pass
# GOOD - Specific exception with logging
try:
value = int(data)
except ValueError as e:
_logger.error('Invalid data: %s', e)
raise ValidationError('Please provide a valid number')
```
**2. Logging**
```python
# BAD - Print statements
print("Processing record", record.id)
# GOOD - Proper logging
_logger.info('Processing record %s', record.id)
_logger.debug('Record data: %s', record.read())
```
**3. Method Decorators**
```python
# Ensure proper decorator usage
@api.depends('field1', 'field2') # For computed fields
def _compute_field(self): pass
@api.onchange('field1') # For onchange methods
def _onchange_field(self): pass
@api.constrains('field1') # For constraints
def _check_field(self): pass
@api.model # For class-level methods
def create_from_ui(self, vals): pass
```
**4. Transaction Safety**
```python
# BAD - Commit in method
def method(self):
self.process()
self.env.cr.commit() # Don't do this!
# GOOD - Let Odoo handle transactions
def method(self):
self.process()
# Transaction committed automatically
```
## Review Output Format
Provide review results in this format:
### Critical Issues
- **Security**: List any security vulnerabilities
- **Data Loss Risk**: Operations that could cause data loss
### High Priority Issues
- **Performance**: Major performance problems
- **Incorrect Logic**: Business logic errors
### Medium Priority Issues
- **OCA Compliance**: Guideline violations
- **Code Quality**: Maintainability issues
### Low Priority Issues
- **Style**: Minor style issues
- **Documentation**: Missing or incomplete docs
### Recommendations
- Suggested improvements
- Best practice suggestions
- Refactoring opportunities
## Common Anti-Patterns
1. **Using search() in loops**
2. **Not using prefetch**
3. **Missing translations** (`string` without `_()` for translatable text)
4. **Hardcoded values** instead of configuration
5. **Incorrect sudo() usage**
6. **Missing input validation**
7. **Poor error messages**
8. **Inefficient computed fields**
9. **Missing access rights**
10. **No unit tests**
## Example Review
```python
# CODE BEING REVIEWED
class SaleOrder(models.Model):
_inherit = 'sale.order'
total_weight = fields.Float(compute='_compute_weight')
def _compute_weight(self):
for order in self:
weight = 0
for line in order.order_line:
product = self.env['product.product'].search([('id', '=', line.product_id.id)])
weight += product.weight * line.product_uom_qty
order.total_weight = weight
```
**Review Findings:**
**HIGH - Performance Issues:**
1. Unnecessary search in loop (line 10)
- FIX: Use `line.product_id.weight` directly
2. Not storing computed field
- FIX: Add `store=True` and `@api.depends` decorator
**MEDIUM - Best Practices:**
1. Missing `@api.depends` decorator
- FIX: Add `@api.depends('order_line.product_id.weight', 'order_line.product_uom_qty')`
2. Variable could be clearer
- FIX: Rename `weight` to `total_weight`
**Improved Code:**
```python
class SaleOrder(models.Model):
_inherit = 'sale.order'
total_weight = fields.Float(
string='Total Weight',
compute='_compute_weight',
store=True,
help='Total weight of all order lines'
)
@api.depends('order_line.product_id.weight', 'order_line.product_uom_qty')
def _compute_weight(self):
"""Compute total weight from order lines."""
for order in self:
order.total_weight = sum(
line.product_id.weight * line.product_uom_qty
for line in order.order_line
)
```
## Resources
### references/oca_guidelines.md
Complete OCA (Odoo Community Association) coding guidelines for Odoo modules.
### references/security_checklist.md
Comprehensive security checklist for Odoo development.
### references/performance_patterns.md
Common performance patterns and anti-patterns with examples and fixes.

View File

@@ -0,0 +1 @@
OCA Guidelines reference

View File

@@ -0,0 +1 @@
Performance patterns

View File

@@ -0,0 +1 @@
Security checklist

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,558 @@
# API Integration Guide for Odoo Connectors
## REST API Integration
### Standard REST Pattern
**Adapter Structure**:
```python
class RESTAdapter(GenericAdapter):
def get_resource(self, resource_id):
"""GET /resources/{id}"""
return self.get(f'/{self.resource_name}/{resource_id}')
def list_resources(self, filters=None):
"""GET /resources"""
return self.get(f'/{self.resource_name}', params=filters)
def create_resource(self, data):
"""POST /resources"""
return self.post(f'/{self.resource_name}', data=data)
def update_resource(self, resource_id, data):
"""PUT /resources/{id}"""
return self.put(f'/{self.resource_name}/{resource_id}', data=data)
def delete_resource(self, resource_id):
"""DELETE /resources/{id}"""
return self.delete(f'/{self.resource_name}/{resource_id}')
```
### Pagination Handling
**Offset-Based Pagination**:
```python
def get_all_resources(self, filters=None):
"""Fetch all resources with pagination."""
all_resources = []
page = 1
per_page = 100
while True:
params = filters.copy() if filters else {}
params.update({'page': page, 'per_page': per_page})
response = self.get('/resources', params=params)
resources = response.get('data', [])
if not resources:
break
all_resources.extend(resources)
# Check if more pages exist
total = response.get('total', 0)
if len(all_resources) >= total:
break
page += 1
return all_resources
```
**Cursor-Based Pagination**:
```python
def get_all_resources(self, filters=None):
"""Fetch all resources with cursor pagination."""
all_resources = []
cursor = None
while True:
params = filters.copy() if filters else {}
if cursor:
params['cursor'] = cursor
response = self.get('/resources', params=params)
resources = response.get('data', [])
if not resources:
break
all_resources.extend(resources)
# Get next cursor
cursor = response.get('next_cursor')
if not cursor:
break
return all_resources
```
**Link Header Pagination**:
```python
def get_all_resources(self):
"""Follow Link headers for pagination."""
all_resources = []
url = '/resources'
while url:
response = requests.get(self.build_url(url), headers=self.get_api_headers())
response.raise_for_status()
all_resources.extend(response.json())
# Parse Link header
link_header = response.headers.get('Link', '')
url = self._extract_next_url(link_header)
return all_resources
def _extract_next_url(self, link_header):
"""Extract next URL from Link header."""
import re
match = re.search(r'<([^>]+)>; rel="next"', link_header)
return match.group(1) if match else None
```
### Response Envelope Handling
**Wrapped Response**:
```python
def get_products(self):
"""Handle wrapped API response."""
response = self.get('/products')
# Response: {"status": "success", "data": {"products": [...]}}
if response.get('status') == 'success':
return response.get('data', {}).get('products', [])
raise ValueError(f"API error: {response.get('message')}")
```
**Nested Data**:
```python
def extract_data(self, response):
"""Extract data from nested structure."""
# Response: {"response": {"result": {"items": [...]}}}
return response.get('response', {}).get('result', {}).get('items', [])
```
## GraphQL API Integration
**GraphQL Adapter**:
```python
class GraphQLAdapter(GenericAdapter):
def query(self, query, variables=None):
"""Execute GraphQL query."""
payload = {'query': query}
if variables:
payload['variables'] = variables
response = self.post('/graphql', data=payload)
if 'errors' in response:
raise ValueError(f"GraphQL errors: {response['errors']}")
return response.get('data')
def get_products(self, first=100, after=None):
"""Fetch products using GraphQL."""
query = """
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
edges {
node {
id
title
description
variants {
edges {
node {
id
price
sku
}
}
}
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
"""
variables = {'first': first}
if after:
variables['after'] = after
return self.query(query, variables)
def get_all_products(self):
"""Fetch all products with pagination."""
all_products = []
has_next_page = True
cursor = None
while has_next_page:
data = self.get_products(after=cursor)
products_data = data.get('products', {})
edges = products_data.get('edges', [])
all_products.extend([edge['node'] for edge in edges])
page_info = products_data.get('pageInfo', {})
has_next_page = page_info.get('hasNextPage', False)
cursor = page_info.get('endCursor')
return all_products
```
## SOAP API Integration
**SOAP Adapter**:
```python
from zeep import Client
from zeep.transports import Transport
class SOAPAdapter(GenericAdapter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = self._create_client()
def _create_client(self):
"""Create SOAP client."""
wsdl = f'{self.backend_record.api_url}?wsdl'
# Configure transport
session = requests.Session()
session.auth = (
self.backend_record.api_username,
self.backend_record.api_password
)
transport = Transport(session=session)
return Client(wsdl, transport=transport)
def get_products(self):
"""Call SOAP method."""
try:
response = self.client.service.GetProducts()
return response
except Exception as e:
_logger.error("SOAP call failed: %s", str(e))
raise
```
## Webhook Integration
### Webhook Controller
```python
from odoo import http
from odoo.http import request
import json
import hmac
import hashlib
class MyConnectorWebhookController(http.Controller):
@http.route('/myconnector/webhook', type='json', auth='none', csrf=False)
def webhook(self):
"""Handle incoming webhooks."""
try:
# Get raw payload
payload = request.httprequest.get_data(as_text=True)
# Get headers
signature = request.httprequest.headers.get('X-Signature')
event_type = request.httprequest.headers.get('X-Event-Type')
# Find backend (by API key or other identifier)
api_key = request.httprequest.headers.get('X-API-Key')
backend = request.env['myconnector.backend'].sudo().search([
('api_key', '=', api_key)
], limit=1)
if not backend:
return {'error': 'Invalid API key'}, 401
# Verify signature
if not self._verify_signature(payload, signature, backend.webhook_secret):
return {'error': 'Invalid signature'}, 401
# Create webhook record
webhook = request.env['generic.webhook'].sudo().create({
'backend_id': backend.id,
'event_type': event_type,
'payload': payload,
'signature': signature,
'processing_status': 'pending',
})
# Process asynchronously
webhook.with_delay().process_webhook()
return {'status': 'accepted', 'webhook_id': webhook.id}
except Exception as e:
_logger.exception("Webhook processing failed")
return {'error': str(e)}, 500
def _verify_signature(self, payload, signature, secret):
"""Verify HMAC signature."""
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
```
### Webhook Processing
```python
class MyBackend(models.Model):
def process_webhook(self, webhook):
"""Process webhook by event type."""
handlers = {
'order.created': self._handle_order_created,
'order.updated': self._handle_order_updated,
'product.updated': self._handle_product_updated,
'inventory.updated': self._handle_inventory_updated,
}
handler = handlers.get(webhook.event_type)
if handler:
try:
handler(webhook)
webhook.mark_as_processed()
except Exception as e:
_logger.exception("Webhook handler failed")
webhook.mark_as_failed(str(e))
else:
webhook.mark_as_ignored(f"No handler for {webhook.event_type}")
def _handle_order_created(self, webhook):
"""Handle order.created event."""
payload = json.loads(webhook.payload)
order_id = payload['order']['id']
# Import the order
self.env['myconnector.sale.order'].import_record(
backend=self,
external_id=str(order_id)
)
```
## Rate Limiting
### Token Bucket Implementation
```python
from datetime import datetime, timedelta
from collections import defaultdict
class RateLimiter:
def __init__(self, rate_limit=100, window=60):
"""
Args:
rate_limit: Number of requests allowed
window: Time window in seconds
"""
self.rate_limit = rate_limit
self.window = window
self.buckets = defaultdict(list)
def allow_request(self, key):
"""Check if request is allowed."""
now = datetime.now()
window_start = now - timedelta(seconds=self.window)
# Clean old requests
self.buckets[key] = [
req_time for req_time in self.buckets[key]
if req_time > window_start
]
# Check limit
if len(self.buckets[key]) >= self.rate_limit:
return False
# Add current request
self.buckets[key].append(now)
return True
class RateLimitedAdapter(GenericAdapter):
_rate_limiter = None
@classmethod
def get_rate_limiter(cls):
if cls._rate_limiter is None:
cls._rate_limiter = RateLimiter(rate_limit=100, window=60)
return cls._rate_limiter
def make_request(self, method, endpoint, **kwargs):
"""Make request with rate limiting."""
limiter = self.get_rate_limiter()
key = f"{self.backend_record.id}"
if not limiter.allow_request(key):
# Wait and retry
import time
time.sleep(1)
return self.make_request(method, endpoint, **kwargs)
return super().make_request(method, endpoint, **kwargs)
```
### Response Header Rate Limiting
```python
def make_request(self, method, endpoint, **kwargs):
"""Check rate limit from response headers."""
response = super().make_request(method, endpoint, **kwargs)
# Check rate limit headers
remaining = response.headers.get('X-RateLimit-Remaining')
reset_time = response.headers.get('X-RateLimit-Reset')
if remaining and int(remaining) < 10:
_logger.warning(
"Rate limit nearly exceeded. Remaining: %s, Resets at: %s",
remaining,
reset_time
)
# Optionally delay next request
if int(remaining) == 0:
import time
reset_timestamp = int(reset_time)
wait_time = reset_timestamp - time.time()
if wait_time > 0:
time.sleep(wait_time)
return response
```
## Error Handling
### Retry with Exponential Backoff
```python
import time
from requests.exceptions import RequestException
class ResilientAdapter(GenericAdapter):
def make_request(self, method, endpoint, max_retries=3, **kwargs):
"""Make request with retry logic."""
for attempt in range(max_retries):
try:
return super().make_request(method, endpoint, **kwargs)
except RequestException as e:
if attempt == max_retries - 1:
# Last attempt, re-raise
raise
# Calculate backoff
wait_time = (2 ** attempt) + (random.random() * 0.1)
_logger.warning(
"Request failed (attempt %d/%d): %s. Retrying in %.2fs",
attempt + 1,
max_retries,
str(e),
wait_time
)
time.sleep(wait_time)
```
### Status Code Handling
```python
def make_request(self, method, endpoint, **kwargs):
"""Handle different HTTP status codes."""
response = requests.request(
method=method,
url=self.build_url(endpoint),
headers=self.get_api_headers(),
**kwargs
)
if response.status_code == 200:
return response.json()
elif response.status_code == 201:
return response.json()
elif response.status_code == 204:
return None # No content
elif response.status_code == 400:
raise ValueError(f"Bad request: {response.text}")
elif response.status_code == 401:
raise PermissionError("Unauthorized. Check API credentials.")
elif response.status_code == 403:
raise PermissionError("Forbidden. Insufficient permissions.")
elif response.status_code == 404:
return None # Resource not found
elif response.status_code == 429:
# Rate limited
retry_after = response.headers.get('Retry-After', 60)
raise RateLimitExceeded(f"Rate limited. Retry after {retry_after}s")
elif response.status_code >= 500:
raise ServerError(f"Server error: {response.status_code}")
else:
response.raise_for_status()
```
## Testing APIs
### Mock Adapter for Testing
```python
class MockAdapter(GenericAdapter):
"""Mock adapter for testing."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mock_data = {}
def set_mock_response(self, endpoint, data):
"""Set mock response for endpoint."""
self.mock_data[endpoint] = data
def get(self, endpoint, **kwargs):
"""Return mock data instead of making real request."""
return self.mock_data.get(endpoint, {})
# In tests
def test_product_import(self):
backend = self.env['myconnector.backend'].create({...})
# Use mock adapter
adapter = MockAdapter(self.env, backend)
adapter.set_mock_response('/products/123', {
'id': 123,
'title': 'Test Product',
'price': 99.99
})
# Test import
importer = ProductImporter(...)
result = importer.run(external_id='123')
self.assertEqual(result.name, 'Test Product')
```

View File

@@ -0,0 +1,451 @@
# Generic Connector Architecture Reference
## Overview
The `generic_connector` module provides a reusable framework for building connectors to external systems. It follows a component-based architecture with clear separation of concerns.
## Core Components
### 1. Backend Model
**Purpose**: Configuration and orchestration center for connector operations.
**Key Responsibilities**:
- Store API credentials and configuration
- Manage connection status
- Orchestrate synchronization operations
- Configure webhooks
- Define business logic (warehouse, pricelist, etc.)
**Implementation Pattern**:
```python
class MyConnectorBackend(models.Model):
_name = 'myconnector.backend'
_inherit = 'generic.backend'
_description = 'My Connector Backend'
_backend_type = 'myconnector' # Unique identifier
# API Configuration fields
api_url = fields.Char(required=True, default='https://api.example.com')
api_key = fields.Char(required=True)
api_secret = fields.Char()
# Override template methods
def _test_connection_implementation(self):
"""Implement connection testing logic."""
adapter = self.get_adapter('myconnector.adapter')
return adapter.test_connection()
def _sync_orders_implementation(self):
"""Implement order import logic."""
# Import orders from external system
pass
```
**Template Methods** (override these):
- `_test_connection_implementation()` - Test API connection
- `_sync_orders_implementation()` - Import orders
- `_sync_products_implementation()` - Export/import products
- `_sync_inventory_implementation()` - Export inventory
- `_sync_customers_implementation()` - Import/export customers
### 2. Binding Models
**Purpose**: Link Odoo records to external system entities.
**Key Characteristics**:
- Uses `_inherits` to extend Odoo models
- Stores external ID and sync metadata
- Tracks sync status and retry count
**Implementation Pattern**:
```python
class MyConnectorProductBinding(models.Model):
_name = 'myconnector.product.template'
_inherit = 'generic.binding'
_inherits = {'product.template': 'odoo_id'}
_description = 'My Connector Product Binding'
odoo_id = fields.Many2one(
'product.template',
required=True,
ondelete='cascade'
)
# External system fields
external_sku = fields.Char(readonly=True)
external_price = fields.Float(readonly=True)
_sql_constraints = [
('backend_external_uniq',
'unique(backend_id, external_id)',
'Product binding must be unique per backend')
]
```
**Generic Binding Fields** (automatically inherited):
- `backend_id` - Link to backend
- `external_id` - ID in external system
- `sync_date` - Last sync timestamp
- `sync_status` - pending/in_progress/success/failed/skipped
- `retry_count` - Number of retry attempts
- `last_error` - Last error message
**Generic Binding Methods**:
- `mark_sync_success()` - Mark record as successfully synced
- `mark_sync_failed(error_msg)` - Mark record as failed with error
- `can_retry_sync()` - Check if retry is allowed
### 3. Adapter Component
**Purpose**: HTTP client for API communication.
**Key Responsibilities**:
- Make HTTP requests (GET, POST, PUT, DELETE)
- Handle authentication
- Build URLs
- Manage headers and timeouts
- Transform API responses
**Implementation Pattern**:
```python
from odoo.addons.generic_connector.components.adapter import GenericAdapter
class MyConnectorAdapter(GenericAdapter):
_name = 'myconnector.adapter'
_inherit = 'generic.adapter'
_usage = 'backend.adapter'
def get_api_headers(self):
"""Build API request headers."""
headers = super().get_api_headers()
headers.update({
'Authorization': f'Bearer {self.backend_record.api_key}',
'X-API-Version': '2.0'
})
return headers
# CRUD operations
def get_product(self, external_id):
"""Get single product."""
return self.get(f'/products/{external_id}')
def get_products(self, filters=None):
"""Get list of products."""
return self.get('/products', params=filters)
def create_product(self, data):
"""Create product."""
return self.post('/products', data=data)
def update_product(self, external_id, data):
"""Update product."""
return self.put(f'/products/{external_id}', data=data)
```
**Available HTTP Methods** (from GenericAdapter):
- `get(endpoint, params=None, **kwargs)` - GET request
- `post(endpoint, data=None, **kwargs)` - POST request
- `put(endpoint, data=None, **kwargs)` - PUT request
- `delete(endpoint, **kwargs)` - DELETE request
- `make_request(method, endpoint, **kwargs)` - Generic request
**Helper Methods**:
- `build_url(endpoint)` - Construct full URL
- `get_api_headers()` - Get request headers
- `get_api_auth()` - Get authentication tuple
### 4. Mapper Components
**Purpose**: Transform data between Odoo and external system formats.
**Implementation Pattern**:
```python
from odoo.addons.generic_connector.components.mapper import GenericImportMapper
class ProductImportMapper(GenericImportMapper):
_name = 'myconnector.product.import.mapper'
_inherit = 'generic.import.mapper'
_apply_on = 'myconnector.product.template'
direct = [
('name', 'name'), # Simple field mapping
('sku', 'default_code'),
('price', 'list_price'),
]
@mapping
def backend_id(self, record):
"""Map backend."""
return {'backend_id': self.backend_record.id}
@mapping
def external_id(self, record):
"""Map external ID."""
return {'external_id': str(record['id'])}
@mapping
def category_id(self, record):
"""Map category with lookup."""
external_cat_id = record.get('category_id')
if external_cat_id:
category = self.env['product.category'].search([
('name', '=', record.get('category_name'))
], limit=1)
return {'categ_id': category.id if category else False}
return {}
```
**Mapping Decorators**:
- `@mapping` - Define a custom mapping method
- `@only_create` - Apply only when creating records
- `@changed_by('field1', 'field2')` - Apply only when specified fields change
**Direct Mappings**:
```python
direct = [
('external_field', 'odoo_field'), # Simple mapping
(transform('external_field'), 'odoo_field'), # With transformation
]
```
### 5. Importer Components
**Purpose**: Import data from external system to Odoo.
**Implementation Pattern**:
```python
from odoo.addons.generic_connector.components.importer import GenericImporter
class ProductImporter(GenericImporter):
_name = 'myconnector.product.importer'
_inherit = 'generic.importer'
_apply_on = 'myconnector.product.template'
def _import_record(self, external_id, force=False):
"""Import a single product."""
# 1. Fetch from external system
adapter = self.component(usage='backend.adapter')
external_data = adapter.get_product(external_id)
# 2. Transform data
mapper = self.component(usage='import.mapper')
mapped_data = mapper.map_record(external_data).values()
# 3. Create or update binding
binding = self._get_binding()
if binding:
binding.write(mapped_data)
else:
binding = self.model.create(mapped_data)
return binding
def _get_binding(self):
"""Get existing binding by external_id."""
return self.env[self.model._name].search([
('backend_id', '=', self.backend_record.id),
('external_id', '=', self.external_id),
], limit=1)
```
**Batch Importer**:
```python
class ProductBatchImporter(GenericBatchImporter):
_name = 'myconnector.product.batch.importer'
_inherit = 'generic.batch.importer'
_apply_on = 'myconnector.product.template'
def run(self, filters=None):
"""Import products in batch."""
adapter = self.component(usage='backend.adapter')
products = adapter.get_products(filters=filters)
for product in products:
external_id = str(product['id'])
self._import_record(external_id, force=False)
```
### 6. Exporter Components
**Purpose**: Export data from Odoo to external system.
**Implementation Pattern**:
```python
from odoo.addons.generic_connector.components.exporter import GenericExporter
class ProductExporter(GenericExporter):
_name = 'myconnector.product.exporter'
_inherit = 'generic.exporter'
_apply_on = 'myconnector.product.template'
def _export_record(self, binding):
"""Export a single product."""
# 1. Transform data
mapper = self.component(usage='export.mapper')
mapped_data = mapper.map_record(binding).values()
# 2. Send to external system
adapter = self.component(usage='backend.adapter')
if binding.external_id:
# Update existing
adapter.update_product(binding.external_id, mapped_data)
else:
# Create new
result = adapter.create_product(mapped_data)
binding.write({
'external_id': str(result['id']),
'sync_date': fields.Datetime.now()
})
```
### 7. Webhook Model
**Purpose**: Receive and process webhooks from external systems.
**Features**:
- Store raw webhook payloads
- Verify webhook signatures
- Queue async processing
- Track processing status
- Retry failed webhooks
**Usage Pattern**:
```python
# In controller (receive webhook)
webhook = request.env['generic.webhook'].sudo().create({
'backend_id': backend.id,
'event_type': 'order.created',
'payload': json.dumps(payload),
'signature': request.httprequest.headers.get('X-Webhook-Signature'),
'processing_status': 'pending'
})
# Process with delay
webhook.with_delay().process_webhook()
# In backend model (handle webhook)
def _handle_webhook_order_created(self, webhook):
"""Handle order.created webhook event."""
payload = json.loads(webhook.payload)
order_id = payload['order']['id']
# Import the order
self.env['myconnector.sale.order'].import_record(
backend=self,
external_id=str(order_id)
)
```
## Component Registration
Components must be registered with specific attributes:
- `_name` - Unique component identifier
- `_inherit` - Parent component(s)
- `_apply_on` - Model(s) this component applies to
- `_usage` - Usage context (e.g., 'backend.adapter', 'import.mapper')
**Example**:
```python
class MyAdapter(GenericAdapter):
_name = 'myconnector.product.adapter'
_inherit = 'generic.adapter'
_apply_on = 'myconnector.product.template'
_usage = 'backend.adapter'
```
## Queue Job Integration
Use `with_delay()` for async operations:
```python
# Queue a sync job
backend.with_delay().sync_orders()
# Queue with custom settings
backend.with_delay(priority=5, eta=60).sync_products()
# Queue from binding
binding.with_delay().export_record()
```
## Security Model
### Groups (from generic_connector):
- `group_generic_connector_user` - Basic access
- `group_generic_connector_manager` - Configuration access
- `group_generic_connector_admin` - Full control
### Access Rules Pattern:
```csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_backend_user,myconnector.backend user,model_myconnector_backend,group_generic_connector_user,1,0,0,0
access_backend_manager,myconnector.backend manager,model_myconnector_backend,group_generic_connector_manager,1,1,1,1
```
## View Pattern
### Backend Form View Structure:
```xml
<form>
<header>
<button name="action_test_connection" type="object" string="Test Connection"/>
<button name="action_sync_all" type="object" string="Sync All"/>
</header>
<sheet>
<group name="api_config" string="API Configuration">
<field name="api_url"/>
<field name="api_key" password="True"/>
</group>
<notebook>
<page name="sync" string="Synchronization">
<!-- Sync settings -->
</page>
<page name="advanced" string="Advanced">
<!-- Advanced settings -->
</page>
</notebook>
</sheet>
</form>
```
## Odoo Model File Locations Reference
When building connectors, you'll interact with these core Odoo models:
### Product Models
- `product.template` - Product template (variants container)
- `product.product` - Product variant
- `product.category` - Product categories
### Sales Models
- `sale.order` - Sales orders
- `sale.order.line` - Order lines
- `res.partner` - Customers/contacts
### Inventory Models
- `stock.picking` - Stock transfers
- `stock.move` - Stock movements
- `stock.quant` - Inventory quantities
- `stock.warehouse` - Warehouses
- `stock.location` - Stock locations
### Accounting Models
- `account.move` - Invoices/bills
- `account.payment` - Payments
- `account.tax` - Taxes
## Best Practices
1. **Always use bindings** - Never directly modify Odoo records from external data
2. **Use queue jobs** - For any operation that might take >2 seconds
3. **Implement retry logic** - Use binding's retry_count and max_retries
4. **Log extensively** - Use `_logger` for debugging
5. **Handle API errors** - Wrap adapter calls in try/except
6. **Validate data** - Check required fields before creating/updating
7. **Use transactions** - Leverage Odoo's automatic transaction management
8. **Test connection** - Always implement `_test_connection_implementation()`
9. **Document API** - Add docstrings to all adapter methods
10. **Follow naming conventions** - Use consistent model/component names

View File

@@ -0,0 +1,441 @@
# Authentication Patterns for Odoo Connectors
## 1. API Key Authentication
**Usage**: Simple, static authentication
**Backend Fields**:
```python
class MyBackend(models.Model):
api_key = fields.Char(string='API Key', required=True)
api_secret = fields.Char(string='API Secret') # Optional
```
**Adapter Implementation**:
```python
class MyAdapter(GenericAdapter):
def get_api_headers(self):
headers = super().get_api_headers()
headers['X-API-Key'] = self.backend_record.api_key
return headers
```
**Variants**:
- Header-based: `Authorization: ApiKey YOUR_KEY`
- Query parameter: `?api_key=YOUR_KEY`
- Custom header: `X-API-Key: YOUR_KEY`
## 2. Bearer Token Authentication
**Usage**: Token-based auth (common in modern APIs)
**Backend Fields**:
```python
class MyBackend(models.Model):
access_token = fields.Char(string='Access Token')
```
**Adapter Implementation**:
```python
class MyAdapter(GenericAdapter):
def get_api_headers(self):
headers = super().get_api_headers()
headers['Authorization'] = f'Bearer {self.backend_record.access_token}'
return headers
```
## 3. OAuth 2.0 Authentication
**Usage**: Delegated authorization (Shopify, Google, etc.)
### Authorization Code Flow
**Backend Fields**:
```python
class MyBackend(models.Model):
oauth_client_id = fields.Char(string='Client ID', required=True)
oauth_client_secret = fields.Char(string='Client Secret', required=True)
oauth_redirect_uri = fields.Char(string='Redirect URI', compute='_compute_redirect_uri')
access_token = fields.Char(string='Access Token', readonly=True)
refresh_token = fields.Char(string='Refresh Token', readonly=True)
token_expires_at = fields.Datetime(string='Token Expires At', readonly=True)
token_type = fields.Char(string='Token Type', readonly=True, default='Bearer')
@api.depends()
def _compute_redirect_uri(self):
"""Compute OAuth redirect URI."""
for backend in self:
base_url = backend.env['ir.config_parameter'].sudo().get_param('web.base.url')
backend.oauth_redirect_uri = f'{base_url}/myconnector/oauth/callback'
def action_start_oauth_flow(self):
"""Start OAuth authorization flow."""
self.ensure_one()
auth_url = self._build_authorization_url()
return {
'type': 'ir.actions.act_url',
'url': auth_url,
'target': 'new',
}
def _build_authorization_url(self):
"""Build OAuth authorization URL."""
from urllib.parse import urlencode
params = {
'client_id': self.oauth_client_id,
'redirect_uri': self.oauth_redirect_uri,
'response_type': 'code',
'scope': 'read_products write_orders', # Adjust scopes
'state': self._generate_oauth_state(),
}
return f'{self.api_url}/oauth/authorize?{urlencode(params)}'
def _generate_oauth_state(self):
"""Generate OAuth state parameter for CSRF protection."""
import secrets
state = secrets.token_urlsafe(32)
# Store state in session or database for validation
self.env['ir.config_parameter'].sudo().set_param(
f'oauth_state_{self.id}',
state
)
return state
def exchange_code_for_token(self, code, state):
"""Exchange authorization code for access token."""
self.ensure_one()
# Validate state
stored_state = self.env['ir.config_parameter'].sudo().get_param(
f'oauth_state_{self.id}'
)
if state != stored_state:
raise ValueError('Invalid OAuth state')
# Exchange code for token
token_url = f'{self.api_url}/oauth/token'
data = {
'client_id': self.oauth_client_id,
'client_secret': self.oauth_client_secret,
'code': code,
'redirect_uri': self.oauth_redirect_uri,
'grant_type': 'authorization_code',
}
response = requests.post(token_url, data=data)
response.raise_for_status()
token_data = response.json()
self._save_token_data(token_data)
def _save_token_data(self, token_data):
"""Save OAuth token data."""
from datetime import datetime, timedelta
expires_in = token_data.get('expires_in', 3600)
expires_at = datetime.now() + timedelta(seconds=expires_in)
self.write({
'access_token': token_data['access_token'],
'refresh_token': token_data.get('refresh_token'),
'token_expires_at': expires_at,
'token_type': token_data.get('token_type', 'Bearer'),
})
def refresh_access_token(self):
"""Refresh expired access token."""
self.ensure_one()
if not self.refresh_token:
raise ValueError('No refresh token available')
token_url = f'{self.api_url}/oauth/token'
data = {
'client_id': self.oauth_client_id,
'client_secret': self.oauth_client_secret,
'refresh_token': self.refresh_token,
'grant_type': 'refresh_token',
}
response = requests.post(token_url, data=data)
response.raise_for_status()
token_data = response.json()
self._save_token_data(token_data)
```
**OAuth Callback Controller**:
```python
from odoo import http
from odoo.http import request
class MyConnectorOAuthController(http.Controller):
@http.route('/myconnector/oauth/callback', type='http', auth='user', csrf=False)
def oauth_callback(self, code=None, state=None, error=None):
"""Handle OAuth callback."""
if error:
return request.render('myconnector.oauth_error', {'error': error})
if not code or not state:
return request.render('myconnector.oauth_error',
{'error': 'Missing code or state'})
# Find backend by state or use session
backend_id = request.session.get('oauth_backend_id')
if not backend_id:
return request.render('myconnector.oauth_error',
{'error': 'Invalid session'})
backend = request.env['myconnector.backend'].sudo().browse(backend_id)
try:
backend.exchange_code_for_token(code, state)
return request.render('myconnector.oauth_success')
except Exception as e:
return request.render('myconnector.oauth_error', {'error': str(e)})
```
**Adapter with Token Refresh**:
```python
class MyAdapter(GenericAdapter):
def make_request(self, method, endpoint, **kwargs):
"""Make request with automatic token refresh."""
# Check if token is expired
if self._is_token_expired():
self.backend_record.refresh_access_token()
return super().make_request(method, endpoint, **kwargs)
def _is_token_expired(self):
"""Check if access token is expired."""
from datetime import datetime, timedelta
if not self.backend_record.token_expires_at:
return False
# Refresh 5 minutes before expiry
buffer = timedelta(minutes=5)
return datetime.now() + buffer >= self.backend_record.token_expires_at
def get_api_headers(self):
headers = super().get_api_headers()
headers['Authorization'] = (
f'{self.backend_record.token_type} {self.backend_record.access_token}'
)
return headers
```
## 4. Basic Authentication
**Usage**: Username/password (less common, less secure)
**Backend Fields**:
```python
class MyBackend(models.Model):
api_username = fields.Char(string='Username', required=True)
api_password = fields.Char(string='Password', required=True)
```
**Adapter Implementation**:
```python
class MyAdapter(GenericAdapter):
def get_api_auth(self):
"""Return (username, password) tuple for requests."""
return (
self.backend_record.api_username,
self.backend_record.api_password
)
def make_request(self, method, endpoint, **kwargs):
"""Add basic auth to requests."""
kwargs['auth'] = self.get_api_auth()
return super().make_request(method, endpoint, **kwargs)
```
## 5. HMAC Signature Authentication
**Usage**: Signed requests (high security)
**Backend Fields**:
```python
class MyBackend(models.Model):
api_key = fields.Char(string='API Key', required=True)
api_secret = fields.Char(string='API Secret', required=True)
```
**Adapter Implementation**:
```python
import hmac
import hashlib
import base64
from datetime import datetime
class MyAdapter(GenericAdapter):
def make_request(self, method, endpoint, **kwargs):
"""Add HMAC signature to request."""
# Generate signature
timestamp = str(int(datetime.now().timestamp()))
signature = self._generate_signature(method, endpoint, timestamp, kwargs.get('data'))
# Add to headers
headers = kwargs.get('headers', {})
headers.update({
'X-API-Key': self.backend_record.api_key,
'X-Signature': signature,
'X-Timestamp': timestamp,
})
kwargs['headers'] = headers
return super().make_request(method, endpoint, **kwargs)
def _generate_signature(self, method, endpoint, timestamp, data=None):
"""Generate HMAC signature."""
# Build signature string
message_parts = [
method.upper(),
endpoint,
timestamp,
]
if data:
import json
message_parts.append(json.dumps(data, sort_keys=True))
message = '\n'.join(message_parts)
# Generate HMAC
secret = self.backend_record.api_secret.encode('utf-8')
signature = hmac.new(
secret,
message.encode('utf-8'),
hashlib.sha256
).digest()
# Return base64-encoded signature
return base64.b64encode(signature).decode('utf-8')
```
## 6. JWT Authentication
**Usage**: JSON Web Tokens (stateless auth)
**Backend Fields**:
```python
class MyBackend(models.Model):
jwt_secret = fields.Char(string='JWT Secret', required=True)
jwt_algorithm = fields.Selection([
('HS256', 'HMAC SHA-256'),
('RS256', 'RSA SHA-256'),
], default='HS256', string='Algorithm')
jwt_expiration = fields.Integer(string='Token Expiration (seconds)', default=3600)
```
**Adapter Implementation**:
```python
import jwt
from datetime import datetime, timedelta
class MyAdapter(GenericAdapter):
def get_api_headers(self):
headers = super().get_api_headers()
token = self._generate_jwt()
headers['Authorization'] = f'Bearer {token}'
return headers
def _generate_jwt(self):
"""Generate JWT token."""
payload = {
'iss': self.backend_record.api_key, # Issuer
'iat': datetime.utcnow(), # Issued at
'exp': datetime.utcnow() + timedelta(
seconds=self.backend_record.jwt_expiration
),
}
return jwt.encode(
payload,
self.backend_record.jwt_secret,
algorithm=self.backend_record.jwt_algorithm
)
```
## 7. Store-Specific Headers (ZID Pattern)
**Usage**: Multi-tenant systems requiring store identification
**Backend Fields**:
```python
class MyBackend(models.Model):
store_id = fields.Char(string='Store ID', required=True)
api_key = fields.Char(string='API Key', required=True)
```
**Adapter Implementation**:
```python
class MyAdapter(GenericAdapter):
def get_api_headers(self):
headers = super().get_api_headers()
headers.update({
'X-Manager-Token': self.backend_record.api_key,
'X-Store-Id': self.backend_record.store_id,
'Accept': 'application/json',
})
return headers
```
## Webhook Signature Verification
### HMAC-SHA256 Verification
```python
import hmac
import hashlib
class GenericWebhook(models.Model):
def verify_signature(self, payload, signature, secret):
"""Verify webhook signature."""
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# In controller
class WebhookController(http.Controller):
@http.route('/myconnector/webhook', type='json', auth='none', csrf=False)
def webhook(self):
payload = request.httprequest.get_data(as_text=True)
signature = request.httprequest.headers.get('X-Signature')
backend = self._find_backend()
webhook_model = request.env['generic.webhook'].sudo()
if not webhook_model.verify_signature(payload, signature, backend.webhook_secret):
return {'error': 'Invalid signature'}, 401
# Process webhook
...
```
## Security Best Practices
1. **Never log credentials** - Mask API keys/secrets in logs
2. **Use password fields** - Set `password=True` for sensitive fields
3. **Rotate tokens** - Implement token refresh before expiry
4. **Validate signatures** - Always verify webhook signatures
5. **Use HTTPS** - Never send credentials over HTTP
6. **Store securely** - Consider using `ir.config_parameter` for secrets
7. **Limit scopes** - Request minimum required OAuth scopes
8. **Handle expiry** - Implement token refresh logic
9. **CSRF protection** - Use state parameter in OAuth
10. **Rate limit** - Implement rate limiting to prevent abuse

View File

@@ -0,0 +1,562 @@
# Design Patterns in Odoo Connectors
## 1. Template Method Pattern
**Usage**: Backend model orchestration
**Implementation**:
```python
# generic_connector provides template methods
class GenericBackend(models.Model):
def sync_orders(self):
"""Template method."""
self._pre_sync_validation()
result = self._sync_orders_implementation() # Hook
self._post_sync_actions()
return result
def _sync_orders_implementation(self):
"""Override this in concrete implementations."""
raise NotImplementedError()
# Concrete connector overrides the hook
class ShopifyBackend(models.Model):
_inherit = 'generic.backend'
def _sync_orders_implementation(self):
"""Shopify-specific implementation."""
# Actual sync logic here
pass
```
**Benefits**:
- Enforces consistent workflow
- Allows customization at specific points
- Reduces code duplication
## 2. Adapter Pattern
**Usage**: API communication abstraction
**Implementation**:
```python
class ShopifyAdapter(GenericAdapter):
"""Adapts Shopify API to generic interface."""
def get_orders(self, filters=None):
"""Translate to Shopify API call."""
# Shopify uses /admin/api/2024-01/orders.json
endpoint = '/admin/api/2024-01/orders.json'
response = self.get(endpoint, params=self._build_params(filters))
# Shopify wraps response in 'orders' key
return response.get('orders', [])
def _build_params(self, filters):
"""Transform generic filters to Shopify params."""
params = {}
if filters and 'created_after' in filters:
params['created_at_min'] = filters['created_after']
return params
```
**Benefits**:
- Hides API differences
- Provides consistent interface
- Simplifies testing (mock adapter)
## 3. Strategy Pattern
**Usage**: Different sync strategies per backend
**Implementation**:
```python
class GenericImporter(Component):
_name = 'generic.importer'
def run(self, external_id, force=False):
"""Import strategy can vary."""
if self._should_skip_import(external_id, force):
return None
# Different strategies:
# - Direct import
# - Delayed import
# - Batch import
return self._import_record(external_id)
class RealtimeImporter(GenericImporter):
"""Strategy: Import immediately."""
_name = 'shopify.realtime.importer'
def _import_record(self, external_id):
# Import synchronously
pass
class BatchImporter(GenericImporter):
"""Strategy: Queue for batch processing."""
_name = 'shopify.batch.importer'
def _import_record(self, external_id):
# Queue for later
self.with_delay().import_record(external_id)
```
## 4. Factory Pattern
**Usage**: Component selection based on context
**Implementation**:
```python
# Component framework acts as factory
with backend.work_on('shopify.product.template') as work:
# Factory automatically selects appropriate components
adapter = work.component(usage='backend.adapter')
# Returns ShopifyProductAdapter
importer = work.component(usage='record.importer')
# Returns ShopifyProductImporter
mapper = work.component(usage='import.mapper')
# Returns ShopifyProductImportMapper
```
**Benefits**:
- Automatic component selection
- Decoupled component creation
- Easy to extend with new components
## 5. Observer Pattern
**Usage**: Webhook event handling
**Implementation**:
```python
# Event source (webhook controller)
class WebhookController(http.Controller):
@http.route('/shopify/webhook', type='json', auth='none')
def webhook_handler(self):
payload = request.jsonrequest
event_type = request.httprequest.headers.get('X-Shopify-Topic')
# Notify observers
webhook = request.env['generic.webhook'].sudo().create({
'event_type': event_type,
'payload': json.dumps(payload),
'processing_status': 'pending'
})
webhook.with_delay().process_webhook()
# Observer (backend model)
class ShopifyBackend(models.Model):
def process_webhook(self, webhook):
"""Observe and handle webhook events."""
handlers = {
'orders/create': self._handle_order_created,
'products/update': self._handle_product_updated,
}
handler = handlers.get(webhook.event_type)
if handler:
handler(webhook)
```
## 6. Delegation Pattern
**Usage**: Multi-level model inheritance
**Implementation**:
```python
class GenericBackend(models.Model):
_name = 'generic.backend'
_inherits = {'connector.base.backend': 'connector_backend_id'}
connector_backend_id = fields.Many2one(
'connector.base.backend',
required=True,
ondelete='cascade'
)
# Delegates fields: name, version, etc.
# Accessing backend.name actually accesses backend.connector_backend_id.name
```
**Benefits**:
- Reuse existing model functionality
- Avoid deep inheritance hierarchies
- Maintain database normalization
## 7. Mapper Pattern (Data Transfer Object)
**Usage**: Data transformation between systems
**Implementation**:
```python
class ProductImportMapper(GenericImportMapper):
_name = 'shopify.product.import.mapper'
# Simple mappings
direct = [
('title', 'name'),
('vendor', 'manufacturer'),
]
# Complex mapping
@mapping
def description(self, record):
"""Transform HTML description to plain text."""
html_desc = record.get('body_html', '')
return {'description': self._strip_html(html_desc)}
@mapping
def price(self, record):
"""Extract price from variants."""
variants = record.get('variants', [])
if variants:
return {'list_price': float(variants[0].get('price', 0))}
return {}
@only_create
def default_code(self, record):
"""Set SKU only when creating."""
return {'default_code': record.get('sku')}
```
**Benefits**:
- Centralized data transformation
- Declarative mapping definitions
- Reusable transformations
## 8. Retry Pattern
**Usage**: Handling transient failures
**Implementation**:
```python
class GenericBinding(models.AbstractModel):
retry_count = fields.Integer(default=0)
max_retries = fields.Integer(default=3)
def can_retry_sync(self):
"""Check if retry is allowed."""
return self.retry_count < self.max_retries
def export_with_retry(self):
"""Export with automatic retry."""
try:
self.export_record()
self.mark_sync_success()
except Exception as e:
self.retry_count += 1
self.last_error = str(e)
if self.can_retry_sync():
# Retry with exponential backoff
delay = 60 * (2 ** self.retry_count)
self.with_delay(eta=delay).export_with_retry()
else:
self.mark_sync_failed(str(e))
```
**Benefits**:
- Resilient to temporary failures
- Configurable retry behavior
- Exponential backoff prevents API overload
## 9. Rate Limiting Pattern
**Usage**: Respect API rate limits
**Implementation**:
```python
from datetime import datetime, timedelta
from cachetools import TTLCache
class RateLimitedAdapter(GenericAdapter):
# Class-level cache (shared across instances)
_rate_limit_cache = TTLCache(maxsize=100, ttl=60)
def make_request(self, method, endpoint, **kwargs):
"""Make request with rate limiting."""
cache_key = f"{self.backend_record.id}:requests"
# Get request count in current window
request_count = self._rate_limit_cache.get(cache_key, 0)
max_requests = self.backend_record.rate_limit_calls or 100
if request_count >= max_requests:
# Wait for next window
raise RateLimitExceeded(f"Rate limit of {max_requests}/min exceeded")
# Make request
response = super().make_request(method, endpoint, **kwargs)
# Increment counter
self._rate_limit_cache[cache_key] = request_count + 1
return response
```
**Variants**:
- **Token Bucket**: For bursty traffic
- **Leaky Bucket**: For steady rate
- **Sliding Window**: For precise limits
## 10. Circuit Breaker Pattern
**Usage**: Prevent cascading failures
**Implementation**:
```python
class CircuitBreaker:
def __init__(self, failure_threshold=5, timeout=60):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failures = 0
self.last_failure_time = None
self.state = 'CLOSED' # CLOSED, OPEN, HALF_OPEN
def call(self, func, *args, **kwargs):
if self.state == 'OPEN':
if datetime.now() - self.last_failure_time > timedelta(seconds=self.timeout):
self.state = 'HALF_OPEN'
else:
raise CircuitBreakerOpen("Circuit breaker is OPEN")
try:
result = func(*args, **kwargs)
if self.state == 'HALF_OPEN':
self.state = 'CLOSED'
self.failures = 0
return result
except Exception as e:
self.failures += 1
self.last_failure_time = datetime.now()
if self.failures >= self.failure_threshold:
self.state = 'OPEN'
raise
class ResilientAdapter(GenericAdapter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.circuit_breaker = CircuitBreaker()
def make_request(self, *args, **kwargs):
return self.circuit_breaker.call(
super().make_request,
*args,
**kwargs
)
```
## 11. Saga Pattern
**Usage**: Distributed transaction management
**Implementation**:
```python
class OrderImportSaga:
"""Multi-step order import with compensation."""
def __init__(self, backend, external_order_id):
self.backend = backend
self.external_order_id = external_order_id
self.steps_completed = []
def execute(self):
"""Execute saga steps."""
try:
# Step 1: Import customer
customer = self._import_customer()
self.steps_completed.append(('customer', customer.id))
# Step 2: Import order
order = self._import_order(customer)
self.steps_completed.append(('order', order.id))
# Step 3: Import order lines
lines = self._import_order_lines(order)
self.steps_completed.append(('lines', [l.id for l in lines]))
# Step 4: Confirm order
order.action_confirm()
return order
except Exception as e:
# Compensate (rollback completed steps)
self._compensate()
raise
def _compensate(self):
"""Rollback completed steps."""
for step_type, record_ids in reversed(self.steps_completed):
if step_type == 'order':
self.env['sale.order'].browse(record_ids).action_cancel()
elif step_type == 'customer':
# Mark as not synced (don't delete)
partner = self.env['res.partner'].browse(record_ids)
partner.write({'active': False})
```
## 12. Repository Pattern
**Usage**: Centralize data access logic
**Implementation**:
```python
class ProductBindingRepository:
"""Repository for product bindings."""
def __init__(self, env, backend):
self.env = env
self.backend = backend
self.model = env['shopify.product.template']
def find_by_external_id(self, external_id):
"""Find binding by external ID."""
return self.model.search([
('backend_id', '=', self.backend.id),
('external_id', '=', str(external_id))
], limit=1)
def find_by_sku(self, sku):
"""Find binding by SKU."""
return self.model.search([
('backend_id', '=', self.backend.id),
('default_code', '=', sku)
], limit=1)
def find_or_create(self, external_id, defaults=None):
"""Find existing or create new binding."""
binding = self.find_by_external_id(external_id)
if not binding:
values = {'backend_id': self.backend.id, 'external_id': str(external_id)}
if defaults:
values.update(defaults)
binding = self.model.create(values)
return binding
def find_pending_export(self, limit=100):
"""Find bindings pending export."""
return self.model.search([
('backend_id', '=', self.backend.id),
('sync_status', '=', 'pending'),
('external_id', '=', False)
], limit=limit)
```
## Pattern Selection Guidelines
| Use Case | Pattern | Reason |
|----------|---------|--------|
| Define sync workflow | Template Method | Consistent process, extensible hooks |
| API communication | Adapter | Abstract API differences |
| Different import modes | Strategy | Pluggable algorithms |
| Select components | Factory | Automatic selection based on context |
| Handle webhooks | Observer | Event-driven architecture |
| Extend core models | Delegation | Reuse without deep inheritance |
| Transform data | Mapper | Declarative transformations |
| Handle failures | Retry + Circuit Breaker | Resilient operations |
| Respect API limits | Rate Limiting | Prevent API throttling |
| Multi-step operations | Saga | Rollback on failure |
| Data access | Repository | Centralized queries |
## Anti-Patterns to Avoid
### ❌ Direct Odoo Record Modification
```python
# BAD: Directly modify product
product = self.env['product.template'].browse(product_id)
product.write({'name': external_data['title']})
```
```python
# GOOD: Use binding
binding = self.env['shopify.product.template'].search([
('odoo_id', '=', product_id)
])
binding.write({'name': external_data['title']})
```
### ❌ Synchronous Long Operations
```python
# BAD: Block user while importing 1000 products
def import_all_products(self):
for product_id in range(1, 1000):
self.import_product(product_id) # Takes 30 minutes!
```
```python
# GOOD: Queue async job
def import_all_products(self):
self.with_delay().import_products_batch()
```
### ❌ No Error Handling
```python
# BAD: Unhandled API errors crash sync
def sync_orders(self):
response = adapter.get_orders() # What if API is down?
for order in response:
self.import_order(order)
```
```python
# GOOD: Graceful error handling
def sync_orders(self):
try:
response = adapter.get_orders()
except requests.HTTPError as e:
_logger.error("Failed to fetch orders: %s", e)
return False
for order in response:
try:
self.import_order(order)
except Exception as e:
_logger.error("Failed to import order %s: %s", order['id'], e)
continue # Continue with next order
```
### ❌ Hardcoded Configuration
```python
# BAD: Hardcoded values
API_URL = 'https://api.shopify.com'
API_KEY = 'hardcoded-key-123'
```
```python
# GOOD: Backend configuration
api_url = self.backend_record.api_url
api_key = self.backend_record.api_key
```
### ❌ God Object
```python
# BAD: Backend does everything
class ShopifyBackend(models.Model):
def sync_orders(self):
# 500 lines of code doing:
# - API calls
# - Data transformation
# - Validation
# - Record creation
# - Email notifications
# etc.
```
```python
# GOOD: Separated concerns
class ShopifyBackend(models.Model):
def sync_orders(self):
# Orchestration only
with self.work_on('shopify.sale.order') as work:
importer = work.component(usage='batch.importer')
return importer.run()
# Adapter handles API
# Mapper handles transformation
# Importer handles record creation
```

View File

@@ -0,0 +1,576 @@
# Troubleshooting Guide for Odoo Connectors
## Common Issues and Solutions
### 1. Connection Issues
#### Problem: "Connection test failed" or timeout errors
**Possible Causes**:
- Incorrect API URL
- Invalid API credentials
- Network/firewall blocking requests
- API endpoint not accessible
**Solutions**:
```python
# 1. Verify API URL format
api_url = backend.api_url
print(f"Testing connection to: {api_url}")
# 2. Test with curl/requests directly
import requests
response = requests.get(f"{api_url}/health", timeout=10)
print(f"Status: {response.status_code}")
# 3. Check credentials
adapter = backend.get_adapter('backend.adapter')
headers = adapter.get_api_headers()
print(f"Headers: {headers}") # Don't log in production!
# 4. Add detailed logging
import logging
logging.getLogger('requests').setLevel(logging.DEBUG)
```
#### Problem: SSL Certificate verification failed
**Solution**:
```python
# Temporary: Disable SSL verification (NOT for production!)
def make_request(self, method, endpoint, **kwargs):
kwargs['verify'] = False
return super().make_request(method, endpoint, **kwargs)
# Production: Add CA certificate
import certifi
kwargs['verify'] = certifi.where()
```
### 2. Authentication Issues
#### Problem: 401 Unauthorized
**Diagnosis**:
```python
# Check token expiry
if backend.token_expires_at:
from datetime import datetime
is_expired = datetime.now() >= backend.token_expires_at
print(f"Token expired: {is_expired}")
# Check authentication header
adapter = backend.get_adapter('backend.adapter')
headers = adapter.get_api_headers()
print(f"Auth header: {headers.get('Authorization', 'MISSING')}")
```
**Solutions**:
```python
# 1. Refresh OAuth token
backend.refresh_access_token()
# 2. Re-authenticate
backend.action_start_oauth_flow()
# 3. Verify API key is correct
# Go to backend form and re-enter API key
```
#### Problem: OAuth callback not working
**Common Issues**:
- Redirect URI mismatch
- State parameter validation failed
- CORS issues
**Solutions**:
```python
# 1. Check redirect URI matches exactly
print(f"Configured: {backend.oauth_redirect_uri}")
print(f"Expected: https://yourodoo.com/myconnector/oauth/callback")
# 2. Disable state validation temporarily for debugging
def exchange_code_for_token(self, code, state):
# Skip state validation
# if state != stored_state:
# raise ValueError('Invalid OAuth state')
...
# 3. Add CORS headers in controller
@http.route('/myconnector/oauth/callback', cors='*')
```
### 3. Import/Sync Issues
#### Problem: Records not importing
**Diagnosis**:
```python
# 1. Check if importer is registered
with backend.work_on('myconnector.product.template') as work:
try:
importer = work.component(usage='record.importer')
print(f"Importer found: {importer._name}")
except ComponentNotFound:
print("ERROR: Importer component not registered!")
# 2. Check adapter methods
adapter = work.component(usage='backend.adapter')
products = adapter.get_products()
print(f"Fetched {len(products)} products from API")
# 3. Test mapper
mapper = work.component(usage='import.mapper')
if products:
mapped = mapper.map_record(products[0])
print(f"Mapped data: {mapped.values()}")
```
**Solutions**:
```python
# 1. Register component properly
class ProductImporter(GenericImporter):
_name = 'myconnector.product.importer'
_inherit = 'generic.importer'
_apply_on = 'myconnector.product.template' # Must match model!
_usage = 'record.importer' # Required!
# 2. Check model name consistency
# Backend: myconnector.backend
# Binding: myconnector.product.template
# Component: _apply_on = 'myconnector.product.template'
# 3. Add logging
def _import_record(self, external_id, force=False):
_logger.info("Importing product %s", external_id)
# ... import logic
_logger.info("Successfully imported product %s", external_id)
```
#### Problem: Duplicate records created
**Cause**: External ID not properly set or constraint not working
**Solution**:
```python
# 1. Verify SQL constraint
class ProductBinding(models.Model):
_sql_constraints = [
('backend_external_uniq',
'unique(backend_id, external_id)',
'Product must be unique per backend')
]
# 2. Check external ID is set
def _import_record(self, external_id, force=False):
# Always set external_id in mapped data
mapped_data = mapper.map_record(external_data).values()
if 'external_id' not in mapped_data:
mapped_data['external_id'] = str(external_id)
# 3. Search for existing binding before creating
binding = self.env['myconnector.product.template'].search([
('backend_id', '=', backend.id),
('external_id', '=', str(external_id))
], limit=1)
if binding:
binding.write(mapped_data)
else:
binding = self.env['myconnector.product.template'].create(mapped_data)
```
### 4. Export Issues
#### Problem: Records not exporting to external system
**Diagnosis**:
```python
# 1. Check exporter is registered
with backend.work_on('myconnector.product.template') as work:
exporter = work.component(usage='record.exporter')
# 2. Test export mapper
mapper = work.component(usage='export.mapper')
external_data = mapper.map_record(binding).values()
print(f"Export data: {json.dumps(external_data, indent=2)}")
# 3. Test adapter create method
adapter = work.component(usage='backend.adapter')
result = adapter.create_product(external_data)
print(f"Created external ID: {result.get('id')}")
```
**Solutions**:
```python
# 1. Check no_export flag
binding.write({'no_export': False})
# 2. Ensure export mapper returns correct format
class ProductExportMapper(GenericExportMapper):
direct = [
('name', 'title'), # Odoo field -> External field
('list_price', 'price'),
]
# 3. Handle API response correctly
def _export_record(self, binding):
mapper = self.component(usage='export.mapper')
data = mapper.map_record(binding).values()
adapter = self.component(usage='backend.adapter')
if binding.external_id:
adapter.update_product(binding.external_id, data)
else:
result = adapter.create_product(data)
# Save external ID!
binding.write({'external_id': str(result['id'])})
```
### 5. Queue Job Issues
#### Problem: Queue jobs not running
**Diagnosis**:
```bash
# 1. Check queue job workers are running
ps aux | grep odoo
# 2. Check queued jobs
# Go to Queue > Jobs in Odoo UI
# 3. Check job configuration
# Settings > Technical > Queue Jobs > Functions
```
**Solutions**:
```python
# 1. Ensure queue_job is installed and loaded
# In odoo.conf:
# server_wide_modules = base,web,queue_job
# 2. Start job runner
# odoo-bin -c odoo.conf --workers=2
# 3. Register job functions
# In data/queue_job_function_data.xml
<record id="queue_job_function_sync_products" model="queue.job.function">
<field name="name">myconnector.backend.sync_products</field>
<field name="channel_id" ref="queue_job.channel_root"/>
</record>
# 4. Use with_delay correctly
backend.with_delay().sync_products() # Correct
backend.sync_products() # Wrong - runs synchronously
```
#### Problem: Jobs failing silently
**Solution**:
```python
# 1. Check job logs
# Queue > Jobs > Failed
# Click on job to see error details
# 2. Add try/except with logging
@job
def sync_products(self):
try:
# Sync logic
_logger.info("Product sync completed successfully")
except Exception as e:
_logger.exception("Product sync failed")
raise # Re-raise to mark job as failed
# 3. Configure retry pattern
<record id="queue_job_function_sync_products" model="queue.job.function">
<field name="retry_pattern">{1: 60, 5: 300, 10: 600}</field>
<!-- Retry after 60s, 300s, 600s -->
</record>
```
### 6. Webhook Issues
#### Problem: Webhooks not received
**Diagnosis**:
```bash
# 1. Check route is registered
# In Odoo shell:
routes = request.env['ir.http']._get_routes()
webhook_routes = [r for r in routes if 'webhook' in r]
print(webhook_routes)
# 2. Test webhook endpoint manually
curl -X POST https://yourodoo.com/myconnector/webhook \
-H "Content-Type: application/json" \
-H "X-API-Key: your_key" \
-d '{"test": "data"}'
# 3. Check webhook URL configuration
print(f"Webhook URL: {backend.webhook_url}")
# Ensure this matches the URL configured in external system
```
**Solutions**:
```python
# 1. Ensure controller is registered
class WebhookController(http.Controller):
@http.route('/myconnector/webhook', type='json', auth='none', csrf=False)
def webhook(self):
# IMPORTANT: auth='none', csrf=False for external calls
...
# 2. Update module to load controllers
# In __init__.py:
from . import controllers
# 3. Check firewall/reverse proxy allows POST to webhook URL
# 4. Add debug logging
@http.route('/myconnector/webhook', type='json', auth='none', csrf=False)
def webhook(self):
_logger.info("Webhook received: %s", request.jsonrequest)
...
```
#### Problem: Webhook signature verification failing
**Diagnosis**:
```python
# In controller:
payload = request.httprequest.get_data(as_text=True)
signature = request.httprequest.headers.get('X-Signature')
secret = backend.webhook_secret
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
print(f"Received signature: {signature}")
print(f"Expected signature: {expected}")
print(f"Match: {signature == expected}")
```
**Solutions**:
```python
# 1. Ensure secret matches between Odoo and external system
# 2. Check signature algorithm matches
# Some systems use base64, others hex
# 3. Verify payload encoding
# Use raw payload, not parsed JSON
# 4. Check header name
# Could be X-Signature, X-Webhook-Signature, etc.
# 5. Temporarily disable verification for debugging
if not self._verify_signature(...):
_logger.warning("Signature verification failed, but processing anyway")
# return {'error': 'Invalid signature'}, 401
```
### 7. Data Mapping Issues
#### Problem: Fields not mapping correctly
**Diagnosis**:
```python
# Test mapper in isolation
mapper = ProductImportMapper(work)
external_data = {
'id': 123,
'title': 'Test Product',
'price': 99.99,
}
mapped = mapper.map_record(external_data)
print(f"Mapped values: {mapped.values()}")
# Check each mapping method
for method_name in dir(mapper):
if hasattr(getattr(mapper, method_name), '_mapping'):
result = getattr(mapper, method_name)(external_data)
print(f"{method_name}: {result}")
```
**Solutions**:
```python
# 1. Use @mapping decorator
@mapping
def product_name(self, record):
return {'name': record['title']} # Must return dict!
# 2. Handle missing fields
@mapping
def category(self, record):
category_name = record.get('category', {}).get('name')
if not category_name:
return {} # Return empty dict, not None
# 3. Use only_create for default values
@only_create
def default_code(self, record):
return {'default_code': record.get('sku', 'SKU_MISSING')}
```
### 8. Performance Issues
#### Problem: Sync takes too long
**Diagnosis**:
```python
import time
def sync_products(self):
start = time.time()
# Time each step
t1 = time.time()
products = adapter.get_products()
print(f"Fetch: {time.time() - t1:.2f}s for {len(products)} products")
t2 = time.time()
for product in products:
self.import_product(product['id'])
print(f"Import: {time.time() - t2:.2f}s")
print(f"Total: {time.time() - start:.2f}s")
```
**Solutions**:
```python
# 1. Use batch operations
def sync_products(self):
# Fetch all products at once (if API supports)
products = adapter.get_all_products()
# Process in batches
batch_size = 100
for i in range(0, len(products), batch_size):
batch = products[i:i+batch_size]
self.with_delay().import_product_batch(batch)
# 2. Reduce database queries
# Use search_read instead of browse
products = env['product.template'].search_read(
[('id', 'in', product_ids)],
['name', 'list_price']
)
# 3. Use SQL for bulk operations
self.env.cr.execute("""
UPDATE myconnector_product_template
SET sync_status = 'success'
WHERE backend_id = %s
""", (backend.id,))
# 4. Disable expensive computations during import
# Use context flags
binding.with_context(skip_compute=True).write(values)
```
### 9. Module Installation Issues
#### Problem: Module won't install/upgrade
**Common Errors**:
```
ParseError: Invalid XML
SyntaxError: Invalid Python syntax
ProgrammingError: relation does not exist
```
**Solutions**:
```bash
# 1. Check logs
tail -f /var/log/odoo/odoo.log
# 2. Validate XML syntax
xmllint --noout views/*.xml
# 3. Check Python syntax
python3 -m py_compile models/*.py
# 4. Drop and recreate database (dev only!)
dropdb test_db
createdb test_db
odoo-bin -c odoo.conf -d test_db -i myconnector
# 5. Update with stop-after-init to see errors
odoo-bin -c odoo.conf -d test_db -u myconnector --stop-after-init
# 6. Check dependencies
# In __manifest__.py, ensure all 'depends' modules are installed
```
### 10. Debugging Tips
#### Enable Debug Logging
```python
# In code:
import logging
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.DEBUG)
# In odoo.conf:
log_level = debug
log_handler = :DEBUG
```
#### Use Odoo Shell
```bash
odoo-bin shell -c odoo.conf -d your_db
>>> backend = env['myconnector.backend'].browse(1)
>>> backend.sync_products()
>>> env.cr.rollback() # Rollback changes
```
#### Use pdb Debugger
```python
def sync_products(self):
import pdb; pdb.set_trace() # Debugger will pause here
products = adapter.get_products()
...
```
#### Monitor API Calls
```python
# Add request/response logging
def make_request(self, method, endpoint, **kwargs):
_logger.debug("Request: %s %s", method, endpoint)
_logger.debug("Params: %s", kwargs.get('params'))
_logger.debug("Data: %s", kwargs.get('data'))
response = super().make_request(method, endpoint, **kwargs)
_logger.debug("Response status: %s", response.status_code if hasattr(response, 'status_code') else 'N/A')
_logger.debug("Response data: %s", response[:500] if isinstance(response, str) else str(response)[:500])
return response
```
## Error Reference
| Error | Cause | Solution |
|-------|-------|----------|
| ComponentNotFound | Component not registered | Check `_name`, `_apply_on`, `_usage` |
| MissingError | Record deleted | Check `exists()` before operations |
| AccessError | Permission denied | Check security rules and groups |
| ValidationError | Constraint violated | Check required fields and constraints |
| HTTPError 401 | Invalid credentials | Refresh tokens or re-authenticate |
| HTTPError 429 | Rate limited | Implement rate limiting and backoff |
| HTTPError 500 | Server error | Retry with exponential backoff |
| TypeError in mapper | Wrong return type | Mappers must return dict |
| IntegrityError | Duplicate key | Check SQL constraints |
| JSONDecodeError | Invalid JSON | Check API response format |

View File

@@ -0,0 +1,537 @@
#!/usr/bin/env python3
"""
Add a new binding model to an existing Odoo connector module.
Usage:
python3 add_binding.py <connector_module_path> <entity_name> [--odoo-model <model>]
Arguments:
connector_module_path: Path to existing connector module
entity_name: Name of the entity (e.g., 'order', 'customer', 'invoice')
--odoo-model: Odoo model to bind (default: inferred from entity name)
Examples:
python3 add_binding.py ~/addons/shopify_connector order
python3 add_binding.py ~/addons/shopify_connector customer --odoo-model res.partner
python3 add_binding.py ~/addons/shopify_connector invoice --odoo-model account.move
"""
import argparse
import os
import sys
from pathlib import Path
# Map common entity names to Odoo models
ENTITY_MODEL_MAP = {
'product': 'product.template',
'variant': 'product.product',
'order': 'sale.order',
'customer': 'res.partner',
'invoice': 'account.move',
'payment': 'account.payment',
'picking': 'stock.picking',
'inventory': 'stock.quant',
'category': 'product.category',
'pricelist': 'product.pricelist',
'tax': 'account.tax',
}
def sanitize_name(name):
"""Convert name to valid Python identifier."""
return name.lower().replace('-', '_').replace(' ', '_')
def get_connector_info(module_path):
"""Extract connector name and module name from path."""
manifest_path = module_path / '__manifest__.py'
if not manifest_path.exists():
raise ValueError(f"No __manifest__.py found at {module_path}")
with open(manifest_path, 'r') as f:
manifest_content = f.read()
# Try to extract module name from path
module_name = module_path.name
if module_name.endswith('_connector'):
connector_name = module_name.replace('_connector', '')
else:
connector_name = module_name
return {
'module_name': module_name,
'connector_name': connector_name,
'module_path': module_path
}
def generate_binding_model(connector_info, entity_name, odoo_model):
"""Generate binding model Python code."""
module_name = connector_info['module_name']
connector_name = connector_info['connector_name']
entity_lower = sanitize_name(entity_name)
# Generate class name
class_name_parts = [word.capitalize() for word in connector_name.split('_')]
class_name_parts.append(entity_name.capitalize())
class_name = ''.join(class_name_parts) + 'Binding'
# Determine binding model name
binding_model_name = f"{connector_name}.{odoo_model.replace('.', '.')}"
# Extract Odoo model information
odoo_model_parts = odoo_model.split('.')
inherits_field_name = odoo_model_parts[-1] + '_id'
code = f'''# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class {class_name}(models.Model):
"""
Binding between Odoo {odoo_model} and {connector_name.title()} {entity_name}s.
This model links Odoo {entity_name} records with their counterparts
in the {connector_name.title()} system.
"""
_name = '{binding_model_name}'
_inherit = 'generic.binding'
_inherits = {{'{odoo_model}': 'odoo_id'}}
_description = '{connector_name.title()} {entity_name.title()} Binding'
odoo_id = fields.Many2one(
comodel_name='{odoo_model}',
string='{entity_name.title()}',
required=True,
ondelete='cascade',
help='Odoo {entity_name} record'
)
# External system fields
external_status = fields.Char(
string='External Status',
readonly=True,
help='Status in {connector_name.title()}'
)
external_number = fields.Char(
string='External Number',
readonly=True,
help='Reference number in {connector_name.title()}'
)
# Synchronization metadata
sync_date = fields.Datetime(
string='Last Sync Date',
readonly=True,
help='Last successful synchronization date'
)
external_created_at = fields.Datetime(
string='Created in {connector_name.title()}',
readonly=True
)
external_updated_at = fields.Datetime(
string='Updated in {connector_name.title()}',
readonly=True
)
# Sync control flags
no_export = fields.Boolean(
string='No Export',
help='Prevent automatic export to {connector_name.title()}'
)
_sql_constraints = [
('backend_external_uniq', 'unique(backend_id, external_id)',
'A {entity_name} binding with the same external ID already exists for this backend.')
]
@api.model
def import_record(self, backend, external_id):
"""
Import a single {entity_name} from {connector_name.title()}.
Args:
backend: Backend record
external_id: External ID of the {entity_name}
Returns:
Binding record
"""
_logger.info('Importing {entity_name} %s from backend %s', external_id, backend.name)
with backend.work_on(self._name) as work:
importer = work.component(usage='record.importer')
return importer.run(external_id)
@api.model
def import_batch(self, backend, filters=None):
"""
Import {entity_name}s in batch from {connector_name.title()}.
Args:
backend: Backend record
filters: Optional filters for the import
Returns:
List of imported binding records
"""
_logger.info('Starting batch import of {entity_name}s for backend %s', backend.name)
with backend.work_on(self._name) as work:
importer = work.component(usage='batch.importer')
return importer.run(filters=filters)
def export_record(self):
"""
Export {entity_name} to {connector_name.title()}.
Returns:
External ID of the exported record
"""
self.ensure_one()
if self.no_export:
_logger.info('{entity_name.title()} %s marked as no_export, skipping', self.odoo_id.display_name)
return False
_logger.info('Exporting {entity_name} %s to backend %s', self.odoo_id.display_name, self.backend_id.name)
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='record.exporter')
return exporter.run(self)
@api.model
def export_batch(self, backend, domain=None):
"""
Export multiple {entity_name}s to {connector_name.title()}.
Args:
backend: Backend record
domain: Optional domain to filter records
Returns:
Number of exported records
"""
if domain is None:
domain = []
domain.append(('backend_id', '=', backend.id))
domain.append(('no_export', '=', False))
bindings = self.search(domain)
_logger.info('Exporting %d {entity_name}s to backend %s', len(bindings), backend.name)
for binding in bindings:
binding.with_delay().export_record()
return len(bindings)
def resync_record(self):
"""Re-import the record from {connector_name.title()}."""
self.ensure_one()
if not self.external_id:
raise UserError(_('Cannot resync: No external ID found'))
return self.import_record(self.backend_id, self.external_id)
'''
return code
def generate_binding_view(connector_info, entity_name, odoo_model):
"""Generate binding view XML."""
module_name = connector_info['module_name']
connector_name = connector_info['connector_name']
entity_lower = sanitize_name(entity_name)
binding_model_name = f"{connector_name}.{odoo_model.replace('.', '.')}"
view_id_prefix = f"view_{connector_name}_{entity_lower}"
xml = f'''<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- {entity_name.title()} Binding Form View -->
<record id="{view_id_prefix}_form" model="ir.ui.view">
<field name="name">{binding_model_name}.form</field>
<field name="model">{binding_model_name}</field>
<field name="arch" type="xml">
<form string="{connector_name.title()} {entity_name.title()}">
<header>
<button name="export_record"
type="object"
string="Export to {connector_name.title()}"
class="oe_highlight"
attrs="{{'invisible': [('external_id', '!=', False)]}}"/>
<button name="resync_record"
type="object"
string="Resync"
attrs="{{'invisible': [('external_id', '=', False)]}}"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<!-- Add smart buttons here -->
</div>
<group>
<group name="odoo_info" string="Odoo Information">
<field name="odoo_id"/>
<field name="backend_id"/>
</group>
<group name="external_info" string="{connector_name.title()} Information">
<field name="external_id" readonly="1"/>
<field name="external_number" readonly="1"/>
<field name="external_status" readonly="1"/>
</group>
</group>
<group>
<group name="sync_info" string="Synchronization">
<field name="sync_date" readonly="1"/>
<field name="external_created_at" readonly="1"/>
<field name="external_updated_at" readonly="1"/>
</group>
<group name="sync_control" string="Control">
<field name="no_export"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- {entity_name.title()} Binding Tree View -->
<record id="{view_id_prefix}_tree" model="ir.ui.view">
<field name="name">{binding_model_name}.tree</field>
<field name="model">{binding_model_name}</field>
<field name="arch" type="xml">
<tree string="{connector_name.title()} {entity_name.title()}s">
<field name="odoo_id"/>
<field name="external_id"/>
<field name="external_status"/>
<field name="backend_id"/>
<field name="sync_date"/>
</tree>
</field>
</record>
<!-- {entity_name.title()} Binding Search View -->
<record id="{view_id_prefix}_search" model="ir.ui.view">
<field name="name">{binding_model_name}.search</field>
<field name="model">{binding_model_name}</field>
<field name="arch" type="xml">
<search string="{connector_name.title()} {entity_name.title()}s">
<field name="odoo_id"/>
<field name="external_id"/>
<field name="backend_id"/>
<filter string="Not Exported" name="not_exported"
domain="[('external_id', '=', False)]"/>
<filter string="No Export Flag" name="no_export"
domain="[('no_export', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Backend" name="group_backend"
context="{{'group_by': 'backend_id'}}"/>
<filter string="Status" name="group_status"
context="{{'group_by': 'external_status'}}"/>
</group>
</search>
</field>
</record>
<!-- {entity_name.title()} Binding Action -->
<record id="action_{connector_name}_{entity_lower}" model="ir.actions.act_window">
<field name="name">{connector_name.title()} {entity_name.title()}s</field>
<field name="res_model">{binding_model_name}</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="{view_id_prefix}_search"/>
</record>
<!-- Menu Item (add under existing Data menu) -->
<menuitem id="menu_{connector_name}_{entity_lower}s"
name="{entity_name.title()}s"
parent="menu_{connector_name}_data"
action="action_{connector_name}_{entity_lower}"
sequence="20"/>
</odoo>
'''
return xml
def generate_adapter_methods(connector_info, entity_name):
"""Generate adapter methods to add to existing adapter."""
entity_lower = sanitize_name(entity_name)
entity_plural = entity_lower + 's' # Simple pluralization
code = f'''
# CRUD operations for {entity_plural}
def get_{entity_lower}(self, external_id):
"""Get {entity_lower} by external ID."""
return self._make_request('GET', f'/{entity_plural}/{{external_id}}')
def get_{entity_plural}(self, filters=None):
"""Get list of {entity_plural}."""
return self._make_request('GET', '/{entity_plural}', params=filters)
def create_{entity_lower}(self, data):
"""Create {entity_lower}."""
return self._make_request('POST', '/{entity_plural}', data=data)
def update_{entity_lower}(self, external_id, data):
"""Update {entity_lower}."""
return self._make_request('PUT', f'/{entity_plural}/{{external_id}}', data=data)
def delete_{entity_lower}(self, external_id):
"""Delete {entity_lower}."""
return self._make_request('DELETE', f'/{entity_plural}/{{external_id}}')
'''
return code
def generate_security_entries(connector_info, entity_name, odoo_model):
"""Generate security entries for ir.model.access.csv."""
module_name = connector_info['module_name']
connector_name = connector_info['connector_name']
entity_lower = sanitize_name(entity_name)
model_name_sanitized = odoo_model.replace('.', '_')
entries = f'''access_{connector_name}_{model_name_sanitized}_user,{connector_name}.{odoo_model} user,model_{connector_name}_{model_name_sanitized},group_{connector_name}_user,1,0,0,0
access_{connector_name}_{model_name_sanitized}_manager,{connector_name}.{odoo_model} manager,model_{connector_name}_{model_name_sanitized},group_{connector_name}_manager,1,1,1,1
'''
return entries
def update_module_files(connector_info, entity_name, odoo_model):
"""Update module files to include new binding."""
module_path = connector_info['module_path']
entity_lower = sanitize_name(entity_name)
print(f"\\n📝 Updating module files...")
# 1. Create binding model file
binding_file = module_path / 'models' / f'{entity_lower}_binding.py'
binding_code = generate_binding_model(connector_info, entity_name, odoo_model)
with open(binding_file, 'w') as f:
f.write(binding_code)
print(f"✅ Created: models/{entity_lower}_binding.py")
# 2. Update models/__init__.py
models_init = module_path / 'models' / '__init__.py'
with open(models_init, 'r') as f:
init_content = f.read()
import_line = f"from . import {entity_lower}_binding\\n"
if import_line not in init_content:
with open(models_init, 'a') as f:
f.write(import_line)
print(f"✅ Updated: models/__init__.py")
# 3. Create view file
view_file = module_path / 'views' / f'{entity_lower}_views.xml'
view_xml = generate_binding_view(connector_info, entity_name, odoo_model)
with open(view_file, 'w') as f:
f.write(view_xml)
print(f"✅ Created: views/{entity_lower}_views.xml")
# 4. Update __manifest__.py data list
manifest_file = module_path / '__manifest__.py'
with open(manifest_file, 'r') as f:
manifest_content = f.read()
# Add view file to manifest if not already there
view_entry = f" 'views/{entity_lower}_views.xml',\\n"
if view_entry not in manifest_content:
# Find the views section and add
manifest_content = manifest_content.replace(
" 'views/binding_views.xml',",
f" 'views/binding_views.xml',\\n{view_entry}"
)
with open(manifest_file, 'w') as f:
f.write(manifest_content)
print(f"✅ Updated: __manifest__.py (added view reference)")
# 5. Update security file
security_file = module_path / 'security' / 'ir.model.access.csv'
security_entries = generate_security_entries(connector_info, entity_name, odoo_model)
with open(security_file, 'a') as f:
f.write(security_entries)
print(f"✅ Updated: security/ir.model.access.csv")
# 6. Show adapter methods to add manually
print(f"\\n📋 Add these methods to models/adapter.py:")
print("=" * 60)
print(generate_adapter_methods(connector_info, entity_name))
print("=" * 60)
def main():
parser = argparse.ArgumentParser(
description='Add a new binding model to an existing Odoo connector module'
)
parser.add_argument(
'connector_module_path',
help='Path to existing connector module'
)
parser.add_argument(
'entity_name',
help='Name of the entity (e.g., order, customer, invoice)'
)
parser.add_argument(
'--odoo-model',
help='Odoo model to bind (default: auto-detected from entity name)'
)
args = parser.parse_args()
try:
# Validate module path
module_path = Path(args.connector_module_path)
if not module_path.exists():
raise ValueError(f"Module path does not exist: {module_path}")
# Get connector info
connector_info = get_connector_info(module_path)
# Determine Odoo model
odoo_model = args.odoo_model
if not odoo_model:
entity_lower = sanitize_name(args.entity_name)
odoo_model = ENTITY_MODEL_MAP.get(entity_lower)
if not odoo_model:
print(f"\\n⚠ Could not auto-detect Odoo model for '{args.entity_name}'")
print(f"Please specify using --odoo-model option")
print(f"\\nCommon mappings:")
for entity, model in ENTITY_MODEL_MAP.items():
print(f" {entity}: {model}")
sys.exit(1)
print(f"\\n🚀 Adding {args.entity_name.title()} Binding")
print(f" Module: {connector_info['module_name']}")
print(f" Odoo Model: {odoo_model}")
print(f" Location: {module_path}\\n")
# Update module files
update_module_files(connector_info, args.entity_name, odoo_model)
print(f"\\n✅ Binding for '{args.entity_name}' added successfully!")
print(f"\\nNext steps:")
print(f"1. Add adapter methods to models/adapter.py (see output above)")
print(f"2. Implement importer component for {args.entity_name}")
print(f"3. Implement exporter component for {args.entity_name}")
print(f"4. Update module and test: odoo-bin -u {connector_info['module_name']}")
except Exception as e:
print(f"\\n❌ Error: {str(e)}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""
Validate Odoo connector module structure and dependencies.
Usage:
python3 validate_connector.py <connector_module_path>
Example:
python3 validate_connector.py ~/addons/shopify_connector
"""
import argparse
import sys
from pathlib import Path
import ast
class ConnectorValidator:
"""Validator for Odoo connector modules."""
def __init__(self, module_path):
self.module_path = Path(module_path)
self.errors = []
self.warnings = []
self.info = []
def error(self, message):
"""Add error message."""
self.errors.append(f"❌ ERROR: {message}")
def warning(self, message):
"""Add warning message."""
self.warnings.append(f"⚠️ WARNING: {message}")
def info_msg(self, message):
"""Add info message."""
self.info.append(f" INFO: {message}")
def validate_structure(self):
"""Validate basic directory structure."""
print("\\n🔍 Validating module structure...")
required_files = [
'__manifest__.py',
'__init__.py',
'models/__init__.py',
'security/ir.model.access.csv',
]
for file_path in required_files:
full_path = self.module_path / file_path
if not full_path.exists():
self.error(f"Required file missing: {file_path}")
else:
self.info_msg(f"Found: {file_path}")
recommended_dirs = ['views', 'security', 'models', 'wizards', 'data']
for dir_name in recommended_dirs:
dir_path = self.module_path / dir_name
if not dir_path.exists():
self.warning(f"Recommended directory missing: {dir_name}/")
def validate_manifest(self):
"""Validate __manifest__.py."""
print("\\n🔍 Validating __manifest__.py...")
manifest_path = self.module_path / '__manifest__.py'
if not manifest_path.exists():
return # Already reported in structure validation
try:
with open(manifest_path, 'r') as f:
content = f.read()
manifest = ast.literal_eval(content)
# Check required fields
required_fields = ['name', 'version', 'depends', 'data']
for field in required_fields:
if field not in manifest:
self.error(f"__manifest__.py missing required field: {field}")
else:
self.info_msg(f"Found manifest field: {field}")
# Check dependencies
if 'depends' in manifest:
depends = manifest['depends']
if 'generic_connector' not in depends:
self.error("Missing dependency: generic_connector")
else:
self.info_msg("Found dependency: generic_connector")
if 'queue_job' not in depends:
self.warning("Missing recommended dependency: queue_job")
else:
self.info_msg("Found dependency: queue_job")
# Check version format
if 'version' in manifest:
version = manifest['version']
if not version.startswith('16.0.'):
self.warning(f"Version should start with '16.0.' for Odoo 16: {version}")
except Exception as e:
self.error(f"Failed to parse __manifest__.py: {str(e)}")
def validate_models(self):
"""Validate model files."""
print("\\n🔍 Validating models...")
models_dir = self.module_path / 'models'
if not models_dir.exists():
return
# Check for backend model
backend_files = list(models_dir.glob('*backend*.py'))
if not backend_files:
self.error("No backend model found (should have a file like 'backend.py')")
else:
self.info_msg(f"Found backend model: {backend_files[0].name}")
self._validate_backend_model(backend_files[0])
# Check for adapter
adapter_files = list(models_dir.glob('*adapter*.py'))
if not adapter_files:
self.warning("No adapter model found (recommended: 'adapter.py')")
else:
self.info_msg(f"Found adapter model: {adapter_files[0].name}")
# Check for binding models
binding_files = list(models_dir.glob('*binding*.py'))
if not binding_files:
self.warning("No binding models found")
else:
self.info_msg(f"Found {len(binding_files)} binding model(s)")
def _validate_backend_model(self, backend_file):
"""Validate backend model content."""
try:
with open(backend_file, 'r') as f:
content = f.read()
# Check for inheritance from generic.backend
if 'generic.backend' not in content:
self.error("Backend model should inherit from 'generic.backend'")
# Check for _backend_type
if '_backend_type' not in content:
self.warning("Backend model should define '_backend_type'")
# Check for API configuration fields
recommended_fields = ['api_url', 'api_key']
for field in recommended_fields:
if field not in content:
self.warning(f"Backend model missing recommended field: {field}")
except Exception as e:
self.error(f"Failed to validate backend model: {str(e)}")
def validate_security(self):
"""Validate security configuration."""
print("\\n🔍 Validating security...")
access_file = self.module_path / 'security' / 'ir.model.access.csv'
if not access_file.exists():
return
try:
with open(access_file, 'r') as f:
lines = f.readlines()
if len(lines) < 2:
self.warning("Security file seems empty (should have header + access rules)")
return
# Check header
header = lines[0].strip()
expected_header = 'id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink'
if header != expected_header:
self.error("Invalid security file header")
# Count rules
num_rules = len(lines) - 1
self.info_msg(f"Found {num_rules} access rule(s)")
if num_rules == 0:
self.warning("No access rules defined")
except Exception as e:
self.error(f"Failed to validate security file: {str(e)}")
def validate_views(self):
"""Validate view files."""
print("\\n🔍 Validating views...")
views_dir = self.module_path / 'views'
if not views_dir.exists():
self.warning("No views directory found")
return
view_files = list(views_dir.glob('*.xml'))
if not view_files:
self.warning("No view XML files found")
return
self.info_msg(f"Found {len(view_files)} view file(s)")
# Check for backend views
backend_view_files = [f for f in view_files if 'backend' in f.name]
if not backend_view_files:
self.warning("No backend views found")
else:
self.info_msg("Found backend views")
def print_results(self):
"""Print validation results."""
print("\\n" + "=" * 70)
print("VALIDATION RESULTS")
print("=" * 70)
if self.errors:
print(f"\\n🔴 ERRORS ({len(self.errors)}):")
for error in self.errors:
print(f" {error}")
if self.warnings:
print(f"\\n🟡 WARNINGS ({len(self.warnings)}):")
for warning in self.warnings:
print(f" {warning}")
if self.info:
print(f"\\n🟢 INFO ({len(self.info)}):")
for info in self.info:
print(f" {info}")
print("\\n" + "=" * 70)
if self.errors:
print("\\n❌ VALIDATION FAILED: Please fix errors above")
return False
elif self.warnings:
print("\\n⚠ VALIDATION PASSED WITH WARNINGS: Consider addressing warnings")
return True
else:
print("\\n✅ VALIDATION PASSED: Module structure looks good!")
return True
def validate(self):
"""Run all validations."""
self.validate_structure()
self.validate_manifest()
self.validate_models()
self.validate_security()
self.validate_views()
return self.print_results()
def main():
parser = argparse.ArgumentParser(
description='Validate Odoo connector module structure and dependencies'
)
parser.add_argument(
'connector_module_path',
help='Path to connector module to validate'
)
args = parser.parse_args()
module_path = Path(args.connector_module_path)
if not module_path.exists():
print(f"\\n❌ Error: Module path does not exist: {module_path}", file=sys.stderr)
sys.exit(1)
if not module_path.is_dir():
print(f"\\n❌ Error: Path is not a directory: {module_path}", file=sys.stderr)
sys.exit(1)
print(f"\\n🚀 Validating Connector Module")
print(f" Location: {module_path}\\n")
validator = ConnectorValidator(module_path)
success = validator.validate()
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,356 @@
---
name: odoo-debugger
description: Analyzes and resolves Odoo 16.0 issues including SVL linking problems, queue job failures, view errors, and business logic bugs. This skill should be used when the user reports problems such as "Debug this SVL linking issue" or "Queue job is failing" or "View not showing correctly" or "Figure out why this vendor bill isn't linking to stock moves".
---
# Odoo Debugger & Issue Resolver
## Overview
This skill provides systematic debugging approaches for common Odoo 16.0 issues, with specialized knowledge of SVL (Stock Valuation Layer) linking, queue jobs, view inheritance problems, and business logic errors specific to the Siafa project.
## Issue Categories
### 1. SVL (Stock Valuation Layer) Issues
Stock valuation layers not linking properly to vendor bills or account moves.
### 2. Queue Job Failures
Background jobs failing or getting stuck in queue_job system.
### 3. View/XML Errors
Views not rendering, XPath inheritance issues, missing fields.
### 4. Business Logic Bugs
Computed fields not calculating, onchange not triggering, constraints failing.
### 5. Data Integrity Issues
Orphaned records, inconsistent data, broken relationships.
### 6. Performance Problems
Slow queries, N+1 problems, inefficient computed fields.
### 7. Access Rights Issues
Permission errors, record rules blocking access.
## Debugging Workflow
### Step 1: Gather Information
Ask for:
- Error message or traceback (full text)
- Steps to reproduce the issue
- Which module/model is affected
- Recent changes or updates
- Database name and Odoo version
### Step 2: Categorize the Issue
Identify which category the issue falls into and follow the specialized workflow.
## Debugging Patterns by Category
### Pattern 1: SVL Linking Issues
**Common Symptoms:**
- Stock valuation layers exist but not linked to vendor bills
- Account move lines don't reference SVL
- Quantity mismatch between stock moves and SVL
- Missing SVL for stock moves
**Investigation Script:**
```python
# In Odoo shell (python3 src/odoo-bin shell -c src/odoo.conf -d DATABASE_NAME)
# Get SVL record
svl = env['stock.valuation.layer'].browse(SVL_ID)
# Check SVL details
print(f"SVL ID: {svl.id}")
print(f"Product: {svl.product_id.name}")
print(f"Quantity: {svl.quantity}")
print(f"Value: {svl.value}")
print(f"Unit Cost: {svl.unit_cost}")
print(f"Stock Move: {svl.stock_move_id.name if svl.stock_move_id else 'NONE'}")
print(f"Account Move: {svl.account_move_id.name if svl.account_move_id else 'NONE'}")
# Check stock move linkage
if svl.stock_move_id:
move = svl.stock_move_id
print(f"\nStock Move Details:")
print(f" Name: {move.name}")
print(f" State: {move.state}")
print(f" Picking: {move.picking_id.name if move.picking_id else 'NONE'}")
print(f" Purchase Line: {move.purchase_line_id.id if move.purchase_line_id else 'NONE'}")
# Check for vendor bill
if move.purchase_line_id:
po_line = move.purchase_line_id
print(f"\nPurchase Order Line:")
print(f" Order: {po_line.order_id.name}")
print(f" Invoice Lines: {po_line.invoice_lines}")
for inv_line in po_line.invoice_lines:
print(f" Invoice: {inv_line.move_id.name}, State: {inv_line.move_id.state}")
# Check account move lines
if svl.account_move_id:
print(f"\nAccount Move Lines:")
for line in svl.account_move_id.line_ids:
print(f" Account: {line.account_id.code} - {line.account_id.name}")
print(f" Debit: {line.debit}, Credit: {line.credit}")
print(f" SVL ID in context: {line.stock_valuation_layer_id.id if line.stock_valuation_layer_id else 'NONE'}")
# Find orphaned SVLs (SQL)
env.cr.execute("""
SELECT svl.id, svl.product_id, svl.quantity, svl.value
FROM stock_valuation_layer svl
WHERE svl.stock_move_id IS NULL
OR svl.account_move_id IS NULL
LIMIT 100
""")
orphaned = env.cr.dictfetchall()
print(f"\nFound {len(orphaned)} potentially orphaned SVLs")
```
**Common Fixes:**
1. **Re-link SVL to Account Move:**
```python
svl = env['stock.valuation.layer'].browse(SVL_ID)
account_move = env['account.move'].browse(ACCOUNT_MOVE_ID)
# Update SVL
svl.write({'account_move_id': account_move.id})
# Update account move lines
for line in account_move.line_ids:
if line.account_id == svl.product_id.categ_id.property_stock_valuation_account_id:
line.write({'stock_valuation_layer_id': svl.id})
```
2. **Regenerate SVL:**
```python
stock_move = env['stock.move'].browse(MOVE_ID)
stock_move._create_stock_valuation_layers()
```
### Pattern 2: Queue Job Failures
**Investigation:**
```python
# Find failed jobs
failed_jobs = env['queue.job'].search([
('state', '=', 'failed'),
('date_created', '>=', '2025-01-01')
])
for job in failed_jobs:
print(f"\nJob: {job.name}")
print(f" UUID: {job.uuid}")
print(f" State: {job.state}")
print(f" Date Failed: {job.date_done}")
print(f" Exception:\n{job.exc_info}")
# Retry the job
# job.requeue()
```
**Common Fixes:**
1. **Retry failed job:**
```python
job = env['queue.job'].browse(JOB_ID)
job.requeue()
```
2. **Cancel stuck job:**
```python
job.write({'state': 'done'}) # or 'cancelled'
```
### Pattern 3: View/XML Errors
**Common Issues:**
- XPath not finding target element
- Field doesn't exist in model
- View inheritance loop
- Incorrect XML ID reference
**Investigation:**
1. **Check if view exists:**
```python
view = env.ref('module_name.view_id')
print(view.arch_db) # Print XML
```
2. **Find view by model:**
```python
views = env['ir.ui.view'].search([('model', '=', 'stock.picking')])
for v in views:
print(f"{v.name}: {v.xml_id}")
```
3. **Test XPath expression:**
```python
from lxml import etree
view = env.ref('stock.view_picking_form')
arch = etree.fromstring(view.arch_db)
# Test xpath
result = arch.xpath("//field[@name='partner_id']")
print(f"Found {len(result)} elements")
```
**Common Fixes:**
1. **Fix XPath - Use Developer Mode to inspect actual view structure**
2. **Ensure field exists in model before adding to view**
3. **Check view priority if inheritance not working**
### Pattern 4: Business Logic Bugs
**Computed Field Not Updating:**
```python
# Force recompute
record = env['model.name'].browse(RECORD_ID)
record._recompute_field('field_name')
# Check dependencies
field = env['model.name']._fields['field_name']
print(f"Depends: {field.depends}")
# Test compute method directly
record._compute_field_name()
```
**Onchange Not Triggering:**
```python
# Onchange methods only work in UI
# Test via form:
record.onchange('field_name', 'partner_id')
```
**Constraint Failing:**
```python
# Test constraint
try:
record._check_constraint_name()
print("Constraint passed")
except ValidationError as e:
print(f"Constraint failed: {e}")
```
### Pattern 5: Data Investigation (SQL)
**Finding Data Inconsistencies:**
```python
# Stock moves without SVL
env.cr.execute("""
SELECT sm.id, sm.name, sm.product_id, sm.state
FROM stock_move sm
LEFT JOIN stock_valuation_layer svl ON svl.stock_move_id = sm.id
WHERE sm.state = 'done'
AND svl.id IS NULL
AND sm.product_id IN (
SELECT id FROM product_product WHERE type = 'product'
)
LIMIT 50
""")
print(env.cr.dictfetchall())
# Vendor bills with no SVL link
env.cr.execute("""
SELECT am.id, am.name, am.partner_id, am.amount_total
FROM account_move am
WHERE am.move_type = 'in_invoice'
AND am.state = 'posted'
AND am.id NOT IN (
SELECT DISTINCT account_move_id
FROM stock_valuation_layer
WHERE account_move_id IS NOT NULL
)
LIMIT 50
""")
print(env.cr.dictfetchall())
```
## Debugging Tools
### Enable Debug Logging
In `odoo.conf`:
```ini
log_level = debug
log_handler = :DEBUG
```
Or via command line:
```bash
python3 src/odoo-bin -c src/odoo.conf --log-level=debug --log-handler=:DEBUG
```
### Add Logging to Code
```python
import logging
_logger = logging.getLogger(__name__)
def method(self):
_logger.info('Method called with %s', self)
_logger.debug('Detailed debug info: %s', self.read())
_logger.warning('Something unusual: %s', issue)
_logger.error('Error occurred: %s', error)
```
### Use pdb for Debugging
```python
import pdb; pdb.set_trace()
```
## Common Error Patterns
### AccessError
```
User does not have access rights
```
**Fix:** Check `ir.model.access.csv` and record rules
### ValidationError
```
Constraint validation failed
```
**Fix:** Check `@api.constrains` methods
### MissingError
```
Record does not exist
```
**Fix:** Check if record was deleted, use `exists()` method
### SQL Errors
```
column does not exist
```
**Fix:** Update module to create/modify fields
## Resources
### scripts/investigate_svl.py
Python script for automated SVL investigation - checks linkage, finds orphaned records, validates data consistency.
### scripts/check_queue_jobs.py
Script to analyze queue job status, identify stuck jobs, and generate repair commands.
### references/debugging_queries.md
Collection of useful SQL queries for data investigation and debugging common issues.
### references/common_issues.md
Database of known issues specific to the Siafa project with solutions and workarounds.

View File

@@ -0,0 +1 @@
Created common issues database

View File

@@ -0,0 +1 @@
Created basic debugging query reference

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""
Example helper script for odoo-debugger
This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.
Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""
def main():
print("This is an example script for odoo-debugger")
# TODO: Add actual script logic here
# This could be data processing, file conversion, API calls, etc.
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,440 @@
---
name: odoo-feature-enhancer
description: Extends existing Odoo 16.0 modules with new features, fields, views, business logic, wizards, and reports. This skill should be used when the user requests enhancements to existing functionality, such as "Add a field to track serial numbers in stock.picking" or "Create a wizard for bulk invoice generation" or "Add a report for vendor bill analysis".
---
# Odoo Feature Enhancer
## Overview
This skill enables extension of existing Odoo 16.0 modules by adding new fields, views, business logic, wizards, reports, and automated actions. It follows module inheritance patterns and ensures proper integration with existing functionality.
## Enhancement Categories
When a user requests a feature enhancement, identify the category and follow the appropriate workflow:
### 1. Field Additions
Add new fields to existing models (stored, computed, related).
### 2. View Modifications
Extend existing views (tree, form, kanban, calendar, pivot, graph) using XML inheritance.
### 3. Business Logic
Add computed methods, onchange handlers, constraints, and custom business rules.
### 4. Wizards
Create transient models for user interactions and batch operations.
### 5. Reports
Generate PDF (QWeb) or Excel reports with custom data aggregation.
### 6. Server Actions
Create automated actions, scheduled actions (cron jobs), and workflow automations.
### 7. Buttons and Actions
Add action buttons to forms and tree views.
## Enhancement Workflow
### Step 1: Identify the Enhancement Type
Ask clarifying questions based on the request:
**For Field Additions:**
- Field technical name (snake_case)
- Field type (Char, Integer, Float, Boolean, Selection, Many2one, One2many, Many2many, Date, Datetime, Text, Html, Binary, Monetary)
- Field label and help text
- Required or optional?
- Computed field or stored?
- Should it appear in specific views?
**For View Modifications:**
- Which view(s) to modify? (form, tree, search, kanban)
- Which model?
- Where to add the element? (header, group, notebook page, after specific field)
- Any conditional visibility?
**For Business Logic:**
- Trigger condition (onchange, compute, constraint, button click)
- Dependencies (which fields trigger the logic)
- Expected behavior
**For Wizards:**
- Wizard purpose (batch update, data export, configuration, etc.)
- Input fields needed
- Target models to affect
- Where to trigger (menu, button on form, action)
**For Reports:**
- Report format (PDF or Excel)
- Data to include
- Grouping and aggregation
- Filters needed
### Step 2: Create or Identify Extension Module
Determine if enhancement goes in:
- New extension module (e.g., `stock_picking_serial_tracking`)
- Existing extension module
If creating new module, provide:
- Module name: `[base_module]_[feature]` (e.g., `stock_picking_enhancements`)
- Dependencies: base module being extended
- Target directory: appropriate `addons-*` folder
### Step 3: Implement the Enhancement
Follow the appropriate implementation pattern below.
## Implementation Patterns
### Pattern 1: Adding Fields to Existing Models
Create model inheritance file:
```python
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import logging
_logger = logging.getLogger(__name__)
class StockPickingInherit(models.Model):
"""Extend stock.picking with additional fields."""
_inherit = 'stock.picking'
# Simple stored field
serial_number = fields.Char(
string='Serial Number',
index=True,
tracking=True,
help='Serial number for tracking purposes'
)
# Selection field
priority_level = fields.Selection([
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('urgent', 'Urgent'),
], string='Priority Level', default='medium', required=True)
# Many2one field
responsible_id = fields.Many2one(
'res.users',
string='Responsible Person',
default=lambda self: self.env.user,
tracking=True
)
# Computed field (stored)
total_weight = fields.Float(
string='Total Weight',
compute='_compute_total_weight',
store=True,
digits=(10, 2)
)
# Computed field (non-stored, real-time)
is_urgent = fields.Boolean(
string='Is Urgent',
compute='_compute_is_urgent'
)
# Related field (from related record)
partner_country_id = fields.Many2one(
'res.country',
string='Partner Country',
related='partner_id.country_id',
store=True,
readonly=True
)
@api.depends('move_line_ids', 'move_line_ids.qty_done', 'move_line_ids.product_id.weight')
def _compute_total_weight(self):
"""Compute total weight from move lines."""
for picking in self:
total = sum(
line.qty_done * line.product_id.weight
for line in picking.move_line_ids
if line.product_id.weight
)
picking.total_weight = total
@api.depends('priority_level', 'scheduled_date')
def _compute_is_urgent(self):
"""Determine if picking is urgent."""
from datetime import datetime, timedelta
for picking in self:
is_urgent = picking.priority_level == 'urgent'
if picking.scheduled_date:
due_soon = picking.scheduled_date <= datetime.now() + timedelta(hours=24)
is_urgent = is_urgent or due_soon
picking.is_urgent = is_urgent
@api.onchange('partner_id')
def _onchange_partner_id(self):
"""Auto-fill fields when partner changes."""
if self.partner_id and self.partner_id.user_id:
self.responsible_id = self.partner_id.user_id
@api.constrains('serial_number')
def _check_serial_number(self):
"""Validate serial number format."""
for picking in self:
if picking.serial_number and len(picking.serial_number) < 5:
raise ValidationError('Serial number must be at least 5 characters long!')
```
Create view inheritance to display the new fields:
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Extend stock.picking form view -->
<record id="view_picking_form_inherit" model="ir.ui.view">
<field name="name">stock.picking.form.inherit</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<!-- Add fields in header -->
<xpath expr="//header" position="after">
<div class="alert alert-danger" role="alert" attrs="{'invisible': [('is_urgent', '=', False)]}">
<strong>URGENT:</strong> This picking requires immediate attention!
</div>
</xpath>
<!-- Add field after existing field -->
<xpath expr="//field[@name='partner_id']" position="after">
<field name="responsible_id"/>
<field name="serial_number"/>
</xpath>
<!-- Add field inside existing group -->
<xpath expr="//group[@name='other_info']//field[@name='origin']" position="after">
<field name="priority_level"/>
<field name="total_weight"/>
</xpath>
<!-- Add new notebook page -->
<xpath expr="//notebook" position="inside">
<page string="Tracking Info">
<group>
<field name="serial_number"/>
<field name="is_urgent"/>
<field name="partner_country_id"/>
</group>
</page>
</xpath>
</field>
</record>
<!-- Extend tree view -->
<record id="view_picking_tree_inherit" model="ir.ui.view">
<field name="name">stock.picking.tree.inherit</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="decoration-danger">is_urgent</attribute>
</xpath>
<xpath expr="//field[@name='name']" position="after">
<field name="serial_number"/>
<field name="priority_level"/>
<field name="is_urgent" invisible="1"/>
</xpath>
</field>
</record>
<!-- Extend search view with filters -->
<record id="view_picking_search_inherit" model="ir.ui.view">
<field name="name">stock.picking.search.inherit</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<field name="serial_number"/>
<filter string="Urgent" name="urgent" domain="[('is_urgent', '=', True)]"/>
<filter string="High Priority" name="high_priority" domain="[('priority_level', '=', 'high')]"/>
<group expand="0" string="Group By">
<filter string="Priority Level" name="priority" context="{'group_by': 'priority_level'}"/>
</group>
</xpath>
</field>
</record>
</odoo>
```
### Pattern 2: Creating Wizards
Wizard model (transient):
```python
from odoo import models, fields, api
from odoo.exceptions import UserError
class BulkInvoiceWizard(models.TransientModel):
"""Wizard for bulk invoice generation."""
_name = 'bulk.invoice.wizard'
_description = 'Bulk Invoice Generation Wizard'
partner_id = fields.Many2one(
'res.partner',
string='Partner',
help='Leave empty to process all partners'
)
date_from = fields.Date(
string='Date From',
required=True
)
date_to = fields.Date(
string='Date To',
required=True
)
invoice_date = fields.Date(
string='Invoice Date',
required=True,
default=fields.Date.context_today
)
group_by_partner = fields.Boolean(
string='Group by Partner',
default=True,
help='Create one invoice per partner'
)
@api.constrains('date_from', 'date_to')
def _check_dates(self):
"""Validate date range."""
if self.date_from > self.date_to:
raise UserError('Date From must be before Date To!')
def action_generate_invoices(self):
"""Generate invoices based on wizard parameters."""
self.ensure_one()
# Get records to invoice
domain = [
('date', '>=', self.date_from),
('date', '<=', self.date_to),
('invoice_status', '=', 'to invoice'),
]
if self.partner_id:
domain.append(('partner_id', '=', self.partner_id.id))
orders = self.env['sale.order'].search(domain)
if not orders:
raise UserError('No orders found matching the criteria!')
# Group by partner if requested
if self.group_by_partner:
partners = orders.mapped('partner_id')
invoices = self.env['account.move']
for partner in partners:
partner_orders = orders.filtered(lambda o: o.partner_id == partner)
invoice = partner_orders._create_invoices()
invoices |= invoice
else:
invoices = orders._create_invoices()
# Update invoice dates
invoices.write({'invoice_date': self.invoice_date})
# Return action to view created invoices
return {
'name': 'Generated Invoices',
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'view_mode': 'tree,form',
'domain': [('id', 'in', invoices.ids)],
'context': {'create': False},
}
```
Wizard view:
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_bulk_invoice_wizard_form" model="ir.ui.view">
<field name="name">bulk.invoice.wizard.form</field>
<field name="model">bulk.invoice.wizard</field>
<field name="arch" type="xml">
<form string="Generate Bulk Invoices">
<group>
<group>
<field name="partner_id"/>
<field name="group_by_partner"/>
</group>
<group>
<field name="date_from"/>
<field name="date_to"/>
<field name="invoice_date"/>
</group>
</group>
<footer>
<button string="Generate Invoices" name="action_generate_invoices"
type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Action to open wizard -->
<record id="action_bulk_invoice_wizard" model="ir.actions.act_window">
<field name="name">Generate Bulk Invoices</field>
<field name="res_model">bulk.invoice.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Menu item -->
<menuitem id="menu_bulk_invoice_wizard"
name="Generate Bulk Invoices"
parent="account.menu_finance"
action="action_bulk_invoice_wizard"
sequence="100"/>
</odoo>
```
For more implementation patterns including action buttons, reports (PDF/Excel), and scheduled actions, reference the `references/implementation_patterns.md` file.
## Update Instructions
After implementing enhancements:
1. **Update __manifest__.py** - Add new data files and dependencies
2. **Update security** - Add access rights for new models
3. **Update module** - Run with `-u module_name`
4. **Test** - Verify all functionality works
```bash
# Update module
python3 /Users/jamshid/PycharmProjects/Siafa/src/odoo-bin \
-c /Users/jamshid/PycharmProjects/Siafa/src/odoo.conf \
-d DATABASE_NAME \
-u module_name
# Run tests if available
python3 /Users/jamshid/PycharmProjects/Siafa/src/odoo-bin \
-c /Users/jamshid/PycharmProjects/Siafa/src/odoo.conf \
-d DATABASE_NAME \
--test-enable \
--stop-after-init \
-u module_name
```
## Resources
### references/xpath_patterns.md
Comprehensive collection of XPath expressions for view inheritance - how to add fields before/after elements, replace content, add attributes, etc.
### references/field_types.md
Complete reference of Odoo field types with examples and common attributes for each type.
### references/implementation_patterns.md
Additional implementation patterns for action buttons, PDF reports, Excel reports, and scheduled actions (cron jobs).

View File

@@ -0,0 +1,34 @@
# Reference Documentation for Odoo Feature Enhancer
This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.
Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples
## When Reference Docs Are Useful
Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases
## Structure Suggestions
### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits
### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices

View File

@@ -0,0 +1,81 @@
# Odoo Field Types Reference
## Basic Field Types
### Char - String field
```python
name = fields.Char(string='Name', required=True, size=128, index=True)
```
### Text - Multi-line text
```python
description = fields.Text(string='Description')
```
### Integer
```python
quantity = fields.Integer(string='Quantity', default=1)
```
### Float
```python
price = fields.Float(string='Price', digits=(10, 2))
```
### Boolean
```python
active = fields.Boolean(string='Active', default=True)
```
### Selection
```python
state = fields.Selection([
('draft', 'Draft'),
('done', 'Done'),
], string='Status', default='draft')
```
### Date / Datetime
```python
date = fields.Date(string='Date', default=fields.Date.context_today)
datetime = fields.Datetime(string='DateTime', default=fields.Datetime.now)
```
### Monetary
```python
amount = fields.Monetary(string='Amount', currency_field='currency_id')
currency_id = fields.Many2one('res.currency')
```
## Relational Fields
### Many2one - Foreign key
```python
partner_id = fields.Many2one('res.partner', string='Partner', ondelete='cascade')
```
### One2many - Reverse relationship
```python
line_ids = fields.One2many('model.line', 'parent_id', string='Lines')
```
### Many2many
```python
tag_ids = fields.Many2many('model.tag', string='Tags')
```
### Related Field
```python
partner_email = fields.Char(related='partner_id.email', string='Email', store=True, readonly=True)
```
## Computed Fields
```python
total = fields.Float(compute='_compute_total', store=True)
@api.depends('line_ids.amount')
def _compute_total(self):
for record in self:
record.total = sum(record.line_ids.mapped('amount'))
```

View File

@@ -0,0 +1,64 @@
# Additional Implementation Patterns
## Action Buttons
```python
def action_custom(self):
self.ensure_one()
# Logic here
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'Action completed',
'type': 'success',
}
}
```
## PDF Reports (QWeb)
```python
class CustomReport(models.AbstractModel):
_name = 'report.module.report_name'
@api.model
def _get_report_values(self, docids, data=None):
docs = self.env['model.name'].browse(docids)
return {
'doc_ids': docids,
'docs': docs,
'custom_data': self._prepare_data(docs),
}
```
## Excel Reports
```python
from odoo import models
class ExcelReport(models.AbstractModel):
_name = 'report.module.report_xlsx'
_inherit = 'report.report_xlsx.abstract'
def generate_xlsx_report(self, workbook, data, objects):
sheet = workbook.add_worksheet('Report')
header_format = workbook.add_format({'bold': True})
sheet.write(0, 0, 'Header 1', header_format)
# Add data rows
```
## Scheduled Actions (Cron)
```xml
<record id="ir_cron_task" model="ir.cron">
<field name="name">Task Name</field>
<field name="model_id" ref="model_model_name"/>
<field name="state">code</field>
<field name="code">model._cron_method()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
</record>
```

View File

@@ -0,0 +1,58 @@
# XPath Patterns for Odoo View Inheritance
## Common XPath Positions
- `before` - Insert before the target element
- `after` - Insert after the target element
- `inside` - Insert inside the target element (as last child)
- `replace` - Replace the target element entirely
- `attributes` - Add/modify attributes of the target element
## Examples
### Add Field After Another Field
```xml
<xpath expr="//field[@name='partner_id']" position="after">
<field name="new_field"/>
</xpath>
```
### Add Field Inside Group
```xml
<xpath expr="//group[@name='group_name']" position="inside">
<field name="new_field"/>
</xpath>
```
### Replace Field
```xml
<xpath expr="//field[@name='old_field']" position="replace">
<field name="new_field"/>
</xpath>
```
### Add Attributes
```xml
<xpath expr="//field[@name='field_name']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="required">1</attribute>
</xpath>
```
### Add to Header
```xml
<xpath expr="//header" position="inside">
<button name="action_custom" string="Custom" type="object"/>
</xpath>
```
### Add Notebook Page
```xml
<xpath expr="//notebook" position="inside">
<page string="New Page">
<group>
<field name="field1"/>
</group>
</page>
</xpath>
```

View File

@@ -0,0 +1,342 @@
---
name: odoo-migration-assistant
description: Helps migrate Odoo modules and customizations between versions, specifically focusing on upgrades to/from Odoo 16.0. This skill should be used when the user requests migration help, such as "Migrate this module to Odoo 16" or "Upgrade from version 15 to 16" or "What changed in Odoo 16 for stock valuation?" or "Migration guide for this module".
---
# Odoo Migration Assistant
## Overview
This skill provides guidance for migrating Odoo modules between versions, with specialized knowledge of Odoo 16.0 changes, API differences, and upgrade procedures.
## Migration Scenarios
### 1. Upgrade TO Odoo 16.0
Migrating modules from older versions (14.0, 15.0) to 16.0.
### 2. Upgrade FROM Odoo 16.0
Preparing modules for future Odoo versions.
### 3. Version Compatibility Check
Determining what changes are needed for version compatibility.
## Migration Workflow
### Step 1: Identify Source and Target Versions
Ask for:
- Current Odoo version
- Target Odoo version
- Module name and purpose
- Dependencies
### Step 2: Assess Changes Required
Review:
- API changes between versions
- Deprecated features
- New required fields or methods
- View structure changes
- Dependency updates
### Step 3: Create Migration Plan
Provide step-by-step migration guide.
## Odoo 16.0 Specific Changes
### Key Changes in Odoo 16.0
**1. Python Version**
- Minimum: Python 3.8
- Recommended: Python 3.10+
**2. Manifest Changes**
```python
# Odoo 15 and earlier
{
'version': '15.0.1.0.0',
'depends': ['base', 'stock'],
'license': 'LGPL-3',
}
# Odoo 16.0
{
'version': '16.0.1.0.0', # Updated version
'depends': ['base', 'stock'],
'license': 'LGPL-3',
# New optional keys
'assets': { # Replaces some XML asset declarations
'web.assets_backend': [
'module/static/src/**/*',
],
},
}
```
**3. Stock Valuation Changes**
```python
# Odoo 15
class StockMove(models.Model):
_inherit = 'stock.move'
def _create_account_move_line(self):
# Old method signature
pass
# Odoo 16
class StockMove(models.Model):
_inherit = 'stock.move'
def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost):
# Updated method signature with more parameters
pass
```
**4. Widget Changes**
```xml
<!-- Odoo 15 -->
<field name="amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<!-- Odoo 16 - Same, but some widgets renamed or removed -->
<field name="amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
```
**5. Removed/Deprecated Methods**
- `_update_average_price()` - Replaced with new accounting methods
- Some portal methods reorganized
## Migration Patterns
### Pattern 1: Update Manifest
```python
# Step 1: Update version number
'version': '16.0.1.0.0',
# Step 2: Check dependencies
# Ensure all depends modules are compatible with Odoo 16
# Step 3: Update data files if needed
'data': [
'security/ir.model.access.csv',
'views/views.xml',
# Remove any deprecated files
],
# Step 4: Move assets if needed
'assets': {
'web.assets_backend': [
'module_name/static/src/js/*.js',
],
},
```
### Pattern 2: Update Model Fields
```python
# Check for removed fields in base models
# Example: If inheriting stock.move, check release notes
class StockMove(models.Model):
_inherit = 'stock.move'
# Update field definitions if base definition changed
custom_field = fields.Char(...)
# Update method signatures to match new base methods
def _action_done(self, cancel_backorder=False):
# Match new signature
return super()._action_done(cancel_backorder=cancel_backorder)
```
### Pattern 3: Update Views
```xml
<!-- Check for removed/renamed view references -->
<record id="view_form" model="ir.ui.view">
<field name="inherit_id" ref="stock.view_move_form"/>
<!-- Update XPath if base view structure changed -->
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="custom_field"/>
</xpath>
</field>
</record>
```
### Pattern 4: Create Migration Script
```python
# migrations/16.0.1.0.0/pre-migrate.py
def migrate(cr, version):
"""Pre-migration script for 16.0.1.0.0"""
# Update data before module upgrade
cr.execute("""
UPDATE model_table
SET new_field = old_field
WHERE new_field IS NULL
""")
# migrations/16.0.1.0.0/post-migrate.py
def migrate(cr, version):
"""Post-migration script for 16.0.1.0.0"""
from odoo import api, SUPERUSER_ID
env = api.Environment(cr, SUPERUSER_ID, {})
# Recompute fields
records = env['model.name'].search([])
records._compute_field_name()
# Clean up old data
old_records = env['old.model'].search([])
old_records.unlink()
```
### Pattern 5: Update Tests
```python
# Update test imports if needed
from odoo.tests import TransactionCase # Unchanged
class TestModule(TransactionCase):
def setUp(self):
super().setUp()
# Update test data for new field requirements
def test_feature(self):
# Update assertions for new behavior
record = self.env['model.name'].create({
'name': 'Test',
# Add new required fields for Odoo 16
})
self.assertTrue(record)
```
## Version-Specific Changes
### Migrating FROM 15.0 TO 16.0
**Major Changes:**
1. Stock accounting methods updated
2. Some JavaScript widgets updated
3. Python 3.10 support added
4. Minor ORM improvements
**Steps:**
1. Update `__manifest__.py` version to `16.0.x.x.x`
2. Test on Odoo 16 test database
3. Check deprecation warnings
4. Update any changed method signatures
5. Test all functionality
6. Create migration scripts if data changes needed
### Migrating FROM 14.0 TO 16.0
**Major Changes:**
- All changes from 14→15 plus 15→16
- Significant OWL (JavaScript framework) changes
- Python 2 completely removed
- Many deprecated features removed
**Steps:**
1. Consider migrating 14→15→16 (two-step migration)
2. Review all custom JavaScript (major changes)
3. Update all deprecated API calls
4. Extensive testing required
## Migration Checklist
- [ ] Update manifest version
- [ ] Check all dependencies compatible with target version
- [ ] Review Odoo release notes for target version
- [ ] Update deprecated method calls
- [ ] Test views render correctly
- [ ] Update method signatures if base methods changed
- [ ] Create migration scripts (pre/post)
- [ ] Update tests
- [ ] Test on copy of production database
- [ ] Check for deprecation warnings in logs
- [ ] Update documentation
- [ ] Test all user workflows
- [ ] Performance test (especially for large datasets)
- [ ] Backup production before upgrade
## Migration Commands
```bash
# Create migration script directory
mkdir -p module_name/migrations/16.0.1.0.0
# Test migration on copy of database
pg_dump production_db > backup.sql
createdb test_migration_db
psql test_migration_db < backup.sql
# Run Odoo with migration
python3 src/odoo-bin -c src/odoo.conf \
-d test_migration_db \
-u module_name \
--stop-after-init
# Check logs for errors
tail -f /var/log/odoo/odoo.log | grep ERROR
```
## Common Migration Issues
### Issue 1: Missing Field Error
```
Error: Field 'xyz' does not exist
```
**Solution:** Add field to model or remove from views
### Issue 2: Method Signature Changed
```
TypeError: method() takes X positional arguments but Y were given
```
**Solution:** Update method call to match new signature
### Issue 3: View Inheritance Broken
```
Error: View inheritance may not use attribute: ...
```
**Solution:** Update XPath or view structure
### Issue 4: Dependencies Not Found
```
Error: Module 'xyz' not found
```
**Solution:** Update dependency version or find replacement
## Testing After Migration
```python
# Run all tests
python3 src/odoo-bin -c src/odoo.conf \
-d DATABASE_NAME \
--test-enable \
--stop-after-init \
-u module_name
# Check specific functionality
python3 src/odoo-bin shell -c src/odoo.conf -d DATABASE_NAME
>>> env['model.name'].search([]).read()
>>> # Test key functionality manually
```
## Resources
### references/odoo16_changes.md
Comprehensive list of changes introduced in Odoo 16.0 affecting common modules and customizations.
### references/api_changes.md
Detailed API changes by module (stock, account, sale, etc.) between Odoo versions.
### scripts/migration_template.py
Template for creating migration scripts with common patterns and examples.

View File

@@ -0,0 +1 @@
API changes reference

View File

@@ -0,0 +1 @@
Odoo 16 changes reference

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""
Example helper script for odoo-migration-assistant
This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.
Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""
def main():
print("This is an example script for odoo-migration-assistant")
# TODO: Add actual script logic here
# This could be data processing, file conversion, API calls, etc.
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
Migration script template

View File

@@ -0,0 +1,514 @@
---
name: odoo-module-creator
description: Creates complete Odoo 16.0 modules with proper structure, manifests, models, views, and security. This skill should be used when the user requests creation of a new Odoo module, such as "Create a new module for inventory tracking" or "I need a new POS customization module" or "Generate module structure for vendor management".
---
# Odoo Module Creator
## Overview
This skill enables creation of complete, production-ready Odoo 16.0 Enterprise modules with proper directory structure, manifest files, models, views, security configurations, and documentation. It follows OCA guidelines and Siafa project standards.
## Module Creation Workflow
### Step 1: Gather Module Requirements
Ask clarifying questions to collect essential information:
1. **Module technical name** (snake_case format, e.g., `stock_batch_tracking`, `pos_custom_receipt`)
2. **Module display name** (human-readable, e.g., "Stock Batch Tracking", "POS Custom Receipt")
3. **Module purpose** (1-2 sentence description of functionality)
4. **Module category** (select from: Sales, Inventory, Accounting, Point of Sale, Human Resources, Manufacturing, Purchases, Warehouse, Website, etc.)
5. **Dependencies** (base modules required, e.g., `stock`, `account`, `point_of_sale`)
6. **Module type** (see Module Types section below)
7. **Target addon directory** (e.g., `addons-stock`, `addons-pos`, `addons-account`)
### Step 2: Determine Module Type
Identify which type of module to create based on the purpose:
**A. Simple Model Module** - CRUD operations for a new business entity
- Creates new models with fields and views
- Example: Customer feedback tracking, equipment registry
**B. Extension Module** - Extends existing Odoo models
- Inherits and adds fields/methods to existing models
- Example: Add serial number tracking to stock.picking
**C. POS Customization** - Point of Sale enhancements
- Extends POS models, screens, or receipts
- Example: Custom receipt format, loyalty points integration
**D. Stock/Inventory Enhancement** - Warehouse and inventory features
- Stock valuation, warehouse operations, batch tracking
- Example: Inter-warehouse transit, GRN-invoice linking
**E. Accounting Customization** - Financial module extensions
- Account moves, vendor bills, analytic accounting
- Example: Multi-dimensional analytics, custom invoicing
**F. Report Module** - Custom reports (PDF, Excel)
- QWeb templates, data aggregation, export functionality
- Example: Sales analysis, inventory valuation reports
**G. Integration Module** - External API/service connectors
- REST API clients, webhooks, data synchronization
- Example: Beatroute connector, payment gateway integration
**H. Widget/UI Customization** - Frontend enhancements
- JavaScript widgets, custom views, web controllers
- Example: Kanban view customizations, dashboard widgets
### Step 3: Generate Module Structure
Create the complete directory structure with all required files:
```
module_name/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── [model_files].py
├── views/
│ ├── [model]_views.xml
│ └── menu_views.xml
├── security/
│ ├── security_groups.xml (if needed)
│ └── ir.model.access.csv
├── data/ (optional)
│ └── data.xml
├── wizards/ (if needed)
│ ├── __init__.py
│ └── [wizard_name].py
├── report/ (if reports needed)
│ ├── __init__.py
│ ├── [report_name].py
│ └── templates/
│ └── [report_template].xml
├── static/
│ ├── description/
│ │ ├── icon.png
│ │ └── index.html
│ └── src/ (for JS/CSS if needed)
│ ├── js/
│ └── css/
└── tests/ (recommended)
├── __init__.py
└── test_[module].py
```
### Step 4: Generate __manifest__.py
Create manifest with standard metadata:
```python
{
'name': '[Module Display Name]',
'version': '16.0.1.0.0',
'category': '[Category]',
'summary': '[Brief one-line description]',
'description': """
[Detailed multi-line description of module functionality]
Key Features:
- Feature 1
- Feature 2
- Feature 3
""",
'author': 'Jamshid K',
'website': 'https://siafadates.com',
'license': 'LGPL-3',
'depends': [
'base',
# Additional dependencies
],
'data': [
'security/security_groups.xml', # Load first
'security/ir.model.access.csv',
'views/[model]_views.xml',
'views/menu_views.xml',
'data/data.xml', # If needed
'report/templates/[report].xml', # If needed
],
'assets': { # If JS/CSS needed
'web.assets_backend': [
'module_name/static/src/js/*.js',
'module_name/static/src/css/*.css',
],
},
'demo': [], # Demo data if applicable
'installable': True,
'auto_install': False,
'application': False, # True for standalone apps
}
```
### Step 5: Generate Model Files
Create model files following Odoo ORM best practices:
```python
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
import logging
_logger = logging.getLogger(__name__)
class ModelName(models.Model):
"""Description of the model."""
_name = 'module.model'
_description = 'Model Description'
_inherit = ['mail.thread', 'mail.activity.mixin'] # If needed
_order = 'create_date desc'
# Fields
name = fields.Char(
string='Name',
required=True,
index=True,
tracking=True,
help='Primary identifier for this record'
)
active = fields.Boolean(
string='Active',
default=True,
help='If unchecked, this record will be hidden'
)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
('cancel', 'Cancelled'),
], string='Status', default='draft', required=True, tracking=True)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company
)
# Relational fields
partner_id = fields.Many2one('res.partner', string='Partner')
line_ids = fields.One2many('module.model.line', 'parent_id', string='Lines')
# Computed fields
total_amount = fields.Float(
string='Total Amount',
compute='_compute_total_amount',
store=True
)
# Constraints
_sql_constraints = [
('name_unique', 'UNIQUE(name, company_id)', 'Name must be unique per company!'),
]
@api.depends('line_ids', 'line_ids.amount')
def _compute_total_amount(self):
"""Compute total amount from lines."""
for record in self:
record.total_amount = sum(record.line_ids.mapped('amount'))
@api.onchange('partner_id')
def _onchange_partner_id(self):
"""Update fields when partner changes."""
if self.partner_id:
# Logic here
pass
@api.constrains('total_amount')
def _check_total_amount(self):
"""Validate total amount is positive."""
for record in self:
if record.total_amount < 0:
raise ValidationError('Total amount must be positive!')
def action_confirm(self):
"""Confirm the record."""
self.ensure_one()
if self.state != 'draft':
raise UserError('Only draft records can be confirmed!')
self.write({'state': 'confirmed'})
_logger.info('Record %s confirmed by user %s', self.name, self.env.user.name)
```
### Step 6: Generate View Files
Create XML view definitions:
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_model_tree" model="ir.ui.view">
<field name="name">module.model.tree</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<tree string="Model Name">
<field name="name"/>
<field name="partner_id"/>
<field name="state" decoration-info="state == 'draft'"
decoration-success="state == 'done'"/>
<field name="total_amount" sum="Total"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_model_form" model="ir.ui.view">
<field name="name">module.model.form</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<form string="Model Name">
<header>
<button name="action_confirm" string="Confirm" type="object"
class="oe_highlight" states="draft"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,confirmed,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Name..."/>
</h1>
</div>
<group>
<group>
<field name="partner_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="total_amount"/>
<field name="active"/>
</group>
</group>
<notebook>
<page string="Lines">
<field name="line_ids">
<tree editable="bottom">
<field name="name"/>
<field name="amount"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_model_search" model="ir.ui.view">
<field name="name">module.model.search</field>
<field name="model">module.model</field>
<field name="arch" type="xml">
<search string="Search Model">
<field name="name"/>
<field name="partner_id"/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Done" name="done" domain="[('state', '=', 'done')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Partner" name="partner" context="{'group_by': 'partner_id'}"/>
<filter string="Status" name="state" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_model" model="ir.actions.act_window">
<field name="name">Model Name</field>
<field name="res_model">module.model</field>
<field name="view_mode">tree,form</field>
<field name="context">{}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first record!
</p>
<p>
Click the create button to add a new record.
</p>
</field>
</record>
</odoo>
```
### Step 7: Generate Security Files
Create security groups (if needed):
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="module_category" model="ir.module.category">
<field name="name">Module Category</field>
<field name="sequence">100</field>
</record>
<record id="group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_category"/>
<field name="implied_ids" eval="[(4, ref('group_user'))]"/>
</record>
</odoo>
```
Create access rights CSV:
```csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_model_user,module.model.user,model_module_model,group_user,1,1,1,0
access_model_manager,module.model.manager,model_module_model,group_manager,1,1,1,1
```
### Step 8: Generate Tests (Recommended)
**Use the `odoo-test-creator` skill** to create comprehensive test suites for the module. The odoo-test-creator skill provides:
- Test templates for different module types (basic models, constraints, inheritance)
- Best practices specific to Siafa project standards
- Solutions to common testing pitfalls (database constraints, HTML fields, permissions)
- Proper import patterns and setUp methods
To create tests with the odoo-test-creator skill, simply invoke:
```
Use the odoo-test-creator skill to create tests for [module_name]
```
The skill will:
1. Analyze the module structure to determine what needs testing
2. Select appropriate test templates based on module type
3. Generate comprehensive test methods for CRUD operations, constraints, computed fields, and business logic
4. Handle database constraints properly (using existing records vs. creating with .sudo())
5. Apply Siafa-specific patterns and best practices
**Quick Test Example** (if not using the skill):
```python
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
class TestModel(TransactionCase):
"""Test cases for module.model"""
def setUp(self):
"""Set up test data"""
super().setUp()
self.Model = self.env['module.model']
# Use existing records when possible
self.partner = self.env['res.partner'].search([], limit=1)
if not self.partner:
self.skipTest("No partner available for testing")
def test_01_create_model(self):
"""Test creating a model record"""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
self.assertTrue(record)
self.assertEqual(record.state, 'draft')
def test_02_constraint_validation(self):
"""Test constraint validation"""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
with self.assertRaises(UserError) as context:
record.write({'invalid_field': 'invalid_value'})
self.assertIn('expected error', str(context.exception))
```
**Important:** For production modules, always use the `odoo-test-creator` skill to ensure comprehensive test coverage and proper handling of Siafa-specific constraints.
## Code Standards and Best Practices
Follow these standards when generating module code:
1. **Naming Conventions**
- Module name: `snake_case` (e.g., `stock_batch_tracking`)
- Model name: `module.model` (e.g., `stock.batch.tracking`)
- Fields: `snake_case` (e.g., `batch_number`, `expiry_date`)
- Methods: `snake_case` with verb prefix (e.g., `action_confirm`, `_compute_total`)
- XML IDs: `view_model_type` (e.g., `view_batch_tracking_form`)
2. **Import Order**
```python
# Standard library
import logging
from datetime import datetime
# Odoo imports
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero
```
3. **Field Attributes**
- Always provide `string` parameter
- Add `help` text for complex fields
- Use `tracking=True` for important fields
- Set `index=True` for searchable fields
- Include `company_id` for multi-company support
4. **Method Decorators**
- Use `@api.depends()` for computed fields
- Use `@api.onchange()` for onchange methods
- Use `@api.constrains()` for validation
- Use `@api.model` for class-level methods
5. **Error Handling**
- Use `UserError` for user-facing errors
- Use `ValidationError` for constraint violations
- Always log important actions with `_logger`
6. **Security**
- Always create access rights CSV
- Use security groups for sensitive operations
- Add record rules if row-level security needed
- Test with different user permissions
## Module Type Templates
Reference the `assets/templates/` directory for complete templates by module type:
- `simple_model/` - Basic CRUD module
- `extension/` - Inheriting existing models
- `pos_custom/` - POS customizations
- `stock_enhancement/` - Inventory features
- `report_module/` - Custom reports
## Resources
### assets/templates/
Contains complete module templates for different module types. Use these as starting points and customize based on specific requirements.
### assets/icon.png
Default module icon. Replace with custom icon if needed (PNG, 128x128px recommended).
### assets/index.html
Module description HTML template for the Apps menu.

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Module Name</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
line-height: 1.6;
color: #333;
}
h1 {
color: #714B67;
border-bottom: 3px solid #714B67;
padding-bottom: 10px;
}
h2 {
color: #875A7B;
margin-top: 30px;
}
.section {
margin: 20px 0;
}
ul {
list-style-type: none;
padding-left: 0;
}
ul li:before {
content: "✓ ";
color: #00A09D;
font-weight: bold;
margin-right: 10px;
}
.author {
background-color: #f5f5f5;
padding: 15px;
border-left: 4px solid #714B67;
margin-top: 30px;
}
</style>
</head>
<body>
<h1>Module Name</h1>
<div class="section">
<p>Brief description of what this module does and its purpose in the Odoo ecosystem.</p>
</div>
<h2>Key Features</h2>
<div class="section">
<ul>
<li>Feature 1: Description of first major feature</li>
<li>Feature 2: Description of second major feature</li>
<li>Feature 3: Description of third major feature</li>
<li>Feature 4: Additional capabilities and enhancements</li>
</ul>
</div>
<h2>Use Cases</h2>
<div class="section">
<ul>
<li>Scenario 1: When to use this feature</li>
<li>Scenario 2: How it solves business problems</li>
<li>Scenario 3: Integration with existing workflows</li>
</ul>
</div>
<h2>Technical Details</h2>
<div class="section">
<p><strong>Version:</strong> 16.0.1.0.0</p>
<p><strong>Dependencies:</strong> base, [other modules]</p>
<p><strong>Models:</strong> Brief description of main models introduced</p>
</div>
<div class="author">
<strong>Author:</strong> Jamshid K<br>
<strong>Website:</strong> <a href="https://siafadates.com">https://siafadates.com</a><br>
<strong>License:</strong> LGPL-3
</div>
</body>
</html>

View File

@@ -0,0 +1,471 @@
---
name: odoo-test-creator
description: Creates comprehensive test suites for Odoo 16.0 modules following Siafa project standards. This skill should be used when creating tests for Odoo modules, such as "Create tests for this module" or "Generate test cases for stock_location_usage_restriction" or "Add unit tests to validate this functionality". The skill provides test templates, patterns, and best practices specific to Odoo 16.0 Enterprise with knowledge of database constraints and common pitfalls in the Siafa codebase.
---
# Odoo Test Creator
## Overview
Create production-ready test suites for Odoo 16.0 Enterprise modules that follow Siafa project standards, handle database constraints properly, and provide comprehensive test coverage.
## When to Use This Skill
Use this skill when:
- Creating tests for new Odoo modules
- Adding test coverage to existing modules
- Validating model logic, constraints, and workflows
- Testing inherited/extended Odoo models
- Ensuring compliance with Siafa testing standards
## Test Creation Workflow
### Step 1: Analyze Module Structure
Examine the module to understand what needs testing:
1. **Identify Components to Test:**
- Models (new models or inherited models)
- Computed fields and @api.depends
- Constraints (@api.constrains and _sql_constraints)
- Onchange methods (@api.onchange)
- Business logic methods
- State transitions and workflows
- Wizards and transient models
- Reports (if applicable)
2. **Review Module Dependencies:**
- Check `__manifest__.py` for dependencies
- Identify which models from dependencies will be used
- Plan to use existing records when possible
3. **Check for Special Requirements:**
- Database constraints (NOT NULL, UNIQUE)
- Multi-company considerations
- Access rights and permissions
- Integration points with other modules
### Step 2: Set Up Test File Structure
Create the test file following Siafa standards:
```python
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
class TestModuleName(TransactionCase):
"""Test cases for module_name functionality."""
def setUp(self):
"""Set up test data."""
super().setUp()
# Initialize model references
self.Model = self.env['model.name']
# Set up test data (Step 3)
```
**Critical Import Pattern:**
- ✅ Use `from odoo.tests.common import TransactionCase`
- ❌ NOT `from odoo.tests import TransactionCase`
### Step 3: Set Up Test Data
Use the appropriate pattern based on database constraints:
#### Pattern A: Use Existing Records (Preferred)
Avoid database constraint issues by using existing records:
```python
def setUp(self):
super().setUp()
self.Model = self.env['model.name']
# Use existing records from database
self.warehouse = self.env['stock.warehouse'].search([], limit=1)
if not self.warehouse:
self.skipTest("No warehouse available for testing")
self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1)
if not self.product:
self.skipTest("No storable product available for testing")
self.partner = self.env['res.partner'].search([], limit=1)
if not self.partner:
self.skipTest("No partner available for testing")
```
**When to use:** For models with complex database constraints (products, partners, companies).
#### Pattern B: Create with .sudo() (When Necessary)
Create new records when specific test data is required:
```python
def setUp(self):
super().setUp()
self.Model = self.env['model.name']
# Create test data with .sudo() to bypass permissions
self.vendor = self.env['res.partner'].sudo().create({
'name': 'Test Vendor',
'is_company': True,
'supplier_rank': 1,
})
self.product = self.env['product.product'].sudo().create({
'name': 'Test Product',
'type': 'product',
'purchase_method': 'receive',
'list_price': 100.0,
'standard_price': 80.0,
})
```
**When to use:** When specific field values are required for tests or existing records may not have the right attributes.
#### Pattern C: Class-Level Setup (For Shared Data)
Use `setUpClass` for data shared across all test methods:
```python
@classmethod
def setUpClass(cls):
"""Set up test data shared across all test methods."""
super().setUpClass()
cls.vendor = cls.env['res.partner'].sudo().create({
'name': 'Test Vendor',
'is_company': True,
})
```
**When to use:** For immutable test data that doesn't change between tests (saves database operations).
### Step 4: Write Test Methods
Create test methods following these guidelines:
#### Test Naming Convention
```python
def test_01_descriptive_name(self):
"""Test description in docstring."""
pass
def test_02_another_scenario(self):
"""Test another scenario."""
pass
```
**Numbering:** Use `01`, `02`, etc. to control execution order.
#### Test Coverage Areas
Create tests for each component identified in Step 1:
**A. CRUD Operations**
```python
def test_01_create_record(self):
"""Test creating a new record with valid data."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
self.assertTrue(record)
self.assertEqual(record.name, 'Test Record')
self.assertEqual(record.state, 'draft')
def test_02_update_record(self):
"""Test updating an existing record."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
record.write({'name': 'Updated Record'})
self.assertEqual(record.name, 'Updated Record')
```
**B. Computed Fields**
```python
def test_03_computed_field(self):
"""Test computed field calculation."""
record = self.Model.create({
'name': 'Test Record',
'quantity': 10,
'unit_price': 5.0,
})
self.assertEqual(record.total_amount, 50.0)
# Test recomputation on dependency change
record.write({'quantity': 20})
self.assertEqual(record.total_amount, 100.0)
```
**C. Constraints**
```python
def test_04_constraint_validation(self):
"""Test constraint prevents invalid data."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
with self.assertRaises(ValidationError) as context:
record.write({'amount': -10.0})
self.assertIn('must be positive', str(context.exception).lower())
```
**D. Onchange Methods**
```python
def test_05_onchange_method(self):
"""Test onchange method updates dependent fields."""
record = self.Model.new({
'name': 'Test Record',
})
record.partner_id = self.partner
record._onchange_partner_id()
# Verify onchange updated related fields
# self.assertEqual(record.expected_field, expected_value)
```
**E. State Transitions**
```python
def test_06_state_transition(self):
"""Test state transition workflow."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
self.assertEqual(record.state, 'draft')
record.action_confirm()
self.assertEqual(record.state, 'confirmed')
# Test invalid transition
with self.assertRaises(UserError) as context:
record.action_confirm() # Already confirmed
self.assertIn('Cannot confirm', str(context.exception))
```
**F. Inheritance/Extension Tests**
For modules that inherit existing models:
```python
def test_07_inherited_method_override(self):
"""Test overridden method applies custom logic."""
location = self.Location.create({
'name': 'Test Location',
'usage': 'internal',
'location_id': self.parent_location.id,
})
# Create stock move using this location
self.StockMove.create({
'name': 'Test Move',
'product_id': self.product.id,
'product_uom_qty': 10,
'product_uom': self.product.uom_id.id,
'location_id': location.id,
'location_dest_id': self.parent_location.id,
})
# Test that custom validation prevents usage change
with self.assertRaises(UserError) as context:
location.write({'usage': 'inventory'})
self.assertIn('Cannot change the usage type', str(context.exception))
```
### Step 5: Handle Common Pitfalls
Apply fixes for known issues in the Siafa codebase:
#### Pitfall 1: Database Constraints
**Problem:** Creating products fails with "null value in column 'sale_line_warn' violates not-null constraint"
**Solution:** Use existing products:
```python
self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1)
```
#### Pitfall 2: HTML Field Comparisons
**Problem:** HTML fields return `Markup` objects: `Markup('<p>Text</p>') != 'Text'`
**Solution:** Use non-HTML fields or convert to string:
```python
# Instead of comment field
self.assertEqual(record.barcode, 'TEST001')
# Or convert to string
self.assertIn('expected text', str(record.html_field))
```
#### Pitfall 3: Permission Errors
**Problem:** Tests fail with access rights errors.
**Solution:** Use `.sudo()` when creating test data:
```python
self.partner = self.env['res.partner'].sudo().create({...})
```
#### Pitfall 4: Incorrect Super() Call
**Problem:** Using old-style `super(ClassName, self).setUp()`
**Solution:** Use modern syntax:
```python
super().setUp() # ✅ Correct
```
### Step 6: Run and Validate Tests
Execute tests and verify results:
```bash
# Run tests during module update
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
-u MODULE_NAME
# Run with verbose output
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
--log-level=test \
-u MODULE_NAME
```
**Expected Output:**
```
INFO MODULE_NAME: 0 failed, 0 error(s) of N tests when loading database 'DATABASE_NAME'
```
**If tests fail:**
1. Read the full traceback carefully
2. Check for database constraint violations
3. Verify test data setup is correct
4. Ensure imports are correct
5. Review field types (especially HTML fields)
### Step 7: Document Tests
Add comprehensive docstrings to each test method:
```python
def test_prevent_usage_change_with_moves(self):
"""
Test that location usage cannot be changed when moves exist.
This test verifies that the module prevents changing a location's
usage type after it has been used in stock movements, protecting
data integrity.
"""
# Test implementation
```
## Resources
### references/test_patterns.md
Comprehensive documentation of:
- Test infrastructure patterns
- Common setup patterns for different scenarios
- Database constraint handling strategies
- Test organization best practices
- Assertion patterns
- Complete list of common pitfalls and solutions
- Running tests with various options
Load this reference when:
- Creating complex test scenarios
- Handling database constraints
- Troubleshooting test failures
- Learning Siafa-specific testing patterns
### assets/test_model_basic.py
Template for testing basic model operations:
- CRUD operations (Create, Read, Update, Delete)
- Computed field testing
- Onchange method testing
- Constraint validation
- State transitions
- Search operations
Use as starting point for new model tests.
### assets/test_model_constraints.py
Template for testing constraints:
- Python constraints (@api.constrains)
- SQL constraints (_sql_constraints)
- Required field validation
- Domain constraints
- Dependent field constraints
- Conditional constraints
- Cascading constraints
Use when module has complex validation logic.
### assets/test_model_inheritance.py
Template for testing model inheritance and extensions:
- New field validation
- Overridden method testing
- Super() call behavior
- Added constraints
- Computed field extensions
- Onchange extensions
- Backward compatibility
Use when module extends existing Odoo models.
## Best Practices
1. **Always use existing records when possible** to avoid database constraints
2. **Test both success and failure cases** for comprehensive coverage
3. **Verify error messages** when testing exceptions
4. **Use .sudo() for test data creation** to bypass permission issues
5. **Add descriptive docstrings** to every test method
6. **Number test methods** for predictable execution order
7. **Keep tests isolated** - each test should work independently
8. **Test edge cases** - empty data, maximum values, invalid combinations
9. **Follow naming conventions** - clear, descriptive test names
10. **Run tests frequently** during development to catch issues early
## Example: Complete Test File
For reference, see `/Users/jamshid/PycharmProjects/Siafa/odoo16e_simc/addons-stock/stock_location_usage_restriction/tests/test_stock_location_usage_restriction.py`
This file demonstrates:
- Proper imports (`from odoo.tests.common import TransactionCase`)
- Using existing records (`self.product = self.Product.search(...)`)
- Comprehensive test coverage (7 test methods)
- Exception testing with message validation
- Proper super() call (`super().setUp()`)
- Avoiding HTML field comparison issues

View File

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
class TestModelName(TransactionCase):
"""Test cases for model.name functionality."""
def setUp(self):
"""Set up test data."""
super().setUp()
self.Model = self.env['model.name']
# Use existing records when possible
self.partner = self.env['res.partner'].search([], limit=1)
if not self.partner:
self.skipTest("No partner available for testing")
# Or create with .sudo()
self.test_partner = self.env['res.partner'].sudo().create({
'name': 'Test Partner',
'is_company': True,
})
def test_01_create_record(self):
"""Test creating a new record with valid data."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
# Add other required fields
})
self.assertTrue(record)
self.assertEqual(record.name, 'Test Record')
self.assertEqual(record.state, 'draft') # Adjust as needed
def test_02_update_record(self):
"""Test updating an existing record."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
# Update record
record.write({
'name': 'Updated Record',
})
self.assertEqual(record.name, 'Updated Record')
def test_03_search_records(self):
"""Test searching for records."""
# Create test records
self.Model.create({
'name': 'Record A',
'partner_id': self.partner.id,
})
self.Model.create({
'name': 'Record B',
'partner_id': self.partner.id,
})
# Search for records
records = self.Model.search([('partner_id', '=', self.partner.id)])
self.assertGreaterEqual(len(records), 2)
def test_04_computed_field(self):
"""Test computed field calculation."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
'quantity': 10,
'unit_price': 5.0,
})
# Test computed total
self.assertEqual(record.total_amount, 50.0)
# Update dependency and verify recomputation
record.write({'quantity': 20})
self.assertEqual(record.total_amount, 100.0)
def test_05_onchange_method(self):
"""Test onchange method behavior."""
record = self.Model.new({
'name': 'Test Record',
})
# Trigger onchange
record.partner_id = self.partner
record._onchange_partner_id()
# Verify onchange updated fields
# self.assertEqual(record.some_field, expected_value)
def test_06_constraint_validation(self):
"""Test constraint validation."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
# Test that invalid value raises ValidationError
with self.assertRaises(ValidationError) as context:
record.write({'invalid_field': 'invalid_value'})
# Verify error message
self.assertIn('expected error message', str(context.exception))
def test_07_state_transition(self):
"""Test state transition workflow."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
# Initially in draft state
self.assertEqual(record.state, 'draft')
# Confirm record
record.action_confirm()
self.assertEqual(record.state, 'confirmed')
# Test invalid transition
with self.assertRaises(UserError) as context:
record.action_confirm() # Already confirmed
self.assertIn('Cannot confirm', str(context.exception))
def test_08_delete_record(self):
"""Test deleting a record (if applicable)."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
})
record_id = record.id
# Delete record
record.unlink()
# Verify record no longer exists
self.assertFalse(self.Model.browse(record_id).exists())

View File

@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
from psycopg2 import IntegrityError
class TestModelConstraints(TransactionCase):
"""Test cases for model constraints and validation."""
def setUp(self):
"""Set up test data."""
super().setUp()
self.Model = self.env['model.name']
# Set up minimal test data
self.partner = self.env['res.partner'].search([], limit=1)
if not self.partner:
self.skipTest("No partner available for testing")
def test_01_python_constraint_positive_value(self):
"""Test Python constraint for positive values."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
'amount': 100.0,
})
# Test valid positive value
record.write({'amount': 50.0})
self.assertEqual(record.amount, 50.0)
# Test that negative value raises ValidationError
with self.assertRaises(ValidationError) as context:
record.write({'amount': -10.0})
self.assertIn('must be positive', str(context.exception).lower())
def test_02_sql_constraint_unique(self):
"""Test SQL constraint for unique values."""
# Create first record
self.Model.create({
'name': 'Test Record',
'code': 'UNIQUE001',
'partner_id': self.partner.id,
})
# Try to create duplicate
with self.assertRaises(IntegrityError):
with self.cr.savepoint():
self.Model.create({
'name': 'Test Record 2',
'code': 'UNIQUE001', # Duplicate code
'partner_id': self.partner.id,
})
def test_03_required_field_validation(self):
"""Test that required fields are enforced."""
# Test missing required field raises ValidationError
with self.assertRaises(ValidationError):
self.Model.create({
'name': 'Test Record',
# Missing required 'partner_id'
})
def test_04_field_domain_constraint(self):
"""Test field domain constraints."""
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
'state': 'draft',
})
# Test valid state
record.write({'state': 'confirmed'})
self.assertEqual(record.state, 'confirmed')
# Test invalid state raises ValidationError
with self.assertRaises(ValidationError):
record.write({'state': 'invalid_state'})
def test_05_dependent_field_constraint(self):
"""Test constraints that depend on multiple fields."""
# Test that start_date must be before end_date
with self.assertRaises(ValidationError) as context:
self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
'start_date': '2024-12-31',
'end_date': '2024-01-01', # End before start
})
self.assertIn('end date', str(context.exception).lower())
self.assertIn('start date', str(context.exception).lower())
def test_06_conditional_constraint(self):
"""Test constraints that apply conditionally."""
# Create record in state where constraint doesn't apply
record = self.Model.create({
'name': 'Test Record',
'partner_id': self.partner.id,
'state': 'draft',
'approval_required': False,
})
# Confirm - now constraint should apply
record.write({'state': 'confirmed', 'approval_required': True})
# Test that missing approval raises error
with self.assertRaises(ValidationError) as context:
record.write({'approved_by': False}) # Clear approval
self.assertIn('approval', str(context.exception).lower())
def test_07_cascading_constraint(self):
"""Test constraints that cascade to related records."""
parent = self.Model.create({
'name': 'Parent Record',
'partner_id': self.partner.id,
})
child = self.Model.create({
'name': 'Child Record',
'parent_id': parent.id,
'partner_id': self.partner.id,
})
# Test that deleting parent with children raises error
with self.assertRaises(ValidationError) as context:
parent.unlink()
self.assertIn('child', str(context.exception).lower())
def test_08_constraint_bypass_with_context(self):
"""Test bypassing constraints with context (if applicable)."""
# Some constraints can be bypassed with special context
record = self.Model.with_context(skip_validation=True).create({
'name': 'Test Record',
'partner_id': self.partner.id,
'amount': -100.0, # Normally not allowed
})
self.assertEqual(record.amount, -100.0)

View File

@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
class TestModelInheritance(TransactionCase):
"""Test cases for model inheritance and extensions."""
def setUp(self):
"""Set up test data."""
super().setUp()
self.Model = self.env['base.model.name'] # The model being extended
# Set up test data
self.partner = self.env['res.partner'].search([], limit=1)
if not self.partner:
self.skipTest("No partner available for testing")
def test_01_new_fields_exist(self):
"""Test that new fields added by inheritance exist."""
record = self.Model.create({
'name': 'Test Record',
# Base fields
})
# Test that new fields exist and have default values
self.assertTrue(hasattr(record, 'new_field'))
self.assertEqual(record.new_field, False) # Or expected default
def test_02_inherited_method_override(self):
"""Test that overridden methods work correctly."""
record = self.Model.create({
'name': 'Test Record',
})
# Call overridden method
result = record.action_confirm()
# Verify custom behavior was applied
# self.assertEqual(record.state, 'custom_state')
self.assertTrue(result)
def test_03_super_call_behavior(self):
"""Test that super() calls preserve base functionality."""
record = self.Model.create({
'name': 'Test Record',
})
initial_state = record.state
# Call overridden method that should call super()
record.write({'name': 'Updated Record'})
# Verify both base and custom behavior applied
self.assertEqual(record.name, 'Updated Record') # Base behavior
# self.assertEqual(record.custom_field, 'value') # Custom behavior
def test_04_added_constraint(self):
"""Test new constraints added by inheritance."""
record = self.Model.create({
'name': 'Test Record',
'new_field': 'valid_value',
})
# Test new constraint
with self.assertRaises(UserError) as context:
record.write({'new_field': 'invalid_value'})
self.assertIn('expected error', str(context.exception))
def test_05_computed_field_extension(self):
"""Test computed fields added by inheritance."""
record = self.Model.create({
'name': 'Test Record',
'quantity': 10,
'unit_price': 5.0,
})
# Test new computed field
self.assertEqual(record.total_with_tax, 52.5) # Example with 5% tax
def test_06_onchange_extension(self):
"""Test onchange methods added by inheritance."""
record = self.Model.new({
'name': 'Test Record',
})
# Trigger new onchange
record.partner_id = self.partner
record._onchange_partner_id_custom()
# Verify custom onchange behavior
# self.assertEqual(record.custom_field, expected_value)
def test_07_api_depends_extension(self):
"""Test that @api.depends works correctly on inherited fields."""
record = self.Model.create({
'name': 'Test Record',
'line_ids': [(0, 0, {
'product_id': self.env['product.product'].search([], limit=1).id,
'quantity': 5,
'price_unit': 10.0,
})],
})
# Initial computed value
initial_total = record.total_amount
# Add more lines
record.write({
'line_ids': [(0, 0, {
'product_id': self.env['product.product'].search([], limit=1).id,
'quantity': 3,
'price_unit': 20.0,
})],
})
# Verify recomputation
self.assertGreater(record.total_amount, initial_total)
def test_08_prevent_base_operation(self):
"""Test blocking base operations with custom validation."""
record = self.Model.create({
'name': 'Test Record',
'state': 'draft',
})
# Transition to state that prevents deletion
record.write({'state': 'confirmed'})
# Test that deletion is now blocked
with self.assertRaises(UserError) as context:
record.unlink()
self.assertIn('cannot delete', str(context.exception).lower())
def test_09_backward_compatibility(self):
"""Test that base functionality still works after inheritance."""
# Test base model functionality isn't broken
record = self.Model.create({
'name': 'Test Record',
})
# Base operations should still work
record.write({'name': 'Updated Name'})
self.assertEqual(record.name, 'Updated Name')
# Base methods should still be callable
if hasattr(record, 'base_method'):
result = record.base_method()
self.assertTrue(result)

View File

@@ -0,0 +1,369 @@
# Odoo 16.0 Test Patterns and Best Practices
This document provides comprehensive patterns and best practices for writing tests in Odoo 16.0 Enterprise modules, based on the Siafa project standards.
## Table of Contents
1. [Test Infrastructure](#test-infrastructure)
2. [Common Setup Patterns](#common-setup-patterns)
3. [Database Constraints](#database-constraints)
4. [Test Organization](#test-organization)
5. [Assertion Patterns](#assertion-patterns)
6. [Common Pitfalls](#common-pitfalls)
## Test Infrastructure
### Import Statement
Always use the correct import for `TransactionCase`:
```python
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
```
**Note:** Use `from odoo.tests.common import TransactionCase`, NOT `from odoo.tests import TransactionCase`.
### Base Test Class
```python
class TestModuleName(TransactionCase):
"""Test cases for module functionality."""
def setUp(self):
"""Set up test data."""
super().setUp() # Use super().setUp() not super(ClassName, self).setUp()
# Initialize models
self.Model = self.env['model.name']
# Set up test data
# ...
```
## Common Setup Patterns
### Using Existing Records
**Preferred Pattern:** Use existing database records when possible to avoid database constraint issues:
```python
def setUp(self):
super().setUp()
# Use existing warehouse
self.warehouse = self.env['stock.warehouse'].search([], limit=1)
if not self.warehouse:
self.skipTest("No warehouse available for testing")
# Use existing product
self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1)
if not self.product:
self.skipTest("No storable product available for testing")
```
### Creating New Records with .sudo()
When creating new records is necessary, use `.sudo()` to bypass access rights and permission issues:
```python
def setUp(self):
super().setUp()
# Create vendor
self.vendor = self.env['res.partner'].sudo().create({
'name': 'Test Vendor',
'is_company': True,
'supplier_rank': 1,
})
# Create product with minimal required fields
self.product = self.env['product.product'].sudo().create({
'name': 'Test Product',
'type': 'product',
'purchase_method': 'receive',
'list_price': 100.0,
'standard_price': 80.0,
})
```
### Class-Level Setup
For test data shared across all test methods, use `setUpClass`:
```python
@classmethod
def setUpClass(cls):
"""Set up test data shared across all test methods."""
super().setUpClass()
# Create shared test data
cls.vendor = cls.env['res.partner'].sudo().create({
'name': 'Test Vendor',
'is_company': True,
})
```
## Database Constraints
### Handling NOT NULL Constraints
Many databases have custom NOT NULL constraints. Avoid creating records that trigger these constraints:
**Problem:** Creating a product might fail due to missing `sale_line_warn` field:
```python
# ❌ May fail with "null value in column 'sale_line_warn' violates not-null constraint"
self.product = self.env['product.product'].create({
'name': 'Test Product',
'type': 'product',
})
```
**Solution 1:** Use existing records:
```python
# ✅ Use existing product from database
self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1)
```
**Solution 2:** Provide all required fields (if known):
```python
# ✅ Provide the required field
self.product = self.env['product.product'].sudo().create({
'name': 'Test Product',
'type': 'product',
'sale_line_warn': 'no-message', # Required in some databases
})
```
### HTML/Markup Fields
HTML fields return `Markup` objects, not plain strings:
```python
# ❌ Fails: Markup('<p>Text</p>') != 'Text'
self.assertEqual(record.comment, 'Updated comment')
# ✅ Use a different field for testing
self.assertEqual(record.barcode, 'TEST002')
# ✅ Or convert to string
self.assertIn('Updated comment', str(record.comment))
```
## Test Organization
### Test Method Naming
Use descriptive names with numbering for execution order:
```python
def test_01_create_record(self):
"""Test creating a new record."""
pass
def test_02_update_record(self):
"""Test updating an existing record."""
pass
def test_03_validation_error(self):
"""Test that validation error is raised for invalid data."""
pass
```
### Test Coverage Areas
Comprehensive tests should cover:
1. **CRUD Operations**
- Create records with valid data
- Update records
- Read/search records
- Delete records (if applicable)
2. **Validation Logic**
- Test constraints
- Test computed fields
- Test onchange methods
3. **Business Logic**
- Test state transitions
- Test workflow methods
- Test custom methods
4. **Edge Cases**
- Test with empty data
- Test with maximum values
- Test with invalid combinations
5. **Error Handling**
- Test that appropriate errors are raised
- Test error messages are clear
## Assertion Patterns
### Basic Assertions
```python
# Equality
self.assertEqual(record.field, expected_value)
self.assertNotEqual(record.field, unexpected_value)
# Boolean
self.assertTrue(record.active)
self.assertFalse(record.archived)
# Containment
self.assertIn(item, collection)
self.assertNotIn(item, collection)
# String matching
self.assertIn('substring', record.message)
```
### Exception Testing
```python
# Test that exception is raised
with self.assertRaises(UserError) as context:
record.forbidden_action()
# Check exception message
self.assertIn('Cannot perform action', str(context.exception))
```
### Recordset Assertions
```python
# Check recordset size
self.assertEqual(len(records), 3)
# Check recordset is empty
self.assertFalse(records)
# Check record exists
self.assertTrue(record)
self.assertTrue(record.exists())
```
### Computed Field Testing
```python
# Test computed field
self.assertEqual(record.total_amount, 100.0)
# Test compute dependency
record.line_ids = [(5, 0, 0)] # Clear lines
self.assertEqual(record.total_amount, 0.0)
```
## Common Pitfalls
### 1. Product Creation Failures
**Issue:** Creating products fails due to database constraints.
**Solution:** Use existing products or create with `.sudo()` and minimal fields:
```python
self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1)
```
### 2. Incorrect Import Statement
**Issue:** `from odoo.tests import TransactionCase` causes import errors.
**Solution:** Use `from odoo.tests.common import TransactionCase`
### 3. Super() Call Format
**Issue:** Using old-style super() calls.
**Solution:** Use `super().setUp()` instead of `super(ClassName, self).setUp()`
### 4. Missing .sudo()
**Issue:** Tests fail due to access rights.
**Solution:** Use `.sudo()` when creating test data:
```python
self.partner = self.env['res.partner'].sudo().create({...})
```
### 5. HTML Field Comparisons
**Issue:** Comparing HTML fields directly fails.
**Solution:** Use non-HTML fields or convert to string:
```python
# Instead of comment field, use barcode or another text field
self.assertEqual(record.barcode, 'TEST001')
```
### 6. Transaction Isolation
**Issue:** Tests affect each other due to shared data.
**Solution:** Each test method runs in its own transaction (automatic with `TransactionCase`)
### 7. Insufficient Error Checking
**Issue:** Not verifying error messages.
**Solution:** Always check exception messages:
```python
with self.assertRaises(UserError) as context:
record.action()
self.assertIn('expected error message', str(context.exception))
```
### 8. Missing Test Documentation
**Issue:** Tests without docstrings are hard to understand.
**Solution:** Always add descriptive docstrings:
```python
def test_prevent_usage_change_with_moves(self):
"""Test that usage cannot be changed when location has stock moves."""
# ...
```
## Running Tests
### Command Patterns
```bash
# Run tests during module installation
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
-i MODULE_NAME
# Run tests during module update
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
-u MODULE_NAME
# Run with verbose output
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
--log-level=test \
-u MODULE_NAME
```
### Test Tags
Tag tests for selective execution:
```python
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install', 'module_name')
class TestModuleName(TransactionCase):
"""Test cases with tags."""
pass
```
Run tagged tests:
```bash
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
--test-tags module_name
```