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
_inheritsto 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 backendexternal_id- ID in external systemsync_date- Last sync timestampsync_status- pending/in_progress/success/failed/skippedretry_count- Number of retry attemptslast_error- Last error message
Generic Binding Methods:
mark_sync_success()- Mark record as successfully syncedmark_sync_failed(error_msg)- Mark record as failed with errorcan_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 requestpost(endpoint, data=None, **kwargs)- POST requestput(endpoint, data=None, **kwargs)- PUT requestdelete(endpoint, **kwargs)- DELETE requestmake_request(method, endpoint, **kwargs)- Generic request
Helper Methods:
build_url(endpoint)- Construct full URLget_api_headers()- Get request headersget_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 accessgroup_generic_connector_manager- Configuration accessgroup_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 variantproduct.category- Product categories
Sales Models
sale.order- Sales orderssale.order.line- Order linesres.partner- Customers/contacts
Inventory Models
stock.picking- Stock transfersstock.move- Stock movementsstock.quant- Inventory quantitiesstock.warehouse- Warehousesstock.location- Stock locations
Accounting Models
account.move- Invoices/billsaccount.payment- Paymentsaccount.tax- Taxes
Best Practices
- Always use bindings - Never directly modify Odoo records from external data
- Use queue jobs - For any operation that might take >2 seconds
- Implement retry logic - Use binding's retry_count and max_retries
- Log extensively - Use
_loggerfor debugging - Handle API errors - Wrap adapter calls in try/except
- Validate data - Check required fields before creating/updating
- Use transactions - Leverage Odoo's automatic transaction management
- Test connection - Always implement
_test_connection_implementation() - Document API - Add docstrings to all adapter methods
- Follow naming conventions - Use consistent model/component names