fix error in gitignore
This commit is contained in:
@@ -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"),
|
||||
)
|
||||
Reference in New Issue
Block a user