Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:04 +08:00
commit 3e809e35ad
41 changed files with 10010 additions and 0 deletions

View 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