Files
social-media-automation/app/services/odoo_service.py
Consultoría AS ecc2ca73ea feat: Add Analytics, Odoo Integration, A/B Testing, and Content features
Phase 1 - Analytics y Reportes:
- PostMetrics and AnalyticsReport models for tracking engagement
- Analytics service with dashboard stats, top posts, optimal times
- 8 API endpoints at /api/analytics/*
- Interactive dashboard with Chart.js charts
- Celery tasks for metrics fetch (15min) and weekly reports

Phase 2 - Integración Odoo:
- Lead and OdooSyncLog models for CRM integration
- Odoo fields added to Product and Service models
- XML-RPC service for bidirectional sync
- Lead management API at /api/leads/*
- Leads dashboard template
- Celery tasks for product/service sync and lead export

Phase 3 - A/B Testing y Recycling:
- ABTest, ABTestVariant, RecycledPost models
- Statistical winner analysis using chi-square test
- Content recycling with engagement-based scoring
- APIs at /api/ab-tests/* and /api/recycling/*
- Automated test evaluation and content recycling tasks

Phase 4 - Thread Series y Templates:
- ThreadSeries and ThreadPost models for multi-post threads
- AI-powered thread generation
- Enhanced ImageTemplate with HTML template support
- APIs at /api/threads/* and /api/templates/*
- Thread scheduling with reply chain support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 03:10:42 +00:00

494 lines
16 KiB
Python

"""
Odoo Integration Service - Sync products, services, and leads with Odoo ERP.
"""
import xmlrpc.client
from datetime import datetime
from typing import List, Dict, Optional, Any
import logging
from app.core.config import settings
from app.core.database import SessionLocal
from app.models.product import Product
from app.models.service import Service
from app.models.lead import Lead
from app.models.odoo_sync_log import OdooSyncLog
logger = logging.getLogger(__name__)
class OdooService:
"""Service for Odoo ERP integration."""
def __init__(self):
self.url = settings.ODOO_URL
self.db = settings.ODOO_DB
self.username = settings.ODOO_USERNAME
self.password = settings.ODOO_PASSWORD
self.enabled = settings.ODOO_SYNC_ENABLED
self._uid = None
self._common = None
self._models = None
def _get_db(self):
"""Get database session."""
return SessionLocal()
def _connect(self) -> bool:
"""Establish connection to Odoo."""
if not self.enabled or not all([self.url, self.db, self.username, self.password]):
logger.warning("Odoo not configured or disabled")
return False
try:
# Common endpoint for authentication
self._common = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
# Authenticate
self._uid = self._common.authenticate(
self.db, self.username, self.password, {}
)
if not self._uid:
logger.error("Odoo authentication failed")
return False
# Models endpoint for data operations
self._models = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/object")
return True
except Exception as e:
logger.error(f"Odoo connection error: {e}")
return False
def _execute(self, model: str, method: str, *args, **kwargs) -> Any:
"""Execute an Odoo model method."""
if not self._models or not self._uid:
if not self._connect():
raise ConnectionError("Cannot connect to Odoo")
return self._models.execute_kw(
self.db, self._uid, self.password,
model, method, list(args), kwargs
)
async def test_connection(self) -> Dict:
"""Test connection to Odoo."""
if not self.enabled:
return {
"connected": False,
"error": "Odoo integration not enabled",
"configured": bool(self.url)
}
try:
if self._connect():
version = self._common.version()
return {
"connected": True,
"version": version.get("server_version"),
"uid": self._uid
}
else:
return {
"connected": False,
"error": "Authentication failed"
}
except Exception as e:
return {
"connected": False,
"error": str(e)
}
async def sync_products(self, limit: int = 100) -> Dict:
"""
Sync products from Odoo to local database.
Returns:
Dict with sync statistics
"""
db = self._get_db()
log = OdooSyncLog(
sync_type="products",
direction="import",
status="started"
)
db.add(log)
db.commit()
try:
if not self._connect():
log.mark_failed("Cannot connect to Odoo")
db.commit()
return {"success": False, "error": "Connection failed"}
# Fetch products from Odoo
product_ids = self._execute(
"product.template", "search",
[["sale_ok", "=", True], ["active", "=", True]],
limit=limit
)
products_data = self._execute(
"product.template", "read",
product_ids,
fields=["id", "name", "description_sale", "list_price", "categ_id",
"image_1920", "qty_available", "default_code"]
)
created = 0
updated = 0
failed = 0
for odoo_product in products_data:
try:
# Check if product exists locally
local_product = db.query(Product).filter(
Product.odoo_product_id == odoo_product["id"]
).first()
product_data = {
"name": odoo_product["name"],
"description": odoo_product.get("description_sale") or "",
"price": odoo_product.get("list_price", 0),
"category": odoo_product.get("categ_id", [0, "general"])[1] if odoo_product.get("categ_id") else "general",
"stock": int(odoo_product.get("qty_available", 0)),
"is_available": odoo_product.get("qty_available", 0) > 0,
"odoo_product_id": odoo_product["id"],
"odoo_last_synced": datetime.utcnow()
}
if local_product:
# Update existing
for key, value in product_data.items():
setattr(local_product, key, value)
updated += 1
else:
# Create new
local_product = Product(**product_data)
db.add(local_product)
created += 1
except Exception as e:
logger.error(f"Error syncing product {odoo_product.get('id')}: {e}")
failed += 1
db.commit()
log.records_processed = len(products_data)
log.records_created = created
log.records_updated = updated
log.records_failed = failed
log.mark_completed()
db.commit()
return {
"success": True,
"processed": len(products_data),
"created": created,
"updated": updated,
"failed": failed
}
except Exception as e:
logger.error(f"Error in sync_products: {e}")
log.mark_failed(str(e))
db.commit()
return {"success": False, "error": str(e)}
finally:
db.close()
async def sync_services(self, limit: int = 100) -> Dict:
"""
Sync services from Odoo to local database.
Services in Odoo are typically products with type='service'.
Returns:
Dict with sync statistics
"""
db = self._get_db()
log = OdooSyncLog(
sync_type="services",
direction="import",
status="started"
)
db.add(log)
db.commit()
try:
if not self._connect():
log.mark_failed("Cannot connect to Odoo")
db.commit()
return {"success": False, "error": "Connection failed"}
# Fetch service-type products from Odoo
service_ids = self._execute(
"product.template", "search",
[["type", "=", "service"], ["sale_ok", "=", True], ["active", "=", True]],
limit=limit
)
services_data = self._execute(
"product.template", "read",
service_ids,
fields=["id", "name", "description_sale", "list_price", "categ_id"]
)
created = 0
updated = 0
failed = 0
for odoo_service in services_data:
try:
# Check if service exists locally
local_service = db.query(Service).filter(
Service.odoo_service_id == odoo_service["id"]
).first()
service_data = {
"name": odoo_service["name"],
"description": odoo_service.get("description_sale") or "",
"category": odoo_service.get("categ_id", [0, "general"])[1] if odoo_service.get("categ_id") else "general",
"price_range": f"${odoo_service.get('list_price', 0):,.0f} MXN" if odoo_service.get("list_price") else None,
"odoo_service_id": odoo_service["id"],
"odoo_last_synced": datetime.utcnow()
}
if local_service:
# Update existing
for key, value in service_data.items():
setattr(local_service, key, value)
updated += 1
else:
# Create new
local_service = Service(**service_data)
db.add(local_service)
created += 1
except Exception as e:
logger.error(f"Error syncing service {odoo_service.get('id')}: {e}")
failed += 1
db.commit()
log.records_processed = len(services_data)
log.records_created = created
log.records_updated = updated
log.records_failed = failed
log.mark_completed()
db.commit()
return {
"success": True,
"processed": len(services_data),
"created": created,
"updated": updated,
"failed": failed
}
except Exception as e:
logger.error(f"Error in sync_services: {e}")
log.mark_failed(str(e))
db.commit()
return {"success": False, "error": str(e)}
finally:
db.close()
async def create_lead(self, lead: Lead) -> Dict:
"""
Create a lead in Odoo CRM.
Args:
lead: Local lead object
Returns:
Dict with Odoo lead ID and status
"""
try:
if not self._connect():
return {"success": False, "error": "Cannot connect to Odoo"}
# Prepare lead data for Odoo
odoo_lead_data = {
"name": lead.interest or f"Lead from {lead.platform}",
"contact_name": lead.name or lead.username,
"email_from": lead.email,
"phone": lead.phone,
"partner_name": lead.company,
"description": f"""
Source: {lead.platform}
Username: @{lead.username}
Profile: {lead.profile_url}
Original Content:
{lead.source_content}
Notes:
{lead.notes or 'No notes'}
""".strip(),
"type": "lead",
"priority": "2" if lead.priority == "high" else "1" if lead.priority == "medium" else "0",
}
# Create lead in Odoo
odoo_lead_id = self._execute(
"crm.lead", "create",
[odoo_lead_data]
)
return {
"success": True,
"odoo_lead_id": odoo_lead_id
}
except Exception as e:
logger.error(f"Error creating Odoo lead: {e}")
return {"success": False, "error": str(e)}
async def export_leads_to_odoo(self) -> Dict:
"""
Export unsynced leads to Odoo CRM.
Returns:
Dict with export statistics
"""
db = self._get_db()
log = OdooSyncLog(
sync_type="leads",
direction="export",
status="started"
)
db.add(log)
db.commit()
try:
if not self._connect():
log.mark_failed("Cannot connect to Odoo")
db.commit()
return {"success": False, "error": "Connection failed"}
# Get unsynced leads
unsynced_leads = db.query(Lead).filter(
Lead.synced_to_odoo == False
).all()
created = 0
failed = 0
errors = []
for lead in unsynced_leads:
try:
result = await self.create_lead(lead)
if result["success"]:
lead.odoo_lead_id = result["odoo_lead_id"]
lead.synced_to_odoo = True
lead.odoo_synced_at = datetime.utcnow()
created += 1
else:
failed += 1
errors.append({
"lead_id": lead.id,
"error": result.get("error")
})
except Exception as e:
logger.error(f"Error exporting lead {lead.id}: {e}")
failed += 1
errors.append({
"lead_id": lead.id,
"error": str(e)
})
db.commit()
log.records_processed = len(unsynced_leads)
log.records_created = created
log.records_failed = failed
if errors:
log.error_details = errors
log.mark_completed()
db.commit()
return {
"success": True,
"processed": len(unsynced_leads),
"created": created,
"failed": failed,
"errors": errors if errors else None
}
except Exception as e:
logger.error(f"Error in export_leads_to_odoo: {e}")
log.mark_failed(str(e))
db.commit()
return {"success": False, "error": str(e)}
finally:
db.close()
async def get_sales_summary(self, days: int = 30) -> Dict:
"""
Get sales summary from Odoo.
Args:
days: Number of days to look back
Returns:
Dict with sales statistics
"""
try:
if not self._connect():
return {"success": False, "error": "Cannot connect to Odoo"}
from datetime import timedelta
start_date = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d")
# Get confirmed sales orders
order_ids = self._execute(
"sale.order", "search",
[["state", "in", ["sale", "done"]], ["date_order", ">=", start_date]]
)
orders_data = self._execute(
"sale.order", "read",
order_ids,
fields=["id", "name", "amount_total", "date_order", "partner_id", "state"]
)
total_revenue = sum(order.get("amount_total", 0) for order in orders_data)
total_orders = len(orders_data)
return {
"success": True,
"period_days": days,
"total_orders": total_orders,
"total_revenue": total_revenue,
"avg_order_value": total_revenue / total_orders if total_orders > 0 else 0,
"orders": orders_data[:10] # Return latest 10
}
except Exception as e:
logger.error(f"Error getting sales summary: {e}")
return {"success": False, "error": str(e)}
async def get_sync_logs(self, limit: int = 20) -> List[Dict]:
"""Get recent sync logs."""
db = self._get_db()
try:
logs = db.query(OdooSyncLog).order_by(
OdooSyncLog.started_at.desc()
).limit(limit).all()
return [log.to_dict() for log in logs]
finally:
db.close()
# Global instance
odoo_service = OdooService()