Created
February 8, 2025 08:06
-
-
Save jasalt/d352f8d7b5398b8191a5e963f53f9bcc to your computer and use it in GitHub Desktop.
Phel Odoo XMLRPC wrapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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