Claude Code Plugins

Community-maintained marketplace

Feedback

odoo-knowledge-agent

@jgtolentino/insightpulse-odoo
0
0

Scrape Odoo forum solved threads to build error prevention guardrails and auto-fix scripts. Extract patterns from 1000+ solved issues to prevent common mistakes before deployment and auto-heal production problems. Combines web scraping, AI pattern extraction, preventive guardrails generation, and auto-patch scripts for Odoo custom development lifecycle.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name odoo-knowledge-agent
description Scrape Odoo forum solved threads to build error prevention guardrails and auto-fix scripts. Extract patterns from 1000+ solved issues to prevent common mistakes before deployment and auto-heal production problems. Combines web scraping, AI pattern extraction, preventive guardrails generation, and auto-patch scripts for Odoo custom development lifecycle.

Odoo Knowledge Agent - Error Prevention & Auto-Fix System

When to Use This Skill

Use this skill when you need to:

  • Prevent Odoo errors before deployment with guardrails
  • Auto-heal production issues using community-validated fixes
  • Build intelligence from Odoo forum solved threads (1000+ cases)
  • Generate error code reference and troubleshooting guides
  • Create preventive checks for module development
  • Implement auto-patch systems for known failure patterns
  • Reduce debugging time by learning from community solutions
  • Build SOP library for common Odoo development errors

Core Capabilities

1. Forum Scraping Intelligence

Scrape Odoo Forum for Solved Threads

  • Extract 1000+ solved issues across all categories
  • Parse accepted answers and code fixes
  • Identify error patterns and root causes
  • Build searchable knowledge base

2. Guardrails Generation

Preventive Error Checks

  • Block common mistakes before deployment
  • Validate manifest files automatically
  • Check field synchronization issues
  • Enforce best practices (OCA standards)

3. Auto-Patch Scripts

Automated Error Fixes

  • Apply community-validated solutions
  • Fix known issues automatically
  • Handle common migration problems
  • Self-healing production systems

4. Vision-Based UI Automation

askui-Compatible Automation

  • UI actions without brittle selectors
  • Cross-browser compatibility
  • Screenshot-based validation
  • Self-healing test scripts

Prerequisites

Required Software

  • Python 3.11+
  • Beautiful Soup 4 / Firecrawl
  • Odoo 19 instance for testing
  • Supabase for knowledge storage

Optional Integrations

  • OpenAI API for pattern extraction
  • GitHub Actions for CI/CD integration
  • Perplexity for research enhancement

Python Dependencies

beautifulsoup4
requests
firecrawl-py
supabase-py
openai
pyyaml
gitpython

Implementation Patterns

Forum Scraper

# scrape_solved_threads.py
import requests
from bs4 import BeautifulSoup
from supabase import create_client
import time
from datetime import datetime

