diff --git a/components/odoo/actions/create-record/create-record.mjs b/components/odoo/actions/create-record/create-record.mjs new file mode 100644 index 0000000000000..88c76ac12a890 --- /dev/null +++ b/components/odoo/actions/create-record/create-record.mjs @@ -0,0 +1,29 @@ +import odoo from "../../odoo.app.mjs"; + +export default { + key: "odoo-create-record", + name: "Create Record", + description: "Create a new record in Odoo. [See the documentation](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html#create-records)", + version: "0.0.1", + type: "action", + props: { + odoo: { + ...odoo, + reloadProps: true, + }, + }, + async additionalProps() { + return await this.odoo.getFieldProps(); + }, + async run({ $ }) { + const { + odoo, + ...data + } = this; + const response = await odoo.createRecord([ + data, + ]); + $.export("$summary", `Successfully created record with ID: ${response}`); + return response; + }, +}; diff --git a/components/odoo/actions/search-read-records/search-read-records.mjs b/components/odoo/actions/search-read-records/search-read-records.mjs new file mode 100644 index 0000000000000..9b66e5e33e849 --- /dev/null +++ b/components/odoo/actions/search-read-records/search-read-records.mjs @@ -0,0 +1,37 @@ +import odoo from "../../odoo.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "odoo-search-read-records", + name: "Search and Read Records", + description: "Search and read records from Odoo. [See the documentation](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html#search-and-read)", + version: "0.0.1", + type: "action", + props: { + odoo, + filter: { + type: "string", + label: "Search Filter", + description: "The criterion to search by. E.g. `[[[\"is_company\", \"=\", true]]]`. [See the documentation](https://www.odoo.com/documentation/18.0/developer/reference/backend/orm.html#search-domains) for information about constructing a search domain", + optional: true, + }, + fields: { + propDefinition: [ + odoo, + "fields", + ], + }, + }, + async run({ $ }) { + const args = this.fields + ? { + fields: this.fields, + } + : {}; + const response = await this.odoo.searchAndReadRecords(parseObject(this.filter), args); + $.export("$summary", `Successfully retrieved ${response.length} record${response.length === 1 + ? "" + : "s"}`); + return response; + }, +}; diff --git a/components/odoo/actions/update-record/update-record.mjs b/components/odoo/actions/update-record/update-record.mjs new file mode 100644 index 0000000000000..d690d5ca701a8 --- /dev/null +++ b/components/odoo/actions/update-record/update-record.mjs @@ -0,0 +1,62 @@ +import odoo from "../../odoo.app.mjs"; +const DEFAULT_LIMIT = 20; + +export default { + key: "odoo-update-record", + name: "Update Record", + description: "Update an existing record in Odoo. [See the documentation](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html#update-records)", + version: "0.0.1", + type: "action", + props: { + odoo: { + ...odoo, + reloadProps: true, + }, + }, + async additionalProps() { + const fieldProps = await this.odoo.getFieldProps({ + update: true, + }); + const recordId = { + type: "integer", + label: "Record ID", + description: "The ID of the record to update", + options: async ({ page }) => await this.getRecordIdOptions(page), + }; + return { + recordId, + ...fieldProps, + }; + }, + methods: { + async getRecordIdOptions(page) { + const records = await this.odoo.searchAndReadRecords([], { + limit: DEFAULT_LIMIT, + offset: page * DEFAULT_LIMIT, + }); + return records?.map(({ + id: value, display_name: label, + }) => ({ + value, + label, + })) || []; + }, + }, + async run({ $ }) { + const { + odoo, + // eslint-disable-next-line no-unused-vars + getRecordIdOptions, + recordId, + ...data + } = this; + const response = await odoo.updateRecord([ + [ + recordId, + ], + data, + ]); + $.export("$summary", `Successfully updated record with ID: ${recordId}`); + return response; + }, +}; diff --git a/components/odoo/common/utils.mjs b/components/odoo/common/utils.mjs new file mode 100644 index 0000000000000..9fdc004c18f32 --- /dev/null +++ b/components/odoo/common/utils.mjs @@ -0,0 +1,25 @@ +export const parseObject = (obj) => { + if (!obj) { + return undefined; + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + if (Array.isArray(obj)) { + return obj.map(parseObject); + } + if (typeof obj === "object") { + return Object.fromEntries(Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + parseObject(value), + ])); + } + return obj; +}; diff --git a/components/odoo/odoo.app.mjs b/components/odoo/odoo.app.mjs index 8d77eafc9cdbc..11bf057e3a910 100644 --- a/components/odoo/odoo.app.mjs +++ b/components/odoo/odoo.app.mjs @@ -1,11 +1,104 @@ +import xmlrpc from "xmlrpc"; + export default { type: "app", app: "odoo", - propDefinitions: {}, + propDefinitions: { + fields: { + type: "string[]", + label: "Fields", + description: "The fields to return in the results. If not provided, all fields will be returned.", + optional: true, + async options() { + const fields = await this.getFields([], { + attributes: [ + "string", + ], + }); + return Object.keys(fields)?.map((key) => ({ + value: key, + label: fields[key].string, + })) || []; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getClient(type = "common") { + return xmlrpc.createSecureClient(`${this.$auth.server_url}/xmlrpc/2/${type}`); + }, + async getUid() { + const db = this.$auth.db; + const username = this.$auth.username; + const password = this.$auth.password; + const common = this.getClient("common"); + const uid = await new Promise((resolve, reject) => { + common.methodCall("authenticate", [ + db, + username, + password, + {}, + ], (error, value) => { + if (error) reject(error); + resolve(value); + }); + }); + return uid; + }, + async makeRequest(method, filter = [], args = {}) { + const db = this.$auth.db; + const uid = await this.getUid(); + const password = this.$auth.password; + const models = this.getClient("object"); + const results = await new Promise((resolve, reject) => { + models.methodCall("execute_kw", [ + db, + uid, + password, + "res.partner", + method, + filter, + args, + ], (error, value) => { + if (error) reject(error); + resolve(value); + }); + }); + return results; + }, + async getFieldProps({ update = false } = {}) { + const props = {}; + const fields = await this.getFields(); + Object.keys(fields).forEach((key) => { + if (fields[key].readonly === true) return; + props[key] = { + type: fields[key].type === "integer" || fields[key].type === "boolean" + ? fields[key].type + : fields[key].type.includes("id") + ? "integer" + : fields[key].type.includes("2many") + ? "string[]" + : "string", + label: fields[key].string, + description: `Value for "${key}"`, + optional: (key !== "name" || update) && fields[key].required === false, + }; + }); + return props; + }, + getFields(filter = [], args = {}) { + return this.makeRequest("fields_get", filter, args); + }, + searchAndReadRecords(filter = [], args = {}) { + return this.makeRequest("search_read", filter, args); + }, + readRecord(data) { + return this.makeRequest("read", data); + }, + createRecord(data) { + return this.makeRequest("create", data); + }, + updateRecord(data) { + return this.makeRequest("write", data); }, }, }; diff --git a/components/odoo/package.json b/components/odoo/package.json index 3a7bf3965dc43..c668dc44b99e1 100644 --- a/components/odoo/package.json +++ b/components/odoo/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/odoo", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Odoo Components", "main": "odoo.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "xmlrpc": "^1.3.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70decf7531586..ad10dde6b7c07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9110,7 +9110,11 @@ importers: specifier: ^1.6.0 version: 1.6.6 - components/odoo: {} + components/odoo: + dependencies: + xmlrpc: + specifier: ^1.3.2 + version: 1.3.2 components/office_365_management: {} @@ -11601,8 +11605,7 @@ importers: specifier: ^2.3.0 version: 2.3.0 - components/salesroom: - specifiers: {} + components/salesroom: {} components/salestown: {} @@ -28622,6 +28625,9 @@ packages: resolution: {integrity: sha512-rOOcZfHYK3haArS4/RaD+DDcPrfMC7G7dCRrzjHLaLjIj+VTs/cbWcbFkCAGwS0OY2DuiUAzBVFVX302zGzw6Q==} engines: {node: '>=18'} + sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -30493,6 +30499,10 @@ packages: resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} engines: {node: '>=6.0'} + xmlbuilder@8.2.2: + resolution: {integrity: sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==} + engines: {node: '>=4.0'} + xmlbuilder@9.0.7: resolution: {integrity: sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==} engines: {node: '>=4.0'} @@ -30508,6 +30518,10 @@ packages: resolution: {integrity: sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==} engines: {node: '>=10.0.0'} + xmlrpc@1.3.2: + resolution: {integrity: sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==} + engines: {node: '>=0.8', npm: '>=1.0.0'} + xpath@0.0.34: resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} engines: {node: '>=0.6.0'} @@ -36155,6 +36169,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: @@ -48603,6 +48619,8 @@ snapshots: - supports-color - typescript + sax@1.2.4: {} + sax@1.4.1: {} sb-promise-queue@2.1.0: {} @@ -50872,6 +50890,8 @@ snapshots: xmlbuilder@13.0.2: {} + xmlbuilder@8.2.2: {} + xmlbuilder@9.0.7: {} xmlcreate@2.0.4: {} @@ -50880,6 +50900,11 @@ snapshots: xmldom@0.6.0: {} + xmlrpc@1.3.2: + dependencies: + sax: 1.2.4 + xmlbuilder: 8.2.2 + xpath@0.0.34: {} xregexp@2.0.0: {}