diff --git a/lib/recycledcloud/odoo.ex b/lib/recycledcloud/odoo.ex new file mode 100644 index 0000000..cb17b06 --- /dev/null +++ b/lib/recycledcloud/odoo.ex @@ -0,0 +1,110 @@ +defmodule RecycledCloud.Odoo do + use GenServer + require Logger + + @common_endpoint "/xmlrpc/2/common" + @object_endpoint "/xmlrpc/2/object" + + # Odoo 14.0 API is documented at https://www.odoo.com/documentation/14.0/webservices/odoo.html + + defp get_odoo_config(key) do + Application.get_env(:recycledcloud, :odoo, []) |> Keyword.get(key) + end + + defp post!(call, endpoint) do + url = get_odoo_config(:server) <> endpoint + opts = [proxy_auth: {get_odoo_config(:user), get_odoo_config(:secret)}] + headers = [] + body = call |> XMLRPC.encode! + + response = HTTPoison.post!(url, body, headers, opts).body |> XMLRPC.decode! + case response do + %{fault_code: _, fault_string: err} -> {:error, err} + %{param: result} -> {:ok, result} + end + end + + defp authenticate do + auth_params = [ + get_odoo_config(:database), + get_odoo_config(:user), + get_odoo_config(:secret), + nil + ] + + auth_response = %XMLRPC.MethodCall{method_name: "authenticate", params: auth_params} + |> post!(@common_endpoint) + case auth_response do + {:ok, false} -> {:error, "Could not authenticate against Odoo."} + {:ok, uid} -> {:ok, uid} + {:error, err} -> {:error, err} + end + end + + defp check_odoo_config(key) do + case get_odoo_config(key) do + nil -> + Logger.warn("Odoo #{key} configuration is missing.") + false + _value -> true + end + end + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(:ok) do + # Make sure Odoo configuration is defined. + conf_ok? = for key <- [:server, :database, :user, :secret], reduce: true do + acc -> acc && check_odoo_config(key) + end + + if conf_ok? do + case authenticate() do + {:ok, uid} -> + # Print Odoo version on console (not quite useful...). + version_call = %XMLRPC.MethodCall{method_name: "version", params: []} |> post!(@common_endpoint) + {:ok, %{"server_version" => version}} = version_call + Logger.info("Successfuly authenticated against Odoo #{version} at #{get_odoo_config(:server)}") + + {:ok, uid} + {:error, err} -> + Logger.warning("Failed to authenticate against Odoo: #{inspect(err)}") + + # We do not fail here since we do not want to hinder the application's + # main supervisor if Odoo is down. + {:ok, nil} + end + else + Logger.warning("Failed to authenticate against Odoo: missing configuration!") + + {:ok, nil} + end + end + + def handle_call({method, params}, _from, uid) do + case uid do + # uid is nil if start_link's init/1 call failed. + # ... we try to authenticate once again! + nil -> + case init(:ok) do + {:ok, new_uid} -> handle_call({method, params}, nil, new_uid) + err -> {:reply, err, nil} + end + _ -> + call = %XMLRPC.MethodCall{ + method_name: method, + params: [get_odoo_config(:database), uid, get_odoo_config(:secret) | params] + } + result = call |> post!(@object_endpoint) + + {:reply, result, uid} + end + end + + def query(params), do: execute_kw(params) + defp execute_kw(params) do + GenServer.call(__MODULE__, {"execute_kw", params}) + end +end