From 807be59f25deb9757158bab2f2c0ce2f1625d465 Mon Sep 17 00:00:00 2001 From: Jack Dane Date: Thu, 29 Jun 2023 14:56:20 +0100 Subject: [PATCH] feat(odoo): add auth and create lead action (#1143) * Add Odoo App and Icon * Add Auth for Odoo * Authorise with API key, the password would also work, but we should encourage an API key. * Odoo Verify Credentials method * Add the xmlrpc dependency so the backend can communicate with Odoo's API. * Add a port to the auth fields to establish a connection that might not be over HTTPS. * Add still verified method * Currently no need to keep uid, so remove it from the auth data. * Await the callback from the xmlrpc method call to ensure we don't verify credentials before the callback has been executed. * Add Odoo create-lead action * Provide basic functionality to create a lead. * Add Odoo type option * Let the user decide if the lead should be a "lead" or "opportunity" in the create-lead action. * Add documentation for Odoo app * Follow project standards * Change indents to 2 spaces * Use single quotes instead of double * Commonise the authentication method (DRY) * Use latest for API doc link * refactor(odoo): abstract and organize implementation --------- Co-authored-by: Ali BARIN --- packages/backend/package.json | 4 +- .../apps/odoo/actions/create-lead/index.ts | 103 ++++++++++++++++++ .../backend/src/apps/odoo/actions/index.ts | 3 + .../backend/src/apps/odoo/assets/favicon.svg | 1 + packages/backend/src/apps/odoo/auth/index.ts | 65 +++++++++++ .../src/apps/odoo/auth/is-still-verified.ts | 9 ++ .../src/apps/odoo/auth/verify-credentials.ts | 16 +++ .../src/apps/odoo/common/xmlrpc-client.ts | 67 ++++++++++++ packages/backend/src/apps/odoo/index.ts | 16 +++ packages/docs/pages/.vitepress/config.js | 9 ++ packages/docs/pages/apps/odoo/actions.md | 12 ++ packages/docs/pages/apps/odoo/connection.md | 16 +++ packages/docs/pages/guide/available-apps.md | 1 + packages/docs/pages/public/favicons/odoo.svg | 1 + yarn.lock | 22 +++- 15 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/apps/odoo/actions/create-lead/index.ts create mode 100644 packages/backend/src/apps/odoo/actions/index.ts create mode 100644 packages/backend/src/apps/odoo/assets/favicon.svg create mode 100644 packages/backend/src/apps/odoo/auth/index.ts create mode 100644 packages/backend/src/apps/odoo/auth/is-still-verified.ts create mode 100644 packages/backend/src/apps/odoo/auth/verify-credentials.ts create mode 100644 packages/backend/src/apps/odoo/common/xmlrpc-client.ts create mode 100644 packages/backend/src/apps/odoo/index.ts create mode 100644 packages/docs/pages/apps/odoo/actions.md create mode 100644 packages/docs/pages/apps/odoo/connection.md create mode 100644 packages/docs/pages/public/favicons/odoo.svg diff --git a/packages/backend/package.json b/packages/backend/package.json index 4421eb9b..f7e8e1c2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -30,6 +30,7 @@ "@sentry/node": "^7.42.0", "@sentry/tracing": "^7.42.0", "@types/luxon": "^2.3.1", + "@types/xmlrpc": "^1.3.7", "ajv-formats": "^2.1.1", "axios": "0.24.0", "bcrypt": "^5.0.1", @@ -62,7 +63,8 @@ "pg": "^8.7.1", "php-serialize": "^4.0.2", "stripe": "^11.13.0", - "winston": "^3.7.1" + "winston": "^3.7.1", + "xmlrpc": "^1.3.2" }, "contributors": [ { diff --git a/packages/backend/src/apps/odoo/actions/create-lead/index.ts b/packages/backend/src/apps/odoo/actions/create-lead/index.ts new file mode 100644 index 00000000..a5935000 --- /dev/null +++ b/packages/backend/src/apps/odoo/actions/create-lead/index.ts @@ -0,0 +1,103 @@ +import defineAction from '../../../../helpers/define-action'; +import { authenticate, asyncMethodCall } from '../../common/xmlrpc-client'; + +export default defineAction({ + name: 'Create Lead', + key: 'createLead', + description: '', + arguments: [ + { + label: 'Name', + key: 'name', + type: 'string' as const, + required: true, + description: 'Lead name', + variables: true, + }, + { + label: 'Type', + key: 'type', + type: 'dropdown' as const, + required: true, + variables: true, + options: [ + { + label: 'Lead', + value: 'lead' + }, + { + label: 'Opportunity', + value: 'opportunity' + } + ] + }, + { + label: "Email", + key: 'email', + type: 'string' as const, + required: false, + description: 'Email of lead contact', + variables: true, + }, + { + label: "Contact Name", + key: 'contactName', + type: 'string' as const, + required: false, + description: 'Name of lead contact', + variables: true + }, + { + label: 'Phone Number', + key: 'phoneNumber', + type: 'string' as const, + required: false, + description: 'Phone number of lead contact', + variables: true + }, + { + label: 'Mobile Number', + key: 'mobileNumber', + type: 'string' as const, + required: false, + description: 'Mobile number of lead contact', + variables: true + } + ], + + async run($) { + const uid = await authenticate($); + const id = await asyncMethodCall( + $, + { + method: 'execute_kw', + params: [ + $.auth.data.databaseName, + uid, + $.auth.data.apiKey, + 'crm.lead', + 'create', + [ + { + name: $.step.parameters.name, + type: $.step.parameters.type, + email_from: $.step.parameters.email, + contact_name: $.step.parameters.contactName, + phone: $.step.parameters.phoneNumber, + mobile: $.step.parameters.mobileNumber + } + ] + ], + path: 'object', + }, + ); + + $.setActionItem( + { + raw: { + id: id + } + } + ) + } +}); diff --git a/packages/backend/src/apps/odoo/actions/index.ts b/packages/backend/src/apps/odoo/actions/index.ts new file mode 100644 index 00000000..70a23831 --- /dev/null +++ b/packages/backend/src/apps/odoo/actions/index.ts @@ -0,0 +1,3 @@ +import createLead from './create-lead'; + +export default [createLead]; diff --git a/packages/backend/src/apps/odoo/assets/favicon.svg b/packages/backend/src/apps/odoo/assets/favicon.svg new file mode 100644 index 00000000..aeb5dd77 --- /dev/null +++ b/packages/backend/src/apps/odoo/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/odoo/auth/index.ts b/packages/backend/src/apps/odoo/auth/index.ts new file mode 100644 index 00000000..4a9b99e5 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/index.ts @@ -0,0 +1,65 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'host', + label: 'Host Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Host name of your Odoo Server', + clickToCopy: false, + }, + { + key: 'port', + label: 'Port', + type: 'string' as const, + required: true, + readOnly: false, + value: '443', + placeholder: null, + description: 'Port that the host is running on, defaults to 443 (HTTPS)', + clickToCopy: false, + }, + { + key: 'databaseName', + label: 'Database Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Name of your Odoo database', + clickToCopy: false, + }, + { + key: 'email', + label: 'Email Address', + type: 'string' as const, + requires: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Email Address of the account that will be interacting with the database', + clickToCopy: false + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Key for your Odoo account', + clickToCopy: false + } + ], + + verifyCredentials, + isStillVerified +}; diff --git a/packages/backend/src/apps/odoo/auth/is-still-verified.ts b/packages/backend/src/apps/odoo/auth/is-still-verified.ts new file mode 100644 index 00000000..f676c026 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import {IGlobalVariable} from '@automatisch/types'; +import verifyCredentials from './verify-credentials'; + +const isStillVerified = async ($: IGlobalVariable) => { + await verifyCredentials($); + return true; +} + +export default isStillVerified; diff --git a/packages/backend/src/apps/odoo/auth/verify-credentials.ts b/packages/backend/src/apps/odoo/auth/verify-credentials.ts new file mode 100644 index 00000000..64dfbeae --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/verify-credentials.ts @@ -0,0 +1,16 @@ +import { IGlobalVariable } from '@automatisch/types'; +import { authenticate } from '../common/xmlrpc-client'; + +const verifyCredentials = async ($: IGlobalVariable) => { + try { + await authenticate($); + + await $.auth.set({ + screenName: `${$.auth.data.email} @ ${$.auth.data.databaseName} - ${$.auth.data.host}`, + }); + } catch (error) { + throw new Error('Failed while authorizing!'); + } +} + +export default verifyCredentials; diff --git a/packages/backend/src/apps/odoo/common/xmlrpc-client.ts b/packages/backend/src/apps/odoo/common/xmlrpc-client.ts new file mode 100644 index 00000000..a29dd374 --- /dev/null +++ b/packages/backend/src/apps/odoo/common/xmlrpc-client.ts @@ -0,0 +1,67 @@ +import { join } from 'node:path'; +import xmlrpc from 'xmlrpc'; +import { IGlobalVariable } from "@automatisch/types"; + +type AsyncMethodCallPayload = { + method: string; + params: any[]; + path?: string; +} + +export const asyncMethodCall = async ($: IGlobalVariable, { method, params, path }: AsyncMethodCallPayload): Promise => { + return new Promise( + (resolve, reject) => { + const client = getClient($, { path }); + + client.methodCall( + method, + params, + (error, response) => { + if (error != null) { + // something went wrong on the server side, display the error returned by Odoo + reject(error); + } + + resolve(response); + } + ) + } + ); +} + +export const getClient = ($: IGlobalVariable, { path = 'common' }) => { + const host = $.auth.data.host as string; + const port = Number($.auth.data.port as string); + + return xmlrpc.createClient( + { + host, + port, + path: join('/xmlrpc/2', path), + } + ); +} + +export const authenticate = async ($: IGlobalVariable) => { + const uid = await asyncMethodCall( + $, + { + method: 'authenticate', + params: [ + $.auth.data.databaseName, + $.auth.data.email, + $.auth.data.apiKey, + [] + ] + } + ); + + if (!Number.isInteger(uid)) { + // failed to authenticate + throw new Error( + 'Failed to connect to the Odoo server. Please, check the credentials!' + ); + } + + return uid; +} diff --git a/packages/backend/src/apps/odoo/index.ts b/packages/backend/src/apps/odoo/index.ts new file mode 100644 index 00000000..3502708b --- /dev/null +++ b/packages/backend/src/apps/odoo/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import auth from './auth'; +import actions from './actions'; + +export default defineApp({ + name: 'Odoo', + key: 'odoo', + iconUrl: '{BASE_URL}/apps/odoo/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/odoo/connection', + supportsConnections: true, + baseUrl: 'https://odoo.com', + apiBaseUrl: '', + primaryColor: '9c5789', + auth, + actions +}); diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 59965de5..7f0fbbaa 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -160,6 +160,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/ntfy/connection' }, ], }, + { + text: 'Odoo', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/odoo/actions' }, + { text: 'Connection', link: '/apps/odoo/connection' }, + ], + }, { text: 'OpenAI', collapsible: true, diff --git a/packages/docs/pages/apps/odoo/actions.md b/packages/docs/pages/apps/odoo/actions.md new file mode 100644 index 00000000..e22fdb84 --- /dev/null +++ b/packages/docs/pages/apps/odoo/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/odoo.svg +items: + - name: Create a lead or opportunity + desc: Creates a new CRM record as a lead or opportunity. +--- + + + + \ No newline at end of file diff --git a/packages/docs/pages/apps/odoo/connection.md b/packages/docs/pages/apps/odoo/connection.md new file mode 100644 index 00000000..49b0d270 --- /dev/null +++ b/packages/docs/pages/apps/odoo/connection.md @@ -0,0 +1,16 @@ +# Odoo + +:::info +This page explains the steps you need to follow to set up the Odoo +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +To create a connection, you need to supply the following information: + +1. Fill the **Host Name** field with the Odoo host. +1. Fill the **Port** field with the Odoo port. +1. Fill the **Database Name** field with the Odoo database. +1. Fill the **Email Address** field with the email address of the account that will be intereacting with the database. +1. Fill the **API Key** field with the API key for your Odoo account. + +Odoo's [API documentation](https://www.odoo.com/documentation/latest/developer/reference/external_api.html#api-keys) explains how to create API keys. diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md index 2fd0e483..b3df524b 100644 --- a/packages/docs/pages/guide/available-apps.md +++ b/packages/docs/pages/guide/available-apps.md @@ -20,6 +20,7 @@ Following integrations are currently supported by Automatisch. - [HTTP Request](/apps/http-request/actions) - [Notion](/apps/notion/triggers) - [Ntfy](/apps/ntfy/actions) +- [Odoo](/apps/odoo/actions) - [OpenAI](/apps/openai/actions) - [PostgreSQL](/apps/postgresql/actions) - [RSS](/apps/rss/triggers) diff --git a/packages/docs/pages/public/favicons/odoo.svg b/packages/docs/pages/public/favicons/odoo.svg new file mode 100644 index 00000000..aeb5dd77 --- /dev/null +++ b/packages/docs/pages/public/favicons/odoo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 14e43ef5..99d33a9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4328,6 +4328,13 @@ dependencies: "@types/node" "*" +"@types/xmlrpc@^1.3.7": + version "1.3.7" + resolved "https://registry.yarnpkg.com/@types/xmlrpc/-/xmlrpc-1.3.7.tgz#a95e8636fe9b848772088cfaa8021d0ad0ad99a0" + integrity sha512-T+jYEZz/dJvI40dkqx/FNNkyyWDyOb0HgQDpni48r4NyB8n7xjKFDACi8O3NkAWz5cLWEmKRzWfzCEZ5EB6CVg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -15412,7 +15419,7 @@ sax@1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@1.2.x, sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -17982,6 +17989,11 @@ xml2js@0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" +xmlbuilder@8.2.x: + version "8.2.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" + integrity sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw== + xmlbuilder@~9.0.1: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" @@ -17992,6 +18004,14 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlrpc@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/xmlrpc/-/xmlrpc-1.3.2.tgz#26b2ea347848d028aac7e7514b5351976de3e83d" + integrity sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ== + dependencies: + sax "1.2.x" + xmlbuilder "8.2.x" + xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"