commit 3e809e35ad085368d3d45a3035eea2774cfa6917 Author: Zhongwei Li Date: Sat Nov 29 18:50:04 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..92fe072 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca63bb0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# odoo-dev + +Comprehensive feature Odoo development workflow with specialized agents for codebase exploration, architecture design, and quality review diff --git a/agents/code-architect.md b/agents/code-architect.md new file mode 100644 index 0000000..2b07ffd --- /dev/null +++ b/agents/code-architect.md @@ -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. diff --git a/agents/code-explorer.md b/agents/code-explorer.md new file mode 100644 index 0000000..cab62b5 --- /dev/null +++ b/agents/code-explorer.md @@ -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. diff --git a/agents/code-reviewer.md b/agents/code-reviewer.md new file mode 100644 index 0000000..c8dc83a --- /dev/null +++ b/agents/code-reviewer.md @@ -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. diff --git a/commands/odoo-dev.md b/commands/odoo-dev.md new file mode 100644 index 0000000..8c1f8ff --- /dev/null +++ b/commands/odoo-dev.md @@ -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 + +--- diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..d253adc --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/skills/odoo-code-reviewer/SKILL.md b/skills/odoo-code-reviewer/SKILL.md new file mode 100644 index 0000000..751da6e --- /dev/null +++ b/skills/odoo-code-reviewer/SKILL.md @@ -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. diff --git a/skills/odoo-code-reviewer/references/oca_guidelines.md b/skills/odoo-code-reviewer/references/oca_guidelines.md new file mode 100644 index 0000000..5e8adaf --- /dev/null +++ b/skills/odoo-code-reviewer/references/oca_guidelines.md @@ -0,0 +1 @@ +OCA Guidelines reference diff --git a/skills/odoo-code-reviewer/references/performance_patterns.md b/skills/odoo-code-reviewer/references/performance_patterns.md new file mode 100644 index 0000000..cdbed3e --- /dev/null +++ b/skills/odoo-code-reviewer/references/performance_patterns.md @@ -0,0 +1 @@ +Performance patterns diff --git a/skills/odoo-code-reviewer/references/security_checklist.md b/skills/odoo-code-reviewer/references/security_checklist.md new file mode 100644 index 0000000..68a59d1 --- /dev/null +++ b/skills/odoo-code-reviewer/references/security_checklist.md @@ -0,0 +1 @@ +Security checklist diff --git a/skills/odoo-connector-module-creator/SKILL.md b/skills/odoo-connector-module-creator/SKILL.md new file mode 100644 index 0000000..d1949fa --- /dev/null +++ b/skills/odoo-connector-module-creator/SKILL.md @@ -0,0 +1,1285 @@ +--- +name: odoo-connector-module-creator +description: Creates and enhances Odoo 16.0 connector modules that integrate with external systems (e-commerce, logistics, accounting, CRM) using the `generic_connector` framework +--- + +# Odoo Connector Module Creator and Enhancer + +## Description + +Creates and enhances Odoo 16.0 connector modules that integrate with external systems (e-commerce, logistics, accounting, CRM) using the `generic_connector` framework. This skill handles: + +- **New Connector Creation**: Build complete integration modules for Shopify, WooCommerce, Amazon, or any external API +- **Connector Enhancement**: Add features like inventory sync, webhook support, or new entity types to existing connectors +- **Troubleshooting**: Debug sync issues, API errors, authentication problems, and queue job failures +- **Architecture Implementation**: Properly implement binding models, adapters, mappers, and importers/exporters + +The skill leverages production-tested patterns from reference connectors (zid_connector_v2, beatroute_connector) and provides automated scripts for generating boilerplate code. + +## Overview + +Create production-ready Odoo 16.0 connector modules that integrate with external systems using the `generic_connector` framework. Handle creation of new connectors, enhancement of existing connectors, troubleshooting sync issues, and debugging integration problems. + +## When to Use This Skill + +Use this skill when the user requests: +- **Creating new connectors**: "Create a Shopify connector", "Build WooCommerce integration", "Connect to Amazon API" +- **Enhancing connectors**: "Add inventory sync to zid_connector", "Implement webhooks for orders", "Add product export" +- **Adding entities**: "Add customer sync to the connector", "Import invoices from the external system" +- **Troubleshooting**: "Orders aren't importing", "Webhook signature verification failing", "Fix sync errors" +- **Debugging**: "Why is the API returning 401?", "Products are duplicating", "Queue jobs not running" + +## Key Concepts + +### Generic Connector Framework + +All connector modules extend `generic_connector`, which provides: + +1. **Backend Model** - Configuration and orchestration +2. **Binding Models** - Link Odoo records to external entities +3. **Adapter Component** - HTTP client for API communication +4. **Mapper Components** - Data transformation (import/export) +5. **Importer/Exporter Components** - Sync logic +6. **Webhook System** - Real-time event processing +7. **Queue Job Integration** - Async operations + +### Reference Code + +Three production connectors serve as references: +- `/Users/jamshid/PycharmProjects/Siafa/odoo16e_simc/addons-connector/generic_connector` - Base framework +- `/Users/jamshid/PycharmProjects/Siafa/odoo16e_simc/addons-connector/zid_connector_v2` - E-commerce example +- `/Users/jamshid/PycharmProjects/Siafa/odoo16e_simc/addons-connector/beatroute_connector` - Logistics example + +## Workflow + +### Creating a New Connector + +When the user requests a new connector: + +**Step 1: Gather Requirements** +- External system name (e.g., "Shopify", "WooCommerce") +- Connector type: ecommerce, logistics, accounting, crm +- Entities to sync: products, orders, customers, inventory +- Sync direction: import, export, or bidirectional +- Authentication method: API key, OAuth, basic auth +- API documentation URL (if available) + +**Step 2: Initialize Module** +```bash +# Use the init_connector.py script +python3 scripts/init_connector.py --path --type + +# Example: +python3 scripts/init_connector.py shopify --path ~/odoo/addons --type ecommerce +``` + +**Step 3: Review Generated Structure** + +The script creates: +``` +shopify_connector/ +├── __manifest__.py # Module metadata +├── __init__.py # Python imports +├── models/ +│ ├── backend.py # Backend configuration +│ ├── adapter.py # API client +│ ├── product_binding.py # Product sync +│ └── __init__.py +├── views/ +│ ├── backend_views.xml # Backend UI +│ ├── binding_views.xml # Binding UI +│ └── menu_views.xml # Menu structure +├── security/ +│ ├── security.xml # Access groups +│ └── ir.model.access.csv # Access rules +├── wizards/ +│ ├── sync_wizard.py # Manual sync wizard +│ └── __init__.py +├── data/ +│ ├── ir_cron_data.xml # Scheduled jobs +│ └── queue_job_function_data.xml +└── README.md +``` + +**Step 4: Customize Backend Model** + +Edit `models/backend.py`: + +1. **Update API configuration fields** to match the external system: + ```python + # Example for Shopify + shop_url = fields.Char(string='Shop URL', required=True) + api_version = fields.Selection([ + ('2024-01', '2024-01'), + ('2024-04', '2024-04'), + ], default='2024-04') + ``` + +2. **Implement template methods**: + ```python + def _test_connection_implementation(self): + """Test API connection.""" + adapter = self.get_adapter('shopify.adapter') + return adapter.test_connection() + + def _sync_orders_implementation(self): + """Import orders.""" + with self.work_on('shopify.sale.order') as work: + importer = work.component(usage='batch.importer') + return importer.run() + ``` + +**Step 5: Implement Adapter** + +Edit `models/adapter.py`: + +1. **Configure authentication** (see `references/authentication.md` for patterns): + ```python + def get_api_headers(self): + headers = super().get_api_headers() + headers.update({ + 'X-Shopify-Access-Token': self.backend_record.api_key, + 'Content-Type': 'application/json', + }) + return headers + ``` + +2. **Add CRUD methods** for each entity type: + ```python + def get_products(self, filters=None): + """Fetch products from Shopify.""" + return self.get('/admin/api/2024-01/products.json', params=filters) + + def create_order(self, data): + """Create order in Shopify.""" + return self.post('/admin/api/2024-01/orders.json', data={'order': data}) + ``` + +3. **Handle pagination** (see `references/api_integration.md`): + ```python + def get_all_products(self): + """Fetch all products with pagination.""" + # Implement based on API pagination style + ``` + +**Step 6: Create Mapper Components** + +Create `components/mapper.py`: + +```python +from odoo.addons.generic_connector.components.mapper import GenericImportMapper + +class ProductImportMapper(GenericImportMapper): + _name = 'shopify.product.import.mapper' + _inherit = 'generic.import.mapper' + _apply_on = 'shopify.product.template' + + direct = [ + ('title', 'name'), + ('vendor', 'manufacturer'), + ] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + @mapping + def price(self, record): + variants = record.get('variants', []) + if variants: + return {'list_price': float(variants[0].get('price', 0))} + return {} +``` + +**Step 7: Implement Importer Components** + +Create `components/importer.py`: + +```python +from odoo.addons.generic_connector.components.importer import GenericImporter + +class ProductImporter(GenericImporter): + _name = 'shopify.product.importer' + _inherit = 'generic.importer' + _apply_on = 'shopify.product.template' + + def _import_record(self, external_id, force=False): + # Fetch from external system + adapter = self.component(usage='backend.adapter') + external_data = adapter.get_product(external_id) + + # Transform data + mapper = self.component(usage='import.mapper') + mapped_data = mapper.map_record(external_data).values() + + # Create or update binding + binding = self._get_binding() + if binding: + binding.write(mapped_data) + else: + binding = self.model.create(mapped_data) + + return binding +``` + +**Step 8: Register Components** + +Create `components/__init__.py`: +```python +from . import adapter +from . import mapper +from . import importer +from . import exporter +``` + +Update main `__init__.py`: +```python +from . import models +from . import wizards +from . import components +``` + +**Step 9: Test the Connector** + +```bash +# Install module +odoo-bin -c odoo.conf -d test_db -i shopify_connector + +# Test in Odoo UI +# 1. Go to Connector > Shopify > Backends +# 2. Create a new backend +# 3. Configure API credentials +# 4. Click "Test Connection" +# 5. Click "Sync All" +``` + +### Enhancing an Existing Connector + +When the user wants to add functionality to an existing connector: + +**Step 1: Identify Enhancement Type** + +- Adding a new entity (orders, customers, invoices) +- Adding a new feature (webhooks, batch export) +- Fixing bugs or improving performance +- Adding authentication method + +**Step 2: Add New Entity Binding** + +Use the `add_binding.py` script: + +```bash +python3 scripts/add_binding.py --odoo-model + +# Example: +python3 scripts/add_binding.py ~/odoo/addons/shopify_connector customer --odoo-model res.partner +``` + +This generates: +- `models/customer_binding.py` - Binding model +- `views/customer_views.xml` - UI views +- Updates to `__manifest__.py` and security files +- Adapter methods to implement manually + +**Step 3: Implement Components** + +Follow steps 6-7 from "Creating a New Connector" to implement mapper and importer/exporter for the new entity. + +**Step 4: Add to Backend Orchestration** + +Update `models/backend.py`: + +```python +def _sync_customers_implementation(self): + """Import customers.""" + with self.work_on('shopify.res.partner') as work: + importer = work.component(usage='batch.importer') + return importer.run() + +def action_sync_all(self): + """Override to include customers.""" + super().action_sync_all() + self.with_delay().sync_customers() +``` + +### Implementing Webhooks + +When the user requests webhook support: + +**Step 1: Create Webhook Controller** + +Create `controllers/webhook_controller.py`: + +```python +from odoo import http +from odoo.http import request +import json +import logging + +_logger = logging.getLogger(__name__) + +class ShopifyWebhookController(http.Controller): + @http.route('/shopify/webhook', type='json', auth='none', csrf=False) + def webhook(self): + """Handle Shopify webhooks.""" + try: + payload = request.httprequest.get_data(as_text=True) + topic = request.httprequest.headers.get('X-Shopify-Topic') + hmac_header = request.httprequest.headers.get('X-Shopify-Hmac-SHA256') + + # Find backend + shop_domain = request.httprequest.headers.get('X-Shopify-Shop-Domain') + backend = request.env['shopify.backend'].sudo().search([ + ('shop_url', 'ilike', shop_domain) + ], limit=1) + + if not backend: + return {'error': 'Backend not found'}, 404 + + # Verify signature + if not self._verify_webhook(payload, hmac_header, backend.webhook_secret): + return {'error': 'Invalid signature'}, 401 + + # Create webhook record + webhook = request.env['generic.webhook'].sudo().create({ + 'backend_id': backend.id, + 'event_type': topic, + 'payload': payload, + 'signature': hmac_header, + '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_webhook(self, payload, hmac_header, secret): + """Verify HMAC-SHA256 signature.""" + import hmac + import hashlib + import base64 + + computed = hmac.new( + secret.encode('utf-8'), + payload.encode('utf-8'), + hashlib.sha256 + ).digest() + + computed_base64 = base64.b64encode(computed).decode() + + return hmac.compare_digest(computed_base64, hmac_header) +``` + +**Step 2: Add Webhook Processing to Backend** + +Update `models/backend.py`: + +```python +def process_webhook(self, webhook): + """Process webhook by topic.""" + handlers = { + 'orders/create': self._handle_order_created, + 'orders/updated': self._handle_order_updated, + 'products/update': self._handle_product_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 orders/create webhook.""" + payload = json.loads(webhook.payload) + order_id = payload['id'] + + # Import the order + self.env['shopify.sale.order'].import_record( + backend=self, + external_id=str(order_id) + ) +``` + +### Implementing Export Using Shared Wizard + +The `connector_base_backend` module provides a **shared export wizard** (`connector.export.wizard`) that works across all connectors without requiring custom UI for each one. + +#### Architecture Overview + +The export system uses delegation inheritance to route export requests: + +``` +User clicks "Export to Connectors" on product.product + ↓ +connector.export.wizard opens (shared UI) + ↓ +User selects backend (e.g., ZID, Shopify) + ↓ +wizard.action_export() calls backend.export_records(model_name, record_ids) + ↓ +connector.base.backend routes to concrete implementation + ↓ (via _inherits delegation chain) +generic.backend (intermediate) + ↓ +zid.backend.export_product_product(record_ids) + ↓ +Creates bindings + queues async exports +``` + +**Inheritance Chain**: +```python +connector.base.backend (has export_records() router) + ↓ _inherits via base_backend_id +generic.backend (intermediate layer) + ↓ _inherits via generic_backend_id +your_connector.backend (concrete implementation) +``` + +#### Step-by-Step Implementation + +**Step 1: Understand the Routing Mechanism** + +The `connector.base.backend.export_records()` method automatically routes to your backend: + +```python +# In connector_base_backend/models/connector_base_backend.py +def export_records(self, model_name, record_ids): + """Generic export method that routes to specific connector implementations""" + method_name = f'export_{model_name.replace(".", "_")}' + + # Find concrete backend via _inherits chain + concrete_backend = self + for model in self._inherits_children: + child = self.env[model].search([('base_backend_id', '=', self.id)], limit=1) + if child: + concrete_backend = child + break + + # Call export_product_product(), export_sale_order(), etc. + if hasattr(concrete_backend, method_name): + return getattr(concrete_backend, method_name)(record_ids) + else: + raise UserError(_( + "Export not implemented for model %s in connector %s" + ) % (model_name, concrete_backend.name)) +``` + +**Step 2: Implement Export Methods in Your Backend** + +For each Odoo model you want to export, implement `export_()` in your backend model: + +**Example: Export Products** + +Add to `models/backend.py`: + +```python +def export_product_product(self, record_ids): + """ + Export product.product records to external system. + + Called by connector.export.wizard when exporting products. + + Args: + record_ids: List of product.product IDs to export + + Returns: + dict: Notification action + """ + self.ensure_one() + + if not record_ids: + return self._build_notification( + _('Export Products'), + _('No products selected for export'), + 'warning' + ) + + products = self.env['product.product'].browse(record_ids) + exported_count = 0 + created_bindings = 0 + skipped_count = 0 + errors = [] + + for product in products: + try: + # Find or create binding + binding = self.env['shopify.product.product'].search([ + ('backend_id', '=', self.id), + ('odoo_id', '=', product.id) + ], limit=1) + + if not binding: + # Create new binding + binding_vals = { + 'backend_id': self.id, + 'odoo_id': product.id, + 'external_sku': product.default_code or '', + 'external_name': product.name, + 'external_price': product.list_price, + 'external_status': 'active' if product.active else 'inactive', + } + binding = self.env['shopify.product.product'].create(binding_vals) + created_bindings += 1 + + # Skip if marked as no_export + if binding.no_export: + skipped_count += 1 + continue + + # Queue async export + binding.with_delay()._export_to_external() + exported_count += 1 + + except Exception as e: + errors.append(f'Product {product.name}: {str(e)}') + _logger.error(f'Export failed for {product.name}: {e}', exc_info=True) + + # Build response message + message_parts = [] + if exported_count > 0: + message_parts.append( + _('%d product(s) scheduled for export') % exported_count + ) + if created_bindings > 0: + message_parts.append(_('%d new binding(s) created') % created_bindings) + if skipped_count > 0: + message_parts.append(_('%d skipped (no_export)') % skipped_count) + if errors: + message_parts.append(_('Errors: %d') % len(errors)) + + message = '. '.join(message_parts) + notif_type = 'success' if exported_count > 0 and not errors else 'warning' + + # Update statistics + if exported_count > 0: + self.last_export_date = datetime.now() + + return self._build_notification(_('Export Products'), message, notif_type) +``` + +**Example: Export Partners** + +```python +def export_res_partner(self, record_ids): + """Export res.partner records to external system.""" + self.ensure_one() + + partners = self.env['res.partner'].browse(record_ids) + exported_count = 0 + + for partner in partners: + # Find or create partner binding + binding = self.env['shopify.res.partner'].search([ + ('backend_id', '=', self.id), + ('odoo_id', '=', partner.id) + ], limit=1) + + if not binding: + binding = self.env['shopify.res.partner'].create({ + 'backend_id': self.id, + 'odoo_id': partner.id, + }) + + # Queue export + binding.with_delay()._export_to_external() + exported_count += 1 + + return self._build_notification( + _('Export Customers'), + _('%d customer(s) scheduled for export') % exported_count, + 'success' + ) +``` + +**Example: Export Sale Orders** + +```python +def export_sale_order(self, record_ids): + """Export sale.order records to external system.""" + self.ensure_one() + + orders = self.env['sale.order'].browse(record_ids) + exported_count = 0 + + for order in orders: + # Validate order state + if order.state not in ['sale', 'done']: + _logger.warning(f'Skipping order {order.name}: not confirmed') + continue + + # Find or create order binding + binding = self.env['shopify.sale.order'].search([ + ('backend_id', '=', self.id), + ('odoo_id', '=', order.id) + ], limit=1) + + if not binding: + binding = self.env['shopify.sale.order'].create({ + 'backend_id': self.id, + 'odoo_id': order.id, + }) + + # Export dependencies first (customer, products) + self._export_order_dependencies(order) + + # Queue order export + binding.with_delay()._export_to_external() + exported_count += 1 + + return self._build_notification( + _('Export Orders'), + _('%d order(s) scheduled for export') % exported_count, + 'success' + ) + +def _export_order_dependencies(self, order): + """Export customer and products before exporting order.""" + # Export customer + if order.partner_id: + self.export_res_partner([order.partner_id.id]) + + # Export products + product_ids = order.order_line.mapped('product_id').ids + if product_ids: + self.export_product_product(product_ids) +``` + +**Step 3: The Shared Wizard is Already Configured** + +The `connector_base_backend` module already includes action bindings: + +```xml + + + + + Export to Connectors + connector.export.wizard + form + new + + list,form + + + + + Export to Connectors + connector.export.wizard + form + new + + list,form + +``` + +**To add export for other models**, create similar actions in your connector or in `connector_base_backend`: + +```xml + + + Export to Connectors + connector.export.wizard + form + new + + list,form + +``` + +**Step 4: Testing the Export** + +```bash +# 1. Update your connector module +odoo-bin -c odoo.conf -d your_db -u your_connector + +# 2. Test from UI +# Navigate to: Inventory → Products → Products +# Select one or more products +# Click: Action → Export to Connectors +# Select your backend +# Click: Export + +# 3. Verify in logs +tail -f /var/log/odoo/odoo.log | grep "export\|binding" + +# 4. Check queue jobs +# Navigate to: Queue Jobs → Jobs +# Look for: your_connector.product.product._export_to_external + +# 5. Check bindings created +# Navigate to: Connector → Your Connector → Products +# Verify bindings were created with correct external_id +``` + +**Step 5: Advanced Export Patterns** + +**Pattern 1: Conditional Export** + +```python +def export_product_product(self, record_ids): + """Export only published products.""" + products = self.env['product.product'].browse(record_ids) + + # Filter products + exportable_products = products.filtered( + lambda p: p.active and getattr(p, 'website_published', True) + ) + + if len(exportable_products) < len(products): + skipped = len(products) - len(exportable_products) + _logger.info(f'Skipped {skipped} unpublished products') + + # Export only exportable products + for product in exportable_products: + # ... create binding and export +``` + +**Pattern 2: Batch Export with Progress** + +```python +def export_product_product(self, record_ids): + """Export products in batches.""" + products = self.env['product.product'].browse(record_ids) + batch_size = 50 + + for i in range(0, len(products), batch_size): + batch = products[i:i + batch_size] + # Process batch with delay + self.with_delay()._export_product_batch(batch.ids) + + return self._build_notification( + _('Export Products'), + _('Queued %d products in %d batches') % ( + len(products), + (len(products) + batch_size - 1) // batch_size + ), + 'success' + ) + +def _export_product_batch(self, product_ids): + """Process a batch of products.""" + for product_id in product_ids: + # Create binding and export + pass +``` + +**Pattern 3: Export with Validation** + +```python +def export_sale_order(self, record_ids): + """Export orders with validation.""" + orders = self.env['sale.order'].browse(record_ids) + validation_errors = [] + + for order in orders: + # Validate before export + if not order.partner_id: + validation_errors.append(f'{order.name}: Missing customer') + continue + + if not order.order_line: + validation_errors.append(f'{order.name}: No order lines') + continue + + if order.state not in ['sale', 'done']: + validation_errors.append(f'{order.name}: Not confirmed') + continue + + # Export if valid + # ... create binding and export + + if validation_errors: + message = '\n'.join(validation_errors[:10]) + return self._build_notification( + _('Export Validation Errors'), + message, + 'warning' + ) +``` + +#### Method Naming Convention + +The export method name **must** follow this pattern: + +```python +export_{model_name_with_underscores} + +# Examples: +export_product_product # for product.product +export_product_template # for product.template +export_sale_order # for sale.order +export_res_partner # for res.partner +export_stock_picking # for stock.picking +export_account_move # for account.move +``` + +The wizard automatically converts model names: +- Replaces dots (`.`) with underscores (`_`) +- `product.product` → calls `export_product_product()` +- `sale.order` → calls `export_sale_order()` + +#### Benefits of Shared Export Wizard + +1. **Single UI**: One wizard works for all models across all connectors +2. **Consistent UX**: Users learn once, use everywhere +3. **No Custom Code**: No need to create custom wizards or actions +4. **Multi-Backend**: Users can export to multiple backends +5. **Extensible**: Add new models just by implementing one method +6. **Backend Filtering**: Wizard shows only relevant backends via domain + +#### Complete Implementation Checklist + +When implementing export for a new model: + +- [ ] Implement `export_()` method in backend model +- [ ] Handle binding creation (find or create) +- [ ] Queue export using `with_delay()._export_to_external()` +- [ ] Handle errors gracefully with try/except +- [ ] Return notification with detailed status +- [ ] Update backend statistics (last_export_date) +- [ ] Add export action binding (if not exists for this model) +- [ ] Test from UI (Action → Export to Connectors) +- [ ] Verify queue jobs are created +- [ ] Check bindings are created correctly +- [ ] Review logs for errors + +#### Troubleshooting Export Issues + +**Issue**: "Export not implemented" error + +```python +# Solution: Check method name matches pattern +# Model: product.product → Method: export_product_product() +# Model: sale.order → Method: export_sale_order() +``` + +**Issue**: Backend not showing in wizard + +```python +# Solution: Check inheritance chain +# Ensure your backend inherits from generic.backend +# which inherits from connector.base.backend + +class YourBackend(models.Model): + _name = 'your.backend' + _inherits = {'generic.backend': 'generic_backend_id'} +``` + +**Issue**: Export creates duplicates + +```python +# Solution: Ensure unique constraint on binding +# In binding model: +_sql_constraints = [ + ('backend_odoo_uniq', + 'unique(backend_id, odoo_id)', + 'A binding already exists for this record on this backend.') +] +``` + +**Issue**: Export completes but nothing happens + +```python +# Solution: Check queue_job is running +# 1. Verify queue_job channel exists +# 2. Start queue job worker: +odoo-bin gevent -c odoo.conf --workers=2 + +# 3. Or run job manually: +>>> job = env['queue.job'].search([...]) +>>> job.requeue() +``` + +### Troubleshooting + +When the user reports sync issues or errors: + +**Step 1: Identify the Problem** + +Common issues: +- Connection/authentication failures → Check `references/authentication.md` +- Import not working → Check component registration +- Duplicates being created → Check SQL constraints +- Queue jobs not running → Check queue_job configuration +- Webhooks not received → Check controller route + +**Step 2: Use Diagnostic Tools** + +```python +# Test in Odoo shell +odoo-bin shell -c odoo.conf -d your_db + +# Find backend +>>> backend = env['shopify.backend'].browse(1) + +# Test connection +>>> backend.action_test_connection() + +# Test adapter +>>> with backend.work_on('shopify.product.template') as work: +... adapter = work.component(usage='backend.adapter') +... products = adapter.get_products() +... print(f"Fetched {len(products)} products") + +# Test mapper +... mapper = work.component(usage='import.mapper') +... if products: +... mapped = mapper.map_record(products[0]) +... print(mapped.values()) +``` + +**Step 3: Enable Debug Logging** + +Add to backend or adapter: +```python +import logging +_logger = logging.getLogger(__name__) +_logger.setLevel(logging.DEBUG) + +def make_request(self, method, endpoint, **kwargs): + _logger.debug("API Request: %s %s", method, self.build_url(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: %s", str(response)[:500]) + return response +``` + +**Step 4: Check Reference Documentation** + +Refer the user to: +- `references/troubleshooting.md` - Common issues and solutions +- `references/architecture.md` - Component structure +- `references/patterns.md` - Design patterns +- `references/api_integration.md` - API communication patterns +- `references/authentication.md` - Authentication methods + +## Available Scripts + +### init_connector.py + +Generate a complete new connector module. + +**Usage**: +```bash +python3 scripts/init_connector.py --path --type +``` + +**Arguments**: +- `connector_name`: Name (e.g., 'shopify', 'woocommerce') +- `--path`: Output directory (default: current directory) +- `--type`: Connector type - 'ecommerce', 'logistics', 'accounting', 'crm' + +**Output**: Complete module with backend, adapter, binding, views, security + +### add_binding.py + +Add a new entity binding to existing connector. + +**Usage**: +```bash +python3 scripts/add_binding.py --odoo-model +``` + +**Arguments**: +- `connector_path`: Path to existing connector module +- `entity_name`: Entity name (e.g., 'order', 'customer') +- `--odoo-model`: Odoo model to bind (e.g., 'sale.order', 'res.partner') + +**Output**: Binding model, views, security rules, adapter methods template + +### validate_connector.py + +Validate connector module structure. + +**Usage**: +```bash +python3 scripts/validate_connector.py +``` + +**Checks**: +- Required files and directories +- Manifest dependencies +- Backend model structure +- Component registration +- Security configuration + +## Reference Documentation + +Load references as needed using the Read tool: + +### references/architecture.md +Comprehensive guide to generic_connector architecture: +- Backend model patterns +- Binding model structure +- Adapter, mapper, importer, exporter components +- Queue job integration +- Security model +- View patterns + +**When to read**: Creating new connectors, understanding component relationships + +### references/patterns.md +Design patterns used in connectors: +- Template Method, Adapter, Strategy, Factory patterns +- Observer pattern for webhooks +- Retry and circuit breaker patterns +- Rate limiting patterns +- Anti-patterns to avoid + +**When to read**: Implementing complex sync logic, handling failures + +### references/api_integration.md +API integration techniques: +- REST, GraphQL, SOAP integrations +- Pagination handling (offset, cursor, link header) +- Response envelope handling +- Webhook integration +- Rate limiting implementation +- Error handling and retries + +**When to read**: Implementing adapters, handling API specifics + +### references/authentication.md +Authentication patterns: +- API key authentication +- OAuth 2.0 (authorization code flow) +- Bearer token +- Basic auth +- HMAC signatures +- JWT tokens +- Webhook signature verification + +**When to read**: Configuring authentication, debugging 401 errors + +### references/troubleshooting.md +Common issues and solutions: +- Connection issues +- Authentication failures +- Import/export problems +- Queue job issues +- Webhook problems +- Data mapping errors +- Performance optimization +- Debugging tips + +**When to read**: Debugging sync issues, performance problems + +## Best Practices + +1. **Always extend generic_connector** - Never build from scratch +2. **Use bindings** - Never directly modify Odoo records from external data +3. **Queue long operations** - Use `with_delay()` for anything >2 seconds +4. **Implement retry logic** - Use binding's retry_count and max_retries +5. **Log extensively** - Debug logging helps troubleshoot production issues +6. **Handle API errors** - Wrap adapter calls in try/except +7. **Validate data** - Check required fields before creating records +8. **Test connection** - Always implement `_test_connection_implementation()` +9. **Use transactions** - Leverage Odoo's automatic transaction management +10. **Document the API** - Add docstrings to all adapter methods + +## Component Registration Checklist + +When creating components, ensure: + +```python +class MyComponent(BaseComponent): + _name = 'unique.component.name' # ✓ Unique identifier + _inherit = 'parent.component' # ✓ Parent component + _apply_on = 'model.name' # ✓ Model this applies to + _usage = 'component.usage' # ✓ Usage context +``` + +Common usages: +- `backend.adapter` - API communication +- `record.importer` - Single record import +- `batch.importer` - Batch import +- `record.exporter` - Single record export +- `batch.exporter` - Batch export +- `import.mapper` - Import data transformation +- `export.mapper` - Export data transformation + +## Testing Checklist + +Before delivering a connector: + +**Backend Configuration**: +- [ ] Backend configuration form loads +- [ ] "Test Connection" button works +- [ ] Backend inherits from generic.backend correctly +- [ ] Backend statistics update (last_sync_date, counters) + +**Import Functionality**: +- [ ] Manual sync imports data +- [ ] No duplicate records created on import +- [ ] External IDs are set correctly +- [ ] Bindings link Odoo records to external records +- [ ] Import handles API pagination correctly +- [ ] Import handles API errors gracefully + +**Export Functionality**: +- [ ] "Export to Connectors" action appears on models (Action menu) +- [ ] Export wizard shows only relevant backends +- [ ] `export_()` methods implemented in backend +- [ ] Export creates bindings if they don't exist +- [ ] Export queues async jobs via `with_delay()` +- [ ] Export notifications show correct counts (exported, created, skipped, errors) +- [ ] Export respects `no_export` flag on bindings +- [ ] Export updates backend statistics (last_export_date) + +**Queue Jobs**: +- [ ] Queue jobs are registered and visible +- [ ] Jobs execute successfully in queue_job worker +- [ ] Failed jobs can be retried +- [ ] Job logs provide useful debugging info + +**Scheduled Jobs**: +- [ ] Scheduled cron jobs exist (disabled by default) +- [ ] Cron jobs can be enabled and run on schedule + +**Security & Access**: +- [ ] Security access rules allow users to view data +- [ ] Users can access backend, bindings, and wizards +- [ ] Proper groups assigned (connector_manager, connector_user) + +**Integration**: +- [ ] Webhooks received and processed (if applicable) +- [ ] Webhook signature verification works +- [ ] Components registered correctly (adapters, mappers, importers, exporters) + +**Error Handling**: +- [ ] Error handling works (test with invalid credentials) +- [ ] API errors don't crash Odoo +- [ ] User-friendly error messages displayed +- [ ] Detailed errors logged for debugging + +**Logging & Debugging**: +- [ ] Logging provides useful debug information +- [ ] Log levels appropriate (INFO for success, ERROR for failures) +- [ ] Sensitive data (tokens, passwords) not logged + +## Module Update Process + +When updating an existing connector: + +```bash +# 1. Update module files +# 2. Upgrade module +odoo-bin -c odoo.conf -d your_db -u connector_module_name + +# 3. Test thoroughly +# 4. Check logs for errors +tail -f /var/log/odoo/odoo.log +``` + +## Common Workflows + +### Workflow: Add Product Sync + +1. Generate binding: `python3 scripts/add_binding.py product --odoo-model product.template` +2. Implement adapter methods in `models/adapter.py` +3. Create mapper in `components/mapper.py` +4. Create importer in `components/importer.py` +5. Update backend `_sync_products_implementation()` +6. Update module: `odoo-bin -u connector_name` +7. Test sync + +### Workflow: Add Order Import + +1. Generate binding: `python3 scripts/add_binding.py order --odoo-model sale.order` +2. Implement adapter methods +3. Create import mapper (transform external order to Odoo format) +4. Create importer (handle order lines, customer lookup) +5. Update backend `_sync_orders_implementation()` +6. Configure webhook for real-time import (optional) +7. Test import + +### Workflow: Debug Sync Failure + +1. Check logs: `tail -f /var/log/odoo/odoo.log` +2. Enable debug logging in adapter +3. Test in Odoo shell +4. Check component registration +5. Verify API credentials +6. Test adapter methods directly +7. Check mapper output +8. Review binding constraints +9. Refer to `references/troubleshooting.md` + +## Output Format + +When creating or enhancing connectors: + +1. **Use scripts** whenever possible (init_connector.py, add_binding.py) +2. **Provide code** for custom components (mappers, importers, exporters) +3. **Show configuration** (backend fields, view changes) +4. **Include testing steps** (how to verify it works) +5. **Reference docs** when needed (point to specific reference sections) +6. **Explain patterns** used (why this approach was chosen) + +## Error Prevention + +Common mistakes to avoid: + +- ❌ Missing `_apply_on` in components +- ❌ Wrong model name in `_apply_on` +- ❌ Forgetting to register components in `__init__.py` +- ❌ Not setting `external_id` in mapper +- ❌ Missing SQL constraint on bindings +- ❌ Using synchronous operations for long tasks +- ❌ Not handling API pagination +- ❌ Hardcoding configuration instead of using backend fields +- ❌ Not implementing retry logic +- ❌ Insufficient error handling + +## Success Criteria + +A successfully created/enhanced connector should: + +1. ✅ Install without errors +2. ✅ Test connection successfully +3. ✅ Import/export data correctly +4. ✅ Handle API errors gracefully +5. ✅ Log useful information for debugging +6. ✅ Use queue jobs for async operations +7. ✅ Not create duplicate records +8. ✅ Follow generic_connector patterns +9. ✅ Have proper security configuration +10. ✅ Be maintainable and extensible + +## When to Use Each Reference + +| Situation | Reference | +|-----------|-----------| +| Creating new connector | architecture.md | +| Implementing OAuth | authentication.md | +| Adding webhooks | api_integration.md | +| Sync not working | troubleshooting.md | +| Implementing retry logic | patterns.md | +| Understanding components | architecture.md | +| API pagination | api_integration.md | +| 401 errors | authentication.md, troubleshooting.md | +| Performance issues | troubleshooting.md, patterns.md | +| Best practices | patterns.md (anti-patterns section) | + +## Final Notes + +- Always test in a development database first +- Use the reference connectors (zid, beatroute) as examples +- Leverage the scripts to generate boilerplate code +- Refer to documentation for specific patterns +- Focus on extensibility and maintainability +- Follow Odoo and generic_connector conventions diff --git a/skills/odoo-connector-module-creator/references/api_integration.md b/skills/odoo-connector-module-creator/references/api_integration.md new file mode 100644 index 0000000..470a178 --- /dev/null +++ b/skills/odoo-connector-module-creator/references/api_integration.md @@ -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') +``` diff --git a/skills/odoo-connector-module-creator/references/architecture.md b/skills/odoo-connector-module-creator/references/architecture.md new file mode 100644 index 0000000..4180a8d --- /dev/null +++ b/skills/odoo-connector-module-creator/references/architecture.md @@ -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 +
+
+
+ + + + + + + + + + + + + + +
+``` + +## 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 diff --git a/skills/odoo-connector-module-creator/references/authentication.md b/skills/odoo-connector-module-creator/references/authentication.md new file mode 100644 index 0000000..5cd2056 --- /dev/null +++ b/skills/odoo-connector-module-creator/references/authentication.md @@ -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 diff --git a/skills/odoo-connector-module-creator/references/patterns.md b/skills/odoo-connector-module-creator/references/patterns.md new file mode 100644 index 0000000..b87ea94 --- /dev/null +++ b/skills/odoo-connector-module-creator/references/patterns.md @@ -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 +``` diff --git a/skills/odoo-connector-module-creator/references/troubleshooting.md b/skills/odoo-connector-module-creator/references/troubleshooting.md new file mode 100644 index 0000000..19526d0 --- /dev/null +++ b/skills/odoo-connector-module-creator/references/troubleshooting.md @@ -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 + + myconnector.backend.sync_products + + + +# 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 + + {1: 60, 5: 300, 10: 600} + + +``` + +### 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 | diff --git a/skills/odoo-connector-module-creator/scripts/add_binding.py b/skills/odoo-connector-module-creator/scripts/add_binding.py new file mode 100644 index 0000000..ea135e9 --- /dev/null +++ b/skills/odoo-connector-module-creator/scripts/add_binding.py @@ -0,0 +1,537 @@ +#!/usr/bin/env python3 +""" +Add a new binding model to an existing Odoo connector module. + +Usage: + python3 add_binding.py [--odoo-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''' + + + + {binding_model_name}.form + {binding_model_name} + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + {binding_model_name}.tree + {binding_model_name} + + + + + + + + + + + + + + {binding_model_name}.search + {binding_model_name} + + + + + + + + + + + + + + + + + + {connector_name.title()} {entity_name.title()}s + {binding_model_name} + tree,form + + + + + +
+''' + + 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() diff --git a/skills/odoo-connector-module-creator/scripts/init_connector.py b/skills/odoo-connector-module-creator/scripts/init_connector.py new file mode 100644 index 0000000..ac5c46e --- /dev/null +++ b/skills/odoo-connector-module-creator/scripts/init_connector.py @@ -0,0 +1,1171 @@ +#!/usr/bin/env python3 +""" +Initialize a new Odoo connector module that extends generic_connector. + +Usage: + python3 init_connector.py [--path ] [--type ] + +Arguments: + connector_name: Name of the connector (e.g., 'shopify', 'amazon', 'woocommerce') + --path: Output directory (default: current directory) + --type: Connector type - 'ecommerce', 'logistics', 'accounting', 'crm' (default: 'ecommerce') + +Example: + python3 init_connector.py shopify --path ~/odoo/custom_addons --type ecommerce +""" + +import argparse +import os +import sys +from pathlib import Path +from datetime import datetime + + +CONNECTOR_TYPES = { + 'ecommerce': { + 'description': 'E-commerce platform integration', + 'models': ['product', 'order', 'customer', 'inventory'], + 'sync_direction': 'bidirectional', + }, + 'logistics': { + 'description': 'Distribution and logistics system', + 'models': ['route', 'delivery', 'driver', 'location'], + 'sync_direction': 'bidirectional', + }, + 'accounting': { + 'description': 'Accounting and financial system', + 'models': ['invoice', 'payment', 'account', 'journal'], + 'sync_direction': 'export', + }, + 'crm': { + 'description': 'CRM and marketing platform', + 'models': ['lead', 'opportunity', 'contact', 'campaign'], + 'sync_direction': 'bidirectional', + } +} + + +def sanitize_name(name): + """Convert name to valid Python module name.""" + return name.lower().replace('-', '_').replace(' ', '_') + + +def create_directory_structure(base_path, connector_name): + """Create the standard Odoo module directory structure.""" + directories = [ + '', + 'models', + 'views', + 'security', + 'wizards', + 'data', + 'controllers', + 'static/description', + ] + + for directory in directories: + path = base_path / directory + path.mkdir(parents=True, exist_ok=True) + + # Create __init__.py files for Python packages + if directory in ['', 'models', 'wizards', 'controllers']: + init_file = path / '__init__.py' + if not init_file.exists(): + init_file.touch() + + return base_path + + +def generate_manifest(connector_name, connector_type_info): + """Generate __manifest__.py content.""" + return f'''# -*- coding: utf-8 -*- +{{ + 'name': '{connector_name.title()} Connector', + 'version': '16.0.1.0.0', + 'category': 'Connector', + 'summary': '{connector_type_info["description"]} for {connector_name.title()}', + 'description': """ +{connector_name.title()} Connector +{'=' * (len(connector_name) + 10)} + +This module provides integration with {connector_name.title()} API. + +Features: +--------- +* Bidirectional synchronization of data +* Webhook support for real-time updates +* Queue job integration for async operations +* Comprehensive error handling and logging +* Configurable retry mechanisms + +Configuration: +-------------- +1. Go to Connector > {connector_name.title()} > Backends +2. Create a new backend and configure API credentials +3. Configure synchronization settings +4. Run manual sync or enable scheduled synchronization + +Dependencies: +------------- +* generic_connector: Core connector framework +* queue_job: Asynchronous job processing + """, + 'author': 'Your Company', + 'website': 'https://www.yourcompany.com', + 'license': 'LGPL-3', + 'depends': [ + 'generic_connector', + 'queue_job', + 'stock', # Common dependency for most connectors + ], + 'data': [ + # Security + 'security/security.xml', + 'security/ir.model.access.csv', + + # Views + 'views/backend_views.xml', + 'views/binding_views.xml', + + # Wizards + 'wizards/sync_wizard_views.xml', + + # Data + 'data/queue_job_function_data.xml', + 'data/ir_cron_data.xml', + + # Menus (must be last) + 'views/menu_views.xml', + ], + 'installable': True, + 'application': False, + 'auto_install': False, +}} +''' + + +def generate_backend_model(connector_name): + """Generate backend model (models/backend.py).""" + module_name = sanitize_name(connector_name) + class_prefix = ''.join(word.capitalize() for word in connector_name.split('_')) + + return f'''# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +import requests +import logging + +_logger = logging.getLogger(__name__) + + +class {class_prefix}Backend(models.Model): + """ + {connector_name.title()} Backend Configuration. + + Extends generic.backend to provide {connector_name.title()}-specific settings. + """ + _name = '{module_name}.backend' + _inherit = 'generic.backend' + _description = '{connector_name.title()} Backend' + _backend_type = '{module_name}' + + # API Configuration + api_url = fields.Char( + string='API URL', + required=True, + default='https://api.{module_name}.com', + help='Base URL for {connector_name.title()} API' + ) + api_key = fields.Char( + string='API Key', + required=True, + help='API key for authentication' + ) + api_secret = fields.Char( + string='API Secret', + help='API secret for authentication (if required)' + ) + + # OAuth Configuration (if applicable) + oauth_client_id = fields.Char(string='OAuth Client ID') + oauth_client_secret = fields.Char(string='OAuth Client Secret') + 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) + + # Synchronization Settings + import_products_from_date = fields.Datetime( + string='Import Products From', + help='Only import products created/modified after this date' + ) + import_orders_from_date = fields.Datetime( + string='Import Orders From', + help='Only import orders created after this date' + ) + + # Rate Limiting + rate_limit_calls = fields.Integer( + string='Rate Limit (calls)', + default=100, + help='Maximum API calls per time window' + ) + rate_limit_window = fields.Integer( + string='Rate Limit Window (seconds)', + default=60, + help='Time window for rate limiting in seconds' + ) + + # Advanced Settings + webhook_secret = fields.Char( + string='Webhook Secret', + help='Secret for validating webhook signatures' + ) + default_warehouse_id = fields.Many2one( + 'stock.warehouse', + string='Default Warehouse', + help='Default warehouse for inventory operations' + ) + + @api.constrains('api_url') + def _check_api_url(self): + """Validate API URL format.""" + for backend in self: + if backend.api_url and not backend.api_url.startswith(('http://', 'https://')): + raise ValidationError(_('API URL must start with http:// or https://')) + + def action_test_connection(self): + """Test connection to {connector_name.title()} API.""" + self.ensure_one() + try: + # Get adapter and test connection + adapter = self.get_adapter('{module_name}.backend') + result = adapter.test_connection() + + if result.get('success'): + return {{ + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': {{ + 'title': _('Connection Successful'), + 'message': _('Successfully connected to {connector_name.title()} API'), + 'type': 'success', + 'sticky': False, + }} + }} + else: + raise ValidationError(_( + 'Connection failed: %s' + ) % result.get('error', 'Unknown error')) + + except Exception as e: + raise ValidationError(_( + 'Connection test failed: %s' + ) % str(e)) + + def _get_api_headers(self): + """Get headers for API requests.""" + self.ensure_one() + headers = {{ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }} + + if self.api_key: + headers['Authorization'] = f'Bearer {{self.api_key}}' + + return headers + + def action_sync_all(self): + """Trigger synchronization for all entity types.""" + self.ensure_one() + + # Queue sync jobs for different entities + self.with_delay().sync_products() + self.with_delay().sync_orders() + self.with_delay().sync_customers() + + return {{ + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': {{ + 'title': _('Synchronization Started'), + 'message': _('Sync jobs have been queued'), + 'type': 'info', + 'sticky': False, + }} + }} + + def sync_products(self): + """Synchronize products from {connector_name.title()}.""" + self.ensure_one() + _logger.info('Starting product synchronization for backend %s', self.name) + # Implementation will be in the importer + pass + + def sync_orders(self): + """Synchronize orders from {connector_name.title()}.""" + self.ensure_one() + _logger.info('Starting order synchronization for backend %s', self.name) + # Implementation will be in the importer + pass + + def sync_customers(self): + """Synchronize customers from {connector_name.title()}.""" + self.ensure_one() + _logger.info('Starting customer synchronization for backend %s', self.name) + # Implementation will be in the importer + pass +''' + + +def generate_adapter_model(connector_name): + """Generate adapter model (models/adapter.py).""" + module_name = sanitize_name(connector_name) + class_prefix = ''.join(word.capitalize() for word in connector_name.split('_')) + + return f'''# -*- coding: utf-8 -*- +from odoo.addons.generic_connector.components.adapter import GenericAdapter +import requests +import logging + +_logger = logging.getLogger(__name__) + + +class {class_prefix}Adapter(GenericAdapter): + """ + Adapter for {connector_name.title()} API. + + Handles all HTTP communication with {connector_name.title()} API endpoints. + """ + _name = '{module_name}.adapter' + _inherit = 'generic.adapter' + _usage = '{module_name}.adapter' + + def __init__(self, env): + super().__init__(env) + self.base_url = None + self.headers = {{}} + + def _build_url(self, endpoint): + """Build full URL for API endpoint.""" + if not self.base_url: + self.base_url = self.backend_record.api_url.rstrip('/') + return f'{{self.base_url}}/{{endpoint.lstrip("/")}}' + + def _get_headers(self): + """Get headers for API requests.""" + if not self.headers: + self.headers = self.backend_record._get_api_headers() + return self.headers + + def _make_request(self, method, endpoint, data=None, params=None): + """ + Make HTTP request to {connector_name.title()} API. + + Args: + method: HTTP method (GET, POST, PUT, DELETE) + endpoint: API endpoint path + data: Request body data (for POST/PUT) + params: URL query parameters + + Returns: + dict: Response data + + Raises: + requests.HTTPError: If request fails + """ + url = self._build_url(endpoint) + headers = self._get_headers() + + _logger.debug('%s request to %s', method, url) + + try: + response = requests.request( + method=method, + url=url, + json=data, + params=params, + headers=headers, + timeout=30 + ) + response.raise_for_status() + + return response.json() if response.content else {{}} + + except requests.exceptions.RequestException as e: + _logger.error('API request failed: %s', str(e)) + if hasattr(e, 'response') and e.response is not None: + _logger.error('Response content: %s', e.response.text) + raise + + def test_connection(self): + """Test API connection.""" + try: + # Attempt to fetch backend info or health check + result = self._make_request('GET', '/health') # Adjust endpoint as needed + return {{'success': True, 'data': result}} + except Exception as e: + return {{'success': False, 'error': str(e)}} + + # CRUD operations for products + def get_product(self, external_id): + """Get product by external ID.""" + return self._make_request('GET', f'/products/{{external_id}}') + + def get_products(self, filters=None): + """Get list of products.""" + return self._make_request('GET', '/products', params=filters) + + def create_product(self, data): + """Create product.""" + return self._make_request('POST', '/products', data=data) + + def update_product(self, external_id, data): + """Update product.""" + return self._make_request('PUT', f'/products/{{external_id}}', data=data) + + def delete_product(self, external_id): + """Delete product.""" + return self._make_request('DELETE', f'/products/{{external_id}}') + + # CRUD operations for orders + def get_order(self, external_id): + """Get order by external ID.""" + return self._make_request('GET', f'/orders/{{external_id}}') + + def get_orders(self, filters=None): + """Get list of orders.""" + return self._make_request('GET', '/orders', params=filters) + + def create_order(self, data): + """Create order.""" + return self._make_request('POST', '/orders', data=data) + + def update_order(self, external_id, data): + """Update order.""" + return self._make_request('PUT', f'/orders/{{external_id}}', data=data) + + # CRUD operations for customers + def get_customer(self, external_id): + """Get customer by external ID.""" + return self._make_request('GET', f'/customers/{{external_id}}') + + def get_customers(self, filters=None): + """Get list of customers.""" + return self._make_request('GET', '/customers', params=filters) + + def create_customer(self, data): + """Create customer.""" + return self._make_request('POST', '/customers', data=data) + + def update_customer(self, external_id, data): + """Update customer.""" + return self._make_request('PUT', f'/customers/{{external_id}}', data=data) +''' + + +def generate_product_binding(connector_name): + """Generate product binding model.""" + module_name = sanitize_name(connector_name) + class_prefix = ''.join(word.capitalize() for word in connector_name.split('_')) + + return f'''# -*- coding: utf-8 -*- +from odoo import models, fields, api +import logging + +_logger = logging.getLogger(__name__) + + +class {class_prefix}ProductBinding(models.Model): + """ + Binding between Odoo product.template and {connector_name.title()} products. + """ + _name = '{module_name}.product.template' + _inherit = 'generic.binding' + _inherits = {{'product.template': 'odoo_id'}} + _description = '{connector_name.title()} Product Binding' + + odoo_id = fields.Many2one( + comodel_name='product.template', + string='Product', + required=True, + ondelete='cascade' + ) + + # {connector_name.title()}-specific fields + external_sku = fields.Char(string='External SKU', readonly=True) + external_barcode = fields.Char(string='External Barcode', readonly=True) + external_category_id = fields.Char(string='External Category ID') + + # Sync metadata + sync_date = fields.Datetime(string='Last Sync Date', readonly=True) + 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) + + _sql_constraints = [ + ('backend_external_uniq', 'unique(backend_id, external_id)', + 'A product binding with the same external ID already exists for this backend.') + ] + + @api.model + def import_record(self, backend, external_id): + """Import a single product from {connector_name.title()}.""" + 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 products in batch from {connector_name.title()}.""" + with backend.work_on(self._name) as work: + importer = work.component(usage='batch.importer') + return importer.run(filters=filters) + + def export_record(self): + """Export product to {connector_name.title()}.""" + self.ensure_one() + with self.backend_id.work_on(self._name) as work: + exporter = work.component(usage='record.exporter') + return exporter.run(self) +''' + + +def generate_init_files(connector_name): + """Generate __init__.py files.""" + module_name = sanitize_name(connector_name) + + main_init = f'''# -*- coding: utf-8 -*- +from . import models +from . import wizards +from . import controllers +''' + + models_init = f'''# -*- coding: utf-8 -*- +from . import backend +from . import adapter +from . import product_binding +# Add more model imports as needed +''' + + wizards_init = f'''# -*- coding: utf-8 -*- +from . import sync_wizard +''' + + controllers_init = f'''# -*- coding: utf-8 -*- +from . import webhook_controller +''' + + return {{ + '__init__.py': main_init, + 'models/__init__.py': models_init, + 'wizards/__init__.py': wizards_init, + 'controllers/__init__.py': controllers_init, + }} + + +def generate_security_files(connector_name): + """Generate security configuration files.""" + module_name = sanitize_name(connector_name) + + security_xml = f''' + + + + {connector_name.title()} User + + + + + + {connector_name.title()} Manager + + + + +''' + + access_csv = f'''id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_{module_name}_backend_user,{module_name}.backend user,model_{module_name}_backend,group_{module_name}_user,1,0,0,0 +access_{module_name}_backend_manager,{module_name}.backend manager,model_{module_name}_backend,group_{module_name}_manager,1,1,1,1 +access_{module_name}_product_template_user,{module_name}.product.template user,model_{module_name}_product_template,group_{module_name}_user,1,0,0,0 +access_{module_name}_product_template_manager,{module_name}.product.template manager,model_{module_name}_product_template,group_{module_name}_manager,1,1,1,1 +''' + + return {{ + 'security/security.xml': security_xml, + 'security/ir.model.access.csv': access_csv, + }} + + +def generate_view_files(connector_name): + """Generate XML view files.""" + module_name = sanitize_name(connector_name) + + backend_views = f''' + + + + {module_name}.backend.form + {module_name}.backend + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + {module_name}.backend.tree + {module_name}.backend + + + + + + + + + + + + {connector_name.title()} Backends + {module_name}.backend + tree,form + +
+''' + + binding_views = f''' + + + + {module_name}.product.template.form + {module_name}.product.template + +
+ + + + + + + + + + + + + + +
+
+
+ + + + {module_name}.product.template.tree + {module_name}.product.template + + + + + + + + + + + + + {connector_name.title()} Products + {module_name}.product.template + tree,form + +
+''' + + menu_views = f''' + + + + + + + + + + + + + + +''' + + return {{ + 'views/backend_views.xml': backend_views, + 'views/binding_views.xml': binding_views, + 'views/menu_views.xml': menu_views, + }} + + +def generate_wizard_files(connector_name): + """Generate wizard files.""" + module_name = sanitize_name(connector_name) + class_prefix = ''.join(word.capitalize() for word in connector_name.split('_')) + + wizard_py = f'''# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + + +class {class_prefix}SyncWizard(models.TransientModel): + """Wizard for manual synchronization.""" + _name = '{module_name}.sync.wizard' + _description = '{connector_name.title()} Sync Wizard' + + backend_id = fields.Many2one( + '{module_name}.backend', + string='Backend', + required=True, + default=lambda self: self._default_backend_id() + ) + + sync_products = fields.Boolean(string='Sync Products', default=True) + sync_orders = fields.Boolean(string='Sync Orders', default=True) + sync_customers = fields.Boolean(string='Sync Customers', default=False) + + from_date = fields.Datetime(string='From Date') + to_date = fields.Datetime(string='To Date') + + def _default_backend_id(self): + """Get default backend from context.""" + backend_id = self.env.context.get('active_id') + if backend_id and self.env.context.get('active_model') == '{module_name}.backend': + return backend_id + return False + + def action_sync(self): + """Execute synchronization.""" + self.ensure_one() + + if not (self.sync_products or self.sync_orders or self.sync_customers): + raise UserError(_('Please select at least one entity to synchronize.')) + + # Queue sync jobs + if self.sync_products: + self.backend_id.with_delay().sync_products() + + if self.sync_orders: + self.backend_id.with_delay().sync_orders() + + if self.sync_customers: + self.backend_id.with_delay().sync_customers() + + return {{ + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': {{ + 'title': _('Synchronization Started'), + 'message': _('Sync jobs have been queued for execution'), + 'type': 'success', + 'sticky': False, + }} + }} +''' + + wizard_view = f''' + + + {module_name}.sync.wizard.form + {module_name}.sync.wizard + +
+ + + + + + + + + + + + +
+
+
+
+
+ + + Synchronize + {module_name}.sync.wizard + form + new + +
+''' + + return {{ + 'wizards/sync_wizard.py': wizard_py, + 'wizards/sync_wizard_views.xml': wizard_view, + }} + + +def generate_data_files(connector_name): + """Generate data files (cron, queue jobs).""" + module_name = sanitize_name(connector_name) + + cron_xml = f''' + + + + + {connector_name.title()}: Sync Products + + code + + backends = env['{{module_name}}.backend'].search([]) + for backend in backends: + backend.sync_products() + + 1 + hours + -1 + + + + + + {connector_name.title()}: Sync Orders + + code + + backends = env['{{module_name}}.backend'].search([]) + for backend in backends: + backend.sync_orders() + + 30 + minutes + -1 + + + + +''' + + queue_job_xml = f''' + + + + + {module_name}.backend.sync_products + + {{1: 60, 5: 300, 10: 600}} + + + + {module_name}.backend.sync_orders + + {{1: 60, 5: 300, 10: 600}} + + + + {module_name}.backend.sync_customers + + {{1: 60, 5: 300, 10: 600}} + + + +''' + + return {{ + 'data/ir_cron_data.xml': cron_xml, + 'data/queue_job_function_data.xml': queue_job_xml, + }} + + +def generate_readme(connector_name, connector_type_info): + """Generate README.md.""" + return f'''# {connector_name.title()} Connector + +{connector_type_info["description"]} for Odoo 16. + +## Features + +- **Bidirectional Sync**: Automatically synchronize data between Odoo and {connector_name.title()} +- **Webhook Support**: Real-time updates via webhooks +- **Queue Jobs**: Async processing for reliable synchronization +- **Rate Limiting**: Built-in API rate limit handling +- **Error Recovery**: Automatic retry with configurable patterns +- **Multi-Backend**: Support multiple {connector_name.title()} accounts + +## Installation + +1. Ensure `generic_connector` and `queue_job` modules are installed +2. Install this module: `{sanitize_name(connector_name)}` +3. Restart the Odoo server + +## Configuration + +1. Go to **Connector > {connector_name.title()} > Backends** +2. Create a new backend +3. Configure API credentials: + - API URL + - API Key + - API Secret (if required) +4. Test the connection using "Test Connection" button +5. Configure synchronization settings + +## Usage + +### Manual Synchronization + +1. Open a backend record +2. Click "Sync All" button to synchronize all entities +3. Or use the sync wizard for selective synchronization + +### Scheduled Synchronization + +1. Go to **Settings > Technical > Automation > Scheduled Actions** +2. Enable the cron jobs for {connector_name.title()}: + - `{connector_name.title()}: Sync Products` + - `{connector_name.title()}: Sync Orders` + +### Webhook Configuration + +1. In your {connector_name.title()} account, configure webhook URL: + ``` + https://your-odoo-domain.com/{sanitize_name(connector_name)}/webhook + ``` +2. Set the webhook secret in the backend configuration + +## Development + +### Adding New Entities + +1. Create binding model in `models/` (e.g., `order_binding.py`) +2. Create views in `views/` (e.g., `order_views.xml`) +3. Add adapter methods in `models/adapter.py` +4. Implement importer/exporter components +5. Update `__manifest__.py` dependencies + +### Testing + +Run tests with: +```bash +odoo-bin -c odoo.conf -d test_db --test-enable --stop-after-init -u {sanitize_name(connector_name)} +``` + +## Architecture + +This module extends the `generic_connector` framework: + +- **Backend Model**: Configuration and orchestration +- **Adapter**: HTTP client for API communication +- **Bindings**: Link Odoo records to external entities +- **Importers/Exporters**: Data transformation and synchronization logic + +## Support + +For issues and questions, contact your Odoo administrator. + +## License + +LGPL-3 + +## Credits + +- Developed by: Your Company +- Based on: generic_connector framework +''' + + +def create_connector_module(connector_name, output_path, connector_type): + """Create complete connector module structure.""" + module_name = sanitize_name(connector_name) + connector_type_info = CONNECTOR_TYPES.get(connector_type, CONNECTOR_TYPES['ecommerce']) + + # Create base directory + module_path = Path(output_path) / f'{{module_name}}_connector' + print(f"\\n🚀 Creating {{connector_name.title()}} Connector Module") + print(f" Type: {{connector_type_info['description']}}") + print(f" Location: {{module_path}}\\n") + + # Create directory structure + create_directory_structure(module_path, connector_name) + + # Generate and write files + files_to_create = {{}} + + # Manifest + files_to_create['__manifest__.py'] = generate_manifest(connector_name, connector_type_info) + + # Models + files_to_create['models/backend.py'] = generate_backend_model(connector_name) + files_to_create['models/adapter.py'] = generate_adapter_model(connector_name) + files_to_create['models/product_binding.py'] = generate_product_binding(connector_name) + + # Init files + init_files = generate_init_files(connector_name) + files_to_create.update(init_files) + + # Security + security_files = generate_security_files(connector_name) + files_to_create.update(security_files) + + # Views + view_files = generate_view_files(connector_name) + files_to_create.update(view_files) + + # Wizards + wizard_files = generate_wizard_files(connector_name) + files_to_create.update(wizard_files) + + # Data + data_files = generate_data_files(connector_name) + files_to_create.update(data_files) + + # README + files_to_create['README.md'] = generate_readme(connector_name, connector_type_info) + + # Write all files + for file_path, content in files_to_create.items(): + full_path = module_path / file_path + full_path.parent.mkdir(parents=True, exist_ok=True) + + with open(full_path, 'w', encoding='utf-8') as f: + f.write(content) + + print(f"✅ Created: {{file_path}}") + + print(f"\\n✅ Module '{{module_name}}_connector' created successfully!") + print(f"\\nNext steps:") + print(f"1. Review and customize the generated code") + print(f"2. Implement importer/exporter components") + print(f"3. Add unit tests") + print(f"4. Update API endpoints in adapter.py") + print(f"5. Configure webhook controller") + print(f"\\nModule location: {{module_path}}") + + return module_path + + +def main(): + parser = argparse.ArgumentParser( + description='Initialize a new Odoo connector module extending generic_connector' + ) + parser.add_argument( + 'connector_name', + help='Name of the connector (e.g., shopify, amazon, woocommerce)' + ) + parser.add_argument( + '--path', + default='.', + help='Output directory (default: current directory)' + ) + parser.add_argument( + '--type', + choices=list(CONNECTOR_TYPES.keys()), + default='ecommerce', + help='Connector type (default: ecommerce)' + ) + + args = parser.parse_args() + + try: + create_connector_module( + connector_name=args.connector_name, + output_path=args.path, + connector_type=args.type + ) + except Exception as e: + print(f"\\n❌ Error: {{str(e)}}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/skills/odoo-connector-module-creator/scripts/validate_connector.py b/skills/odoo-connector-module-creator/scripts/validate_connector.py new file mode 100644 index 0000000..ec7c973 --- /dev/null +++ b/skills/odoo-connector-module-creator/scripts/validate_connector.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +Validate Odoo connector module structure and dependencies. + +Usage: + python3 validate_connector.py + +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() diff --git a/skills/odoo-debugger/SKILL.md b/skills/odoo-debugger/SKILL.md new file mode 100644 index 0000000..00a8a34 --- /dev/null +++ b/skills/odoo-debugger/SKILL.md @@ -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. diff --git a/skills/odoo-debugger/references/common_issues.md b/skills/odoo-debugger/references/common_issues.md new file mode 100644 index 0000000..a2434fa --- /dev/null +++ b/skills/odoo-debugger/references/common_issues.md @@ -0,0 +1 @@ +Created common issues database diff --git a/skills/odoo-debugger/references/debugging_queries.md b/skills/odoo-debugger/references/debugging_queries.md new file mode 100644 index 0000000..7816b2a --- /dev/null +++ b/skills/odoo-debugger/references/debugging_queries.md @@ -0,0 +1 @@ +Created basic debugging query reference diff --git a/skills/odoo-debugger/scripts/example.py b/skills/odoo-debugger/scripts/example.py new file mode 100755 index 0000000..5dcb6a8 --- /dev/null +++ b/skills/odoo-debugger/scripts/example.py @@ -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() diff --git a/skills/odoo-feature-enhancer/SKILL.md b/skills/odoo-feature-enhancer/SKILL.md new file mode 100644 index 0000000..3999e8b --- /dev/null +++ b/skills/odoo-feature-enhancer/SKILL.md @@ -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 + + + + + stock.picking.form.inherit + stock.picking + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + stock.picking.tree.inherit + stock.picking + + + + is_urgent + + + + + + + + + + + + stock.picking.search.inherit + stock.picking + + + + + + + + + + + + + +``` + +### 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 + + + + bulk.invoice.wizard.form + bulk.invoice.wizard + +
+ + + + + + + + + + + +
+
+
+
+
+ + + + Generate Bulk Invoices + bulk.invoice.wizard + form + new + + + + +
+``` + +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). diff --git a/skills/odoo-feature-enhancer/references/api_reference.md b/skills/odoo-feature-enhancer/references/api_reference.md new file mode 100644 index 0000000..8ba61b9 --- /dev/null +++ b/skills/odoo-feature-enhancer/references/api_reference.md @@ -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 diff --git a/skills/odoo-feature-enhancer/references/field_types.md b/skills/odoo-feature-enhancer/references/field_types.md new file mode 100644 index 0000000..4a1e448 --- /dev/null +++ b/skills/odoo-feature-enhancer/references/field_types.md @@ -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')) +``` diff --git a/skills/odoo-feature-enhancer/references/implementation_patterns.md b/skills/odoo-feature-enhancer/references/implementation_patterns.md new file mode 100644 index 0000000..d9716ed --- /dev/null +++ b/skills/odoo-feature-enhancer/references/implementation_patterns.md @@ -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 + + Task Name + + code + model._cron_method() + 1 + days + +``` diff --git a/skills/odoo-feature-enhancer/references/xpath_patterns.md b/skills/odoo-feature-enhancer/references/xpath_patterns.md new file mode 100644 index 0000000..94f97ab --- /dev/null +++ b/skills/odoo-feature-enhancer/references/xpath_patterns.md @@ -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 + + + +``` + +### Add Field Inside Group +```xml + + + +``` + +### Replace Field +```xml + + + +``` + +### Add Attributes +```xml + + 1 + 1 + +``` + +### Add to Header +```xml + +