fix error in gitignore

This commit is contained in:
2026-09-24 12:32:59 +03:30
commit 86c117ee2a
165 changed files with 5963 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# odoo integration (service layer, not a Django app)
+115
View File
@@ -0,0 +1,115 @@
"""Odoo client abstraction.
Cetana never spreads raw Odoo calls through the codebase. Everything goes
through this client behind an interface. The concrete client is chosen at
runtime from the dynamic settings store (apps.core.settings_service), so the
integration can be configured and switched from the admin panel without a
deploy — with a mock fallback when Odoo is not configured.
"""
import xmlrpc.client
from typing import Protocol
from apps.core.settings_service import SettingsService
class OdooClientInterface(Protocol):
def search_partner(self, key: str, value) -> dict | None: ...
def create_partner(self, payload: dict) -> dict: ...
def create_invoice(self, payload: dict) -> dict: ...
class MockOdooClient:
"""Deterministic in-process stand-in used when Odoo is not configured."""
def search_partner(self, key: str, value) -> dict | None:
return None
def create_partner(self, payload: dict) -> dict:
return {"id": f"MOCK-PARTNER-{abs(hash(str(payload))) % 10_000_000}"}
def create_invoice(self, payload: dict) -> dict:
return {"id": f"MOCK-INV-{abs(hash(str(payload))) % 10_000_000}"}
class OdooXmlRpcClient:
"""Real Odoo client over the standard XML-RPC API (stdlib only)."""
def __init__(self, url, db, username, password="", api_key="", auth_method="password"):
self.url = (url or "").rstrip("/")
self.db = db
self.username = username
self.password = password
self.api_key = api_key
self.auth_method = auth_method
self._uid = None
def _authenticate(self):
common = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
secret = self.api_key if self.auth_method == "api_key" else self.password
self._uid = common.authenticate(self.db, self.username, secret, {})
return self._uid
def _execute(self, model, method, method_args, kwargs=None):
if self._uid is None:
self._authenticate()
secret = self.api_key if self.auth_method == "api_key" else self.password
models = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/object")
return models.execute_kw(self.db, self._uid, secret, model, method, method_args, kwargs or {})
def search_partner(self, key: str, value) -> dict | None:
results = self._execute(
"res.partner", "search_read",
[[[key, "=", value]]],
{"limit": 1, "fields": ["id", "name"]},
)
return results[0] if results else None
def create_partner(self, payload: dict) -> dict:
partner_id = self._execute("res.partner", "create", [payload])
return {"id": partner_id}
def create_invoice(self, payload: dict) -> dict:
ref = payload.get("partner_ref") or ""
partner = None
if ref:
partner = self.search_partner("ref", ref) or self.search_partner("name", ref)
if partner is None and ref:
partner = {"id": self._execute("res.partner", "create", [{"name": ref}])}
vals = {
"move_type": "out_invoice",
"ref": payload.get("reference"),
"partner_id": partner["id"] if partner else False,
"invoice_line_ids": [(0, 0, {
"name": payload.get("reference") or "Processing",
"quantity": 1,
"price_unit": float(payload.get("amount") or 0),
})],
}
invoice_id = self._execute("account.move", "create", [vals])
return {"id": invoice_id}
def test_connection(self) -> dict:
"""Validate reachability + credentials. Returns server version and uid."""
common = xmlrpc.client.ServerProxy(f"{self.url}/xmlrpc/2/common")
version = common.version()
uid = self._authenticate()
if not uid:
raise ValueError("Authentication failed — check database, username and secret.")
return {"version": version, "uid": uid}
def get_odoo_client() -> OdooClientInterface:
service = SettingsService()
if not service.get_bool("odoo.enabled", False):
return MockOdooClient()
url = service.get("odoo.url", "")
if not url:
return MockOdooClient()
return OdooXmlRpcClient(
url=url,
db=service.get("odoo.database", ""),
username=service.get("odoo.username", ""),
password=service.get("odoo.password", ""),
api_key=service.get("odoo.api_key", ""),
auth_method=service.get("odoo.auth_method", "password"),
)
+26
View File
@@ -0,0 +1,26 @@
from apps.integrations.odoo.client import OdooClientInterface, get_odoo_client
class OdooPartnerService:
def __init__(self, client: OdooClientInterface | None = None):
self.client = client or get_odoo_client()
def lookup(self, key: str, value) -> dict | None:
return self.client.search_partner(key, value)
def create(self, payload: dict) -> dict:
return self.client.create_partner(payload)
class OdooInvoiceService:
def __init__(self, client: OdooClientInterface | None = None):
self.client = client or get_odoo_client()
def sync_invoice(self, invoice) -> dict:
payload = {
"reference": invoice.number,
"amount": str(invoice.amount),
"currency": invoice.currency,
"partner_ref": invoice.order.customer.odoo_partner_id or "",
}
return self.client.create_invoice(payload)