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