class OdooForumScraper:
    def __init__(self, supabase_url, supabase_key):
        self.base_url = "https://www.odoo.com/forum"
        self.supabase = create_client(supabase_url, supabase_key)
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'OdooKnowledgeBot/1.0'
        })
    
    def scrape_solved_threads(self, pages=100):
        """
        Scrape Odoo forum for solved threads
        """
        threads = []
        
        for page in range(1, pages + 1):
            print(f"Scraping page {page}/{pages}...")
            
            url = f"{self.base_url}/questions?filters=solved&page={page}"
            response = self.session.get(url)
            soup = BeautifulSoup(response.content, 'html.parser')
            
            # Extract thread links
            thread_elements = soup.select('.o_wforum_question')
            
            for thread in thread_elements:
                thread_url = thread.find('a')['href']
                if not thread_url.startswith('http'):
                    thread_url = f"https://www.odoo.com{thread_url}"
                
                # Get thread details
                thread_data = self.scrape_thread_details(thread_url)
                if thread_data:
                    threads.append(thread_data)
                    self.store_thread(thread_data)
                
                time.sleep(2)  # Rate limiting
            
            time.sleep(5)  # Pause between pages
        
        return threads
    
    def scrape_thread_details(self, url):
        """
        Extract question, accepted answer, and code snippets
        """
        try:
            response = self.session.get(url)
            soup = BeautifulSoup(response.content, 'html.parser')
            
            # Extract question
            question_elem = soup.select_one('.o_wforum_question_text')
            question = question_elem.text.strip() if question_elem else ""
            
            # Extract title
            title_elem = soup.select_one('h1.o_wforum_title')
            title = title_elem.text.strip() if title_elem else ""
            
            # Extract accepted answer
            accepted_answer = soup.select_one('.o_wforum_answer.o_wforum_answer_correct')
            if not accepted_answer:
                return None  # Skip if no accepted answer
            
            answer_text = accepted_answer.select_one('.o_wforum_answer_text').text.strip()
            
            # Extract code snippets
            code_blocks = accepted_answer.select('pre code')
            code_snippets = [block.text.strip() for block in code_blocks]
            
            # Extract tags/category
            tags = [tag.text.strip() for tag in soup.select('.o_wforum_tag')]
            
            # Extract metadata
            views_elem = soup.select_one('.o_wforum_views')
            views = int(views_elem.text.strip()) if views_elem else 0
            
            return {
                'url': url,
                'title': title,
                'question': question,
                'answer': answer_text,
                'code_snippets': code_snippets,
                'tags': tags,
                'views': views,
                'scraped_at': datetime.now().isoformat()
            }
        
        except Exception as e:
            print(f"Error scraping {url}: {e}")
            return None
    
    def store_thread(self, thread_data):
        """
        Store thread in Supabase knowledge base
        """
        self.supabase.table('odoo_solved_threads').upsert({
            'thread_url': thread_data['url'],
            'title': thread_data['title'],
            'question': thread_data['question'],
            'answer': thread_data['answer'],
            'code_snippets': thread_data['code_snippets'],
            'tags': thread_data['tags'],
            'views': thread_data['views'],
            'scraped_at': thread_data['scraped_at']
        }).execute()
    
    def extract_error_patterns(self):
        """
        Use AI to extract common error patterns
        """
        # Fetch all threads
        threads = self.supabase.table('odoo_solved_threads').select('*').execute()
        
        # Group by error type using AI
        from openai import OpenAI
        client = OpenAI()
        
        patterns = {}
        
        for thread in threads.data:
            # Ask GPT to categorize the error
            response = client.chat.completions.create(
                model="gpt-4",
                messages=[
                    {"role": "system", "content": "You are an Odoo expert. Categorize this error and extract the fix pattern."},
                    {"role": "user", "content": f"Title: {thread['title']}\n\nQuestion: {thread['question']}\n\nAnswer: {thread['answer']}"}
                ]
            )
            
            pattern = response.choices[0].message.content
            
            # Extract category
            category = self.extract_category(pattern)
            
            if category not in patterns:
                patterns[category] = []
            
            patterns[category].append({
                'thread': thread,
                'pattern': pattern
            })
        
        return patterns
    
    def extract_category(self, pattern_text):
        """
        Extract error category from AI analysis
        """
        # Common Odoo error categories
        categories = {
            'manifest': ['__manifest__', 'module', 'dependency'],
            'field': ['field', 'column', 'attribute'],
            'view': ['view', 'xml', 'template'],
            'security': ['access', 'rights', 'permission'],
            'orm': ['orm', 'recordset', 'browse'],
            'workflow': ['workflow', 'state', 'transition'],
            'accounting': ['invoice', 'payment', 'journal'],
            'inventory': ['stock', 'picking', 'quant']
        }
        
        pattern_lower = pattern_text.lower()
        
        for category, keywords in categories.items():
            if any(keyword in pattern_lower for keyword in keywords):
                return category
        
        return 'general'

# Usage
scraper = OdooForumScraper(
    supabase_url=os.getenv('SUPABASE_URL'),
    supabase_key=os.getenv('SUPABASE_KEY')
)

# Scrape 100 pages (~1,100 solved threads)
threads = scraper.scrape_solved_threads(pages=100)

# Extract patterns
patterns = scraper.extract_error_patterns()

print(f"Scraped {len(threads)} solved threads")
print(f"Identified {len(patterns)} error patterns")

Guardrail Generator

# generate_guardrails.py
import yaml
from pathlib import Path

class GuardrailGenerator:
    def __init__(self, patterns):
        self.patterns = patterns
        self.guardrails_dir = Path('guardrails')
        self.guardrails_dir.mkdir(exist_ok=True)
    
    def generate_manifest_guardrail(self):
        """
        GR-INSTALL-004: Manifest validation
        """
        guardrail = {
            'id': 'GR-INSTALL-004',
            'name': 'Manifest Validation',
            'description': 'Prevent module installation failures due to invalid __manifest__.py',
            'category': 'manifest',
            'severity': 'HIGH',
            'checks': [
                {
                    'name': 'Required Keys Present',
                    'pattern': r"'name':|\"name\":",
                    'error_message': "Missing required 'name' key in __manifest__.py",
                    'fix': 'Add name = "Module Name" to __manifest__.py'
                },
                {
                    'name': 'Valid Dependencies',
                    'pattern': r"'depends':\s*\[.*?\]",
                    'error_message': "Invalid or missing 'depends' list",
                    'fix': 'Ensure depends list contains only installed modules'
                },
                {
                    'name': 'Version Format',
                    'pattern': r"'version':\s*'\\d+\\.\\d+'",
                    'error_message': "Invalid version format (should be X.Y)",
                    'fix': 'Use semantic versioning: version = "1.0"'
                }
            ],
            'prevention_script': '''
def validate_manifest(manifest_path):
    with open(manifest_path) as f:
        manifest = eval(f.read())
    
    required_keys = ['name', 'version', 'depends', 'data']
    missing = [k for k in required_keys if k not in manifest]
    
    if missing:
        raise ValueError(f"Missing required keys: {missing}")
    
    # Validate version format
    version = manifest['version']
    if not re.match(r'^\\d+\\.\\d+$', version):
        raise ValueError(f"Invalid version format: {version}")
    
    return True
''',
            'auto_fix_script': '''
def fix_manifest(manifest_path):
    with open(manifest_path) as f:
        content = f.read()
    
    # Add missing keys with defaults
    if "'name'" not in content and '"name"' not in content:
        content = "{'name': 'My Module',\n" + content
    
    if "'version'" not in content:
        content += "\\n'version': '1.0',"
    
    if "'depends'" not in content:
        content += "\\n'depends': ['base'],"
    
    with open(manifest_path, 'w') as f:
        f.write(content)
'''
        }
        
        output_path = self.guardrails_dir / 'GR-INSTALL-004.yaml'
        with open(output_path, 'w') as f:
            yaml.dump(guardrail, f, default_flow_style=False)
        
        return guardrail
    
    def generate_field_sync_guardrail(self):
        """
        GR-POS-001: POS field synchronization
        """
        guardrail = {
            'id': 'GR-POS-001',
            'name': 'POS Field Sync Prevention',
            'description': 'Prevent POS order/line desync when adding custom fields',
            'category': 'field',
            'severity': 'CRITICAL',
            'background': '''
Common Issue: Adding fields to pos.order.line but forgetting to add 
export/import in pos.order causes data loss on session closure.

Affected Models:
- pos.order (parent)
- pos.order.line (child)

Root Cause: POS uses JSON export/import for order data persistence.
Custom fields not included in _export_for_ui() are silently dropped.
''',
            'checks': [
                {
                    'name': 'Check POS Line Fields',
                    'pattern': r'class PosOrderLine.*?:',
                    'error_message': 'Added field to pos.order.line without updating pos.order export',
                    'fix': 'Add field to _export_for_ui() in pos.order'
                }
            ],
            'prevention_script': '''
def check_pos_field_sync(module_path):
    """Validate POS field synchronization"""
    pos_order_line_file = module_path / 'models' / 'pos_order_line.py'
    pos_order_file = module_path / 'models' / 'pos_order.py'
    
    if pos_order_line_file.exists():
        # Extract added fields
        with open(pos_order_line_file) as f:
            line_content = f.read()
            line_fields = re.findall(r'(\\w+)\\s*=\\s*fields\\.', line_content)
        
        # Check if fields are in pos.order export
        with open(pos_order_file) as f:
            order_content = f.read()
            export_func = re.search(
                r'def _export_for_ui.*?return.*?\\}',
                order_content,
                re.DOTALL
            )
            
            if export_func:
                missing_fields = [
                    f for f in line_fields 
                    if f not in export_func.group()
                ]
                
                if missing_fields:
                    raise ValueError(
                        f"Fields {missing_fields} not in _export_for_ui()"
                    )
''',
            'auto_fix_script': '''
def fix_pos_field_sync(pos_order_file, new_field):
    """Auto-add field to POS export"""
    with open(pos_order_file) as f:
        content = f.read()
    
    # Find _export_for_ui method
    pattern = r'(def _export_for_ui.*?return\\s*\\{)(.*?)(\\})'
    
    def add_field(match):
        prefix, fields, suffix = match.groups()
        return f"{prefix}{fields}\\n            '{new_field}': self.{new_field},{suffix}"
    
    fixed_content = re.sub(pattern, add_field, content, flags=re.DOTALL)
    
    with open(pos_order_file, 'w') as f:
        f.write(fixed_content)
'''
        }
        
        output_path = self.guardrails_dir / 'GR-POS-001.yaml'
        with open(output_path, 'w') as f:
            yaml.dump(guardrail, f, default_flow_style=False)
        
        return guardrail
    
    def generate_all_guardrails(self):
        """
        Generate complete guardrail library
        """
        guardrails = [
            self.generate_manifest_guardrail(),
            self.generate_field_sync_guardrail(),
            # Add more based on scraped patterns...
        ]
        
        # Generate index
        index = {
            'total': len(guardrails),
            'categories': {},
            'severity_distribution': {}
        }
        
        for gr in guardrails:
            category = gr['category']
            severity = gr['severity']
            
            if category not in index['categories']:
                index['categories'][category] = []
            index['categories'][category].append(gr['id'])
            
            if severity not in index['severity_distribution']:
                index['severity_distribution'][severity] = 0
            index['severity_distribution'][severity] += 1
        
        # Save index
        with open(self.guardrails_dir / 'index.json', 'w') as f:
            json.dump(index, f, indent=2)
        
        return guardrails

# Usage
generator = GuardrailGenerator(patterns)
guardrails = generator.generate_all_guardrails()

print(f"Generated {len(guardrails)} guardrails")

Auto-Patch System

# apply_autopatch.py
import os
import re
from pathlib import Path

class AutoPatcher:
    def __init__(self, module_path):
        self.module_path = Path(module_path)
        self.applied_patches = []
    
    def detect_issues(self):
        """
        Scan module for known issues
        """
        issues = []
        
        # Check for POS field sync issue
        if self.has_pos_field_issue():
            issues.append({
                'type': 'POS_FIELD_SYNC',
                'severity': 'HIGH',
                'file': 'models/pos_order.py',
                'fix': 'apply_pos_export_import_fix'
            })
        
        # Check for manifest issues
        if self.has_manifest_issue():
            issues.append({
                'type': 'MANIFEST_VALIDATION',
                'severity': 'CRITICAL',
                'file': '__manifest__.py',
                'fix': 'fix_manifest_validation'
            })
        
        # Check for ir.sequence vs. auto-increment
        if self.has_sequence_issue():
            issues.append({
                'type': 'SEQUENCE_NUMBER',
                'severity': 'MEDIUM',
                'file': 'models/*.py',
                'fix': 'switch_to_ir_sequence'
            })
        
        return issues
    
    def has_pos_field_issue(self):
        """
        Detect POS field synchronization issue
        """
        pos_line_file = self.module_path / 'models' / 'pos_order_line.py'
        pos_order_file = self.module_path / 'models' / 'pos_order.py'
        
        if not pos_line_file.exists():
            return False
        
        with open(pos_line_file) as f:
            line_content = f.read()
            # Check if custom fields were added
            if 'fields.' in line_content:
                # Check if _export_for_ui is updated
                if pos_order_file.exists():
                    with open(pos_order_file) as f:
                        order_content = f.read()
                        if '_export_for_ui' not in order_content:
                            return True
        
        return False
    
    def apply_pos_export_import_fix(self):
        """
        Auto-fix: Add field to POS export/import
        """
        pos_order_file = self.module_path / 'models' / 'pos_order.py'
        
        # Extract custom fields from pos.order.line
        pos_line_file = self.module_path / 'models' / 'pos_order_line.py'
        with open(pos_line_file) as f:
            line_content = f.read()
            custom_fields = re.findall(
                r'(\\w+)\\s*=\\s*fields\\.(Char|Integer|Float|Boolean)',
                line_content
            )
        
        # Add to _export_for_ui
        with open(pos_order_file) as f:
            order_content = f.read()
        
        # Generate export code
        export_code = "\\n        ".join([
            f"'{field}': line.{field},"
            for field, _ in custom_fields
        ])
        
        # Insert into _export_for_ui
        fixed_content = order_content.replace(
            "'amount_tax': line.price_subtotal_incl - line.price_subtotal,",
            f"'amount_tax': line.price_subtotal_incl - line.price_subtotal,\\n        {export_code}"
        )
        
        with open(pos_order_file, 'w') as f:
            f.write(fixed_content)
        
        self.applied_patches.append('POS_FIELD_SYNC')
        
        return True
    
    def apply_all_fixes(self):
        """
        Detect and apply all available fixes
        """
        issues = self.detect_issues()
        
        for issue in issues:
            fix_method = getattr(self, issue['fix'])
            success = fix_method()
            
            if success:
                print(f"✅ Applied fix: {issue['type']}")
            else:
                print(f"❌ Failed to apply: {issue['type']}")
        
        return self.applied_patches

# Usage
patcher = AutoPatcher('/path/to/custom_module')
applied = patcher.apply_all_fixes()

print(f"Applied {len(applied)} patches")

Integration Points

With CI/CD (GitHub Actions)

# .github/workflows/odoo-guardrails.yml
name: Odoo Guardrails Check

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Guardrails
        run: |
          python guardrails/check_all.py
      
      - name: Apply Auto-Fixes
        if: failure()
        run: |
          python autopatches/apply_all.py
          git add .
          git commit -m "🤖 Auto-applied fixes"
          git push

With Odoo Development

# Integrate into Odoo module development workflow
from odoo_knowledge_agent import AutoPatcher

# Before deployment
patcher = AutoPatcher('./custom_modules/my_module')
issues = patcher.detect_issues()

if issues:
    print("⚠️ Issues detected:")
    for issue in issues:
        print(f"  - {issue['type']}: {issue['file']}")
    
    # Auto-fix
    patcher.apply_all_fixes()
    print("✅ All issues resolved")

Output Formats

Guardrail YAML

id: GR-POS-001
name: POS Field Sync Prevention
description: Prevent POS order/line desync when adding custom fields
category: field
severity: CRITICAL
checks:
  - name: Check POS Line Fields
    pattern: 'class PosOrderLine.*?:'
    error_message: Added field without updating export
    fix: Add field to _export_for_ui()
prevention_script: |
  def check_pos_field_sync(module_path):
      # Validation logic
auto_fix_script: |
  def fix_pos_field_sync(pos_order_file):
      # Auto-fix logic

Error Knowledge Base

Supabase schema:

CREATE TABLE odoo_solved_threads (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    thread_url TEXT UNIQUE NOT NULL,
    title TEXT NOT NULL,
    question TEXT,
    answer TEXT NOT NULL,
    code_snippets JSONB,
    tags TEXT[],
    views INTEGER,
    error_category TEXT,
    scraped_at TIMESTAMPTZ DEFAULT NOW(),
    embedding vector(1536)
);

CREATE INDEX idx_threads_category ON odoo_solved_threads(error_category);
CREATE INDEX idx_threads_tags ON odoo_solved_threads USING GIN(tags);
CREATE INDEX idx_threads_embedding ON odoo_solved_threads USING ivfflat(embedding vector_cosine_ops);

Examples

Example 1: Pre-Deployment Validation

# Check module before deployment
python guardrails/check_all.py ./custom_modules/expense_management

# Output:
⚠️ 3 issues detected:
  - [CRITICAL] GR-INSTALL-004: Invalid __manifest__.py (missing version)
  - [HIGH] GR-POS-001: POS field sync missing for 'receipt_url'
  - [MEDIUM] GR-ACCT-002: Invoice numbering using auto-increment

✅ Auto-fixes available for all issues. Apply? [y/N]

Example 2: Production Auto-Heal

# Monitor production for known issues
from odoo_knowledge_agent import ProductionMonitor

monitor = ProductionMonitor(odoo_db_uri)

# Detect issues
issues = monitor.check_for_known_issues()

# Auto-apply community-validated fixes
for issue in issues:
    if issue['confidence'] > 0.8:
        monitor.apply_fix(issue['fix_id'])
        monitor.log_fix(issue)

Example 3: Knowledge Base Search

# Search for similar issues
from odoo_knowledge_agent import KnowledgeBase

kb = KnowledgeBase(supabase_url, supabase_key)

# Semantic search using pgvector
similar_issues = kb.search(
    query="POS session won't close after adding custom field",
    limit=5
)

for issue in similar_issues:
    print(f"Thread: {issue['title']}")
    print(f"Solution: {issue['answer'][:200]}...")
    print(f"Similarity: {issue['similarity']:.2f}")

Cost Savings

vs. Manual Debugging

Activity Manual Time With Agent Savings
Debug POS issue 4 hours 5 minutes $150
Fix manifest error 1 hour 2 minutes $40
Research solution 2 hours Instant $80

Monthly Savings (10 issues/month): $2,700
Annual Savings: $32,400

vs. Paid Support

  • Odoo Partner Support: $5,000/year
  • Self-Healing System: $0 (open source)
  • Savings: $5,000/year

Total Annual Value: $37,400

Best Practices

Scraping Frequency

Weekly updates: Keep knowledge base current
Monitor new threads: Catch emerging patterns
Version-specific: Filter by Odoo version

Guardrail Deployment

Pre-commit hooks: Block bad code before commit
CI/CD integration: Validate in pipeline
IDE plugins: Real-time error prevention

Auto-Patch Safety

Test in staging first: Never auto-patch production directly
Version control: All patches in Git
Rollback plan: Keep fix history

Troubleshooting

Scraping Blocked

Issue: Odoo forum blocks scraper IP
Fix: Use Firecrawl with rotating proxies, add delays

False Positives

Issue: Guardrail blocks valid code
Fix: Whitelist patterns, tune regex

Auto-Fix Breaks Code

Issue: Patch causes new errors
Fix: Improve test coverage, manual review high-risk fixes

License

Apache 2.0

References


Learn from 1000+ community solutions. Prevent errors before they happen. Auto-heal production issues. Build better Odoo systems. 🚀