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