"""Cliente Odoo 16 XML-RPC — atribuição de perfis via res.groups / res.users.""" from __future__ import annotations import os import secrets import xmlrpc.client from typing import Any from app.vm123.role_map import DESK_ROLE_ODOO_GROUP_NAMES, DESK_ROLE_ODOO_XMLIDS ODOO_URL = os.getenv("ODOO_URL", "http://10.10.10.123:8069").rstrip("/") ODOO_DB = os.getenv("ODOO_DB", "ligbox") ODOO_LOGIN = os.getenv("ODOO_LOGIN", "admin@ligbox.com.br") ODOO_API_KEY = os.getenv("ODOO_API_KEY", os.getenv("ODOO_PASSWORD", "")) ODOO_PUBLIC_URL = os.getenv( "ODOO_PUBLIC_URL", "https://financeiro.ligbox.com.br/odoo/web/login?db=ligbox", ) class OdooConfigError(RuntimeError): pass class OdooProvisionError(RuntimeError): pass def _configured() -> bool: return bool(ODOO_API_KEY and ODOO_LOGIN and ODOO_DB) def _client(): if not _configured(): raise OdooConfigError("ODOO_LOGIN / ODOO_API_KEY não configurados no Desk") common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common", allow_none=True) uid = common.authenticate(ODOO_DB, ODOO_LOGIN, ODOO_API_KEY, {}) if not uid: raise OdooConfigError("falha autenticação Odoo — verifique credenciais") models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object", allow_none=True) return uid, models def _resolve_xmlid(uid: int, models, xmlid: str) -> int | None: if "." not in xmlid: return None module, name = xmlid.split(".", 1) rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "ir.model.data", "search_read", [[("module", "=", module), ("name", "=", name)]], {"fields": ["res_id"], "limit": 1}, ) if rows: return int(rows[0]["res_id"]) return None def _resolve_group_names(uid: int, models, names: tuple[str, ...]) -> list[int]: ids: list[int] = [] for label in names: rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.groups", "search_read", [[("full_name", "=", label)]], {"fields": ["id"], "limit": 1}, ) if not rows: rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.groups", "search_read", [[("name", "=", label)]], {"fields": ["id"], "limit": 1}, ) if rows: ids.append(int(rows[0]["id"])) return ids def group_ids_for_desk_role(role: str) -> list[int]: """Resolve group IDs Odoo para função Desk. Levanta se apps não instaladas.""" uid, models = _client() xmlids = DESK_ROLE_ODOO_XMLIDS.get(role, ()) group_ids: list[int] = [] missing_xmlids: list[str] = [] for xid in xmlids: gid = _resolve_xmlid(uid, models, xid) if gid: group_ids.append(gid) else: missing_xmlids.append(xid) if group_ids: return group_ids # fallback por nome names = DESK_ROLE_ODOO_GROUP_NAMES.get(role, ()) group_ids = _resolve_group_names(uid, models, names) if group_ids: return group_ids hint = ", ".join(missing_xmlids) or role raise OdooProvisionError( f"grupos Odoo não encontrados para role={role} ({hint}). " "Instale apps Sales/Accounting no Odoo ou crie grupos custom." ) def list_role_model(role: str) -> dict[str, Any]: """Introspecção — grupos mapeados e estado das apps (para Roger / debug).""" if not _configured(): return {"configured": False, "role": role, "groups": [], "note": "ODOO_API_KEY ausente"} uid, models = _client() xmlids = DESK_ROLE_ODOO_XMLIDS.get(role, ()) resolved = [] for xid in xmlids: gid = _resolve_xmlid(uid, models, xid) item: dict[str, Any] = {"xmlid": xid, "group_id": gid} if gid: g = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.groups", "read", [[gid]], {"fields": ["name", "full_name"]}, )[0] item["name"] = g.get("full_name") or g.get("name") else: item["missing"] = True resolved.append(item) installed = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "ir.module.module", "search_read", [[("name", "in", ["sale", "sale_management", "account", "crm"]), ("state", "=", "installed")]], {"fields": ["name", "state"], "limit": 20}, ) return { "configured": True, "role": role, "db": ODOO_DB, "public_url": ODOO_PUBLIC_URL, "groups": resolved, "installed_sales_account_modules": [m["name"] for m in installed], } def find_partner_by_email(email: str) -> dict[str, Any] | None: uid, models = _client() rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.partner", "search_read", [[("email", "=ilike", email.strip())]], {"fields": ["id", "name", "email", "vat", "ref"], "limit": 1}, ) return rows[0] if rows else None def find_partner_by_ref(ref: str) -> dict[str, Any] | None: uid, models = _client() rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.partner", "search_read", [[("ref", "=", ref.strip().lower())]], {"fields": ["id", "name", "email", "vat", "ref"], "limit": 1}, ) return rows[0] if rows else None def _country_id(uid: int, models, country_code: str) -> int | None: code = (country_code or "BR").strip().upper() rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.country", "search_read", [[("code", "=", code)]], {"fields": ["id"], "limit": 1}, ) return int(rows[0]["id"]) if rows else None def upsert_customer_partner(*, mapped: dict[str, Any]) -> dict[str, Any]: """Empresa cliente res.partner — Spec 043 §6.1.""" import json uid, models = _client() ref = str(mapped.get("ref") or "").strip().lower() email = str(mapped.get("email") or "").strip() existing = find_partner_by_ref(ref) if ref else None if not existing and email: existing = find_partner_by_email(email) comment = mapped.get("comment") if isinstance(comment, dict): comment = json.dumps(comment, ensure_ascii=False) vals: dict[str, Any] = { "name": mapped.get("name") or ref or email, "vat": mapped.get("vat") or False, "email": email or False, "phone": mapped.get("phone") or False, "street": mapped.get("street") or False, "city": mapped.get("city") or False, "zip": mapped.get("zip") or False, "company_type": mapped.get("company_type") or "company", "customer_rank": mapped.get("customer_rank") or 1, "ref": ref or False, "comment": comment or False, } country_id = _country_id(uid, models, str(mapped.get("country_code") or "BR")) if country_id: vals["country_id"] = country_id if existing: models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.partner", "write", [[int(existing["id"])], vals], ) return { "partner_id": int(existing["id"]), "created": False, "updated": True, "login_url": ODOO_PUBLIC_URL, } partner_id = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.partner", "create", [vals], ) return { "partner_id": int(partner_id), "created": True, "updated": False, "login_url": ODOO_PUBLIC_URL, } def find_user_by_login(login: str) -> dict[str, Any] | None: uid, models = _client() rows = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.users", "search_read", [[("login", "=", login.strip().lower())]], {"fields": ["id", "name", "login", "groups_id"], "limit": 1}, ) return rows[0] if rows else None def upsert_internal_user( *, email: str, name: str, desk_role: str, password: str | None = None, ) -> dict[str, Any]: """Cria ou actualiza utilizador interno Ligbox com groups_id conforme função Desk.""" uid, models = _client() login = email.strip().lower() group_ids = group_ids_for_desk_role(desk_role) # Internal User (base.group_user) — xmlid base.group_user base_user_gid = _resolve_xmlid(uid, models, "base.group_user") if base_user_gid and base_user_gid not in group_ids: group_ids = [base_user_gid, *group_ids] existing = find_user_by_login(login) groups_cmd = [(6, 0, group_ids)] if existing: models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.users", "write", [[existing["id"]], {"name": name, "groups_id": groups_cmd}], ) odoo_uid = int(existing["id"]) created = False else: pwd = password or secrets.token_urlsafe(16) odoo_uid = models.execute_kw( ODOO_DB, uid, ODOO_API_KEY, "res.users", "create", [ { "name": name, "login": login, "email": login, "password": pwd, "groups_id": groups_cmd, } ], ) created = True return { "odoo_uid": odoo_uid, "login": login, "created": created, "group_ids": group_ids, "login_url": ODOO_PUBLIC_URL, }