Skip to content

Instantly share code, notes, and snippets.

@jasalt
Created February 8, 2025 08:06
Show Gist options
  • Save jasalt/d352f8d7b5398b8191a5e963f53f9bcc to your computer and use it in GitHub Desktop.
Save jasalt/d352f8d7b5398b8191a5e963f53f9bcc to your computer and use it in GitHub Desktop.
Phel Odoo XMLRPC wrapper
(ns woodoo-pos-sync\lib\odoo
(:require phel\str :as str)
(:require woodoo-pos-sync\lib\wp :as wp)
(:use \Laminas\XmlRpc\Client))
### Wrapper for Laminas XMLRPC library and Odoo XMLRPC-API
## Docs:
## - https://docs.laminas.dev/laminas-xmlrpc/client/
## - https://www.odoo.com/documentation/18.0/developer/reference/external_api.html
## - https://www.odoo.com/documentation/18.0/developer/reference/backend/orm.html
(defn odoo-rpc-connection?
"Validate odoo-rpc-connection map as if it was a struct (providing validation fn).
TODO redefining struct (in REPL) leads to error, how to avoid running on reload?
e.g. (defstruct odoo-rpc-connection [uid password db-name api-common api-object])"
[conn]
(= (apply set (keys conn)) (set :uid :password :db-name :api-common :api-object)))
(defn connect
"returns odoo-rpc-connection (struct-like) map that is used as first parameter
with rpc functions `password` is either api key or account password"
[username password instance-url db-name]
(if (contains-value? [username password instance-url db-name] "")
(throw (php/new \Exception "One of parameters was not set for connect, cannot proceed with Odoo XMLRPC connection, make sure Odoo API credentials are set correctly.")))
(when-not (or (str/starts-with? instance-url "http://localhost:")
(str/starts-with? instance-url "http://odoo:8069")
(str/starts-with? instance-url "https://"))
(throw (php/new \Exception "Connection to Odoo XMLRPC over unsafe HTTP is not allowed outside localhost.")))
(let [api-common
(php/-> (php/new \Laminas\XmlRpc\Client (str instance-url "/xmlrpc/2/common"))
(getProxy))
api-object
(php/-> (php/new \Laminas\XmlRpc\Client (str instance-url "/xmlrpc/2/object"))
(getProxy))
uid (php/-> api-common (login db-name username password))]
(when-not (= (type uid) :int)
(throw (php/new \Exception
(str "Odoo login problem, uid should be integer but is " (type uid)))))
# (odoo-rpc-connection uid password db-name api-common api-object) # BUG
{:uid uid :password password :db-name db-name
:api-common api-common :api-object api-object}))
## TODO move non-generic library fns somewhere else
(defn connect-using-wp-credentials
"Get connection to Odoo using credentials set in WP plugin settings.
In REPL, wp-load.php needs to have been required."
[]
(let [odoo_username (wp/get-cf-option (str php/ODOO_PREFIX "_username"))
odoo_password (wp/get-cf-option (str php/ODOO_PREFIX "_password"))
odoo_url (wp/get-cf-option (str php/ODOO_PREFIX "_url"))
odoo_db (wp/get-cf-option (str php/ODOO_PREFIX "_db"))]
(connect odoo_username odoo_password odoo_url odoo_db)))
### Basic CRUD functions
(defn- validate-conn [conn]
(when (not (odoo-rpc-connection? conn))
(throw (php/new \TypeError (str "conn is not odoo-rpc-connection struct, got " (type conn))))))
(defn- validate-model [model]
(when (not (string? model))
(throw (php/new \TypeError (str "model is not string, got " (type model))))))
#(defn- validate-ids [ids] # TODO
# (when (not ...)
# (throw (php/new \TypeError (str "ids is not array of ints, got " (type ids))))))
(defn keyword-map-to-php-array
"Helper function for passing Phel hash maps to RPC calls
Converts map keyword keys into strings."
[m]
(to-php-array (for [[k v] :pairs m :reduce [acc {}]]
(put acc (case (type k)
:keyword (name k)
k) v))))
(defn rpc-create
"Creates a single record and returns its database identifier.
While payload is wrapped in sequential ds, Odoo expects single item inside it
and throws positional argument exception when multiple are passed."
[conn model data]
(validate-conn conn)
(validate-model model)
(when-not (hash-map? data)
(throw (php/new \InvalidArgumentException
(str "rpc-create expects map for m but got " (type data)))))
(php/-> (get conn :api-object)
(execute_kw (get conn :db-name) (get conn :uid) (get conn :password)
model "create"
(php/array (keyword-map-to-php-array data)))))
(defn rpc-update
"Update `model` records set by `ids` with values in `data` map."
[conn model ids data]
(validate-conn conn)
(validate-model model)
(php/-> (get conn :api-object)
(execute_kw (get conn :db-name) (get conn :uid) (get conn :password)
model "write"
(php/array (to-php-array ids)
(keyword-map-to-php-array data)))))
(defn rpc-delete
"Delete model records by ids given as vector"
[conn model ids]
(validate-conn conn)
(validate-model model)
(php/-> (get conn :api-object)
(execute_kw (get conn :db-name) (get conn :uid) (get conn :password)
model "unlink"
(php/array (to-php-array ids)))))
(defn rpc-read
"Fetch records for vector of ids. Opts map accepts :fields [str*]."
[conn model ids & [opts]]
(validate-conn conn)
(validate-model model)
(let [opts (if opts opts {})] # opts default to empty map
(php/-> (get conn :api-object)
(execute_kw (get conn :db-name) (get conn :uid) (get conn :password)
model "read"
(php/array (to-php-array ids))
(to-php-array {"fields" (to-php-array (opts :fields))})))))
### Misc Odoo XMLRPC functions
(defn rpc-search-read
"Queries search_read endpoint returning resulting records.
Accepts optional :filters :fields :limit in `opts` map with following format:
:filters [[str str val]*] vector of three item vectors, each converting to Python
triple (field_name, operator, value), consult Odoo ORM domain filter docs:
https://www.odoo.com/documentation/18.0/developer/reference/backend/orm.html#reference-orm-domains
:limit int
:fields [str*]"
[conn model & [opts]]
(validate-conn conn)
(validate-model model)
(let [opts (if opts opts {})] # opts default to empty map
(php/-> (get conn :api-object)
(execute_kw (get conn :db-name) (get conn :uid) (get conn :password)
model
"search_read"
(php/array (to-php-array (map to-php-array (opts :filters))))
(to-php-array {"fields" (to-php-array (opts :fields))
"limit" (opts :limit)})))))
### Higher level helper functions
### Model extension / "Custom Fields"
(defn add-field
"Add field to model.
The model_id is automatically set based on given `model_name`.
Documentation tells that custom model names must start with x_ and
state must be provided and set to manual, otherwise the model will not be
loaded."
[conn model field-data]
(validate-conn conn)
(validate-model model)
(let [model-id
(-> (rpc-search-read conn "ir.model" {:filters [["model" "=" model]]
:fields ["id"]})
first
(get "id"))
new-field-data (put field-data :model_id model-id)
new-field-id (rpc-create conn "ir.model.fields" new-field-data)]
new-field-id))
(defn get-field
[conn model field-name]
(validate-conn conn)
(validate-model model)
(first (rpc-search-read conn "ir.model.fields"
{:filters [["name" "=" field-name]
["model" "=" model]]})))
(defn get-fields
[conn model]
(validate-conn conn)
(validate-model model)
(rpc-search-read conn "ir.model.fields"
{:filters [["model" "=" model]]}))
(defn delete-field
[conn model field-name]
(validate-conn conn)
(validate-model model)
(rpc-delete conn "ir.model.fields" [(get (get-field conn model field-name) "id")]))
### product.product
## NOTE tax excluded if odoo configured to include
(defn create-product
"Create single product in Odoo with data in map.
Returns product id when successful and throws error otherwise."
[conn m]
(rpc-create conn "product.product" m))
(defn get-product-by-barcode [conn barcode]
(php-array-to-map
(first (rpc-search-read conn "product.product"
{:filters [["barcode" "=" barcode]]
:limit 1}))))
(defn get-product-by-id [conn product-id]
(php-array-to-map
(first (rpc-search-read conn "product.product"
{:filters [["id" "=" product-id]]
:limit 1}))))
(defn update-product
"Update single product in Odoo with data in map.
Return true for success and throws error otherwise."
[conn product-id m]
(rpc-update conn "product.product" [product-id] m))
(defn update-product-by-barcode
"Updates Odoo product by barcode, returns product id on success."
[conn barcode m]
(let [odoo-id (get (get-product-by-barcode conn barcode) "id")]
(if (int? odoo-id)
(do (update-product conn odoo-id m)
odoo-id)
(throw
(php/new \RuntimeException
"update-product-by-barcode cannot resolve odoo-id for update")))))
(defn delete-product
"Delete single product in Odoo."
[conn product-id]
(rpc-delete conn "product.product" [product-id]))
(defn upsert-product
"Create product or attempt to update existing.
Returns Odoo product id on successful creation or update update."
[conn m]
(try
(create-product conn m)
(catch \Laminas\XmlRpc\Client\Exception\FaultException e
(update-product-by-barcode conn (:barcode m) m))))
(comment
(require woodoo-pos-sync\tests\lib\odoo :refer [conn])
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment