Files
gh-jamshu-jamshi-marketplac…/skills/odoo-connector-module-creator/references/architecture.md
2025-11-29 18:50:04 +08:00

14 KiB

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:

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:

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:

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:

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:

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:

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:

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:

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:

# 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:

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:

# 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:

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:

<form>
    <header>
        <button name="action_test_connection" type="object" string="Test Connection"/>
        <button name="action_sync_all" type="object" string="Sync All"/>
    </header>
    <sheet>
        <group name="api_config" string="API Configuration">
            <field name="api_url"/>
            <field name="api_key" password="True"/>
        </group>
        <notebook>
            <page name="sync" string="Synchronization">
                <!-- Sync settings -->
            </page>
            <page name="advanced" string="Advanced">
                <!-- Advanced settings -->
            </page>
        </notebook>
    </sheet>
</form>

Odoo Model File Locations Reference

When building connectors, you'll interact with these core Odoo models:

Product Models

  • product.template - Product template (variants container)
  • product.product - Product variant
  • product.category - Product categories

Sales Models

  • sale.order - Sales orders
  • sale.order.line - Order lines
  • res.partner - Customers/contacts

Inventory Models

  • stock.picking - Stock transfers
  • stock.move - Stock movements
  • stock.quant - Inventory quantities
  • stock.warehouse - Warehouses
  • stock.location - Stock locations

Accounting Models

  • account.move - Invoices/bills
  • account.payment - Payments
  • account.tax - Taxes

Best Practices

  1. Always use bindings - Never directly modify Odoo records from external data
  2. Use queue jobs - For any operation that might take >2 seconds
  3. Implement retry logic - Use binding's retry_count and max_retries
  4. Log extensively - Use _logger for debugging
  5. Handle API errors - Wrap adapter calls in try/except
  6. Validate data - Check required fields before creating/updating
  7. Use transactions - Leverage Odoo's automatic transaction management
  8. Test connection - Always implement _test_connection_implementation()
  9. Document API - Add docstrings to all adapter methods
  10. Follow naming conventions - Use consistent model/component names