# Troubleshooting Guide for Odoo Connectors
## Common Issues and Solutions
### 1. Connection Issues
#### Problem: "Connection test failed" or timeout errors
**Possible Causes**:
- Incorrect API URL
- Invalid API credentials
- Network/firewall blocking requests
- API endpoint not accessible
**Solutions**:
```python
# 1. Verify API URL format
api_url = backend.api_url
print(f"Testing connection to: {api_url}")
# 2. Test with curl/requests directly
import requests
response = requests.get(f"{api_url}/health", timeout=10)
print(f"Status: {response.status_code}")
# 3. Check credentials
adapter = backend.get_adapter('backend.adapter')
headers = adapter.get_api_headers()
print(f"Headers: {headers}") # Don't log in production!
# 4. Add detailed logging
import logging
logging.getLogger('requests').setLevel(logging.DEBUG)
```
#### Problem: SSL Certificate verification failed
**Solution**:
```python
# Temporary: Disable SSL verification (NOT for production!)
def make_request(self, method, endpoint, **kwargs):
kwargs['verify'] = False
return super().make_request(method, endpoint, **kwargs)
# Production: Add CA certificate
import certifi
kwargs['verify'] = certifi.where()
```
### 2. Authentication Issues
#### Problem: 401 Unauthorized
**Diagnosis**:
```python
# Check token expiry
if backend.token_expires_at:
from datetime import datetime
is_expired = datetime.now() >= backend.token_expires_at
print(f"Token expired: {is_expired}")
# Check authentication header
adapter = backend.get_adapter('backend.adapter')
headers = adapter.get_api_headers()
print(f"Auth header: {headers.get('Authorization', 'MISSING')}")
```
**Solutions**:
```python
# 1. Refresh OAuth token
backend.refresh_access_token()
# 2. Re-authenticate
backend.action_start_oauth_flow()
# 3. Verify API key is correct
# Go to backend form and re-enter API key
```
#### Problem: OAuth callback not working
**Common Issues**:
- Redirect URI mismatch
- State parameter validation failed
- CORS issues
**Solutions**:
```python
# 1. Check redirect URI matches exactly
print(f"Configured: {backend.oauth_redirect_uri}")
print(f"Expected: https://yourodoo.com/myconnector/oauth/callback")
# 2. Disable state validation temporarily for debugging
def exchange_code_for_token(self, code, state):
# Skip state validation
# if state != stored_state:
# raise ValueError('Invalid OAuth state')
...
# 3. Add CORS headers in controller
@http.route('/myconnector/oauth/callback', cors='*')
```
### 3. Import/Sync Issues
#### Problem: Records not importing
**Diagnosis**:
```python
# 1. Check if importer is registered
with backend.work_on('myconnector.product.template') as work:
try:
importer = work.component(usage='record.importer')
print(f"Importer found: {importer._name}")
except ComponentNotFound:
print("ERROR: Importer component not registered!")
# 2. Check adapter methods
adapter = work.component(usage='backend.adapter')
products = adapter.get_products()
print(f"Fetched {len(products)} products from API")
# 3. Test mapper
mapper = work.component(usage='import.mapper')
if products:
mapped = mapper.map_record(products[0])
print(f"Mapped data: {mapped.values()}")
```
**Solutions**:
```python
# 1. Register component properly
class ProductImporter(GenericImporter):
_name = 'myconnector.product.importer'
_inherit = 'generic.importer'
_apply_on = 'myconnector.product.template' # Must match model!
_usage = 'record.importer' # Required!
# 2. Check model name consistency
# Backend: myconnector.backend
# Binding: myconnector.product.template
# Component: _apply_on = 'myconnector.product.template'
# 3. Add logging
def _import_record(self, external_id, force=False):
_logger.info("Importing product %s", external_id)
# ... import logic
_logger.info("Successfully imported product %s", external_id)
```
#### Problem: Duplicate records created
**Cause**: External ID not properly set or constraint not working
**Solution**:
```python
# 1. Verify SQL constraint
class ProductBinding(models.Model):
_sql_constraints = [
('backend_external_uniq',
'unique(backend_id, external_id)',
'Product must be unique per backend')
]
# 2. Check external ID is set
def _import_record(self, external_id, force=False):
# Always set external_id in mapped data
mapped_data = mapper.map_record(external_data).values()
if 'external_id' not in mapped_data:
mapped_data['external_id'] = str(external_id)
# 3. Search for existing binding before creating
binding = self.env['myconnector.product.template'].search([
('backend_id', '=', backend.id),
('external_id', '=', str(external_id))
], limit=1)
if binding:
binding.write(mapped_data)
else:
binding = self.env['myconnector.product.template'].create(mapped_data)
```
### 4. Export Issues
#### Problem: Records not exporting to external system
**Diagnosis**:
```python
# 1. Check exporter is registered
with backend.work_on('myconnector.product.template') as work:
exporter = work.component(usage='record.exporter')
# 2. Test export mapper
mapper = work.component(usage='export.mapper')
external_data = mapper.map_record(binding).values()
print(f"Export data: {json.dumps(external_data, indent=2)}")
# 3. Test adapter create method
adapter = work.component(usage='backend.adapter')
result = adapter.create_product(external_data)
print(f"Created external ID: {result.get('id')}")
```
**Solutions**:
```python
# 1. Check no_export flag
binding.write({'no_export': False})
# 2. Ensure export mapper returns correct format
class ProductExportMapper(GenericExportMapper):
direct = [
('name', 'title'), # Odoo field -> External field
('list_price', 'price'),
]
# 3. Handle API response correctly
def _export_record(self, binding):
mapper = self.component(usage='export.mapper')
data = mapper.map_record(binding).values()
adapter = self.component(usage='backend.adapter')
if binding.external_id:
adapter.update_product(binding.external_id, data)
else:
result = adapter.create_product(data)
# Save external ID!
binding.write({'external_id': str(result['id'])})
```
### 5. Queue Job Issues
#### Problem: Queue jobs not running
**Diagnosis**:
```bash
# 1. Check queue job workers are running
ps aux | grep odoo
# 2. Check queued jobs
# Go to Queue > Jobs in Odoo UI
# 3. Check job configuration
# Settings > Technical > Queue Jobs > Functions
```
**Solutions**:
```python
# 1. Ensure queue_job is installed and loaded
# In odoo.conf:
# server_wide_modules = base,web,queue_job
# 2. Start job runner
# odoo-bin -c odoo.conf --workers=2
# 3. Register job functions
# In data/queue_job_function_data.xml
myconnector.backend.sync_products
# 4. Use with_delay correctly
backend.with_delay().sync_products() # Correct
backend.sync_products() # Wrong - runs synchronously
```
#### Problem: Jobs failing silently
**Solution**:
```python
# 1. Check job logs
# Queue > Jobs > Failed
# Click on job to see error details
# 2. Add try/except with logging
@job
def sync_products(self):
try:
# Sync logic
_logger.info("Product sync completed successfully")
except Exception as e:
_logger.exception("Product sync failed")
raise # Re-raise to mark job as failed
# 3. Configure retry pattern
{1: 60, 5: 300, 10: 600}
```
### 6. Webhook Issues
#### Problem: Webhooks not received
**Diagnosis**:
```bash
# 1. Check route is registered
# In Odoo shell:
routes = request.env['ir.http']._get_routes()
webhook_routes = [r for r in routes if 'webhook' in r]
print(webhook_routes)
# 2. Test webhook endpoint manually
curl -X POST https://yourodoo.com/myconnector/webhook \
-H "Content-Type: application/json" \
-H "X-API-Key: your_key" \
-d '{"test": "data"}'
# 3. Check webhook URL configuration
print(f"Webhook URL: {backend.webhook_url}")
# Ensure this matches the URL configured in external system
```
**Solutions**:
```python
# 1. Ensure controller is registered
class WebhookController(http.Controller):
@http.route('/myconnector/webhook', type='json', auth='none', csrf=False)
def webhook(self):
# IMPORTANT: auth='none', csrf=False for external calls
...
# 2. Update module to load controllers
# In __init__.py:
from . import controllers
# 3. Check firewall/reverse proxy allows POST to webhook URL
# 4. Add debug logging
@http.route('/myconnector/webhook', type='json', auth='none', csrf=False)
def webhook(self):
_logger.info("Webhook received: %s", request.jsonrequest)
...
```
#### Problem: Webhook signature verification failing
**Diagnosis**:
```python
# In controller:
payload = request.httprequest.get_data(as_text=True)
signature = request.httprequest.headers.get('X-Signature')
secret = backend.webhook_secret
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
print(f"Received signature: {signature}")
print(f"Expected signature: {expected}")
print(f"Match: {signature == expected}")
```
**Solutions**:
```python
# 1. Ensure secret matches between Odoo and external system
# 2. Check signature algorithm matches
# Some systems use base64, others hex
# 3. Verify payload encoding
# Use raw payload, not parsed JSON
# 4. Check header name
# Could be X-Signature, X-Webhook-Signature, etc.
# 5. Temporarily disable verification for debugging
if not self._verify_signature(...):
_logger.warning("Signature verification failed, but processing anyway")
# return {'error': 'Invalid signature'}, 401
```
### 7. Data Mapping Issues
#### Problem: Fields not mapping correctly
**Diagnosis**:
```python
# Test mapper in isolation
mapper = ProductImportMapper(work)
external_data = {
'id': 123,
'title': 'Test Product',
'price': 99.99,
}
mapped = mapper.map_record(external_data)
print(f"Mapped values: {mapped.values()}")
# Check each mapping method
for method_name in dir(mapper):
if hasattr(getattr(mapper, method_name), '_mapping'):
result = getattr(mapper, method_name)(external_data)
print(f"{method_name}: {result}")
```
**Solutions**:
```python
# 1. Use @mapping decorator
@mapping
def product_name(self, record):
return {'name': record['title']} # Must return dict!
# 2. Handle missing fields
@mapping
def category(self, record):
category_name = record.get('category', {}).get('name')
if not category_name:
return {} # Return empty dict, not None
# 3. Use only_create for default values
@only_create
def default_code(self, record):
return {'default_code': record.get('sku', 'SKU_MISSING')}
```
### 8. Performance Issues
#### Problem: Sync takes too long
**Diagnosis**:
```python
import time
def sync_products(self):
start = time.time()
# Time each step
t1 = time.time()
products = adapter.get_products()
print(f"Fetch: {time.time() - t1:.2f}s for {len(products)} products")
t2 = time.time()
for product in products:
self.import_product(product['id'])
print(f"Import: {time.time() - t2:.2f}s")
print(f"Total: {time.time() - start:.2f}s")
```
**Solutions**:
```python
# 1. Use batch operations
def sync_products(self):
# Fetch all products at once (if API supports)
products = adapter.get_all_products()
# Process in batches
batch_size = 100
for i in range(0, len(products), batch_size):
batch = products[i:i+batch_size]
self.with_delay().import_product_batch(batch)
# 2. Reduce database queries
# Use search_read instead of browse
products = env['product.template'].search_read(
[('id', 'in', product_ids)],
['name', 'list_price']
)
# 3. Use SQL for bulk operations
self.env.cr.execute("""
UPDATE myconnector_product_template
SET sync_status = 'success'
WHERE backend_id = %s
""", (backend.id,))
# 4. Disable expensive computations during import
# Use context flags
binding.with_context(skip_compute=True).write(values)
```
### 9. Module Installation Issues
#### Problem: Module won't install/upgrade
**Common Errors**:
```
ParseError: Invalid XML
SyntaxError: Invalid Python syntax
ProgrammingError: relation does not exist
```
**Solutions**:
```bash
# 1. Check logs
tail -f /var/log/odoo/odoo.log
# 2. Validate XML syntax
xmllint --noout views/*.xml
# 3. Check Python syntax
python3 -m py_compile models/*.py
# 4. Drop and recreate database (dev only!)
dropdb test_db
createdb test_db
odoo-bin -c odoo.conf -d test_db -i myconnector
# 5. Update with stop-after-init to see errors
odoo-bin -c odoo.conf -d test_db -u myconnector --stop-after-init
# 6. Check dependencies
# In __manifest__.py, ensure all 'depends' modules are installed
```
### 10. Debugging Tips
#### Enable Debug Logging
```python
# In code:
import logging
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.DEBUG)
# In odoo.conf:
log_level = debug
log_handler = :DEBUG
```
#### Use Odoo Shell
```bash
odoo-bin shell -c odoo.conf -d your_db
>>> backend = env['myconnector.backend'].browse(1)
>>> backend.sync_products()
>>> env.cr.rollback() # Rollback changes
```
#### Use pdb Debugger
```python
def sync_products(self):
import pdb; pdb.set_trace() # Debugger will pause here
products = adapter.get_products()
...
```
#### Monitor API Calls
```python
# Add request/response logging
def make_request(self, method, endpoint, **kwargs):
_logger.debug("Request: %s %s", method, endpoint)
_logger.debug("Params: %s", kwargs.get('params'))
_logger.debug("Data: %s", kwargs.get('data'))
response = super().make_request(method, endpoint, **kwargs)
_logger.debug("Response status: %s", response.status_code if hasattr(response, 'status_code') else 'N/A')
_logger.debug("Response data: %s", response[:500] if isinstance(response, str) else str(response)[:500])
return response
```
## Error Reference
| Error | Cause | Solution |
|-------|-------|----------|
| ComponentNotFound | Component not registered | Check `_name`, `_apply_on`, `_usage` |
| MissingError | Record deleted | Check `exists()` before operations |
| AccessError | Permission denied | Check security rules and groups |
| ValidationError | Constraint violated | Check required fields and constraints |
| HTTPError 401 | Invalid credentials | Refresh tokens or re-authenticate |
| HTTPError 429 | Rate limited | Implement rate limiting and backoff |
| HTTPError 500 | Server error | Retry with exponential backoff |
| TypeError in mapper | Wrong return type | Mappers must return dict |
| IntegrityError | Duplicate key | Check SQL constraints |
| JSONDecodeError | Invalid JSON | Check API response format |