Initial commit
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
# 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
|
||||
<record id="queue_job_function_sync_products" model="queue.job.function">
|
||||
<field name="name">myconnector.backend.sync_products</field>
|
||||
<field name="channel_id" ref="queue_job.channel_root"/>
|
||||
</record>
|
||||
|
||||
# 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
|
||||
<record id="queue_job_function_sync_products" model="queue.job.function">
|
||||
<field name="retry_pattern">{1: 60, 5: 300, 10: 600}</field>
|
||||
<!-- Retry after 60s, 300s, 600s -->
|
||||
</record>
|
||||
```
|
||||
|
||||
### 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 |
|
||||
Reference in New Issue
Block a user