defmodule Odoo do @moduledoc """ Library to access Odoo JSON-RPC API from Elixir. Provides the following methods for interacting with Odoo: - login - search - search_read - read - read_group - create - write - delete - execute """ @doc """ - Login in Odoo and set session_id for future calls ### Params (required) - user: string Odoo user - password: string Odoo password - database: string Odoo database - url: string Odoo url, http or https ### Example ``` iex> {:ok, odoo} = Odoo.login( "admin", "admin", "mydatabasename", "https://mydatabasename.odoo.com") {:ok, %Odoo.Session{ cookie: "session_id=c8e544d0b305920adgsfdfdsa7b0cfe; Expires=Fri, 06-May-2022 23:16:12 GMT; Max-Age=7776000; HttpOnly; Path=/", database: "mydatabasename", password: "admin", url: "https://mydatabasename.odoo.com", user: "admin", user_context: %{"lang" => "en_US", "tz" => "Asia/Calcutta", "uid" => 2} }} ``` """ @spec login(String.t(), String.t(), String.t(), String.t()) :: {:ok, %Odoo.Session{}} | {:error, String.t()} def login(user, password, database, url) do case check_params(user, password, database, url) do {:ok, _} -> Odoo.Core.login(user, password, database, url) {:error, message} -> {:error, message} end end @doc """ ### Example - Search and read with default options (limit, domain, fields, offset and order) ```{:ok, res} = Odoo.search_read(odoo, "res.partner")``` - Put options to tune the query: ```elixir {:ok, partners} = Odoo.search_read( odoo, "res.partner", [ limit: 1, domain: [["name", "ilike", "Antonio"]], fields: ["name", "street"], offset: 11, order: "name asc"]) {:ok, [ %{ "id" => 226, "name" => "Antonio Fulanito de tal", "street" => "Calle principal 1" } ]} ``` - Search and read active and archived records (by default odoo only return active records) ```elixir {:ok, partners} = Odoo.search_read( odoo, "res.partner", [ fields: ["name"], offset: 0, limit: 10, order: "id", domain: [["active", "in", [true,false]]] ] ) ``` """ @type option :: {:limit, non_neg_integer()} | {:offset, non_neg_integer()} | {:order, String.t()} | {:fields, [String.t(), ...]} | {:domain, [list(), ...]} @spec search_read(%Odoo.Session{}, String.t(), [option]) :: {:ok, %Odoo.Result{}} | {:error, String.t()} def search_read(odoo = %Odoo.Session{}, model, opts \\ []) do Odoo.Core.search_read(odoo, model, opts) end @doc """ - Search by domain. Return single id or id list. ### Params - Arguments in keyword list format. - Required opts: - domain: list of list - Optional arguments - limit: int, max number of rows to return from odoo - offset: int, offset over the default values to return ### Example ```elixir iex> {:ok, partner_ids} = Odoo.search( odoo, "res.partner", [ domain: [ ["name", "ilike", "Antonia%"], ["customer", "=", true], ["create_date",">=","2021-06-01"] ], limit: 5, offset: 10, order: "name" ]) {:ok, %Odoo.Result{ data: '[44]', model: "res.partner", opts: [domain: [["name", "ilike", "Lorraine%"], ["customer", "=", true], ["create_date",">=","2021-06-01"]]] }} ``` - Return one value is a single integer for the id - Return more than one value is a list of integers - Can also return {:error, message} if the operation fails. """ @type option_valid_for_search :: {:limit, non_neg_integer()} | {:offset, non_neg_integer()} | {:order, String.t()} | {:domain, [list(), ...]} @spec search(%Odoo.Session{}, String.t(), [option_valid_for_search]) :: {:ok, %Odoo.Result{}} | {:error, String.t()} def search(odoo = %Odoo.Session{}, model, opts \\ []) do Odoo.Core.search(odoo, model, opts) end @doc """ - Create objects - Return {:ok, new_object_id} or {:error, message} ## Example iex> {:ok, product_id} = Odoo.create( odoo, "product.product", [name: "mi mega producto3"]) {:ok, 63} You can specify the context language to create the object in the desired language. ```elixir iex(44)> {:ok, result} = Odoo.create(odoo, "product.product", [name: "Product2"], [lang: "en_US"]) {:ok, %Odoo.Result{data: 185, model: "product.product", opts: [name: "Product2"]}} ``` """ @type odoo_fields :: keyword() @spec create(%Odoo.Session{}, String.t(), [odoo_fields]) :: {:ok, integer()} | {:error, String.t()} def create(odoo = %Odoo.Session{}, model, opts \\ []) do Odoo.Core.create(odoo, model, opts) end @doc """ - Read objects by id - Return {:ok, objects_list} or {:error, message} ### Example ```elixir iex> {:ok, product} = Odoo.read( odoo, "product.product", [63], [fields: ["name", "categ_id"]]) {:ok, %Odoo.Result{ data: [ %{"id" => 63, "name" => "mi mega producto3", "categ_id" => [1, "All"]} ], model: "product.product", opts: [fields: ["name", "categ_id"]] }} ``` """ @type option_valid_for_read :: {:fields, [String.t(), ...]} @spec read(%Odoo.Session{}, String.t(), [non_neg_integer(), ...], [option_valid_for_read]) :: {:ok, %Odoo.Result{}} | {:error, String.t()} def read(odoo = %Odoo.Session{}, model, object_id, opts \\ []) do Odoo.Core.read(odoo, model, object_id, opts) end @doc """ - Read and group objects ### Params - :fields - :domain - :groupby - :lazy - :orderby - :offset ### Example ```elixir iex> {:ok, result} = Odoo.read_group( odoo, "account.invoice", [ domain: [["date_invoice", ">=", "2021-11-01"]], groupby: ["date_invoice:month"], fields: ["number", "partner_id"], limit: 2, lazy: true]) {:ok, %Odoo.Result{ data: [ %{ "__domain" => [ "&", "&", ["date_invoice", ">=", "2022-01-01"], ["date_invoice", "<", "2022-02-01"], ["date_invoice", ">=", "2021-11-01"] ], "date_invoice:month" => "enero 2022", "date_invoice_count" => 61 }, %{ "__domain" => [ "&", "&", ["date_invoice", ">=", "2022-02-01"], ["date_invoice", "<", "2022-03-01"], ["date_invoice", ">=", "2021-11-01"] ], "date_invoice:month" => "febrero 2022", "date_invoice_count" => 32 } ]}} ``` """ @type option_valid_for_read_group :: {:domain, [list(), ...]} | {:fields, [String.t(), ...]} | {:groupby, [String.t(), ...]} | {:limit, non_neg_integer()} | {:lazy, bool()} @spec read_group(%Odoo.Session{}, String.t(), [option_valid_for_read]) :: {:ok, %Odoo.Result{}} | {:error, String.t()} def read_group(odoo = %Odoo.Session{}, model, opts \\ []) do Odoo.Core.read_group(odoo, model, opts) end @doc """ - Update objects by id ### Example ```elixir iex> {:ok, result}=Odoo.write odoo, "product.product", [63], [name: "Mega Pro 3"] {:ok, %Odoo.Result{ data: true, model: "product.product", opts: [name: "Mega Pro 3"] }} Example adding a many2many value to an odoo object: category_to_link_id = 9 {:ok, result} = Odoo.write( odoo, "res.partner", partner_ids, [category_id: [[4, category_to_link_id]] ]) ``` """ @spec write(%Odoo.Session{}, String.t(), list(non_neg_integer()), keyword()) :: {:ok, %Odoo.Result{}} | {:error, String.t()} def write(odoo = %Odoo.Session{}, model, object_id, fields \\ []) do Odoo.Core.write(odoo, model, object_id, fields) end @doc """ - Delete objects by id ### Example ```elixir iex(10)> {:ok, result} = Odoo.delete odoo, "product.template", [116] {:ok, %Odoo.Result{data: true, model: "product.template", opts: 't'}} ``` """ def delete(odoo = %Odoo.Session{}, model, object_id) do Odoo.Core.delete(odoo, model, object_id) end @doc """ Pagination over results in search_read (call to api odoo) ### Example ```elixir iex> {:ok, result} = Odoo.search_read( ...> odoo, "product.product", limit: 5, fields: ["name"], order: "id asc") {:ok, %Odoo.Result{ data: [ %{"id" => 1, "name" => "Restaurant Expenses"}, %{"id" => 2, "name" => "Hotel Accommodation"}, %{"id" => 3, "name" => "Virtual Interior Design"}, %{"id" => 4, "name" => "Virtual Home Staging"}, %{"id" => 5, "name" => "Office Chair"} ], model: "product.product", opts: [limit: 5, fields: ["name"], order: "id asc"] }} iex> {:ok, result2} = Odoo.next(odoo, result) {:ok, %Odoo.Result{ data: [ %{"id" => 6, "name" => "Office Lamp"}, %{"id" => 7, "name" => "Office Design Software"}, %{"id" => 8, "name" => "Desk Combination"}, %{"id" => 9, "name" => "Customizable Desk"}, %{"id" => 10, "name" => "Customizable Desk"} ], model: "product.product", opts: [offset: 5, limit: 5, fields: ["name"], order: "id asc"] }} ``` """ def next(odoo = %Odoo.Session{}, result) do new_opts = Odoo.Result.next(result.opts) Odoo.search_read(odoo, result.model, new_opts) end @doc """ Get previous page results (call to api odoo) ### Example ```elixir iex> {:ok, odoo} = Odoo.login( ...> "admin", "admin", "mydatabasename", "https://mydatabasename.odoo.com") {:ok, %Odoo.Session{ cookie: "session_id=c8e544d0b305920afgdgsfdfdsa7b0cfe; Expires=Fri, 06-May-2022 23:16:12 GMT; Max-Age=7776000; HttpOnly; Path=/", database: "mydatabasename", password: "admin", url: "https://mydatabasename.odoo.com", user: "admin", user_context: %{"lang" => "en_US", "tz" => "Asia/Calcutta", "uid" => 2} }} iex> {:ok, result} = Odoo.search_read( ...> odoo, "product.product", limit: 5, fields: ["name"], order: "id asc") {:ok, %Odoo.Result{ data: [ %{"id" => 1, "name" => "Restaurant Expenses"}, %{"id" => 2, "name" => "Hotel Accommodation"}, %{"id" => 3, "name" => "Virtual Interior Design"}, %{"id" => 4, "name" => "Virtual Home Staging"}, %{"id" => 5, "name" => "Office Chair"} ], model: "product.product", opts: [limit: 5, fields: ["name"], order: "id asc"] }} iex> {:ok, result2} = Odoo.next(odoo, result) {:ok, %Odoo.Result{ data: [ %{"id" => 6, "name" => "Office Lamp"}, %{"id" => 7, "name" => "Office Design Software"}, %{"id" => 8, "name" => "Desk Combination"}, %{"id" => 12, "name" => "Customizable Desk"}, %{"id" => 13, "name" => "Customizable Desk"} ], model: "product.product", opts: [offset: 5, limit: 5, fields: ["name"], order: "id asc"] }} iex> {:ok, result3} = Odoo.prev(odoo, result2) {:ok, %Odoo.Result{ data: [ %{"id" => 1, "name" => "Restaurant Expenses"}, %{"id" => 2, "name" => "Hotel Accommodation"}, %{"id" => 3, "name" => "Virtual Interior Design"}, %{"id" => 4, "name" => "Virtual Home Staging"}, %{"id" => 5, "name" => "Office Chair"} ], model: "product.product", opts: [offset: 0, limit: 5, fields: ["name"], order: "id asc"] }} ``` """ def prev(odoo = %Odoo.Session{}, result) do new_opts = Odoo.Result.prev(result.opts) Odoo.search_read(odoo, result.model, new_opts) end @doc """ Execute a method on a model (call to api odoo). * Confirm a sale order with id 3 iex> {:ok, res2} = Odoo.execute(odoo, "sale.order", "action_confirm", [3]) {:ok, %Odoo.Result{data: true, model: "sale.order", opts: [3]}} """ @spec execute(Odoo.Session.t(), String.t(), String.t(), list(non_neg_integer())) :: {:ok, %Odoo.Result{}} | {:error, String.t()} def execute(odoo = %Odoo.Session{}, model, method, args) do Odoo.Core.execute(odoo, model, method, args) end defp check_params(user, password, database, url) do cond do is_nil(user) or String.length(user) == 0 -> {:error, "User is required"} is_nil(password) || String.length(password) == 0 -> {:error, "Password is required"} is_nil(database) || String.length(database) == 0 -> {:error, "Database is required"} is_nil(url) || String.length(url) == 0 -> {:error, "Url is required"} true -> {:ok, "ok"} end end end